相关文章推荐
完美的鸵鸟  ·  使用SO_REUSEADDR和SO_EXCL ...·  6 天前    · 
考研的大脸猫  ·  Socket ...·  6 天前    · 
温柔的西瓜  ·  C#教程推荐 - 知乎·  1 年前    · 

linux进程间通信--socket套接字(三)--多线程实现一个server对应多个client

先给自己打个广告,本人的微信公众号正式上线了,搜索:张笑生的地盘,主要关注嵌入式软件开发,股票基金定投,足球等等,希望大家多多关注,有问题可以直接留言给我,一定尽心尽力回答大家的问题

一 why

一般地,socket server端会对接多个client,在server端需要支持连接多个client,并进行数据交互,在《linux进程间通信—本地socket套接字(二)—多进程实现一个server对应多个client》中,我们采样了多进程法来实现。其实,我们也可以采用多线程法来实现

二 what

那么,我们如何利用多线程实现一个server对接多个client呢?我们知道,每次server接收到client的连接请求是通过accept函数实现的,这个函数返回值为client的文件描述符,因此每次server接收到一个client的连接请求,就创建一个子线程,用于和这个client建立数据交互,如下如所示

实现原理如下:

1. server端有一个主线程,只用于接收client端的连接请求,每接收到一次连接请求,就创建一个子线程,这个子线程用来实现和client的数据交互。

2. 子线程用来实现和client端进行数据交互。

三 how

