柳大的Linux講義·基礎篇(4)網路程式設計基礎

鍾超發表於2012-03-12

柳大的Linux遊記·基礎篇(4)網路程式設計基礎

  • Author: 柳大·Poechant
  • Blog:Blog.CSDN.net/Poechant
  • Email:zhongchao.usytc#gmail.com (#->@)
  • Date:March 11th, 2012
  • Copyright © 柳大·Poechant(鍾超·Michael)

回顧

  1. 《柳大的Linux遊記·基礎篇(1)磁碟與檔案系統》
  2. 《柳大的Linux遊記·基礎篇(2)Linux檔案系統的inode》
  3. 《柳大的Linux遊記·基礎篇(3)許可權、連結與許可權管理》

閒話

最近很忙,博文寫的少。感謝一些博友的理解。

有博友發郵件說《柳大的Linux遊記》希望繼續寫下去,希望瞭解一些與 socket 入門有關的內容。對此深表慚愧,當時也是應一個博友的來信而開始寫這個系列的,但僅寫了三篇就沒繼續了。與 socket 相關的文章,在網路上非常多,socket 程式設計也是基本功。要我來寫的話,寫出心意很難,我只希望能寫系統一些,所以我想先介紹 socket 的基礎,然後說說 select,poll 和 epoll 等 IO 複用技術,可能這樣會系統一些,也更實用。

W. Richard StevensUNIX Network Programming Volume 1中講解例子的時候都使用了include "unp.h",這是Stevens先生在隨書原始碼中的提供的一個包含了所有 UNIX 網路程式設計會用到的標頭檔案的的一個標頭檔案。但這樣對於不瞭解 UNIX 網路程式設計以及 socket 的朋友來說,並不是一個好的學習途徑。所以我想看完本文後讀Stevens先生的傑出作品更好一些 :)

另外,《JVM深入筆記》的第四篇正在整理,最近確實空閒時間比較少,對此感到很抱歉。我會盡量抽時間多分享一些的。

言歸正傳,下面還是沿襲我的一貫風格,先以最簡單的例項開始。

目錄

  1. 快速開始
    • 1.1 TCP C/S
      • 1.1.1 TCP Server
      • 1.1.2 TCP Client
    • 1.2. UCP C/S
      • 1.2.1 UDP Server
      • 1.2.2 UDP Client
  2. TCP 和 UCP 的 Socket 程式設計對比
    • 2.1 Server
    • 2.2 Client
    • 2.3 所使用的 API 對比
  3. 裸用 socket 的效能很差

1 快速開始

1.1 TCP C/S

無論你是使用 Windows 還是 UNIX-like 的系統,作業系統提供給應用層的網路程式設計介面都是 Socket。在 5 層的 TCP/IP 網路結構或者 7 層的 OSI 網路結構中,都有傳輸層,TCP 和 UDP 協議就是為傳輸層服務的。而網路層的最常用協議就是 IP(IPv4 或 IPv6)在高層編寫程式,就需要用到 TCP 協議和 UDP 協議。其直接使用,就是通過 Socket 來實現的。

先看一段簡單的 TCP 通訊的 Server 與 Client 例程。

1.1.1 TCP Server

下面是一個 TCP 連線的 Server 例項。

#include <string.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    // Get Port option
    if (argc < 2)
    {
        fprintf(stderr, "ERROR, no port provided\n");
        exit(1);
    }
    int port_no = atoi(argv[1]);

    // Get socket
    int socket_fd = socket(AF_INET, SOCK_STREAM, 0);

    // Bind
    struct sockaddr_in server_addr;
    bzero((char *) &server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(port_no);
    bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));

    // Listen
    listen(socket_fd, 5);

    while (1) {

        // Accept
        struct sockaddr_in client_addr;
        socklen_t client_addr_length = sizeof(client_addr);
        int socket_to_client = accept(socket_fd, (struct sockaddr *) &client_addr, &client_addr_length);

        // Read
        char buffer[1024];
        bzero(buffer, sizeof(buffer));
        read(socket_to_client, buffer, sizeof(buffer) - 1);
        printf("Here is the message: %s\n", buffer);

        // Write
        const char *data = "I got your message.";
        write(socket_to_client, data, sizeof(data));

        // Close
        close(socket_to_client);
    }

    close(socket_fd);

    return 0;
}

