一、服务器核心类型区分
服务器类型 |
核心特点 |
单循环服务器 |
同一时刻仅能处理一个客户端的任务 |
并发服务器 |
同一时刻可处理多个客户端的任务 |
二、TCP并发服务器构建基础
1. TCP连接特性
TCP协议下,服务端与客户端需建立一对一连接,每个客户端(如cli1、cli2、cli3、cli4)会对应服务端的一个连接文件描述符(如connfd1、connfd2、connfd3、connfd4),通过该描述符实现数据交互。
三、TCP服务端并发模型(4种核心方案)
1. 多进程模型
- 资源开销:进程资源开销大(进程间地址空间独立,切换成本高)。
- 安全性:安全性高(进程间数据隔离,一个进程异常不影响其他进程)。
- 代码
cs
复制代码
#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. 多线程模型
- 资源开销:相对进程开销小(线程共享进程地址空间,切换成本低)。
- 并发能力:相同资源环境下,并发处理客户端的数量比多进程模型更优。
- 代码
cs
复制代码
#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>- NULL :无超时,select() 一直阻塞等待事件; <br>- 非NULL:超时后若无事件,select() 解除阻塞 |
(4)返回值
- 成功:返回内核监测到的"触发事件的fd个数"。
- 失败 :返回
-1
(如参数错误、系统调用异常)。
- 超时 :返回
0
(超时时间到达,且无任何事件触发)。
(5)代码
cs
复制代码
#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;
}
// 是否有监听套接字事件到达 ----》三次握手已完成,可以accept
if(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 错误 ,从集合中删除i
close(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;
}