1. 程式人生 > 實用技巧 >Linux C++ 網路 socket 基礎

Linux C++ 網路 socket 基礎

目錄

socket 概述

在一個典型的 C\S 場景中,應用程式使用 socket 進行通訊的方式如下:

  • 各個應用程式建立一個 socket。socket 是一個允許通訊的“裝置”,兩個應用程式都需要用到它
  • 伺服器將自己的 socket 繫結到一個眾所周知的地址上使得客戶端能夠定位到它的位置

通訊 domain

UNIX domain

Internet domain


socket 存在於一個通訊 domain 中,它確定:

  • 識別出一個socket 的方法(即 socket “地址” 的格式),UNIX domain socket 使用路徑名, 而 Internet domain socket 使用IP地址和埠號
  • 通訊範圍(即在位於同一主機的應用程式間,還是位於使用一個網路連線起來的不同主機的應用程式之間)
  • 具體用什麼資料結構來表示。UNIX domain 用:sockaddr_un;Internet domain 使用 sockaddr_in;

現代作業系統都支援以下 domain :

  • UNIX(AF_UNIX) domain 允許在同一主機上的應用程式進行通訊
  • IPv4(AF_INET) domain 允許在使用因特網協議第 4 版(IPv4)網路連線起來的主機上的應用程式之間進行通訊
  • IPv6(AF_INET6)domain 允許在使用因特網協議第 6 版(IPv6)網路連線起來的主機上的應用程式之間進行通訊

IPv4 和 IPv6 可合成為 Internet domain

在 sockaddr_in 結構體中會出現 PF 和 AF, PF 表示 “協議族” (protocol family),AF 表示 “地址族” (address family)
但是 PF 現在基本用不到了。

socket 型別

每個 socket 實現至少提供兩種 socket:流(SOCK_STREAM)和資料報(SOCK_DGRAM)

流 socket 提供了一個可靠的雙向的位元組通訊通道。

  • 可靠的:表示傳送端的資料可以完整無缺地到底接收端或收到一個傳輸失敗的通知
  • 雙向的:表示資料可以在兩個 socket 的任意方向上傳輸
  • 位元組的:表示與管道一樣不存在訊息邊界
    (不存在訊息邊界指的是:接受方接受的資料塊大小是隨意的,沒有確定的。)

資料報 socket 允許資料以被稱為資料報的訊息的形式進行交換。它具有以下特點:

  • 不可靠
  • 無連線
  • 存在訊息邊界

在 Internet domain(AF_INET 或 AF_INET6) 中,資料報 socket 使用 UDP 協議,而流 socket 使用 TCP 協議。一般來講在稱呼這兩種 socket 時不會使用術語 “Internet domain 資料報 socket” 或 “Internet domain 流 socket” ,而是分別使用術語 “UDP socket” 和 “TCP socket” 。

通用 socket 地址結構:struct sockaddr

struct sockaddr{
    sa_family_t sa_family;          /* Address family (AF_* constant) */
    char        sa_data[14];        /* Socket address (size varies according to socket domain) */
};

UNIX domain socket 地址:struct sockaddr_un

struct sockaddr{
    sa_family_t sun_family;          /* always AF_UNIX */
    char        sun_path[108];        /* null-terminated socket pathname */
};

一般向 sun_path 欄位寫入內容時使用 snprintf() 或 strncpy() 以避免緩衝區溢位。

UNIX domain 中的流 socket

以下實現一個簡單的通訊 domain 為 UNIX domain 的迭代式伺服器(伺服器在處理下一個客戶端之前一次只處理一個客戶端)

cs.h 標頭檔案:

#include <errno.h>
#include <unistd.h>
#include <sys/un.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>

#define SV_SOCK_PATH "/tmp/codroc"
#define BUFSZ 100
#define BACKLOG 5

int errExit(char *msg){
    printf("%s error!\n", msg);
    exit(0);
}

