Linux下套接字創建及連接建立簡介 - APUE


Unix環境高級編程(APUE)中介紹了套接字socket的使用,本文從開發者使用過程角度簡單介紹了服務器開啓監聽、客戶端發起連接、子線程創建的一些過程以及Unix中套接字的地址格式等內容。

連接過程

創建套接字地址

  1. 套接字地址結構
     struct sockaddr_in {
         sa_family_t     sin_family;    /* address family */
         in_port_t       sin_port;      /* port number */
         struct in_addr  sin_addr;      /* IPv4 address */
         unsigned char   sin_zero[8];   /* filler */
     };
    
  2. 創建套接字地址 sockaddr_in (IPv4), sockaddr_in6 (IPv6)
  3. 初始化套接字地址,以IPv4爲例
    1. sin_family 表示族,可選IPv4 - AF_INET 或 IPv6 - AF_INET6
    2. sin_port 地址對於的端口,應大於1024,否則需要superuser權限
    3. sin_addr 網絡地址的二進制表示,通過預定義的值如本地環回 INADDR_LOOPBACK 、任意 INADDR_ANY 設置,或通過 inet_aton 函數將點分十進制(127.0.0.1)格式轉換爲所需格式,由於網絡字節序與主機字節序可能不同,因此會再使用 htonl, htons, ntohl, ntohs 等函數進行轉換。
    4. sin_zero Linux中定義的填充字節,爲0
  4. 使用套接字地址 bind, connect

服務器開啓監聽

創建套接字地址及套接字,綁定,開啓監聽。其中開啓監聽在收到請求之前會阻塞(block),請求到來後會返回分配的新套接字描述符。

  1. 創建套接字地址,如上文所述
  2. 創建套接字 socket(int domain, int type, int protocol),返回套接字描述符, -1 on error
    1. domain 指定域,如IPv4 - AF_INET 或 IPv6 - AF_INET6
    2. type 指定連接類型,有如下四種
      1. SOCK_DGRAM 定長、無連接、不可靠報文,默認 UPD
      2. SOCK_RAW IP數據報接口
      3. SOCK_SEQPACKET 定長、有序、可靠、面向連接的報文,需要 AF_UNIX domain
      4. SOCK_STREAM 定長、可靠、雙向、面向連接的數據流,默認 TCP
    3. protocol 協議,0爲默認,其它可選 IPPROTO_IP, IPPROTO_IPv6, IPPROTO_TCP, IPPROTO_UDP
  3. 綁定套接字到指定地址 bind(int sockfd, const struct sockaddr *addr, socklen_t len),0 OK, -1 on error
    1. sockfd 套接字描述符
    2. addr 套接字地址,需要將 sockaddr_in 類型轉換爲此類型(轉換指針的類型)
    3. len 地址的長度
  4. 開啓監聽 listen(int sockfd, int backlog),0 OK, -1 on error
    1. sockfd 如前所述
    2. backlog 等待隊列中的請求個數

代碼

#define SVR_PORT 2300
#define SVR_BACKLOG 10
// listen
void start_listen() {
    // init socket addr
    sockaddr_in svr_addr;
    memset(&svr_addr, 0, sizeof(sockaddr_in));
    svr_addr.sin_family = AF_INET, svr_addr.sin_port = htons(SVR_PORT), svr_addr.sin_addr.s_addr = htonl(INADDR_ANY);

    // init socket to listen
    int sd_li = socket(AF_INET, SOCK_STREAM, 0);
    if (sd_li == -1)
        err_exit(-1, "socket: create socket failed.");

    // bind socket to addr
    int err = bind(sd_li, (sockaddr *)&svr_addr, sizeof(svr_addr));
    if (err != 0)
        err_exit(err, "bind: bind failed.");

    // listen
    err = listen(sd_li, SVR_BACKLOG);
    if (err == -1)
        err_exit(err, "listen: listen failed.");
    printf("start listen on port %d\n", SVR_PORT);

    // accept client request
    while (1) {
        sockaddr_in addr_peer;
        socklen_t len_peer;
        int sd_acc = accept(sd_li, (sockaddr *)&addr_peer, &len_peer);
        if (sd_acc == -1)
            err_exit(sd_acc, "accept: accept client error.");
        printf("connected to client %xd.\n", addr_peer.sin_addr.s_addr);

        // start a new thread and send hello back
        pthread_t subthread;
        err = pthread_create(&subthread, NULL, client_thread_fn, (void *)sd_acc);
        if (err != 0)
            err_exit(err, "pthread_create: create sub thread error.");
    }
}

