多執行緒伺服器的實現

sinkinben發表於2021-02-24

本文基於 C 標準庫提供的網路通訊 API,使用 TCP ,實現一個簡單的多執行緒伺服器 Demo 。

首先要看 API,這是一項十分無聊的工作,我看的頭都暈了 ?️ 。

API

位元組序轉換

函式原型:

#include <arpa/inet.h>
uint64_t htonll(uint64_t hostlonglong);
uint32_t htonl(uint32_t hostlong);
uint16_t htons(uint16_t hostshort);
uint64_t ntohll(uint64_t netlonglong);
uint32_t ntohl(uint32_t netlong);
uint16_t ntohs(uint16_t netshort);

h 表示 host, n 表示 network,這些函式的作用是把主機的位元組序轉換為網路的位元組序(即小端到大端的轉變)。

例如:

#include <arpa/inet.h>
#include <stdio.h>
int main()
{
    uint32_t host = 0x01020304;     // high->low: 01 02 03 04
    uint32_t network = htonl(host); // high->low: 04 03 02 01
    printf("%p\n", network);     // 0x4030201
}

socket

函式原型:

#include <sys/socket.h>
int socket(int domain, int type, int protocol);

建立一個協議族為 domain, 協議型別為 type, 協議編號為 protocol 的套接字檔案描述符。如果函式呼叫成功,會返回一個標識這個套接字的檔案描述符,失敗的時候返回-1。

domain 的取值:

Name                Purpose                          Man page
AF_UNIX, AF_LOCAL   Local communication              unix(7)
AF_INET             IPv4 Internet protocols          ip(7)
AF_INET6            IPv6 Internet protocols          ipv6(7)
AF_IPX              IPX - Novell protocols
AF_NETLINK          Kernel user interface device     netlink(7)
AF_X25              ITU-T X.25 / ISO-8208 protocol   x25(7)
AF_AX25             Amateur radio AX.25 protocol
AF_ATMPVC           Access to raw ATM PVCs
AF_APPLETALK        AppleTalk                        ddp(7)
AF_PACKET           Low level packet interface       packet(7)
AF_ALG              Interface to kernel crypto API

AF 是 Address Family 的縮寫,INET 是 Internet 的縮寫。某些地方可能會使用 PF,即 Protocol Family,應該是同一個東西。

type 的取值:

SOCK_STREAM     Provides sequenced, reliable, two-way, connection-based byte streams.  An out-of-band data transmission mechanism may be supported.

SOCK_DGRAM      Supports datagrams (connectionless, unreliable messages of a fixed maximum length).

SOCK_SEQPACKET  Provides a sequenced, reliable, two-way connection-based data transmission path for datagrams of fixed maximum length; a consumer is required to read an entire packet with each input system call.

SOCK_RAW        Provides raw network protocol access.

SOCK_RDM        Provides a reliable datagram layer that does not guarantee ordering.

SOCK_PACKET     Obsolete and should not be used in new programs; see packet(7).

type 常用的是 STREAMDGRAM ,根據描述,可以確定前者對應 TCP,而後者對應 UDP :

  • SOCK_STREAM 套接字表示一個雙向的位元組流,與管道類似。流式的套接字在進行資料收發之前必須已經連線,連線使用 connect() 函式進行。一旦連線,可以使用 read() 或者 write() 函式進行資料的傳輸,流式通訊方式保證資料不會丟失或者重複接收。
  • SOCK_DGRAMSOCK_RAW 這個兩種套接字可以使用函式 sendto() 來傳送資料,使用 recvfrom() 函式接受資料,recvfrom() 接受來自制定IP地址的傳送方的資料。

對於第 3 個引數 protocal,用於指定某個協議的特定型別,即 type 型別中的某個型別。通常某協議中只有一種特定型別,這 樣protocol 引數僅能設定為 0 ;但是有些協議有多種特定的型別,就需要設定這個引數來選擇特定的型別。

bind

函式原型:

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

如果函式執行成功,返回值為 0,否則為 SOCKET_ERROR

引數:

  • sockfd 是一個有效的 socket 描述符(函式 socket() 的有效返回值)。
  • addrlen 是第二個引數 addr 結構體的長度。
  • addr 是一個 sockaddr 結構體指標,包含 IP 和埠等資訊。

sockaddr 的結構如下:

struct sockaddr {
    sa_family_t sa_family;
    char sa_data[14];
};
// sa_familt_t 是無符號整型,Ubuntu 下是 unsigned short int

sockaddr 的存在是為了統一地址結構的表示方法 ,統一介面函式,使得不同的地址結構可以被 bind(), connect(), recvfrom(), sendto() 等函式呼叫。但一般的程式設計中並不直接對此資料結構進行操作,而使用另一個與之等價的資料結構 sockaddr_in :

struct sockaddr_in {
    short int sin_family;        /* Address family */
    unsigned short int sin_port; /* Port number */
    struct in_addr sin_addr;     /* Internet address */
    unsigned char sin_zero[8];   /* Same size as struct sockaddr */
};

各欄位解析:

  • sin_family :指代協議族,在 socket 程式設計中有 3 個取值 AF_INET, AF_INET6, AF_UNSPEC .
  • sin_port :儲存埠號(使用網路位元組順序)
  • sin_addr :儲存IP地址,使用 in_addr 這個資料結構
  • sin_zero :是為了讓 sockaddrsockaddr_in 兩個資料結構保持大小相同而保留的空位元組。

in_addr 的結構如下:

typedef uint32_t in_addr_t;
struct in_addr{
    in_addr_t s_addr;
};

太陰間了。

listen

int listen(int sockfd, int backlog);

