圆咕噜咕噜 发表于 2024-12-27 23:52:13

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

前言

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

在网络通信中,一定是使用socket。下面初始化socket的固定流程。
#include <netinet/in.h>
#include <stdio.h>
#include <sys/socket.h>
#include <errno.h>
#include <string.h>


int main()
{
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);


    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);

        printf("listen finished\n");
    getchar(); // 阻塞在这里
    printf("exit\n");
    return 0;
}
实现思路

使用socket()创建一个sockfd,用于监听端口。第一个参数AF_INET表示使用IPV4协议族;第二个参数SOCK_STREAM创建流式套接字类型,即TCP;第三个参数0对于 SOCK_STREAM 类型的套接字,通常设置为0,表示使用默认的传输层协议。
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
使用bind()将创建的sockfd与本机端口举行绑定。看下面的代码,我们要给本地端口指定通信协议族、初始化网卡地址、初始化端标语、绑定fd和本机端口。INADDR_ANY,即0.0.0.0,指本机任意网卡;在计算机中,0~1023是默认的端标语,所以我们要指定的端口后须大于1023,这里我们给定的端标语是2000;接下来,我们使用bind()绑定,若函数返回-1,则绑定失败。
struct sockaddr_in sevaddr;
sevaddr.sin_family = AF_INET; //IPV4
sevaddr.sin_addr.s_addr = htonl(INADDR_ANY); //0.0.0.0
sevaddr.sin_port = htons(2000); // 0-1023
if (bind(sockfd, (struct sockaddr*)&sevaddr, sizeof(struct sockaddr)) == -1) {
       printf("bind failed: %s\n",strerror(errno));
       return -1;
    }
完成上面的步调,我们就可以对端口举行监听了。
listen(sockfd, 10);
我们可以先对步伐编译运行,我们可以看到受getchar()的影响,步伐处于阻塞状态。
https://i-blog.csdnimg.cn/direct/0d1c55b8e35b41dab786d570d566e204.png
我们敲一下回车,步伐退出。
https://i-blog.csdnimg.cn/direct/69e20d56b3984aca9ad08a8273f4208e.png
现在,我们再打开一个新的终端,输入下令netstat -anop | grep 2000查看端口2000的使用情况。我们可以看到端口2000已经被使用。
https://i-blog.csdnimg.cn/direct/6d88eabe74d8421cb33af337118959bc.png
或者,我们可以在第二个终端中运行该步伐。编译器返回bind failed: Address already in use,很好,这表明端口已经被占用,这是我们想要看到的结果。
https://i-blog.csdnimg.cn/direct/4730ac4c84fe49e6899bf4024e146ed1.png
netassist

在这里,向大家安利netassist(网络调试助手),作为我们的客户端。这里我直接使用127.0.0.1(本地回环地址)作为我的远程主机地址。
https://i-blog.csdnimg.cn/direct/21b3bebb64d74300b0bdfc329539b268.png
现在,我们连接已经运行的步伐。可以看到,连接乐成了!
https://i-blog.csdnimg.cn/direct/74158a255b0249b68ea70ce55c9ba240.png
因为listen实行乐成而且客户端已经连接,现在我们通过netstat查看端口状态。可以看到,产生了一个新的连接状态。
https://i-blog.csdnimg.cn/direct/17272ab63e1a4c04801ef23fe3b4482c.png
既然已经连接上了,那么是否可以发数据?固然可以了!
点击“发送”,看到netassist的右下角的TX从0变为20,表示发送了20字节的数据到server端。可是,我们在server端并没有看到发送的数据,可以确定的是server已经接收到了信息。因为,server端临时不能将接收到的数据打印出来。
https://i-blog.csdnimg.cn/direct/b59a06b416fb42e7a63e3c0f38207b84.png
小总结


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

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

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

#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);
    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);
    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 = { 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接收客户端发送的数据。
我们先编译运行这段代码。
https://i-blog.csdnimg.cn/direct/44a643001fae4a47830e70a70ebd5990.png
现在可以看到,不管我们敲击多少次回车,步伐依旧处于阻塞的状态。那么步伐阻塞在那里呢?
真相就是——int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);,现在步伐等候着客户端的连接。
我们现在举行连接。可以看到,步伐已经跑出了accept的阻塞。现在收到来自recv的阻塞。
https://i-blog.csdnimg.cn/direct/beea80c73b394916b71ddcc4d920c539.png当步伐收到来自accept的阻塞时,我们可以看到tcp连接状态只有一个。
https://i-blog.csdnimg.cn/direct/f36ef3d9b90949f48c9ddcc3df541db6.png当我们连接server后(阻塞在recv处),tcp连接状态会出现两个。
https://i-blog.csdnimg.cn/direct/fbe36667ef8b4eaf99cf3bcf7d86c44b.png
这表明了,代码中实现的两个fd(sockfd和clientfd)与tcp连接信息是一对一的关系,百万级就有百万个。
出现的题目

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

