一. 继续解决上一篇留下的疑惑
问题抛出 : 为何在一个时间段中, 不可以支持多个用户的同时访问服务器, 只能够支持一个用户访问服务器结束, 断开连接下一个用户才可以进行连接?
原因 : 因为我们前文中的TCP socket 是最简单的, 基本的一对一的通信, 是同步阻塞的方式, 也就是说 当服务器 还没有处理完一个客户的网络 I/O的时候, 或者 读写操作发生阻塞时候 其他客户是没有办法与服务端进行连接操作的
再官方解释一下这个同步阻塞原因 : 就是主线程, 建立连接的线程被阻塞处理 网络 IO 了(占用,不空, 处理完这个IO之前没法建立新的连接)
其实简单的解释一下 同步阻塞含义 : 其他客户端 需要和 正在被服务的客户端一起同步阻塞等待服务器服务结束 哪一个正在服务的客户端 才能够建立新的连接
解决方法综述: 多进程 多线程 线程池 IO 多路复用技术 (本文介绍前三种)
二. 多进程模型实现服务器支持多用户连接
首先是理论支撑
服务器端主进程 (父进程) 仅仅只是负责监听客户端的连接, 每一次accept接受一次连接之后, 我们就 fork 出来一个子进程来处理这个连接所需的服务....
简单回顾fork() : 我不喜欢理解成创建一个子进程, 我喜欢理解成复制一个进程出来, 这个进程中和原来的进程相比, 需要处理的后序代码逻辑是一摸一样的, 内存地址空间, 程序计数器等等都是完成摹刻出来的. 仅有的区别, 就是 pid 不同, 还有 如何区分主进程逻辑还是子进程逻辑, 通过fork 返回值来看, 返回值 为 0 代表是子进程处理逻辑,返回值 > 0, 也就是返回子进程的pid 代表父进程处理逻辑
父子分工 :父进程直接关闭 自己所属的一份 connfd socket文件描述符, 然后只负责监听, 同时负责回收子进程资源, 防止僵尸, 此处为避免阻塞收尸, 可以采取轮询式, 或者我一般直接设置信号处理SIGCHLD 信号. 然后子进程来实现网络IO传输和服务操作....
signal(SIGCHLD, SIG_IGN); //避免子进程僵尸, 设置SIGCHLD信号为SIG_IGN 自动收尸, 不会僵尸..... 丢给系统去处理..
主题fork 多进程实现并发服务逻辑伪代码:
pid = fork(); if (pid < 0) { ERR_EXIT("fork"); } if (pid) { close(connfd); //父进程只进行监听 } else { close(listenfd); //子进程不需要监听 ..... //服务逻辑代码 }
还是上文连接所实现的过程, 服务端将客户端发来的字符转大写写回.
#include <sys/types.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <strings.h> #include <signal.h> #define SERVE_PORT 12345 #define ERR_EXIT(m) \ do { perror(m); exit(EXIT_FAILURE); } while (0) typedef struct sockaddr SA; int listenfd; //设置全局监听套接字, 方便关闭 void handle(int signo) { fprintf(stdout, "ByeBye!\n"); close(listenfd); exit(EXIT_SUCCESS); } int main() { signal(SIGCHLD, SIG_IGN); signal(SIGINT, handle); int listenfd, connfd, pid; struct sockaddr_in serveAdd, clientAdd; socklen_t clientAdd_len; char ipbuff[256]; //创建套接字 if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { ERR_EXIT("socket"); //协议家族 服务类型(套接字类型), 协议弃用(0) } //确定服务端地址簇 bzero(&serveAdd, sizeof(serveAdd)); //清0 serveAdd.sin_family = AF_INET; serveAdd.sin_port = htons(SERVE_PORT); serveAdd.sin_addr.s_addr = htonl(INADDR_ANY); //注意转网络字节序 //bind 端口 地址信息 if (bind(listenfd, (SA*)&serveAdd, sizeof(serveAdd)) == -1) { ERR_EXIT("bind"); } //开始监听.. if (listen(listenfd, 3) == -1) { ERR_EXIT("listen"); } printf("Accepting connections..\n"); //循环不断的接收客户的连接请求进行服务 while (1) { clientAdd_len = sizeof(clientAdd); if ((connfd = accept(listenfd, (SA*)&clientAdd, &clientAdd_len)) == -1) { ERR_EXIT("accept"); } printf("recieve connection from ip is %s and port is %d\n", inet_ntop(AF_INET, &clientAdd.sin_addr, ipbuff, sizeof(ipbuff)), ntohs(clientAdd.sin_port)); pid = fork(); if (pid < 0) { ERR_EXIT("fork"); } if (pid) { close(connfd); //父进程只进行监听 } else { //服务 close(listenfd); //子进程不需要监听 while (1) { char buff[1024] = {0}; int i; int n = read(connfd, buff, sizeof(buff)); //读数据 if (n == -1) { ERR_EXIT("read"); } if (n == 0) { //说明客户端主动断开连接 break; } //处理数据, 简单的小写字符转大写 for (i = 0; i < n; ++i) { buff[i] = toupper(buff[i]); } //写回 write(connfd, buff, n); } fprintf(stdout, "ip %s and port is %d interrupt connfd\n", ipbuff, ntohs(clientAdd.sin_port)); close(connfd); exit(EXIT_SUCCESS);//子进程完成通信(服务)断开 } } return 0; }
- 使用多进程来应付多客户端的弊端 : 进程的创建需要消耗大量的系统资源, 又特别是内存资源这些都是有限的, 所以使用多进程的方式, 处理 <= 100 这种 少量客户端还行, 当C10k问题来临是, 根本无能为力, 毕竟进程的产生, 进程间切换的包袱是很重的......
三. 多线程模型实现服务器支持多用户连接
竟然 进程间切换, 以及进程创建 所耗系统资源太大了, 那我们就使用轻量级进程, 多个线程共用一个进程地址空间, 来减轻负重 ----- 多线程模型
首先还是理论支撑
多线程共享所在进程数据 : 文件描述符列表,进程空间,代码,全局数据,堆,共享库
线程是运行在进程中的一个 "执行流' 单个进程中可以运行多个线程, 同一进程里面多个线程共享进程的部分资源, 这样线程间切换的时候仅仅只是切换线程私有数据, 寄存器等不共享数据, 相比进程间切换开销大大减少....
线程创建函数的图解分析 :
- 然后是线程回收资源的设置, 此处, 我们不设置主线程等待回收, 介绍一个pthread_detach函数
- 过程描述 (线程功能分析) : 每建立一个新的 connect 自然我们就 获取到一个新的connfd, 此时, 我们 就创建一个子线程, 且传入connfd, 子线程的功能, 就是两台主机网络通信的整个服务器的处理逻辑流程 (简单总结, 产生新的客户连接就创建新的子进程服务)....
主体线程部分代码逻辑:
//线程逻辑代码: void* Routine(void* arg) { pthread_detach(pthread_self()); int connfd = (int)arg; //网络通信服务端服务逻辑。。。 return (void*)0; } //循环不断的接收客户的连接请求进行服务 while (1) { clientAdd_len = sizeof(clientAdd); if ((connfd = accept(listenfd, (SA*)&clientAdd, &clientAdd_len)) == -1) { ERR_EXIT("accept"); } printf("recieve connection from ip is %s and port is %d\n", inet_ntop(AF_INET, &clientAdd.sin_addr, ipbuff, sizeof(ipbuff)), ntohs(clientAdd.sin_port)); //创建一个子线程, 来一个新的连接就创建一个 pthread_create(&tid, NULL, Routine, (void*)connfd); }
整个服务端代码实现
[tangyujie@VM-4-9-centos Serve]$ cat server.c #include <sys/types.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <strings.h> #include <signal.h> #include <pthread.h> #define SERVE_PORT 12345 #define ERR_EXIT(m) \ do { perror(m); exit(EXIT_FAILURE); } while (0) typedef struct sockaddr SA; int listenfd; //设置全局监听套接字, 方便关闭 void handle(int signo) { fprintf(stdout, "ByeBye!\n"); close(listenfd); exit(EXIT_SUCCESS); } void* Routine(void* arg) { pthread_detach(pthread_self()); //线程结束自动回收资源 int connfd = (int)arg; //先将参数强转回去 //服务 while (1) { char buff[1024] = {0}; int i; int n = read(connfd, buff, sizeof(buff));//读数据 if (n == -1) { ERR_EXIT("read"); } if (n == 0) { //说明客户端断开连接了 break; } //处理数据, 简单的小写字符转大写 for (i = 0; i < n; ++i) { buff[i] = toupper(buff[i]); } write(connfd, buff, n); //写回 } fprintf(stdout, "interrupt connfd end\n"); close(connfd); //服务结束断开连接 return (void*)0; } int main() { signal(SIGCHLD, SIG_IGN); signal(SIGINT, handle); int listenfd, connfd; pthread_t tid; struct sockaddr_in serveAdd, clientAdd; socklen_t clientAdd_len; char ipbuff[256]; //创建套接字 if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { ERR_EXIT("socket"); //协议家族 服务类型(套接字类型), 协议弃用(0) } //确定服务端地址簇 bzero(&serveAdd, sizeof(serveAdd)); //清0 serveAdd.sin_family = AF_INET; serveAdd.sin_port = htons(SERVE_PORT); serveAdd.sin_addr.s_addr = htonl(INADDR_ANY); //注意转网络字节序 //bind 端口 地址信息 if (bind(listenfd, (SA*)&serveAdd, sizeof(serveAdd)) == -1) { ERR_EXIT("bind"); } //开始监听.. if (listen(listenfd, 3) == -1) { ERR_EXIT("listen"); } printf("Accepting connections..\n"); //循环不断的接收客户的连接请求进行服务 while (1) { clientAdd_len = sizeof(clientAdd); if ((connfd = accept(listenfd, (SA*)&clientAdd, &clientAdd_len)) == -1) { ERR_EXIT("accept"); } printf("recieve connection from ip is %s and port is %d\n", inet_ntop(AF_INET, &clientAdd.sin_addr, ipbuff, sizeof(ipbuff)), ntohs(clientAdd.sin_port)); //创建一个子线程, 来一个新的连接就创建一个 pthread_create(&tid, NULL, Routine, (void*)connfd); } return 0; }
多线程模型 --- 相较多进程模型 缺失 减少了资源消耗, 但是...
每来一个连接就创建一个线程, 线程运行结束之后操作系统还要销毁线程, 这个平凡的创建销毁线程的系统资源销毁(开销) 也是压力相当大的 此时应该可以支持 《= 1000 Client。 所以可以使用 ( 线程池避免线程的频繁创建销毁 )
四. 线程池实现服务器支持多用户连接
线程池理论支撑 :附上连接一份, 学完应该足以支撑, 上一份代码是 C++的, 但是所用逻辑理论等是相通的.https://blog.csdn.net/weixin_53695360/article/details/122745816?spm=1001.2014.3001.5502
过程描述, 在整个主线程最开始, 就可以开启, (提前开启消费者工作线程等待任务来临) 我们的所有 工作线程 (饥饿的消费者, 等待客户端任务来临进行服务) 然后是线程逻辑, 我们在Routine 线程中 循环不断的接收 task_queue任务队列中的任务进行服务...... pop 任务 进行服务... (因为线程池中是多线程, 任务队列中的任务就是临界资源) 所以为了整个过程的有序进行, 我们使用 锁 保护临界资源, 条件变量, 避免CPU的无端浪费....
上述语言看不明白, 说明线程池基础却有缺失, 可能需要阅读上文链接,或进一步查询资料
函数刨析
然后废话不多说, 上代码, 还是服务端代码
[tangyujie@VM-4-9-centos Serve]$ cat server.c #include <sys/types.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <strings.h> #include <signal.h> #include <pthread.h> #define SERVE_PORT 12345 #define ERR_EXIT(m) \ do { perror(m); exit(EXIT_FAILURE); } while (0) typedef struct sockaddr SA; int listenfd; //设置全局监听套接字, 方便关闭 //设置任务结构体..... typedef struct Task { int connfd; //任务需要晓得哈是哪个做 struct Task* next; } Task; typedef struct TaskQueue { Task* front; Task* tail; pthread_mutex_t lock; pthread_cond_t cond; } TaskQueue; TaskQueue* tp; void ClearTask(Task* head) { Task* p = head, *q; while (p) { q = p->next; free(p); p = q; } } void DestroyTaskQueue(TaskQueue* tp) { ClearTask(tp->front); pthread_mutex_destroy(&tp->lock); pthread_cond_destroy(&tp->cond); } void handle(int signo) { fprintf(stdout, "ByeBye!\n"); DestroyTaskQueue(tp); close(listenfd); exit(EXIT_SUCCESS); } TaskQueue* InitTaskQueue() { TaskQueue* tp = (TaskQueue*)malloc(sizeof(TaskQueue)); tp->front = tp->tail = NULL; pthread_mutex_init(&tp->lock, NULL); pthread_cond_init(&tp->cond, NULL); return tp; } void Lock(TaskQueue* tp) { pthread_mutex_lock(&tp->lock); } void Unlock(TaskQueue* tp) { pthread_mutex_unlock(&tp->lock); } void WakeUp(TaskQueue* tp) { pthread_cond_signal(&tp->cond); } void Wait(TaskQueue* tp) { pthread_cond_wait(&tp->cond, &tp->lock); } Task* GetNewTask(int connfd) { Task* newTask = (Task*)malloc(sizeof(Task)); newTask->connfd = connfd; newTask->next = NULL; return newTask; } int IsEmpty(TaskQueue* tp) { return tp->front == NULL; } void Push(TaskQueue* tp, Task* task) { Lock(tp); //临界资源操作需要原子操作, 锁之间 if (IsEmpty(tp)) { tp->front = task; tp->tail = task; WakeUp(tp); //唤醒通知有任务了 Unlock(tp); return ; } tp->tail->next = task; tp->tail = task; WakeUp(tp); //唤醒通知有任务了 Unlock(tp); } Task* Pop(TaskQueue* tp) { Lock(tp); Task* poptask; while (IsEmpty(tp)) { //没有任务, 就一直等待生产 Wait(tp); //循环是为了避免伪唤醒. 唤醒多个线程 } //但是有些线程 还要继续Wait, 所以循环 poptask = tp->front; tp->front = tp->front->next; if (tp->front == NULL) tp->tail = NULL; Unlock(tp); return poptask; } void* Routine(void* arg) { TaskQueue* tp = (TaskQueue*)arg; while (1) { //不断的尝试Pop获取任务进行处理 int connfd = Pop(tp)->connfd; //Pop任务后获取connfd //服务 while (1) { char buff[1024] = {0}; int i; int n = read(connfd, buff, sizeof(buff));//读数据 if (n == -1) { ERR_EXIT("read"); } if (n == 0) { //说明客户端断开连接了 break; } //处理数据, 简单的小写字符转大写 for (i = 0; i < n; ++i) { buff[i] = toupper(buff[i]); } write(connfd, buff, n); //写回 } fprintf(stdout, "interrupt connfd end\n"); close(connfd); //服务结束断开连接 } return (void*)0; } void InitPool(TaskQueue* tp, int n) { pthread_t tid; int i; for (i = 0; i < n; ++i) { pthread_create(&tid, NULL, Routine, (void*)tp); //此处传入tp 需要拿取任务 pthread_detach(tid); //线程结束自动回收线程资源 } } int main() { signal(SIGCHLD, SIG_IGN); signal(SIGINT, handle); tp = InitTaskQueue();//初始化任务队列 InitPool(tp, 3); //一开始就开启消费者多线程 int listenfd, connfd; pthread_t tid; struct sockaddr_in serveAdd, clientAdd; socklen_t clientAdd_len; char ipbuff[256]; //创建套接字 if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { ERR_EXIT("socket"); //协议家族 服务类型(套接字类型), 协议弃用(0) } //确定服务端地址簇 bzero(&serveAdd, sizeof(serveAdd)); //清0 serveAdd.sin_family = AF_INET; serveAdd.sin_port = htons(SERVE_PORT); serveAdd.sin_addr.s_addr = htonl(INADDR_ANY); //注意转网络字节序 //bind 端口 地址信息 if (bind(listenfd, (SA*)&serveAdd, sizeof(serveAdd)) == -1) { ERR_EXIT("bind"); } //开始监听.. if (listen(listenfd, 3) == -1) { ERR_EXIT("listen"); } printf("Accepting connections..\n"); //循环不断的接收客户的连接请求进行服务 while (1) { clientAdd_len = sizeof(clientAdd); if ((connfd = accept(listenfd, (SA*)&clientAdd, &clientAdd_len)) == -1) { ERR_EXIT("accept"); } printf("recieve connection from ip is %s and port is %d\n", inet_ntop(AF_INET, &clientAdd.sin_addr, ipbuff, sizeof(ipbuff)), ntohs(clientAdd.sin_port)); Task* newtask = GetNewTask(connfd); Push(tp, newtask); } return 0; }
然后是上述所有服务端均可通用的客户端代码:
#include <sys/types.h> #include <unistd.h> #include <sys/socket.h> #include <arpa/inet.h> #include <stdio.h> #include <stdlib.h> #include <strings.h> #include <signal.h> #include <string.h> #define SERVE_PORT 12345 //端口号 #define ERR_EXIT(m)\ do { perror(m); exit(EXIT_FAILURE); } while (0) //错误处理 typedef struct sockaddr SA; int sockfd; //设置全局, 方便关闭 void handle(int signo) { fprintf(stdout, "ByeBye!\n"); close(sockfd); exit(EXIT_SUCCESS); } int main(int argc, char* argv[]) { if (argc != 2) { fprintf(stderr, "%s <ip>", argv[0]); close(EXIT_FAILURE); } signal(SIGINT, handle); struct sockaddr_in serveAdd; //创建套接字 if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { ERR_EXIT("socket"); //协议家族 服务类型(套接字类型), 协议弃用(0) } //确定服务端地址簇 bzero(&serveAdd, sizeof(serveAdd)); //清0 serveAdd.sin_family = AF_INET; serveAdd.sin_port = htons(SERVE_PORT); //将传入的ip字符串转换为 sin_addr if (inet_pton(AF_INET, argv[1], &serveAdd.sin_addr) == -1) { ERR_EXIT("inet_ntop"); } //不需要绑定端口号 系统随机分配一个临时端口号, 直接连接 if (connect(sockfd, (SA*)&serveAdd, sizeof(serveAdd)) == -1) { ERR_EXIT("connect"); } while (1) { //死循环, 使用ctrl c信号关闭连接 char buff[1024]; printf("请说>>"); scanf("%s", buff); write(sockfd, buff, strlen(buff)); int n = read(sockfd, buff, sizeof(buff)); if (n == -1) { ERR_EXIT("read"); } buff[n] = 0; fprintf(stdout, ">>%s\n", buff); } }
五. 总结本章
本文首先通过 同步阻塞的原因 引出来 主线程被阻塞处理 网络 IO 服务了, 这样当他服务一个客户的时候, 其他客户都无法与服务器建立连接.
然后为了解决这个问题 提出来了 多进程 多线程模型 线程池模型 多路IO复用(遗留)
目的最终是为了解决 C10k 问题, 多进程 弊端分析(进程创建销毁, 切换) 系统资源耗费巨大, 最多支持 100 左右用户
为了减少系统资源消耗 + 减少切换压力, ---- 引出来 多线程模型, 多个线程共享所在进程中的进程资源, 线程间切换 仅仅只是线程私有数据和寄存器的切换(切换压力小)但是不停的创建销毁线程压力大
于是又引出来 线程池来实现线程的复用, 减去线程不停创建和销毁的压力
C10k 问题就成为本章留疑了, 大家可以先自行讨论, 评论区给与简易,以及多路复用的含义解释呀等。。。。 总结时候如果存在不清晰读者可回溯