手把手带你实现多线程服务器

打印 上一主题 下一主题

主题 1016|帖子 1016|积分 3048

前言

由于资源消耗大、线程管理复杂、性能题目等原因,“一哀求一线程”(多线程)已经被淘汰,现在我们多使用select、poll、epoll等IO多路复用技能实现网络IO。我们只讲解实现思路,而且为后面IO多路复用技能的文章打下基础。在看本文之前,可以先看一看 网络IO以及socket编程 这篇文章。
socket使用流程

在网络通信中,一定是使用socket。下面初始化socket的固定流程。
  1. #include <netinet/in.h>
  2. #include <stdio.h>
  3. #include <sys/socket.h>
  4. #include <errno.h>
  5. #include <string.h>
  6. int main()
  7. {
  8.     int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  9.     struct sockaddr_in sevaddr;
  10.     sevaddr.sin_family = AF_INET;
  11.     sevaddr.sin_addr.s_addr = htonl(INADDR_ANY);
  12.     sevaddr.sin_port = htons(2000);
  13.     if (bind(sockfd, (struct sockaddr*)&sevaddr, sizeof(struct sockaddr)) == -1) {
  14.         printf("bind failed: %s\n",strerror(errno));
  15.         return -1;
  16.     }
  17.     listen(sockfd, 10);
  18.         printf("listen finished\n");
  19.     getchar(); // 阻塞在这里
  20.     printf("exit\n");
  21.     return 0;
  22. }
复制代码
实现思路

使用socket()创建一个sockfd,用于监听端口。第一个参数AF_INET表示使用IPV4协议族;第二个参数SOCK_STREAM创建流式套接字类型,即TCP;第三个参数0对于 SOCK_STREAM 类型的套接字,通常设置为0,表示使用默认的传输层协议。
  1. int sockfd = socket(AF_INET, SOCK_STREAM, 0);
复制代码
使用bind()将创建的sockfd与本机端口举行绑定。看下面的代码,我们要给本地端口指定通信协议族、初始化网卡地址、初始化端标语、绑定fd和本机端口。INADDR_ANY,即0.0.0.0,指本机任意网卡;在计算机中,0~1023是默认的端标语,所以我们要指定的端口后须大于1023,这里我们给定的端标语是2000;接下来,我们使用bind()绑定,若函数返回-1,则绑定失败。
  1. struct sockaddr_in sevaddr;
  2. sevaddr.sin_family = AF_INET; //IPV4
  3. sevaddr.sin_addr.s_addr = htonl(INADDR_ANY); //0.0.0.0
  4. sevaddr.sin_port = htons(2000); // 0-1023
  5. if (bind(sockfd, (struct sockaddr*)&sevaddr, sizeof(struct sockaddr)) == -1) {
  6.        printf("bind failed: %s\n",strerror(errno));
  7.        return -1;
  8.     }
复制代码
完成上面的步调,我们就可以对端口举行监听了。
  1. listen(sockfd, 10);
复制代码
我们可以先对步伐编译运行,我们可以看到受getchar()的影响,步伐处于阻塞状态。

我们敲一下回车,步伐退出。

现在,我们再打开一个新的终端,输入下令netstat -anop | grep 2000查看端口2000的使用情况。我们可以看到端口2000已经被使用。

或者,我们可以在第二个终端中运行该步伐。编译器返回bind failed: Address already in use,很好,这表明端口已经被占用,这是我们想要看到的结果。

netassist

在这里,向大家安利netassist(网络调试助手),作为我们的客户端。这里我直接使用127.0.0.1(本地回环地址)作为我的远程主机地址。

现在,我们连接已经运行的步伐。可以看到,连接乐成了!

因为listen实行乐成而且客户端已经连接,现在我们通过netstat查看端口状态。可以看到,产生了一个新的连接状态。

既然已经连接上了,那么是否可以发数据?固然可以了!
点击“发送”,看到netassist的右下角的TX从0变为20,表示发送了20字节的数据到server端。可是,我们在server端并没有看到发送的数据,可以确定的是server已经接收到了信息。因为,server端临时不能将接收到的数据打印出来。

小总结


  • 端口被绑定后,不能再次绑定。
  • 实行listen,可以通过netstat查看IO状态。
  • 进入listen可以被连接,而且产生新连接状态。
  • io与tcp连接。P6中,每天生一条信息代表中一个IO。我们现在没有给它分配IO,我们须要使用accept。
第一版服务器

我们要解决几个已知的题目:

  • 为客户端分配IO。
  • 服务器收发数据。