上面是 TCP 的 Client 的 Simplest Example。概括起來 Scoket Server 程式設計有如下幾個步驟:

1.1.1.1 獲取 Socket Descriptor:
    // socket function is included in sys/socket.h
    // AF_INET is included in sys/socket.h
    // SOCK_STREAM is included in sys/socket.h
    socket(AF_INET, SOCK_STREAM, 0);

通過sys/socket.h中的socket函式。第一個參數列示使用IPv4 Internet Protocol,如果是AF_INET6則表示IPv6 Internet Protocol,其中AF表示Address Family,另外還有PF表示Protocol Family。第二個參數列示流傳輸Socket Stream,流傳輸是序列化的、可靠的、雙向的、面向連線的,Kernel.org 給出的解釋是:“Provides sequenced, reliable, two-way, connection-based byte streams. An out-of-band data transmission mechanism may be supported.”

另外一個常用的是SOCK_DGRAM表示Socket Diagram,是無連線的、不可靠的傳輸方式,Kernel.org 給出的解釋是“Supports datagrams (connectionless, unreliable messages of a fixed maximum length).”

第三個參數列示使用的協議族中的哪個協議。一般來說一個協議族經常只有一個協議,所以長使用“0”。具體參見Kernel.org 給出的解釋

1.1.1.2 繫結地址與埠

首先要建立一個struct sockaddr_in,並設定地址族、監聽的外來地址與本地埠號。如下:

// struct sockaddr_in is inclued in netinet/in.h
// bzero function is included in string.h
// atoi is include in stdlib.h
struct sockaddr_in server_addr;
bzero((char *) &server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(atoi(argv[1]))

然後將第 1 步建立的Socket與這裡建立的地址繫結(實際上直接用的是Socket Descriptor)。

// struct sockaddr is included in sys/socket.h
// bind function is included in sys/socket.h
bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));
1.1.1.3 開始監聽
// listen function is included in sys/socket.h
    listen(socket_fd, 5);

第一個引數不用贅述了,第二個引數是連線佇列(connection queue)的最大長度。當Server進行了Accept後,在新來的請求就得進入佇列等待,如果佇列滿了,再來的連線就會被拒絕。

1.1.1.4 接受連線
// struct sockaddr_in is included in netinet/in.h
// accept function is included in sys/socket.h
struct sockaddr_in client_addr;
socklen_t client_addr_length = sizeof(client_addr);
int socket_to_client = accept(socket_fd, (struct sockaddr *) &client_addr, &client_addr_length);

在開始監聽socket_fd後,接收來自該Socket的連線,將獲取到的客戶端地址和地址長度寫入client_addrclient_addr_length中。該accept在成功接受某連線後會得到該連線的Socket,並將其Socket Descriptor返回,就得到了socket_to_client

1.1.1.5 接收和傳送資料
// bzero function is included in string.h
// read function is included in unistd.h
char buffer[1024];
bzero(buffer, sizeof(buffer));
read(socket_to_client, buffer, sizeof(buffer) - 1);

在接受連線後,就可以從該Socket讀取客戶端傳送來的資料了,資料讀取到char *的字串中。傳送過程也類似。

// write function is included in unistd.h
const char *data = "Server has received your message.";
write(socket_to_client, data, sizeof(data));
1.1.1.6 關閉Socket
// close function is included in unistd.h
close(socket_fd);

以上就簡單解釋了 TCP Server 的 Socket 通訊過程。簡單概括如下:

Create Socket - Bind socket with port - Listen socket - Accept connection - Read/Write - Close

1.1.2 TCP Client

再來看看 Client。以下是例程:

#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>
#include <netdb.h> // bcopy
#include <stdlib.h> 

