codeblock socket 编译错误_linux进程间通信--socket套接字(三)--多线程实现一个server对应多个client...

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

v2-3dc0b330d6e11655d294347a8cf96138_b.jpg

一 why

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

二 what

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

v2-769b31144599d569a7b98f391630de3b_b.jpg

实现原理如下:

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] exitn", cfd);
            break;
        } else {
            printf("now server recv : %sn", 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 : %sn", 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,"exitn")==0)
        {
            printf("client exited.n");
            break;
        }
        recv(client_fd, recvbuf, sizeof(recvbuf),0); ///接收
        printf("client receive: %sn", recvbuf);

        memset(sendbuf, 0, sizeof(sendbuf));
        memset(recvbuf, 0, sizeof(recvbuf));
    }

    close(client_fd);
    return 0;
}

四 test

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

v2-9ec9734cc7b61f9effc09572fe3ac963_b.png

v2-875ba45cf2b49f185d7079877363ebff_b.png

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

v2-0b67b7af2d0595cdf7aedad9ba2aefdd_b.png

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

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

v2-b61ba5fabcd76de1ad7f62a5696343aa_b.png

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

v2-1b812498609ec94d3565fa5ac326b030_b.jpg

我们在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] exitn", cfd);
            break;
        } else {
            printf("now server recv : %sn", 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 erroen");
        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
ret = setsockopt(server_sockfd, SOL_SOCKET, SO_REUSEADDR,
    &reuse, sizeof(reuse));
if (ret < 0) {
    perror("setsockopt erroen");
    return -1;
}

另外,我们考虑另一个问题,如果仍然是之前的代码,我们先ctrl+c退出客户端,然后在退出服务端,会不会出现同样的问题呢?神奇的是,竟然没有发生这种情况,这又是为什么呢?

退出之前,使用netstat -apn | grep 8890查看client和server的socket状态如下

A表示server listen状态,B表示server和client已经建立连接状态。C表示client和server已经建立连接状态

v2-bab1f6d75ad5b43613b3fa7674425cee_b.jpg

当我们先关闭client,然后在关闭server时候,但是client已经发送信号告诉了server,所以实际上server这个时候已经处于close转台了。两个client是处于TIME_WAIT状态,但是由于client的端口号是自动分配的,所以我们下次再启动server,然后再启动client,就不会出现bind: address already in use的状态。

v2-4c90e14946ae9f800777cec18d13064f_b.png

但是,如果我们先关闭server,然后再关闭client,因为此时server发送了FIN信号之后,没有等到client回复ACK信号,所以会处在一个TIME_WAIT状态中,如下:

v2-5a6e012cc5ba23b13551d61673b842f6_b.jpg

请注意最下面的server的状态,这个时候的server是处在TIME_WAIT,还没有处在close状态,这个时候,如果我们仍然用bind去绑定一个ip地址和端口号时,就会出现bind: address already in use.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值