# 第10讲:并发网络服务器 > 🎯 **本节目标**:掌握多进程、多线程、I/O 多路复用三种并发服务器模型 ## 📋 前置知识 - [[09_网络编程]] — Socket 编程基础 - [[06_进程控制]] — 进程创建 - [[07_多线程编程]] — 线程创建 --- ## 🤔 为什么需要这个? 上一讲的服务器一次只能服务一个客户端。如果有 100 个用户同时访问网站,第 100 个用户必须等前 99 个都处理完才能得到响应。 **并发服务器**就是解决这个问题的——让服务器能够同时服务多个客户端。 **生活比喻**: - **迭代服务器** = 一个服务员一次只服务一桌客人 - **多进程服务器** = 每来一桌客人就招一个新服务员 - **多线程服务器** = 一个服务员同时照看多桌客人 - **I/O 多路复用** = 服务员轮流查看哪桌客人需要服务 --- ## 📖 核心概念 ### 1. 三种并发模型 ```mermaid graph TD A[并发服务器模型] --> B[多进程模型] A --> C[多线程模型] A --> D[I/O 多路复用] B --> B1[每连接一个进程] B --> B2[进程间隔离] B --> B3[开销大] C --> C1[每连接一个线程] C --> C2[共享内存] C --> C3[开销较小] D --> D1[单线程处理多连接] D --> D2[select/poll/epoll] D --> D3[开销最小] style B fill:#ffcdd2 style C fill:#fff3e0 style D fill:#e8f5e9 ``` **对比**: | 模型 | 优点 | 缺点 | 适用场景 | |------|------|------|----------| | 多进程 | 隔离性好 | 开销大 | 连接数少 | | 多线程 | 开销较小 | 需要同步 | 连接数中等 | | I/O 多路复用 | 开销最小 | 编程复杂 | 连接数多 | ### 2. 多进程模型 ```mermaid sequenceDiagram participant 主进程 participant 子进程1 participant 子进程2 participant 客户端 主进程->>主进程: accept() 等待连接 客户端->>主进程: 连接请求 主进程->>子进程1: fork() 创建子进程 子进程1->>客户端: 处理请求 主进程->>主进程: 继续 accept() 客户端->>主进程: 新连接请求 主进程->>子进程2: fork() 创建子进程 子进程2->>客户端: 处理请求 ``` **特点**: - 每个连接一个独立进程 - 进程间完全隔离 - 进程创建和销毁开销大 ### 3. 多线程模型 ```mermaid sequenceDiagram participant 主线程 participant 工作线程1 participant 工作线程2 participant 客户端 主线程->>主线程: accept() 等待连接 客户端->>主线程: 连接请求 主线程->>工作线程1: pthread_create() 工作线程1->>客户端: 处理请求 主线程->>主线程: 继续 accept() 客户端->>主线程: 新连接请求 主线程->>工作线程2: pthread_create() 工作线程2->>客户端: 处理请求 ``` **特点**: - 每个连接一个线程 - 线程共享进程资源 - 需要注意线程安全 ### 4. I/O 多路复用(select) ```mermaid graph TD A[主循环] --> B[select 监听所有 fd] B --> C{哪个 fd 就绪?} C -->|监听套接字| D[accept 新连接] C -->|客户端套接字| E[处理请求] D --> A E --> A style B fill:#fff3e0 ``` **select 的工作原理**: 1. 将所有需要监听的文件描述符放入集合 2. 调用 `select()` 等待任意一个就绪 3. 遍历集合,处理就绪的描述符 4. 重复步骤 1 **fd_set 操作**: ```c fd_set read_set; FD_ZERO(&read_set); // 清空集合 FD_SET(fd1, &read_set); // 添加 fd1 FD_SET(fd2, &read_set); // 添加 fd2 select(maxfd+1, &read_set, NULL, NULL, NULL); // 等待 if (FD_ISSET(fd1, &read_set)) // 检查 fd1 是否就绪 // 处理 fd1 ``` ### 5. 线程池模型 ```mermaid graph TD A[主线程] -->|accept| B[任务队列] B --> C[工作线程1] B --> D[工作线程2] B --> E[工作线程3] C -->|处理完毕| B D -->|处理完毕| B E -->|处理完毕| B style B fill:#fff3e0 ``` **优势**: - 避免频繁创建销毁线程 - 控制并发数量 - 提高资源利用率 --- ## 💻 动手实践 ### 示例1:多线程并发服务器 ```c // togglest.c - 多线程并发服务器 #include "wrapper.h" void toggle(int conn_sock); void *serve_client(void *vargp); int main(int argc, char **argv) { int listen_sock, conn_sock, port, *conn_sock_p; struct sockaddr_in clientaddr; struct hostent *hp; char *haddrp; socklen_t clientlen = sizeof(struct sockaddr_in); pthread_t tid; if (argc != 2) { fprintf(stderr, "usage: %s \n", argv[0]); exit(1); } port = atoi(argv[1]); listen_sock = open_listen_sock(port); while (1) { conn_sock_p = malloc(sizeof(int)); *conn_sock_p = accept(listen_sock, (SA *)&clientaddr, &clientlen); // 获取客户端信息 hp = Gethostbyaddr((const char *)&clientaddr.sin_addr.s_addr, sizeof(clientaddr.sin_addr.s_addr), AF_INET); haddrp = inet_ntoa(clientaddr.sin_addr); printf("server connected to %s (%s)\n", hp->h_name, haddrp); // 创建新线程处理客户端 pthread_create(&tid, NULL, serve_client, conn_sock_p); } } void *serve_client(void *vargp) { int conn_sock = *((int *)vargp); pthread_detach(pthread_self()); // 分离线程 free(vargp); toggle(conn_sock); close(conn_sock); return NULL; } ``` **编译运行**: ```bash gcc -o togglest togglest.c -L. -lwrapper -lpthread # 终端1:启动服务器 ./togglest 8080 # 终端2-4:启动多个客户端 ./togglec localhost 8080 ``` ### 示例2:I/O 多路复用服务器 ```c // toggless1.c - select 多路复用服务器 #include "wrapper.h" void toggle(int conn_sock); void read_input(void); int main(int argc, char **argv) { int listen_sock, conn_sock, port; socklen_t clientlen = sizeof(struct sockaddr_in); struct sockaddr_in clientaddr; fd_set read_set, ready_set; if (argc != 2) { fprintf(stderr, "usage: %s \n", argv[0]); exit(1); } port = atoi(argv[1]); listen_sock = open_listen_sock(port); // 初始化 fd_set FD_ZERO(&read_set); FD_SET(STDIN_FILENO, &read_set); // 监听标准输入 FD_SET(listen_sock, &read_set); // 监听套接字 while (1) { ready_set = read_set; select(listen_sock + 1, &ready_set, NULL, NULL, NULL); // 检查标准输入 if (FD_ISSET(STDIN_FILENO, &ready_set)) read_input(); // 检查新连接 if (FD_ISSET(listen_sock, &ready_set)) { conn_sock = accept(listen_sock, (SA *)&clientaddr, &clientlen); toggle(conn_sock); close(conn_sock); } } } void read_input(void) { char buf[MAXLINE]; if (!fgets(buf, MAXLINE, stdin)) exit(0); printf("%s", buf); } ``` ### 示例3:连接池服务器 ```c // toggless2.c - 连接池服务器 #include "wrapper.h" typedef struct { int maxfd; fd_set read_set; fd_set ready_set; int nready; int maxi; int client_sock[FD_SETSIZE]; } sock_pool; void init_sock_pool(int listen_sock, sock_pool *pool); void add_sock(int conn_sock, sock_pool *pool); void serve_clients(sock_pool *pool); int main(int argc, char **argv) { int listen_sock, conn_sock, port; socklen_t clientlen = sizeof(struct sockaddr_in); struct sockaddr_in clientaddr; static sock_pool pool; if (argc != 2) { fprintf(stderr, "usage: %s \n", argv[0]); exit(1); } port = atoi(argv[1]); listen_sock = open_listen_sock(port); init_sock_pool(listen_sock, &pool); while (1) { pool.ready_set = pool.read_set; pool.nready = select(pool.maxfd + 1, &pool.ready_set, NULL, NULL, NULL); // 新连接 if (FD_ISSET(listen_sock, &pool.ready_set)) { conn_sock = accept(listen_sock, (SA *)&clientaddr, &clientlen); add_sock(conn_sock, &pool); } // 处理客户端请求 serve_clients(&pool); } } void init_sock_pool(int listen_sock, sock_pool *p) { int i; p->maxi = -1; for (i = 0; i < FD_SETSIZE; i++) p->client_sock[i] = -1; p->maxfd = listen_sock; FD_ZERO(&p->read_set); FD_SET(listen_sock, &p->read_set); } void add_sock(int conn_sock, sock_pool *p) { int i; p->nready--; for (i = 0; i < FD_SETSIZE; i++) if (p->client_sock[i] < 0) { p->client_sock[i] = conn_sock; FD_SET(conn_sock, &p->read_set); if (conn_sock > p->maxfd) p->maxfd = conn_sock; if (i > p->maxi) p->maxi = i; break; } if (i == FD_SETSIZE) perror("add_sock error: Too many clients"); } void serve_clients(sock_pool *p) { int i, conn_sock, n; char buf[MAXLINE]; for (i = 0; (i <= p->maxi) && (p->nready > 0); i++) { conn_sock = p->client_sock[i]; if ((conn_sock > 0) && (FD_ISSET(conn_sock, &p->ready_set))) { p->nready--; if ((n = recv(conn_sock, buf, MAXLINE, 0)) != 0) { printf("Server received %d bytes on fd %d\n", n, conn_sock); send(conn_sock, buf, n, 0); } else { close(conn_sock); FD_CLR(conn_sock, &p->read_set); p->client_sock[i] = -1; } } } } ``` --- ## 🔗 知识关联 - 多进程模型在 [[06_进程控制]] 中有详细讲解 - 多线程模型在 [[07_多线程编程]] 中有详细讲解 - I/O 多路复用在 [[17_IO系统]] 中有更深入的讨论 --- ## 📝 思考题 1. **为什么 select 有 FD_SETSIZE 限制?** 如何突破这个限制? 2. **多进程 vs 多线程**:在什么情况下多进程比多线程更合适? 3. **epoll 的优势**:为什么 Linux 推荐使用 epoll 而不是 select? --- ## 📚 扩展阅读 - 《UNIX网络编程》第1卷:第6章、第16章 - [epoll 详解](https://man7.org/linux/man-pages/man7/epoll.7.html) - [高性能网络编程](https://www.zhihu.com/question/28594409)