客戶端連接建立

  1. 創建套接字地址及套接字,如上文所述
  2. 連接 connect(int sockfd, const struct sockaddr *addr, socklen_t len), 0 if OK, -1 on eror
    1. sockfd 如前所述
    2. addr 套接字地址
    3. len 地址長度
  3. 關閉套接字 close,該方法實際上是關閉了套接字(文件)描述符,因此需要包含 unistd.h 頭文件

代碼

#define SVR_HOST_STR "127.0.0.1"
// connect to server, return socket descriptor if success
int connect() {
    // init socket
    sockaddr_in addr;
    memset(&addr, 0, sizeof(addr));
    addr.sin_family = AF_INET, addr.sin_port = htons(SVR_PORT);
    inet_aton(SVR_HOST_STR, &addr.sin_addr);

    // create socket
    int sd = socket(AF_INET, SOCK_STREAM, 0);
    if (sd == -1)
        err_exit(sd, "socket: create socket failed.");

    // connect
    int ret_conn = connect(sd, (sockaddr *)&addr, sizeof(addr));
    if (ret_conn != 0)
        err_exit(ret_conn, "connect: connect failed.");
    return sd;
}

套接字地址格式

由於不同的domain地址格式不同,因此Unix定義了統一的格式 sockaddr 用來內部使用,而開發者則針對不同的domain使用特定的格式,如 sockaddr_insockaddr_in6,使用時需要將這些domain特定的格式轉換爲 sockaddr 格式。以下爲Linux下這些格式的定義:

struct sockaddr {  
    sa_family_t   sa_family;  /* address family */  
    char          sa_data[];  /* variable-length address */ 
};
struct sockaddr_in {
  sa_family_t     sin_family;    /* address family */
  in_port_t       sin_port;      /* port number */
  struct in_addr  sin_addr;      /* IPv4 address */
  unsigned char   sin_zero[8];   /* filler */
};
struct in_addr {
  in_addr_t       s_addr;        /* IPv4 address */
};

上述 sockaddr_in 格式中的 in_addr 表示因特網地址(二級制或者說整數形式,不同於點分十進制 127.0.0.1)。當然,Unix提供了 inet_ntopinet_pton 兩個函數,實現點分十進制與二進制之間的轉換。前者將二進制轉換爲點分十進制,後者將點分十進制轉換爲二進制。其中點分十進制爲 char *restrict str 字符串,size 指字符串緩衝區的長度;二進制爲 void *restrict addr

const char *inet_ntop(int domain, const void *restrict addr, char *restrict str, socklen_t size);
int inet_pton(int domain, const char *restrict str, void *restrict addr);

如前所述,由於網絡字節序Byte Order與主機字節序可能不同,因此Unix提供了下列四個函數用來轉換,其中h表示主機hostn表示networkls分別表示longshort

uint32_t htonl(uint32_t hostint32);
Returns: 32-bit integer in network byte order
uint16_t htons(uint16_t hostint16);
Returns: 16-bit integer in network byte order
uint32_t ntohl(uint32_t netint32);
Returns: 32-bit integer in host byte order
uint16_t ntohs(uint16_t netint16);
Returns: 16-bit integer in host byte order

數據傳輸

數據傳輸過程主要爲發送和接收數據兩種,兩種各包含四個相似函數可以使用,這裏主要介紹其中第一種,即 sendrecv ,如下:

ssize_t send(int sockfd, const void *buf, size_t nbytes, int flags);
ssize_t recv(int sockfd, void *buf, size_t nbytes, int flags);

前三個參數不難理解,分別是套接字描述符、待發送/接收消息緩衝區,緩衝區長度,最後一個是標誌。一般爲0即可,其它值如 MSG_OOB, MSG_PEEK 等表示不同含義,此處不一一列出。

線程創建

線程創建主要使用 pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg); 函數,注意,g++需要在編譯時加上參數 -lpthread 才能使用相關函數。 attr 爲屬性,一般設置爲 NULL 即可,start_routine 函數爲子線程的開始函數,arg爲其參數。子線程創建後會從該函數開始執行。

代碼

// thread function
void *thr_fn(void *x) {
    //...
}
int main{
    int sd = 1; // para
    pthread_t sub_thr;
    int err = pthread_create(&sub_thr, NULL, thr_fn, (void *)sd);
    if (err != 0)
        err_exit(err, "create sub thread failed.");
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章