server.c:

#include "cs.h"
char buf[BUFSZ];
int main(int argc, char *argv[]){
    ssize_t readnum;
    int sfd, cfd;
    struct sockaddr_un addr;

    sfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if(-1 == sfd)
        errExit("socket");
    
    memset(&addr, 0, sizeof(struct sockaddr_un));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path) - 1);

    if(-1 == remove(SV_SOCK_PATH) && errno != ENOENT)
        errExit("remove");

    if(-1 == bind(sfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)))
        errExit("bind");
    if(-1 == listen(sfd, BACKLOG))
        errExit("listen");

    for(;;){
        cfd = accept(sfd, NULL,NULL);
        if(-1 == cfd)
            errExit("accept");

        while((readnum = read(cfd, buf, BUFSZ)) > 0)
            if(readnum != write(1, buf, readnum))
                errExit("write");

        if(-1 == readnum)
            errExit("read");

        if(-1 == close(cfd))
            errExit("close");
    }
    exit(0);
}

client.c:

#include "cs.h"
char buf[BUFSZ];
int main(int argc, char *argv[]){
    struct sockaddr_un addr;
    int cfd;
    ssize_t readnum;

    cfd = socket(AF_UNIX, SOCK_STREAM, 0);
    if(cfd == -1)
        errExit("socket");
    memset(&addr, 0, sizeof(struct sockaddr_un));
    addr.sun_family = AF_UNIX;
    strncpy(addr.sun_path, SV_SOCK_PATH, sizeof(addr.sun_path) - 1);
    if(-1 == connect(cfd, (struct sockaddr *) &addr, sizeof(struct sockaddr_un)))
        errExit("connect");
    while((readnum = read(0, buf, BUFSZ)) > 0)
        if(readnum != write(cfd, buf, readnum))
            errExit("write");
    if(-1 == readnum)
        errExit("read");
    exit(0);
}

在 shell 中輸入以下命令來測試:

gcc -o unix_server server.c
gcc -o unix_client client.c

./unix_server > b &         #伺服器程式放到後臺執行,並將輸出重定向到檔案 b
ls -lF /tmp/codroc          #檢視套接字檔案

cat *.c > a                 #將所有 .c 檔案的內容寫入 a 檔案
./unix_client < a           #將 a 檔案中的內容作為 unix_client 的輸入

diff a b                    #比較下 a b 檔案的差異,若無差異 diff 不會返回任何東西

注意在伺服器終止之後,/tmp/codroc 檔案還是存在的!這就是為什麼在 server.c 中的 bind 函式前要先執行 remove 函式,來確保 SV_SOCK_PATH 是不存在的!

UNIX domain 中的資料報 socket

之前有講到資料報 socket 是不可靠的,這是有前提的,那就是在網路上傳輸的情況下,資料報 socket 是不可靠的。但是在 UNIX domain 下的資料報傳輸是在核心中傳送的,所以說是可靠的!

以下實現一個在 UNIX domain 下使用資料報進行通訊的簡單 C\S 程式:

ud_cs.h:


#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <ctype.h>

#define BUFSZ 10
#define SV_SOCK_PATH "/tmp/codroc"

void errExit(char *msg){
    printf("%s error!\n", msg);
    exit(0);
}

ud_dgram_server.c:

#include "ud_cs.h"
char buf[BUFSZ];
int main(int argc, char *argv[]){
    struct sockaddr_un saddr, caddr;
    ssize_t readnum;
    int sfd, cfd;

    sfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if(-1 == sfd)
        errExit("socket");
    
    memset(&saddr, 0, sizeof(struct sockaddr_un));
    saddr.sun_family = AF_UNIX;
    strncpy(saddr.sun_path, SV_SOCK_PATH, sizeof(saddr.sun_path) - 1);

    if(-1 == remove(SV_SOCK_PATH) && errno != ENOENT)
        errExit("remove");
    
    if(-1 == bind(sfd, (struct sockaddr *) &saddr, sizeof(struct sockaddr_un)))
        errExit("bind");
    for(;;){
        memset(buf, 0, sizeof(buf));
        socklen_t len = sizeof(struct sockaddr_un);
        readnum = recvfrom(sfd, buf, BUFSZ, 0, (struct sockaddr *) &caddr, &len);
        if(-1 == readnum)
            errExit("recvfrom");
        printf("Server received %ld bytes from \"%s\"\n", (long) readnum, caddr.sun_path);
        for(int i = 0;i < readnum;i++)
            buf[i] = toupper(buf[i]);
        if(readnum != sendto(sfd, buf, readnum, 0, (struct sockaddr *) &caddr, len))
                errExit("sendto");
    }
    exit(0);
}

ud_dgram_client.c:

#include "ud_cs.h"
char buf[BUFSZ];
int main(int argc, char *argv[]){
    if(argc < 2 || 0==strcmp("--help", argv[1]))
        printf("%s msg...\n", argv[0]);
    struct sockaddr_un saddr, caddr;
    ssize_t readnum;
    int cfd;

    cfd = socket(AF_UNIX, SOCK_DGRAM, 0);
    if(-1 == cfd)
        errExit("socket");
    memset(&caddr, 0, sizeof(struct sockaddr_un));
    caddr.sun_family = AF_UNIX;
    snprintf(caddr.sun_path, sizeof(caddr.sun_path) - 1, "/tmp/codroc_client.%ld", (long)getpid());
    
    memset(&saddr, 0, sizeof(struct sockaddr_un));
    saddr.sun_family = AF_UNIX;
    strncpy(saddr.sun_path, SV_SOCK_PATH, sizeof(struct sockaddr_un) - 1);

    if(-1 == remove(caddr.sun_path) && errno != ENOENT)
        errExit("remove");
    if(-1 == bind(cfd, (struct sockaddr *) &caddr, sizeof(struct sockaddr_un)))
        errExit("bind");
    for(int i = 1;i < argc;i++){
        size_t len = strlen(argv[i]);
        memset(buf, 0, BUFSZ);
        socklen_t socklen = sizeof(struct sockaddr_un);
        if(len != sendto(cfd, argv[i], len, 0, (struct sockaddr *) &saddr, socklen))
            errExit("sendto");
        printf("Client sended %ld bytes: \"%s\" to Server \"%s\"\n", (long) len, argv[i], saddr.sun_path);
        readnum = recvfrom(cfd, buf, BUFSZ, 0, NULL, NULL);
        printf("Client received %ld bytes from \"%s\"\t", (long) readnum, saddr.sun_path);
        printf("the string is: \"%s\"\n", buf);
    }
    close(cfd);
    return 0;
}

在 shell 中輸入以下命令來測試:

gcc -o ud_dgram_server ud_dgram_server.c
gcc -o ud_dgram_client ud_dgram_client.c

./ud_dgram_server  &         #伺服器程式放到後臺執行,並將輸出重定向到檔案 b
ls -lF /tmp/codroc          #檢視套接字檔案

./ud_dgram_client hello world
./ud_dgram_client helloworld codroc
./ud_dgram_client helloworldxxxx

在 執行 ./ud_dgram_client helloworldxxxx 可以看到伺服器只能接受到10個字元,剩餘的都被截掉了!

SOCKET: Internet Domain

  • Internet domain 流 socket 是建立在 TCP 之上的,它們提供了可靠的雙向位元組流通訊通道
  • Internet domain 資料報 socket 是建立在 UDP 之上的。

網路位元組序

眾所周知,不同的硬體結構以不同的順序來儲存一個多位元組整數的位元組。一般順序分為兩種:大端小端,先儲存最高有效位的稱為大端機,儲存順序與整數的位元組順序一致的是小端機。

規定在網路中傳輸的位元組順序以大端為標準