int main(int argc, char *argv[])
{
    // Get options
    if (argc < 3)
    {
        fprintf(stderr, "Usage: %s <hostname> <port>\n", argv[0]);
        exit(1);
    }
    struct hostent *server_host = gethostbyname(argv[1]);
    int server_port = atoi(argv[2]);

    // Get socket
    int socket_fd = socket(AF_INET, SOCK_STREAM, 0);

    // Connect
    struct sockaddr_in server_addr;
    bzero((char *) &server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    bcopy((char *) server_host->h_addr, (char *) &server_addr.sin_addr.s_addr, server_host->h_length);
    server_addr.sin_port = htons(server_port);
    connect(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));

    // Input
    printf("Please enter the message: ");
    char buffer[1024];
    bzero(buffer, sizeof(buffer));
    fgets(buffer, sizeof(buffer) - 1, stdin);

    // Write
    write(socket_fd, buffer, strlen(buffer));

    // Read
    bzero(buffer, sizeof(buffer));
    read(socket_fd, buffer, sizeof(buffer) - 1);
    printf("%s\n", buffer);

    // Close
    close(socket_fd);

    return 0;
}

上面是 TCP 的 Client 的 Simplest Example。概括起來 Scoket Client 程式設計有如下幾個步驟:

1.1.2.1 獲取 Socket Descriptor:

與 Server 一樣。

1.1.2.2 連線伺服器
// struct sockaddr_in is included in netinet/in.h
// struct sockaddr is included in sys/socket.h
// bzero is included in string.h
// bcopy is included in string.h
// AF_INET is included in sys/socket.h
// connect is included in sys/socket.h
connect(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));

第一個引數是int型的Socket Descriptor,通過socket函式得到。第二個引數是指向struct sockaddr地址的指標,第三個引數是該地址的大小。而這個地址是通過struct sockaddr_in得到的,然後用bzero位初始化,再賦初值,包括地址族為AF_INET,地址為struct sockaddr_in中的h_addr,這裡用到了bcopy位拷貝函式,最後再賦上埠號htons(int server_port)connect的作用,就是將本地的Socket與伺服器建立連線,而這個Socket則是通過Socket Descriptor來標示的。

1.1.2.3 傳送或接收資料

首先看傳送資料:

// write function is included in unistd.h
char buffer[1024]
...
write(socket_fd, buffer, strlen(buffer));

然後用write函式,向Socket所連線的伺服器傳送資料,資料是char *的字串。再看下面的接收資料:

// read function is included in unistd.h
read(socket_fd, buffer, sizeof(buffer) - 1);

第一個引數是Socket Descriptor,第二個引數是char *的字串,長度為第三個引數標示的sizeof(buffer)-1。功能就是從socket_fd標示的Socket所連線的伺服器讀取資料。

1.1.2.4 關閉Socket
// close function is included in unistd.h
close(socket_fd);

以上就簡單解釋了客戶端的最基本的Socket通訊。概括起來的過程就是:

Create Socket - Connect socket with server - Write/Read - Close

1.2 UDP C/S

剛才介紹了最簡單的 TCP C/S 模型,下面看看 UDP C/S 模型。

1.2.1 UDP Server

下面是 UDP 連線的 Server 例項:

#include "sys/socket.h"
#include "netinet/in.h"
#include "string.h"

int main(int argc, char *argv[])
{
    // Create Socket
    int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);

    // Bind
    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(atoi(argv[1]));
    bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));

    // As accept
    while (1)
    {
        // Receive
        char buffer[1024];
        struct sockaddr_in client_addr;
        socklen_t client_addr_length = sizeof(client_addr);
        int msg_recv_length = recvfrom(socket_fd, buffer, sizeof(buffer), 0, (struct sockaddr *) &client_addr, &client_addr_length);

        // Write
        const char *msg_send = "Received a datagram: ";
        write(1, msg_send, strlen(msg_send));
        write(1, buffer, msg_recv_length);

        // Send
        const char *msg_send_2 = "Got your message\n";
        sendto(socket_fd, msg_send_2, strlen(msg_send_2), 0, (struct sockaddr *) &client_addr, sizeof(struct sockaddr_in));
    }

    close(socket_fd);

    return 0;
}