实现思路

  1. #include <errno.h>#include <netinet/in.h>#include <stdio.h>#include <string.h>#include <sys/socket.h>int main(){    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  2.     struct sockaddr_in sevaddr;    sevaddr.sin_family = AF_INET;    sevaddr.sin_addr.s_addr = htonl(INADDR_ANY);    sevaddr.sin_port = htons(2000);    if (bind(sockfd, (struct sockaddr*)&sevaddr, sizeof(struct sockaddr)) == -1) {        printf("bind failed: %s\n", strerror(errno));        return -1;    }    listen(sockfd, 10);
  3.     printf("listen finished: %d\n",sockfd);    struct sockaddr clientaddr;    socklen_t len = sizeof(clientaddr);    printf("accept:\n");    int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);    printf("accept finished\n");    char buffer[1024] = { 0 };    int count = recv(clientfd, buffer, 1024, 0);    printf("RECV: %s\n", buffer);    count = send(clientfd, buffer, 1024, 0);    printf("SEND: %d\n", count);    getchar();    printf("exit\n");    return 0;}
复制代码
我们使用accept函数等候客户端的连接,使用recv接收客户端发送的数据。
我们先编译运行这段代码。

现在可以看到,不管我们敲击多少次回车,步伐依旧处于阻塞的状态。那么步伐阻塞在那里呢?
真相就是——int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);,现在步伐等候着客户端的连接。
我们现在举行连接。可以看到,步伐已经跑出了accept的阻塞。现在收到来自recv的阻塞。
当步伐收到来自accept的阻塞时,我们可以看到tcp连接状态只有一个。
当我们连接server后(阻塞在recv处),tcp连接状态会出现两个。

这表明了,代码中实现的两个fd(sockfd和clientfd)与tcp连接信息是一对一的关系,百万级就有百万个。
出现的题目

当我们打开两个netassist(两个客户端)并连接时,是可以连接的而且产生了新的连接信息。可是,由于只有一对sockfd和clientfd,新的客户端并不能举行通信。
第二版服务器

既然要连接多个客户端,是否可以加进循环?可以。
  1. while(1){
  2.         printf("accept:\n");
  3.         int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
  4.         printf("accept finished\n");
  5.         char buffer[1024] = { 0 };
  6.         int count = recv(clientfd, buffer, 1024, 0);
  7.         printf("RECV: %s\n", buffer);
  8.         count = send(clientfd, buffer, 1024, 0);
  9.         printf("SEND: %d\n", count);
  10.     }
复制代码
现在,我们运行后,打开三个客户端,依次(按照1,2,3的次序)举行连接而且发送。

可是这里有致命的题目!当我们依次(按照1,2,3的次序)连接不发送时,这时我们必须按照1,2,3的次序才气发送乐成。这样就酿成了只要前一个不接收到数据,后面的都接收不到。
原因很简朴,就是代码逻辑的题目。当我们依次点击连接后,第一个已经实行完accept函数,而后面两个阻塞在accept处且已经建立连接,但是并没有实行accept。第一个客户端阻塞在recv处,现在只有第一个发送数据并接收回发数据,后面的才气举行。这就扳连到了多路IO(多个客户端)怎么及时响应。
第三版服务器

使用线程

为了避免阻塞在recv处,我们为每一个客户端开一个线程。
  1. void* client_pthread(void* arg)
  2. {
  3.     int clientfd = *(int*)arg;
  4.     // 这里的循环是为了让同一个客户端收发多次数据
  5.     while (1) {
  6.         char buffer[1024] = { 0 };
  7.         int count = recv(clientfd, buffer, 1024, 0);
  8.         printf("RECV: %s\n", buffer);
  9.         count = send(clientfd, buffer, count, 0);
  10.         printf("SEND: %d\n", count);
  11.     }
  12. }
  13. ......
  14. while (1) {
  15.         printf("accept\n");
  16.         int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
  17.         printf("accept finished: %d\n", clientfd);
  18.         pthread_t thid;
  19.         pthread_create(&thid, NULL, client_pthread, &clientfd);
  20.     }
  21. ......
复制代码
这样我们就可以解决recv阻塞的题目了。

解决题目

上面的代码已经可以完成多个客户端独立地发送接收数据,可是还不敷完善。当我们断开连接时,步伐会不停跑,而且CPU占用率极高。