返回值:無錯誤,返回 0,否則 -1 。

作用:listen 函式使用主動連線套接字變為被連線套介面,使得一個程式可以接受其它程式的請求,從而成為一個伺服器程式。在 TCP 伺服器程式設計中 listen 函式把程式變為一個伺服器,並指定相應的套接字變為被動連線。

listen 函式一般在呼叫 bind 之後,呼叫 accept 之前呼叫。

backlog 引數指定連線請求佇列的最大個數。

accept

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

接受連線請求,成功返回一個新的套接字描述符 newfd ,失敗返回-1。返回值 newfd 與引數 sockfd 是不同的,newfd 專門用於與客戶端的通訊,而 sockfd 是專門用於 listen 的 socket 。

addraddrlen 都是指標,用於接收來自客戶端的 addr 的資訊。

inet_addr

函式原型:

in_addr_t inet_addr(const char *cp);

將一個點分十進位制的 IP 字串轉換為網路位元組序的 uint32_t

例子

int main()
{
    const char *ip = "127.0.0.1";  // 7f.00.00.01
    printf("%p\n", inet_addr(ip)); // 0x0100007f
}

send

#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);
ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

其中 send(fd, buf, len, flags)sendto(fd, buf, len, flags, NULL, 0) 等價。

recv

#include <sys/types.h>
#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

其中 recv(fd, buf, len, flags)recvfrom(fd, buf, len, flags, NULL, 0) 等價。

connect

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

成功返回 0 ,失敗返回 -1 。

sockfd 是客戶端程式建立的,用於與服務端通訊的 socket ; addr 是目標伺服器的 IP 地址和埠。

多執行緒伺服器

本次實現的場景如下:

  • 客戶端可以具有多個,客戶端主動連線伺服器,允許每個客戶端傳送 msg 到伺服器,並接受來自伺服器的資訊。
  • 服務端對於每個申請連線到客戶端,建立一個執行緒處理請求。對於客戶端傳送過來的 msg,然後伺服器把 msg 加上一些其他字串,傳送回客戶端。

server

#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <string.h>
#include <unistd.h>
#define PORT 8887
#define QUEUE 10
const char *pattern = "Hello, I am the server. Your msg is received, which is: %s";

typedef struct
{
    struct sockaddr_in addr;
    socklen_t addr_len;
    int connectfd;
} thread_args;

void *handle_thread(void *arg)
{
    thread_args *targs = (thread_args *)arg;
    pthread_t tid = pthread_self();
    printf("tid = %u and socket = %d\n", tid, targs->connectfd);
    char send_buf[BUFSIZ] = {0}, recv_buf[BUFSIZ] = {0};
    while (1)
    {
        int len = recv(targs->connectfd, recv_buf, BUFSIZ, 0);
        printf("[Client %d] %s", targs->connectfd, recv_buf);
        
        if (strcmp("q\n", recv_buf) == 0)
            break;
        
        sprintf(send_buf, pattern, recv_buf);
        send(targs->connectfd, send_buf, strlen(send_buf), 0);

        memset(send_buf, 0, BUFSIZ), memset(recv_buf, 0, BUFSIZ);
    }
    close(targs->connectfd);
    free(targs);
    pthread_exit(NULL);
}

int main()
{
    int listenfd = socket(AF_INET, SOCK_STREAM, 0);
    printf("server is listening at socket fd = %d\n", listenfd);
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = htonl(INADDR_ANY);

    if (bind(listenfd, (struct sockaddr *)&addr, sizeof(addr)) == -1)
    {
        perror("bind error\n");
        exit(-1);
    }

    if (listen(listenfd, QUEUE) == -1)
    {
        perror("listen error\n");
        exit(-1);
    }

    while (1)
    {
        thread_args *targs = malloc(sizeof(thread_args));
        targs->connectfd = accept(listenfd, (struct sockaddr *)&targs->addr, &targs->addr_len);
        // int newfd = accept(sockfd, NULL, NULL);
        pthread_t tid;
        pthread_create(&tid, NULL, handle_thread, (void *)targs);
        pthread_detach(tid);
    }
    close(listenfd);
}

client

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

#define PORT 8887
const char *target_ip = "127.0.0.1";

int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    printf("client socket = %d\n", sockfd);
    struct sockaddr_in addr;
    addr.sin_family = AF_INET;
    addr.sin_port = htons(PORT);
    addr.sin_addr.s_addr = inet_addr(target_ip);

    if (connect(sockfd, (struct sockaddr *)&addr, sizeof(struct sockaddr_in)) < 0)
    {
        perror("connect error\n");
        exit(-1);
    }

    char send_buf[BUFSIZ], recv_buf[BUFSIZ];
    while (fgets(send_buf, BUFSIZ, stdin) != NULL)
    {
        if (strcmp(send_buf, "q\n") == 0)
            break;

        send(sockfd, send_buf, strlen(send_buf), 0);
        printf("[Client] %s\n", send_buf);

        recv(sockfd, recv_buf, BUFSIZ, 0);
        printf("[Server] %s\n", recv_buf);

        memset(send_buf, 0, BUFSIZ), memset(recv_buf, 0, BUFSIZ);
    }
    close(sockfd);
    exit(0);
}

執行結果

編譯:

gcc server.c -o server -lpthread
gcc client.c -o client

先執行 server,後執行多個 client .

多執行緒伺服器的實現

需要注意的是,這裡的伺服器,客戶端都是執行在同一機器上的,所以客戶端使用的目標 IP 是 127.0.0.1 ,如果想進一步更全面地測試,應該把服務端執行在一個雲伺服器上,然後開放 8887 埠,再進行測試。

相關文章