UDP Server 的建立主要有以下幾步:

1.2.1.1 獲取 Socket Descriptor
sock=socket(AF_INET, SOCK_DGRAM, 0);

建立Socket,獲取Socket Descriptor

1.2.1.2 繫結地址與埠
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = INADDR_ANY;
server_addr.sin_port = htons(atoi(argv[1]));
bind(socket_fd, (struct sockaddr *) &server_addr, sizeof(server_addr));

將埠和允許的地址與Socket繫結。

1.2.1.3 接收和傳送資料
char buffer[1024];
struct sockaddr_in client_addr;
socklen_t client_addr_length = sizeof(client_addr);
int msg_recv_length = recvfrom(socket_fd, buffer, sizeof(buffer), 0, (struct sockaddr *) &client_addr, &client_addr_length);

接收資料用的是sys/socket.h中的recvfrom函式,第一個引數是Socket Descriptor,第二個引數是用於儲存資料的buffer,第三個引數是資料儲存區域的長度(注,對於陣列用sizeof取到的值是陣列長度乘以長度;對於指標用sizeof取到的值是指標長度,對於32位機是4,對於64位機是8)。第四個引數是標誌符,一般設定為0,具體可以檢視info recvfrom。第五個引數用於儲存客戶端的地址,第六個引數是儲存客戶端地址的socklen_t型變數的長度。

用起來也很好記(也可以現查現用),先是套接字,然後是儲存區及其大小,接著是標誌符,最後是客戶端地址及其大小。

const char *msg_send_2 = "Got your message\n";
sendto(socket_fd, msg_send_2, strlen(msg_send_2), 0, (struct sockaddr *) &client_addr, sizeof(struct sockaddr_in));

傳送資料用更多是sys/socket.h中的sendto函式,第一個引數Socket Descriptor,第二和第三個引數是所傳送的資料及其大小,然後是標示符(一般為0),最後是客戶端地址及其大小。

1.2.1.4 關閉Socket
close(socket_fd);

簡單概括一下 UDP Server 的 Socket 程式設計步驟:

Create Socket - Bind socket with port - Recv/Send - Close

1.2.2 UDP Client

#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <netdb.h>

int main(int argc, char *argv[])
{
    // Create socket
    int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);

    // Initialize server address
    struct hostent *server_host = gethostbyname(argv[1]);
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET; // socket internet family
    bcopy((char *)server_host->h_addr, (char *)&server_addr.sin_addr.s_addr, server_host->h_length); // socket internet address
    server_addr.sin_port = htons(atoi(argv[2])); // socket internet port

    // Send
    char buffer[1024];
    sendto(socket_fd, buffer, sizeof(buffer), 0, (const struct sockaddr *) &server_addr, sizeof(struct sockaddr_in));

    // Receive and write
    struct sockaddr_in client_addr;
    socklen_t client_addr_length = sizeof(client_addr);
    int msg_recv_length = recvfrom(socket_fd, buffer, sizeof(buffer), 0, (struct sockaddr *) &client_addr, &client_addr_length);
    const char *hint = "Got an ACK: ";
    write(1, hint, strlen(hint));
    write(1, buffer, msg_recv_length);

    // Close
    close(socket_fd);

    return 0;
}
1.2.2.1 建立Socket
int socket_fd = socket(AF_INET, SOCK_DGRAM, 0);
1.2.2.2 傳送和接收資料
sendto(socket_fd, buffer, sizeof(buffer), 0, (const struct sockaddr *) &server_addr, sizeof(struct sockaddr_in));

與 UDP Server 的 sendto 一樣。注意初始化。

1.2.2.3 關閉Socket
close(socket_fd);

總結一下 UDP Client 程式設計的幾個步驟:

Create Socket - Send/Receive - Close

2 圖解 TCP 和 UDP 原理

宣告:圖片來自此處

Resize icon

上圖為 TCP 原理圖示