出现这个征象的原因,是因为我们没有处理客户端断开的代码。现在我们仔细看上图,断开之后server返回的count值为0,证实当我们断开时,recv返回0。现在我们加入断开处理代码。
  1. void* client_pthread(void* arg)
  2. {
  3.     int clientfd = *(int*)arg;
  4.     // 这里的循环是为了让同一个客户端收发多次数据
  5.     while (1) {
  6.         char buffer[1024] = { 0 };
  7.         int count = recv(clientfd, buffer, 1024, 0);
  8.         printf("RECV: %s\n", buffer);
  9.         if (count == 0) { // disconnect
  10.             printf("client disconnect: %d\n", clientfd);
  11.             close(clientfd);
  12.             break;
  13.         }
  14.         count = send(clientfd, buffer, count, 0);
  15.         printf("SEND: %d\n", count);
  16.     }
  17. }
复制代码
运行一下:

可以看到已经可以正常地断开连接,而且返回对应的clientfd。
关于fd

从上图可以知道,fd的值是递增的。当步伐从入口函数main进入时,遇到的第一个fd(sockfd),给它赋值为3。哎,0、1、2在那里呢?来看下图,当我们实行ls /dev/fd时,可以看到0、1、2被占用,再依次实行ls /dev/stdin -l、ls /dev/stdout -l、ls /dev/stderr -l,可以看到0、1、2分别被标准输入、标准输出、标准错误输出占用;而3被网络连接占用。

那么,不停递增数量是否是有限的呢?答案是肯定的。运行ulimit -a看一下,可以看到open files的数量是有限的,所以,IO的数量也是有限的。IO也是fd。这里就可以明确Linux 一切皆文件(Linux操纵全部设备一切皆文件形貌符)了吧。操纵文件是文件,操纵socket是文件,操纵定时器也是文件。

现在再来观察一个征象,当我们断开clientfd后,又再次建立连接,体系会分配一个新的fd值:7,4被接纳了;我们现在又断开,等一段时间又连接,可以看到体系又分配了4。那又须要等候多长时间呢?等候TIME_WAIT,而这个时间是可以设置的。

总结

至此,“一哀求一线程”方式的server已经实现完成。在开篇我们提到这种形式的server已经被淘汰,它有优点也有缺点。
优点:实现简朴。
缺点:不利于并发。线程的并发量就等于客户端的数量,大大占用了资源。
这样我们就引入了IO多路复用。(后面会写
附件

  1. #include <errno.h>#include <netinet/in.h>#include <pthread.h>#include <stdio.h>#include <string.h>#include <sys/socket.h>#include <unistd.h>void* client_pthread(void* arg)
  2. {
  3.     int clientfd = *(int*)arg;
  4.     // 这里的循环是为了让同一个客户端收发多次数据
  5.     while (1) {
  6.         char buffer[1024] = { 0 };
  7.         int count = recv(clientfd, buffer, 1024, 0);
  8.         printf("RECV: %s\n", buffer);
  9.         if (count == 0) { // disconnect
  10.             printf("client disconnect: %d\n", clientfd);
  11.             close(clientfd);
  12.             break;
  13.         }
  14.         count = send(clientfd, buffer, count, 0);
  15.         printf("SEND: %d\n", count);
  16.     }
  17. }
  18. int main(){    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
  19.     struct sockaddr_in servaddr;    servaddr.sin_family = AF_INET;    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);    servaddr.sin_port = htons(2000);    if (bind(sockfd, (struct sockaddr*)&servaddr, sizeof(struct sockaddr)) == -1) {        printf("bind failed: %s\n", strerror(errno));        return -1;    }    listen(sockfd, 10);
  20.     printf("listen finished: %d\n", sockfd);    struct sockaddr_in clientaddr;    socklen_t len = sizeof(clientaddr);//第一版#if 0    printf("accept\n");    int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);    printf("accept finished\n");    char buffer[1024] = { 0 };    int count = recv(clientfd, buffer, 1024, 0);    printf("RECV: %s\n", buffer);    send(clientfd, buffer, count, 0);    printf("SEND: %d\n", count);//第二版#elif 0    while (1) {        printf("accept\n");        int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);        printf("accept finished\n");        char buffer[1024] = { 0 };        int count = recv(clientfd, buffer, 1024, 0);        printf("RECV: %s\n", buffer);        send(clientfd, buffer, count, 0);        printf("SEND: %d\n", count);    }//第三版#else    while (1) {        printf("accept\n");        int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);        printf("accept finished: %d\n", clientfd);        pthread_t thid;        pthread_create(&thid, NULL, client_pthread, &clientfd);    }#endif    getchar();    printf("exit\n");    return 0;}
复制代码
(欢迎勘误)

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

圆咕噜咕噜

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表