因此當主機中的資料打包發出去時,包中的資料肯定是以大端序排列的了。

那麼如何將主機序轉換成網路序呢?C語言庫給我們提供了相應的函式:

#include <arpa/inet.h>

uint16_t htons(uint16_t);
uint32_t htonl(uint32_t);
uint16_t ntohs(uint16_t);
uint32_t ntohl(uint32_t);

這些函式命名規範為(例如第一個):

host to net short (表示 16 位)

在主機位元組序和網路位元組序一致的情況下,這些函式會簡單的原樣返回傳遞給它們的引數。

Internet socket 地址

一個 IPv4 socket 地址會儲存在 sockaddr_in 結構體中,該結構在 <netinet/in.h> 中定義,具體如下:

struct in_addr{
    in_addr_t s_addr;           /* unsigned 32 bits address */
};
struct sockaddr_in{
    sa_family_t     sin_family; /* address family: AF_INET */
    in_port_t       sin_port;   /* unsigned 16 bits port */
    struct in_addr  sin_addr;   /* IPv4 address */
    unsigned char   __pad[X];   /* pad to size of 'sockaddr'(16 bytes) */
};

我們知道在網路中傳輸的資料肯定是二進位制資料。但是對於 ip 地址來說我們看到的是點分十進位制,那麼在網路程式設計時,我們就要把點分十進位制轉換成二進位制,幸運的是,庫函式已經有相應的函數了。

#include <arpa/inet.h>
int inet_pton(int domain, const char *src_str, void *addrptr);
    // Return 1 on successful, 0 if src_str is not in presentation format, or -1 on error
const char *inet_ntop(int domain, const void *addrptr, char *dst_str, size_t len);
    // Return pointer to des_str on success, or NULL on error

這些函式名中,p 表示 “展現” (presentation), n 表示 “網路”。展現形式是人類可讀的形式。
addrptr 都是指向 sockaddr_in 或 sockaddr_in6 結構(根據 AF_INET 或 AF_INET6)。

獨立於協議的主機和服務轉換

getaddrinfo() 函式將主機和服務名轉換成 ip 地址和埠號。getnameinfo() 是 getaddrinfo() 的逆函式

#include <sys/socket.h>
#include <netdb.h>

int getaddrinfo(const char *host, const char *service,
                const struct addrinfo *hints, struct addrinfo **result);

    //Return 0 on success, or nonzero on error

/* addrinfo 結構如下 */
struct addrinfo{
    int                 ai_flags;
    int                 ai_family;      // AF_INET or AF_INET6
    int                 ai_socktype;    // SOCK_STREAM or SOCK_DGRAM
    int                 ai_protocol;
    size_t              ai_addrlen;     // sizeof(sockaddr_in) or sizeof(sockaddr_in6)
    char               *ai_canonname;   // host name
    struct sockaddr     *ai_addr;       // pointer to sockaddr_in or sockaddr_in6
    struct addrinfo     *ai_next;       // pointer to next addrinfo
};

該函式會動態建立一個連結串列,連結串列中的元素是 addrinfo 結構體物件。返回的 *result 是指向連結串列頭部的指標

介紹一下 addrinfo 結構體中各個變數的含義:

  • ai_flags: 僅用於 hints 引數
  • ai_family: 地址族型別,可以是 AF_INET、AF_INET6、AF_UNIX,表示該 socket 地址結構的型別
  • ai_socktype: addrinfo 的物件作為返回引數時,只能是 SOCK_STREAMSOCK_DGRAM,前者表示用於 TCP,後者用於 UDP
  • ai_protocol: 該欄位會返回與地址族和 socket 型別匹配的協議值(ai_family、ai_socktype、ai_protocol 三個欄位為呼叫 socket() 建立該地址上的 socket 時所需的引數提供了取值)
  • ai_addrlen: 該欄位給出了 ai_addr 指向的 socket 地址結構的大小(位元組數)
  • ai_canonname: 該欄位僅由第一個 addrinfo 結構物件使用並且其前提是在 hints.ai_flags 中使用了 AI_CANONNAME 欄位
  • ai_addr: 指向 socket 地址結構物件
  • ai_next: 指向像一個 addrinfo 物件的指標

