服务器类型与TCP并发服务器构建(SELECT)
一、服务器核心类型区分
服务器类型 | 核心特点 |
---|---|
单循环服务器 | 同一时刻仅能处理一个客户端的任务 |
并发服务器 | 同一时刻可处理多个客户端的任务 |
二、TCP并发服务器构建基础
1. TCP连接特性
TCP协议下,服务端与客户端需建立一对一连接,每个客户端(如cli1、cli2、cli3、cli4)会对应服务端的一个连接文件描述符(如connfd1、connfd2、connfd3、connfd4),通过该描述符实现数据交互。
三、TCP服务端并发模型(4种核心方案)
1. 多进程模型
- 资源开销:进程资源开销大(进程间地址空间独立,切换成本高)。
- 安全性:安全性高(进程间数据隔离,一个进程异常不影响其他进程)。
- 代码
#include "head.h"#define SER_PORT 50000
#define SER_ID "192.168.0.164"int init_tcp_ser()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket error");return -1;}struct sockaddr_in seraddr;seraddr.sin_family = AF_INET;seraddr.sin_port = htons(50000);seraddr.sin_addr.s_addr = inet_addr("192.168.0.164");int ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));if(ret < 0){perror("bind error");return -1; }ret = listen(sockfd, 10);if(ret < 0){perror("listen error");return -1;}return sockfd;
}void wait_handler(int signo)
{wait(NULL);
}int main(int argc, char const *argv[])
{signal(SIGCHLD_wait_handler);struct sockaddr_in cliaddr;socklen_t clilen = sizeof(cliaddr);int sockfd = init_tcp_ser();if(sockfd < 0){return -1;}while(1){int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen);if(connfd < 0){perror("accept error");close (sockfd);return -1;}pid_t pid =fork();if(pid > 0){}else if(pid == 0){char buff[1024] = {0};while (1){memset(buff, 0, sizeof(buff));int cnt = recv(connfd, buff, sizeof(buff), 0);if(cnt < 0){perror("recv error");break;}else if(cnt == 0){printf("[ %s : %d ] : cli offline\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));break;}printf("[ %s : %d ] : buff = %s\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port), buff);strcat(buff, "------> ok");cnt = send(connfd, buff, sizeof(buff), 0);if(cnt < 0){perror("send error");break;}}close(connfd);}}close(sockfd);return 0;
}
2. 多线程模型
- 资源开销:相对进程开销小(线程共享进程地址空间,切换成本低)。
- 并发能力:相同资源环境下,并发处理客户端的数量比多进程模型更优。
- 代码
#include "head.h"#define SER_PORT 50000
#define SER_ID "192.168.0.164"int init_tcp_ser()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket error");return -1;}//允许绑定处于TIME_WAIT状态的地址,避免端口占用问题:int optval = 1;setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));struct sockaddr_in seraddr;seraddr.sin_family = AF_INET;seraddr.sin_port = htons(50000);seraddr.sin_addr.s_addr = inet_addr("192.168.0.164");int ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));if(ret < 0){perror("bind error");return -1; }ret = listen(sockfd, 10);if(ret < 0){perror("listen error");return -1;}return sockfd;
}void *task(void *arg)
{int connfd = *(int *)arg;char buff[1024] = {0};while (1){memset(buff, 0, sizeof(buff));int cnt = recv(connfd, buff, sizeof(buff), 0);if(cnt < 0){perror("recv error");break;}else if(cnt == 0){printf(" cli offline\n");break;}printf("buff = %s\n", buff);strcat(buff, "------> ok");cnt = send(connfd, buff, sizeof(buff), 0);if(cnt < 0){perror("send error");break;}}close(connfd);return 0;
}int main(int argc, char const *argv[])
{struct sockaddr_in cliaddr;socklen_t clilen = sizeof(cliaddr);int sockfd = init_tcp_ser();if(sockfd < 0){return -1;}while(1){int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen);if(connfd < 0){perror("accept error");close (sockfd);return -1;}pthread_t tid;int ret = pthread_create(&tid, NULL, task, &connfd);if(ret != 0){perror("pthread_create error");return -1;}pthread_detach(tid);}close(sockfd);return 0;
}
3. 线程池模型
- 设计目的:解决多线程/多进程模型中“频繁创建、销毁线程(或进程)”带来的时间消耗问题。
- 核心原理:基于生产者-消费者编程模型,结合任务队列实现的多线程框架(提前创建固定数量线程,循环处理队列中的任务,避免频繁启停线程)。
4. IO多路复用模型
- 核心逻辑:将“IO操作”与“文件描述符(fd)”关联,复用一个进程即可实现对“多个文件描述符读写状态”的同时监测。
- 关键优势:无需创建新进程或线程,仅用单个进程完成多客户端IO事件处理。
- 关联概念:阻塞IO模式下,多个任务会呈现“同步执行”效果(一个任务阻塞时,其他任务需等待)。
- 实现方式:支持3种主流方案,
select、
poll
、epoll
。
四、select实现IO多路复用(详细流程与函数)
1. 核心实现步骤(5步)
- 创建文件描述符集合:定义
fd_set
类型的集合,用于存放需监测的文件描述符。 - 添加关注的文件描述符:通过
FD_SET()
宏,将待监测的fd加入集合。 - 内核监测事件:调用
select()
函数,将fd集合传递给内核,由内核监测fd的读写/异常事件。 - 解除阻塞并获取结果:当内核监测到目标事件(如fd可读),
select()
解除阻塞,应用层获取事件结果。 - 处理任务:根据
select()
返回的结果,对触发事件的fd进行针对性处理(如recv()
读取数据)。
2. 关键宏函数(操作fd集合)
宏函数原型 | 功能描述 |
---|---|
void FD_CLR(int fd, fd_set *set) | 将指定fd从fd集合中移除 |
int FD_ISSET(int fd, fd_set *set) | 判断指定fd是否在fd集合中(存在则返回非0) |
void FD_SET(int fd, fd_set *set) | 将指定fd添加到fd集合中 |
void FD_ZERO(fd_set *set) | 清空fd集合(初始化集合时必须调用) |
3. select函数详情
(1)函数原型
int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);
(2)功能
将fd集合传递给内核,等待内核返回“触发事件的fd结果”,实现对多fd的事件监测。
(3)参数说明
参数名 | 含义 |
---|---|
nfds | 需关注的“最大文件描述符 + 1”(内核通过该值确定监测范围) |
readfds | 监测“读事件”的fd集合(如客户端发送数据,fd变为可读) |
writefds | 监测“写事件”的fd集合(如fd可写入数据,无缓冲区满问题) |
exceptfds | 监测“异常事件”的fd集合(如fd发生错误) |
timeout | 超时时间设置: <br>- <br>- 非NULL:超时后若无事件, |
(4)返回值
- 成功:返回内核监测到的“触发事件的fd个数”。
- 失败:返回
-1
(如参数错误、系统调用异常)。 - 超时:返回
0
(超时时间到达,且无任何事件触发)。
(5)代码
#include "head.h"#define SER_PORT 50000
#define SER_ID "192.168.0.164"int init_tcp_ser()
{int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0){perror("socket error");return -1;}struct sockaddr_in seraddr;seraddr.sin_family = AF_INET;seraddr.sin_port = htons(SER_PORT);seraddr.sin_addr.s_addr = inet_addr(SER_ID);int ret = bind(sockfd, (struct sockaddr *)&seraddr, sizeof(seraddr));if(ret < 0){perror("bind error");return -1; }ret = listen(sockfd, 10);if(ret < 0){perror("listen error");return -1;}return sockfd;
}int main(int argc, char const *argv[])
{char buff[1024] = {0};struct sockaddr_in cliaddr;socklen_t clilen = sizeof(cliaddr);int sockfd = init_tcp_ser();if(sockfd < 0){return -1;}// 创建文件描述符集合fd_set rdfds;fd_set rdfdstmp;// 清零FD_ZERO(&rdfds);// 添加关注的文件描述符到集合FD_SET(sockfd, &rdfds);int maxfd = sockfd;while(1){rdfdstmp = rdfds;// 传递集合到内核,并等待返回监测结果int cnt = select(maxfd + 1, &rdfdstmp, NULL, NULL, NULL);if(cnt < 0){perror("select error");return -1;}// 是否有监听套接字事件到达 ----》三次握手已完成,可以acceptif(FD_ISSET(sockfd, &rdfdstmp)){int connfd = accept(sockfd, (struct sockaddr *)&cliaddr, &clilen);if(connfd < 0){perror("accept error");close (sockfd);return -1;}FD_SET(connfd, &rdfds); // 加到原始的集合maxfd = maxfd > connfd ? maxfd : connfd;}// 是否有通信套接字事件到达for(int i = sockfd + 1; i <= maxfd; i++){if(FD_ISSET(i, &rdfdstmp)) // 判断i{memset(buff, 0, sizeof(buff));// 接收 ssize_t cnt = recv(i, buff, sizeof(buff), 0);if(cnt < 0){perror("recv error");FD_CLR(i, &rdfds); // i 错误 ,从集合中删除iclose(i); // 先删除再关闭continue; // 不能使用 break return}else if(cnt == 0){FD_CLR(i, &rdfds);close(i);continue;}printf("%s\n", buff);strcat(buff , "-----> ok");cnt = send(i, buff, sizeof(buff), 0);if(cnt < 0){perror("send error");FD_CLR(i, &rdfds);close(i);continue;}}}}close(sockfd);return 0;
}