Resize icon

上圖為 UDP 原理圖示

3 TCP 和 UCP 的 Socket 程式設計對比

3.1 Server

TCP 的過程是:

  1. Create server socket
  2. Bind the server socket with client addresses and a local server port
  3. Listen the server socket
  4. Accept with blocking, until the connection has established and then get a client socket
  5. Read and write data from the client socket
  6. Close the client socket as discontecting
  7. Close the local server socket

UDP 的過程是:

  1. Create server socket
  2. Bind the server socket with client addresses and a local server port
  3. Receive data and client address through the server socket
  4. Send data to the client address through the server socket
  5. Close the local server socket

通過對比,我們可以看到,相同點如下:

  1. 一開始都要建立 socket
  2. 接著都要繫結 socket 與本地埠和指定的客戶端地址
  3. 最後都要關閉本地 socket

不過這些相似點似乎沒什麼價值,還是看看不同點吧。

  1. TCP 要監聽埠,然後阻塞式地等待連線;UDP 則通過自身的迴圈來不斷讀取,不阻塞也不建立連線。
  2. TCP 建立連線後會有一個 client socket,然後通過向這個 socket 的讀寫實現資料傳輸;UDP 則直接向客戶端地址傳送和接收資料。
  3. 因為 TCP 方式有 client socket,所以完成一次傳輸後,可以關閉 client socket。當然也可以一直連著不關閉。

可以看到,TCP 和 UDP 的本質區別就是面向連線還是無連線的。因為面向連線,所以要監聽到是否有 connection 到來,connection 一旦到來,就阻塞住,然後會有一個 socket 跳出來作為代言。通過對這個 socket 的讀寫就實現了對 connection 的另一端的客戶端的讀寫。

3.2 Client

TCP 的過程是:

  1. Create client socket
  2. Connect the server address and port with the client socket
  3. After connection is established, read and write data to the client socket
  4. Close the local socket socket

UDP 的過程是:

  1. Create client socket
  2. Send data to the server address through the client socket
  3. Receive data and the server address throught the client socket
  4. Close the local client socket

可以看到如下區別:

  1. TCP 方式要 connect 伺服器地址/埠與 socket;UDP 則不需要這個過程。
  2. TCP 方式在 connection 建立後,通過 client socket 讀寫資料;而 UDP 方式則直接通過 client socket 向伺服器地址傳送資料。

3.3 所使用的 API 對比

TCP 方式的 Server 用到:

  • socket
  • bindlisten
  • accept
  • readwrite
  • close

UDP 方式的 Server 用到:

  • sokect
  • bind
  • recvfromsendto
  • close

TCP 方式的 Client 用到:

  • socket
  • connect
  • writeread
  • close

UDP 方式的 Client 用到:

  • socket
  • sendtorecvfrom
  • close

4 裸用 socket 的效能很差

是的,這是最傳統的網路程式設計方式:“One traditional way to write network servers is to have the main server block on accept(), waiting for a connection. Once a connection comes in, the server fork()s, the child process handles the connection and the main server is able to service new incoming requests.”

下一篇,我會介紹 IO 複用技術中在 Linux 下常用的 select、poll 和 epoll。

5 參考

  1. http://www.lowtek.com/sockets/select.html
  2. http://www.kernel.org/doc/man-pages/online/pages/man7/socket.7.html
  3. http://www.kernel.org/doc/man-pages/online/pages/man2/listen.2.html
  4. http://www.kernel.org/doc/man-pages/online/pages/man2/send.2.html
  5. http://www.kernel.org/doc/man-pages/online/pages/man2/sendto.2.html
  6. http://www.kernel.org/doc/man-pages/online/pages/man2/recv.2.html
  7. http://www.kernel.org/doc/man-pages/online/pages/man2/recvfrom.2.html
  8. http://www.linuxhowtos.org/C_C++/socket.htm

-

Happy Coding, enjoy sharing!

轉載請註明來自“柳大的CSDN部落格”:Blog.CSDN.net/Poechant

-

相關文章