既然要连接多个客户端,是否可以加进循环?可以。
while(1){
      printf("accept:\n");
      int clientfd = accept(sockfd, (struct sockaddr*)&clientaddr, &len);
      printf("accept finished\n");
      char buffer = { 0 };
      int count = recv(clientfd, buffer, 1024, 0);
      printf("RECV: %s\n", buffer);
      count = send(clientfd, buffer, 1024, 0);
      printf("SEND: %d\n", count);
    }
现在,我们运行后,打开三个客户端,依次(按照1,2,3的次序)举行连接而且发送。
https://i-blog.csdnimg.cn/direct/e81e0e3b53404c9499107babf417175d.png
可是这里有致命的题目!当我们依次(按照1,2,3的次序)连接不发送时,这时我们必须按照1,2,3的次序才气发送乐成。这样就酿成了只要前一个不接收到数据,后面的都接收不到。
原因很简朴,就是代码逻辑的题目。当我们依次点击连接后,第一个已经实行完accept函数,而后面两个阻塞在accept处且已经建立连接,但是并没有实行accept。第一个客户端阻塞在recv处,现在只有第一个发送数据并接收回发数据,后面的才气举行。这就扳连到了多路IO(多个客户端)怎么及时响应。
第三版服务器

使用线程

为了避免阻塞在recv处,我们为每一个客户端开一个线程。
void* client_pthread(void* arg)
{
    int clientfd = *(int*)arg;

    // 这里的循环是为了让同一个客户端收发多次数据
    while (1) {
      char buffer = { 0 };
      int count = recv(clientfd, buffer, 1024, 0);
      printf("RECV: %s\n", buffer);
      count = send(clientfd, buffer, count, 0);
      printf("SEND: %d\n", count);
    }
}
......
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);
    }
......
这样我们就可以解决recv阻塞的题目了。
https://i-blog.csdnimg.cn/direct/4a1a70694d674dc78c4a712306d03b2a.png
解决题目

上面的代码已经可以完成多个客户端独立地发送接收数据,可是还不敷完善。当我们断开连接时,步伐会不停跑,而且CPU占用率极高。
https://i-blog.csdnimg.cn/direct/a2c4624ecb09434db8dde7114671ea81.png
出现这个征象的原因,是因为我们没有处理客户端断开的代码。现在我们仔细看上图,断开之后server返回的count值为0,证实当我们断开时,recv返回0。现在我们加入断开处理代码。
void* client_pthread(void* arg)
{
    int clientfd = *(int*)arg;
    // 这里的循环是为了让同一个客户端收发多次数据
    while (1) {
      char buffer = { 0 };
      int count = recv(clientfd, buffer, 1024, 0);
      printf("RECV: %s\n", buffer);
      if (count == 0) { // disconnect
            printf("client disconnect: %d\n", clientfd);
            close(clientfd);
            break;
      }
      count = send(clientfd, buffer, count, 0);
      printf("SEND: %d\n", count);
    }
}
运行一下:
https://i-blog.csdnimg.cn/direct/c521e76e95f94ba8a640d1268c1582e1.png
可以看到已经可以正常地断开连接,而且返回对应的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被网络连接占用。
https://i-blog.csdnimg.cn/direct/c09a473e4d4746c3be15d554e4b2b85d.png
那么,不停递增数量是否是有限的呢?答案是肯定的。运行ulimit -a看一下,可以看到open files的数量是有限的,所以,IO的数量也是有限的。IO也是fd。这里就可以明确Linux 一切皆文件(Linux操纵全部设备一切皆文件形貌符)了吧。操纵文件是文件,操纵socket是文件,操纵定时器也是文件。
https://i-blog.csdnimg.cn/direct/c428cfe4df614f698c6e784ee49c24f7.png
现在再来观察一个征象,当我们断开clientfd后,又再次建立连接,体系会分配一个新的fd值:7,4被接纳了;我们现在又断开,等一段时间又连接,可以看到体系又分配了4。那又须要等候多长时间呢?等候TIME_WAIT,而这个时间是可以设置的。
https://i-blog.csdnimg.cn/direct/758149302d2b4b1aa25abd926b96b141.png
总结

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

#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)
{
    int clientfd = *(int*)arg;
    // 这里的循环是为了让同一个客户端收发多次数据
    while (1) {
      char buffer = { 0 };
      int count = recv(clientfd, buffer, 1024, 0);
      printf("RECV: %s\n", buffer);
      if (count == 0) { // disconnect
            printf("client disconnect: %d\n", clientfd);
            close(clientfd);
            break;
      }
      count = send(clientfd, buffer, count, 0);
      printf("SEND: %d\n", count);
    }
}
int main(){    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    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);
    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 = { 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 = { 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企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 手把手带你实现多线程服务器