hints 引數:

hints 引數為如何選擇 getaddrinfo() 返回的 socket 地址結構指定了更多的標準。當用作 hints 引數時只能設定 addrinfo 結構的 ai_flags、ai_family、ai_socktype 以及 ai_protocol 欄位,其他不能使用的欄位必須置為 0 或 NULL

hints.ai_family 指定了返回的 socket 地址結構的域,其取值可以是 AF_INET 或 AF_INET6 或其他 AF_*(只要由相應的實現即可)。如果需要獲取全部域型別就令 hints.ai_family = AF_UNSPEC

hints.ai_socktype 欄位指定了使用返回的 socket 地址結構的 socket 型別。如果為 SOCK_DGRAM,那麼查詢將會在 UDP 服務上執行,如果是 SOCK_STREAM,那麼查詢會在 TCP 服務上進行。如果不限定某種特點服務,就令 hints.ai_socktype = 0

hints.ai_protocol 欄位為返回的地址結構選擇了 socket 協議。這個欄位一般總是為 0,表示接受任何協議

hints.ai_flags 欄位是一個位掩碼,它會改變 getaddrinfo 的行為


因為 getaddrinfo 會動態建立一個連結串列,那麼我們不再使用該連結串列時就應該主動地去釋放它。
用 freeaddrinfo 函式來釋放這個連結串列:

#include <sys/socket.h>
#include <netdb.h>

void freeaddrinfo(struct addrinfo *result);

C\S 示例(流 socket)

以下是伺服器程式碼:

is_seqnum.h

#ifndef __SOCK_IS_SEQNUM_H_
#define __SOCK_IS_SEQNUM_H_

#include <stdio.h>
#include <errno.h>
#include <sys/socket.h>
#include <signal.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#define PORT_NUM "10086"
#define INT_LEN 30


ssize_t readLine(int fd, char *buffer, size_t n);
void errExit(char *msg);

#endif // __SOCK_IS_SEQNUM_H_

readLine.c

#include <stdio.h>
#include <errno.h>
#include "is_seqnum.h"


void errExit(char *msg){
    printf("%s errno!\n",msg);
    exit(-1);
}
/*
 *  @parameter fd: file descriptor
 *  @parameter buffer: return argument 
 *  @parameter n: max size of buffer
 */
ssize_t readLine(int fd, char *buffer, size_t n){
    if(buffer == NULL || n <= 0){
        errno = EINVAL;
        exit(-1);
    }
    char *buf = buffer;
    size_t hasRead = 0;
    char ch;
    int readNum;

    for(;;){
        readNum = read(fd, &ch, 1);
        if(readNum == -1)
            errExit("read");
        if(0 == readNum){
            if(hasRead == 0)
                return 0;
            else//EOF
                break;
        }
        else{
            if(hasRead < n - 1){
                *buf++ = ch;
                hasRead++;
            }
            if('\n' == ch)
                break;
        }        
    }
    *buf = '\0';
    return hasRead;
}

is_seqnum_sv.c

#define _BSD_SOURCE
#include <netdb.h>
#include "is_seqnum.h"
#define BACKLOG 5