server.c代码

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/shm.h>
#define PORT  8890
#define QUEUE_SIZE   10
#define BUFFER_SIZE 1024
void *do_communication(void *arg)
    char buf[BUFFER_SIZE] = {0};
    int cfd = *(int *)arg;
    int recvlen;
    while (1) {
        memset(buf, 0, sizeof(buf));
        recvlen = read(cfd, buf, sizeof(buf));
        if (recvlen < 0) {
            perror("recv fail");
            break;
        } else if (recvlen ==0) {
            printf("client[%d] exit\n", cfd);
            break;
        } else {
            printf("now server recv : %s\n", buf);
            write(cfd, buf, recvlen);
    close(cfd);
    return NULL;
int main(int argc, char **argv)
    struct sockaddr_in server_sockaddr, client_addr;
    socklen_t length = sizeof(client_addr);
    char str[16];
    pthread_t pid;
    int server_sockfd, cfd;
    int ret = 0;
    int reuse = 1;
    //定义IPV4的TCP连接的套接字描述符
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sockfd < 0) {
        perror("socket() fail!\n");
        return -1;
    //定义sockaddr_in
    memset(&server_sockaddr, 0, sizeof(server_sockaddr));
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_sockaddr.sin_port = htons(PORT);
    //bind成功返回0,出错返回-1
    ret = bind(server_sockfd, (struct sockaddr *)&server_sockaddr,
        sizeof(server_sockaddr));
    if(ret < 0) {
        perror("bind");
        return -1;//1为异常退出
    printf("bind success.\n");
    //listen成功返回0,出错返回-1,允许同时帧听的连接数为QUEUE_SIZE
    ret = listen(server_sockfd, QUEUE_SIZE);
    if(ret < 0) {
        perror("listen");
        return -1;
    printf("listen success.\n");
    while(1) {
        //进程阻塞在accept上,成功返回非负描述字,出错返回-1
        cfd = accept(server_sockfd, (struct sockaddr*)&client_addr,&length);
        if(cfd < 0) {
            perror("connect");
            return -1;
        printf("new client accepted, ip : %s, port : %d.\n",
            inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)),
            ntohs(client_addr.sin_port));
        ret = pthread_create(&pid, NULL, do_communication, (void *)&cfd);
        if (ret < 0) {
            perror("pthread_create fail");
            return -1;
        pthread_detach(pid); //线程回收,线程结束之后自动回收
    printf("closed.\n");
    close(server_sockfd);
    return 0;
}

client.c代码

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/shm.h>
#define SERVER_PORT  8890
#define BUFFER_SIZE 1024
#define CLIENT_IP_ADDR "127.0.0.1"
int main(int argc, char **argv)
    struct sockaddr_in servaddr;
    char sendbuf[BUFFER_SIZE] = {0};
    char recvbuf[BUFFER_SIZE] = {0};
    char str[16];
    int client_fd;
    //定义IPV4的TCP连接的套接字描述符
    client_fd = socket(AF_INET,SOCK_STREAM, 0);
    //set sockaddr_in
    memset(&servaddr, 0, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = inet_addr(CLIENT_IP_ADDR);
    servaddr.sin_port = htons(SERVER_PORT);  //服务器端口
    printf("ip addr : %s\n", CLIENT_IP_ADDR);
    //连接服务器,成功返回0,错误返回-1
    if (connect(client_fd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
        perror("connect");
        exit(1);
    printf("connect server(IP:%s).\n",
        inet_ntop(AF_INET, &servaddr.sin_addr, str, sizeof(str)));
    //客户端将控制台输入的信息发送给服务器端,服务器原样返回信息
    while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        send(client_fd, sendbuf, strlen(sendbuf),0); ///发送
        if(strcmp(sendbuf,"exit\n")==0)
            printf("client exited.\n");
            break;
        recv(client_fd, recvbuf, sizeof(recvbuf),0); ///接收
        printf("client receive: %s\n", recvbuf);
        memset(sendbuf, 0, sizeof(sendbuf));
        memset(recvbuf, 0, sizeof(recvbuf));
    close(client_fd);
    return 0;
}

四 test

编译,因为需要使用pthread_create,所以编译时需要指定参数-lpthread。

再起一个client,可以发现server端检测到两个client,两者的port端口号不一样。

经过测试,我们发现一个bug,现象是:

  1. 启动server
  2. 启动client1和client2
  3. server和client1,2数据传输
  4. ctrl+c关闭server(注意我们先关闭了server)
  5. ctrl+c关闭client
  6. 再次重新启动server,发现提示

在分析这个问题之前,先插入一个知识,TCP传输分层结构入戏下:

我们在server中使用bind函数,将ip地址和port端口号关联在一起,使用通配符地址(INADDR_ANY),它允许任何接口为到来的连接所使用。

但是使用bind绑定ip地址和port端口号时,可能绑定一个已经存在的端口号,虽然此时不存在活动的socket,但是由于socket存在的TIME_WAIT机制,该端口号状态在套接字关闭后约保留 2 到 4 分钟。在 TIME_WAIT 状态退出之后,套接字被删除,该地址才能被重新绑定而不出问题。

等待 TIME_WAIT 结束是一件令人恼火的事,特别是如果您正在开发一个套接字服务器,就需要停止服务器来做一些改动,然后重启。幸运的是,有方法可以避开 TIME_WAIT 状态。可以给套接字应用 SO_REUSEADDR 套接字选项,以便端口可以马上重用。新的server端程序如下:

#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/shm.h>
#define SERVER_PORT  8890
#define QUEUE_SIZE   10
#define BUFFER_SIZE 1024
void *do_communication(void *arg)
    char buf[BUFFER_SIZE] = {0};
    int cfd = *(int *)arg;
    int recvlen;
    while (1) {
        memset(buf, 0, sizeof(buf));
        recvlen = read(cfd, buf, sizeof(buf));
        if (recvlen < 0) {
            perror("recv fail");
            break;
        } else if (recvlen ==0) {
            printf("client[%d] exit\n", cfd);
            break;
        } else {
            printf("now server recv : %s\n", buf);
            write(cfd, buf, recvlen);
    close(cfd);
    return NULL;
int main(int argc, char **argv)
    struct sockaddr_in server_sockaddr, client_addr;
    socklen_t length = sizeof(client_addr);
    char str[16];
    pthread_t pid;
    int server_sockfd, cfd;
    int ret = 0;
    int reuse = 1;
    //定义IPV4的TCP连接的套接字描述符
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (server_sockfd < 0) {
        perror("socket() fail!\n");
        return -1;
    //使能可以重新使用addr
    ret = setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR,
        &reuse, sizeof(reuse));
    if (ret < 0) {
        perror("setsockopt erroe\n");
        return -1;
    //定义sockaddr_in
    memset(&server_sockaddr, 0, sizeof(server_sockaddr));
    server_sockaddr.sin_family = AF_INET;
    server_sockaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_sockaddr.sin_port = htons(SERVER_PORT);
    //bind成功返回0,出错返回-1
    ret = bind(server_sockfd, (struct sockaddr *)&server_sockaddr,
        sizeof(server_sockaddr));
    if(ret < 0) {
        perror("bind");
        return -1;//1为异常退出
    printf("bind success.\n");
    //listen成功返回0,出错返回-1,允许同时帧听的连接数为QUEUE_SIZE
    ret = listen(server_sockfd, QUEUE_SIZE);
    if(ret < 0) {
        perror("listen");
        return -1;
    printf("listen success.\n");
    while(1) {
        //进程阻塞在accept上,成功返回非负描述字,出错返回-1
        cfd = accept(server_sockfd, (struct sockaddr*)&client_addr,&length);
        if(cfd < 0) {
            perror("connect");
            return -1;
        printf("new client accepted, client_ip : %s, client_port : %d.\n",
            inet_ntop(AF_INET, &client_addr.sin_addr, str, sizeof(str)),
            ntohs(client_addr.sin_port));
        ret = pthread_create(&pid, NULL, do_communication, (void *)&cfd);
        if (ret < 0) {
            perror("pthread_create fail");
            return -1;
        pthread_detach(pid); //线程回收,线程结束之后自动回收
    printf("closed.\n");
    close(server_sockfd);
    return 0;
}

请关注,如下代码片段,这段代码就是实现了重新使用port端口号。这样设置只是为了实现我们方便快速的调试代码,正式版的server,我们不建议这么做。

//使能可以重新使用addr