char ipbuf[100];
const char* myntop(struct sockaddr *addr, socklen_t len){
    if(len == sizeof(struct sockaddr_in)){
        return inet_ntop(AF_INET, (struct sockaddr *) addr, ipbuf, len);
    }
    else{
        return inet_ntop(AF_INET6, (struct sockaddr *)addr, ipbuf, len);
    }
}
int str2int(char *str){
    int res = 0;
    while(*str!='\0'){
        res = 10 * res + (*str - '0');
        str++;
    }
    return res;
}
int main(int argc, char *argv[]){
    int optval;
    size_t readnum;
    int cfd, lfd;
    struct addrinfo saddr, caddr, hints;
    struct addrinfo *rp, *result;
    struct sockaddr_storage claddr;
    socklen_t addrlen;
    unsigned int seqnum;
    char reqLenStr[INT_LEN];
    char seqNumStr[INT_LEN];
    char buf[100];
#define ADDRSTRLEN (NI_MAXHOST + NI_MAXSERV + 10)
    char addrStr[ADDRSTRLEN];
    char host[NI_MAXHOST];
    char service[NI_MAXSERV];
    int i = 0, reqLen;

    if(argc > 1 && strcmp(argv[1], "--help") == 0){
        fprintf(stdout, "%s [init-seq-num]\n", argv[0]);
        exit(0);
    }

    seqnum = (argc == 1) ? 0 : str2int(argv[1]);

    memset(&hints, 0, sizeof(struct addrinfo));
    hints.ai_socktype = SOCK_STREAM; // TCP service
    hints.ai_family = AF_UNSPEC; // allows IPv4 and IPv6
    hints.ai_flags = AI_PASSIVE | AI_NUMERICSERV;
    hints.ai_addr = NULL;
    hints.ai_canonname = NULL;
    hints.ai_next = NULL;

    if(0 != getaddrinfo(NULL, PORT_NUM, &hints, &result))
        errExit("getaddrinfo");

    for(rp = result; rp != NULL; rp = rp->ai_next){
        memset(buf, 0, sizeof(buf));
        printf("%d. rp->ai_family: %s\t rp->ai_socktype: %s\t", i++, rp->ai_family == AF_INET ? "AF_INET"
                : "AF_INET6", rp->ai_socktype == SOCK_STREAM ? "SOCK_STREAM" : "SOCK_DGRAM");
        if((lfd = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol)) == -1)
            continue;
        if(setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) == -1)
            errExit("setsockopt");
        
        printf("ip: %s\t\n", myntop(rp->ai_addr, rp->ai_addrlen));
        if(0 == bind(lfd, rp->ai_addr, rp->ai_addrlen))
            break; //success
        close(lfd);
    }

    if(rp == NULL)
        errExit("Could not bind socket to any address\n");

    if(-1 == listen(lfd, BACKLOG))
        errExit("listen");
    freeaddrinfo(result);

    for(;;){
        addrlen = sizeof(struct sockaddr_storage);
        cfd = accept(lfd, (struct sockaddr *) &claddr, &addrlen);
        if(-1 == cfd)
            errExit("accept");
        if(getnameinfo((struct sockaddr *) &claddr, addrlen, 
                    host, NI_MAXHOST, service, NI_MAXSERV, 0)==0)
            snprintf(addrStr, ADDRSTRLEN, "(%s, %s)", host, service);
        else
            snprintf(addrStr, ADDRSTRLEN, "(?UNKNOWN?)");
        printf("Connection from %s\n", addrStr);

        if(readLine(cfd, reqLenStr, INT_LEN) <= 0){
            close(cfd);
            continue;
        }
        reqLen = atoi(reqLenStr);
        if(reqLen <= 0){
            close(cfd);
            continue;
        }
        snprintf(seqNumStr, INT_LEN, "%d\n", seqnum);
        if(strlen(seqNumStr) != write(cfd, &seqNumStr, strlen(seqNumStr)))
            fprintf(stderr, "Error on write");
        seqnum += reqLen;

        if(close(cfd) == -1)
            errExit("close");
    }
}

最後可以用 telnet localhost 10086 命令進行測試:

codroc:~$ telnet localhost 10086
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
1
0
Connection closed by foreign host.
codroc:~$ telnet localhost 10086
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
11
1
Connection closed by foreign host.
codroc:~$ telnet localhost 10086
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
1
12