网络编程-----服务器(多路复用IO 和 TCP并发模型)

一、单循环服务器模型

1. 核心特征
复制代码
while(1)
			{
				newfd = accept();
					recv();
				close(newfd);
			}
2. 典型应用场景
  • HTTP短连接服务(早期Apache)
  • CGI快速处理
  • 简单测试服务器
3. 综合代码
复制代码
#include <stdio.h>
#include <sys/types.h>	       /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, const char *argv[])
{
	if (argc != 3)
	{
		printf("Usage: %s <port> <ip>\n",argv[0]);
		return -1;
	}
	

	//1.socket 创建通信一端 
	int fd = socket(AF_INET,SOCK_STREAM,0);

	if (fd < 0)
	{
		perror("socket fail\n");
		return -1;
	}

	struct sockaddr_in seraddr;
	bzero(&seraddr,sizeof(seraddr));

	seraddr.sin_family = AF_INET;
	seraddr.sin_port = htons(atoi(argv[1]));
	seraddr.sin_addr.s_addr = inet_addr(argv[2]);
	printf("fd = %d\n",fd);
    //2.bind -- 绑定服务器端的地址信息 
	if (bind(fd,(const struct sockaddr*)&seraddr,sizeof(seraddr)) < 0)
	{
		perror("connect fail");
		return -1;
	}

	printf("connect success!\n");

	//3.listen -- 设置监听 
	if (listen(fd,5) < 0)
	{
		perror("listen fail");
		return -1;
	}


	while (1)
	{
		//4.accept
		int connfd = accept(fd,NULL,NULL);

		if (connfd < 0)
		{
			perror("accept fail");
			return -1;
		}

		printf("----client --- connectted\n");

		char buf[1024];
		char sbuf[1024];
		while (1)
		{
			recv(connfd,buf,sizeof(buf),0);
			printf("c: %s\n",buf);

			if (strncmp(buf,"quit",4) == 0)
			{
				close(connfd);
				break;
			}
			sprintf(sbuf,"server + %s\n",buf);
			send(connfd,sbuf,strlen(sbuf)+1,0);
		}
	}
	close(fd);

	return 0;
}
4. 优缺点分析
优点 缺点
实现简单 无法处理并发请求
无资源竞争问题 长连接会阻塞后续请求
适合低负载场景 吞吐量低(QPS < 100)

二、多进程并发模型

1. 核心实现
复制代码
while(1) {
    int newfd = accept(listen_fd, ...);
    pid_t pid = fork();
    
    if (pid == 0) {  // 子进程
        close(listen_fd);
        handle_connection(newfd);
        close(newfd);
        exit(0);
    } else if (pid > 0) {  // 父进程
        close(newfd);
        waitpid(-1, NULL, WNOHANG);  // 非阻塞回收
    }
}
2. 进程管理优化
复制代码
// 使用信号处理避免僵尸进程
signal(SIGCHLD, SIG_IGN);  // 忽略子进程结束信号

// 或使用waitpid循环
while (waitpid(-1, NULL, WNOHANG) > 0);
3. 典型应用
  • 传统Apache的prefork模式
  • FTP服务器
  • 数据库连接池
4. 资源消耗对比
资源类型 进程创建开销 示例系统调用
内存 需要复制整个PCB fork()
CPU 上下文切换成本高 schedule()
文件描述符 需要显式关闭继承的fd close()
5. 优缺点分析
优点 缺点
可以完成多个进程的实时交互 回收资源不方便
信息的完整性可以保证。 每次fork 占用系统资源多
适合低负载场景 可能出现僵尸进程

6. 综合代码

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>	       /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>

void handler(int signo)
{
	wait(NULL);
}


int init_server(const char *ip,unsigned short port)
{
	//1.socket 创建通信一端 
	int fd = socket(AF_INET,SOCK_STREAM,0);

	if (fd < 0)
	{
		perror("socket fail\n");
		return -1;
	}

	struct sockaddr_in seraddr;
	bzero(&seraddr,sizeof(seraddr));

	seraddr.sin_family = AF_INET;
	seraddr.sin_port = htons(port);
	seraddr.sin_addr.s_addr = inet_addr(ip);

	//2.bind -- 绑定服务器端的地址信息 
	if (bind(fd,(const struct sockaddr*)&seraddr,sizeof(seraddr)) < 0)
	{
		perror("connect fail");
		return -1;
	}

	//3.listen -- 设置监听 
	if (listen(fd,5) < 0)
	{
		perror("listen fail");
		return -1;
	}
	return fd;
}


int client_handler(int connfd)
{
	char buf[1024];
	char sbuf[1024];
	int ret = 0;

	while (1)
	{
		ret = recv(connfd,buf,sizeof(buf),0);
		if (ret < 0)
		{
			perror("client_handler recv fail");
			ret = -1;
		}
		printf("c: %s\n",buf);

		if (strncmp(buf,"quit",4) == 0)
		{
			close(connfd);
			ret = 1;
			break;
		}
		sprintf(sbuf,"server + %s\n",buf);
		ret = send(connfd,sbuf,strlen(sbuf)+1,0);
		if (ret < 0)
		{
			perror("client_handler send fail");
			ret = -1;
		}
	}

	return ret;
	
}


int main(int argc, const char *argv[])
{
	if (argc != 3)
	{
		printf("Usage: %s <ip> <port>\n",argv[0]);
		return -1;
	}
	
	signal(SIGCHLD,handler);

	int fd = init_server(argv[1],atoi(argv[2]));
	if (fd < 0)
	{
		printf("init_server fail\n");
		return -1;
	}

	while (1)
	{
		//4.accept
		int connfd = accept(fd,NULL,NULL);

		if (connfd < 0)
		{
			perror("accept fail");
			return -1;
		}

		pid_t pid = fork();
		if (pid < 0)
		{
			perror("fork fail");
			return -1;
		}
		if (pid == 0)
		{

			int ret = 0;
			if ((ret = client_handler(connfd)) < 0)
			{
				printf("client_handler fail");
				return -1;
			}
			if (ret == 1)
			{
				printf("child exit...\n");
				exit(EXIT_SUCCESS);
			}
 
		}
	}
	close(fd);

	return 0;
}

三、多线程并发模型

1. 核心实现(POSIX线程)
复制代码
while(1) {
    int newfd = accept(listen_fd, ...);
    pthread_t tid;
    pthread_create(&tid, NULL, thread_handler, (void*)newfd);
    pthread_detach(tid);  // 分离线程自动回收
}

void* thread_handler(void* arg) {
    int fd = (int)arg;
    // 处理请求
    close(fd);
    return NULL;
}
2. 线程安全控制
复制代码
// 使用互斥锁保护共享资源
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

void safe_write(int fd, const char* data) {
    pthread_mutex_lock(&lock);
    write(fd, data, strlen(data));
    pthread_mutex_unlock(&lock);
}
3. 典型应用
  • Java Tomcat
  • IIS应用池
  • 实时通信服务器
4. 性能指标对比
指标 进程模型 线程模型
创建速度 慢(10-100ms) 快(0.1-1ms)
上下文切换成本 高(切换页表等) 低(共享地址空间)
内存占用 高(独立资源) 低(共享资源)
5. 优缺点分析
优点 缺点
可以完成多个进程的实时交互 线程共享进程资源
创建速度快,调度快 稳定性 较差
适合低负载场景 安全性 较差

6. 综合代码

cpp 复制代码
#include <stdio.h>
#include <pthread.h>
#include <sys/types.h>	       /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <sys/wait.h>
#include <errno.h>

int init_server(const char *ip,unsigned short port)
{
	//1.socket 创建通信一端 
	int fd = socket(AF_INET,SOCK_STREAM,0);

	if (fd < 0)
	{
		perror("socket fail\n");
		return -1;
	}

	struct sockaddr_in seraddr;
	bzero(&seraddr,sizeof(seraddr));

	seraddr.sin_family = AF_INET;
	seraddr.sin_port = htons(port);
	seraddr.sin_addr.s_addr = inet_addr(ip);

	//2.bind -- 绑定服务器端的地址信息 
	if (bind(fd,(const struct sockaddr*)&seraddr,sizeof(seraddr)) < 0)
	{
		perror("connect fail");
		return -1;
	}

	//3.listen -- 设置监听 
	if (listen(fd,5) < 0)
	{
		perror("listen fail");
		return -1;
	}
	return fd;
}


void* client_handler(void *arg)
{
	int connfd = *(int *)arg;
	char buf[1024];
	char sbuf[1024];
	long int ret = 0;
	

	while (1)
	{
		ret = recv(connfd,buf,sizeof(buf),0);
		if (ret < 0)
		{
			perror("client_handler recv fail");
			ret = -1;
		}
		printf("c: %s\n",buf);

		if (strncmp(buf,"quit",4) == 0)
		{
			close(connfd);
			ret = 1;
			break;
		}
		sprintf(sbuf,"server + %s\n",buf);
		ret = send(connfd,sbuf,strlen(sbuf)+1,0);
		if (ret < 0)
		{
			perror("client_handler send fail");
			ret = -1;
		}
	}

	return (void*)ret;
	
}


int main(int argc, const char *argv[])
{
	if (argc != 3)
	{
		printf("Usage: %s <ip> <port>\n",argv[0]);
		return -1;
	}
	

	int fd = init_server(argv[1],atoi(argv[2]));
	if (fd < 0)
	{
		printf("init_server fail\n");
		return -1;
	}

	while (1)
	{
		//4.accept
		int connfd = accept(fd,NULL,NULL);

		if (connfd < 0)
		{
			perror("accept fail");
			return -1;
		}
		
		pthread_t tid;

		int ret = pthread_create(&tid,NULL,client_handler,&connfd);
		if(ret != 0)
		{
			errno = ret;
			perror("pthread_create fail");
			return -1;
		}
		pthread_detach(tid);//设置分离属性,由系统回收资源
	}
	close(fd);
	return 0;
}

四、并发的服务器模型 ---更高程度上的并发

(一)fcntl 函数与 I/O 模型详解

1. 函数原型
复制代码
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );
2. 主要操作类型
命令 功能描述 参数要求
F_DUPFD 复制文件描述符 指定最小可用fd值
F_GETFD/F_SETFD 获取/设置文件描述符标志 标志值
F_GETFL/F_SETFL 获取/设置文件状态标志 新标志值
F_GETOWN/F_SETOWN 获取/设置异步I/O所有权 进程ID或组ID

(二)非阻塞I/O设置示例

1. 设置流程
cpp 复制代码
int flag = fcntl(connfd,F_GETFL,0);
		flag = flag | O_NONBLOCK;
		fcntl(connfd,F_SETFL,flag);
2. 行为变化对比
操作 阻塞模式 非阻塞模式
read() 阻塞直到数据到达 立即返回,无数据时返回EAGAIN
write() 阻塞直到缓冲区空间可用 立即返回,空间不足返回EAGAIN
accept() 阻塞直到有新连接 立即返回,无连接时返回EAGAIN

(三)I/O 模型对比

1. 阻塞I/O模型
2. 非阻塞I/O模型
cpp 复制代码
#include <stdio.h>
#include <sys/types.h>	       /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h> /* superset of previous */
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>

#include <fcntl.h>
int main(int argc, const char *argv[])
{
	if (argc != 3)
	{
		printf("Usage: %s <port> <ip>\n",argv[0]);
		return -1;
	}
	

	//1.socket 创建通信一端 
	int fd = socket(AF_INET,SOCK_STREAM,0);

	if (fd < 0)
	{
		perror("socket fail\n");
		return -1;
	}

	struct sockaddr_in seraddr;
	bzero(&seraddr,sizeof(seraddr));

	seraddr.sin_family = AF_INET;
	seraddr.sin_port = htons(atoi(argv[1]));
	seraddr.sin_addr.s_addr = inet_addr(argv[2]);
	printf("fd = %d\n",fd);
    //2.bind -- 绑定服务器端的地址信息 
	if (bind(fd,(const struct sockaddr*)&seraddr,sizeof(seraddr)) < 0)
	{
		perror("connect fail");
		return -1;
	}

	printf("connect success!\n");

	//3.listen -- 设置监听 
	if (listen(fd,5) < 0)
	{
		perror("listen fail");
		return -1;
	}


	while (1)
	{
		//4.accept
		int connfd = accept(fd,NULL,NULL);

		if (connfd < 0)
		{
			perror("accept fail");
			return -1;
		}

		printf("----client --- connectted\n");

		char buf[1024];
		char sbuf[1024];
	
		int flag = fcntl(connfd,F_GETFL,0);
		flag = flag | O_NONBLOCK;
		fcntl(connfd,F_SETFL,flag);
		
		while (1)
		{
			recv(connfd,buf,sizeof(buf),0);
			printf("c: %s\n",buf);

			if (strncmp(buf,"quit",4) == 0)
			{
				close(connfd);
				break;
			}
			sprintf(sbuf,"server + %s\n",buf);
			send(connfd,sbuf,strlen(sbuf)+1,0);
		}
	}
	close(fd);

	return 0;
}

(四)信号驱动 I/O 详解

1. 设置异步标志
复制代码
// 获取当前文件状态标志
int flags = fcntl(fd, F_GETFL);
if (flags == -1) {
    perror("fcntl F_GETFL");
    exit(EXIT_FAILURE);
}

// 添加异步I/O标志
if (fcntl(fd, F_SETFL, flags | O_ASYNC) == -1) {
    perror("fcntl F_SETFL");
    exit(EXIT_FAILURE);
}
2. 指定信号接收者
复制代码
// 设置当前进程为信号接收者
if (fcntl(fd, F_SETOWN, getpid()) == -1) {
    perror("fcntl F_SETOWN");
    exit(EXIT_FAILURE);
}
3. 注册信号处理函数
复制代码
// 更安全的sigaction替代signal
struct sigaction sa;
sa.sa_flags = SA_RESTART;
sa.sa_handler = sigio_handler;
sigemptyset(&sa.sa_mask);

if (sigaction(SIGIO, &sa, NULL) == -1) {
    perror("sigaction");
    exit(EXIT_FAILURE);
}
4. 基本处理逻辑
cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <signal.h>

int g_fd;

void handler(int signo)
{
	char buf[1024];
	read(g_fd,buf,sizeof(buf));

	if (strncmp(buf,"quit",4) == 0)
		return;
	printf("buf = %s\n",buf);

}

int main(int argc, const char *argv[])
{
	if (mkfifo(argv[1],0666) < 0 && errno != EEXIST)
	{
		perror("mkfifo fail");
		return -1;
	}

	printf("mkfifo success\n");

	int fd = open(argv[1], O_RDONLY);
	if (fd < 0)
	{
		perror("open fail");
		return -1;
	}

	g_fd = fd;
	int flag = fcntl(fd,F_GETFL,0);
	flag = flag | O_ASYNC;//设置为异步通信
	fcntl(fd,F_SETFL,flag);

	fcntl(fd,F_SETOWN,getpid());//所有者
	signal(SIGIO,handler);

	int i = 0;
	while (1)
	{
		printf("i = %d\n",i);
		sleep(1);
		++i;
	}

	close(fd);
	return 0;
}

5.核心局限性分析
问题类型 具体表现 解决思路
信号合并 快速连续信号可能被合并 使用实时信号(SIGRTMIN+)
多fd区分困难 无法直接判断哪个fd触发信号 每个fd绑定不同信号(不现实)
异步安全限制 信号处理函数中操作受限 仅设置标志,主循环处理
性能瓶颈 高频率信号导致CPU占用高 配合epoll使用

(五)select 函数详解

一、函数原型与参数解析

复制代码
#include <sys/select.h>

int select(int nfds, 
           fd_set *readfds, 
           fd_set *writefds,
           fd_set *exceptfds, 
           struct timeval *timeout);
参数说明
参数 类型 说明
nfds int 监控的文件描述符最大值 +1(优化内核检查范围)
readfds fd_set* 监控可读事件的描述符集合(可NULL)
writefds fd_set* 监控可写事件的描述符集合(可NULL)
exceptfds fd_set* 监控异常事件的描述符集合(可NULL)
timeout timeval* 超时时间:<br>• NULL:阻塞等待<br>• 0:立即返回<br>• 正数:定时等待
返回值
  • 成功:返回就绪的文件描述符总数(可能为0)
  • 失败 :返回-1并设置errno
  • 超时:返回0

二、核心操作宏

功能 示例
FD_ZERO 清空描述符集合 FD_ZERO(&read_fds);
FD_SET 添加描述符到集合 FD_SET(sockfd, &read_fds);
FD_CLR 从集合中移除描述符 FD_CLR(sockfd, &read_fds);
FD_ISSET 检测描述符是否在集合中 if(FD_ISSET(sockfd, &read_fds))

三、典型使用流程

1. 初始化描述符集合
复制代码
fd_set read_fds;
FD_ZERO(&read_fds);
FD_SET(listen_fd, &read_fds);
int max_fd = listen_fd;
2. 等待事件就绪
复制代码
struct timeval tv = {5, 0}; // 5秒超时
fd_set tmp_fds = read_fds;

int ready = select(max_fd + 1, &tmp_fds, NULL, NULL, &tv);
if (ready == -1) {
    if (errno == EINTR) continue; // 处理信号中断
    perror("select error");
    break;
} else if (ready == 0) {
    printf("Timeout\n");
    continue;
}
3. 处理就绪事件
复制代码
for (int fd = 0; fd <= max_fd; fd++) {
    if (FD_ISSET(fd, &tmp_fds)) {
        if (fd == listen_fd) {
            // 处理新连接
            int new_fd = accept(listen_fd, ...);
            FD_SET(new_fd, &read_fds);
            max_fd = (new_fd > max_fd) ? new_fd : max_fd;
        } else {
            // 处理客户端数据
            ssize_t n = read(fd, ...);
            if (n <= 0) {
                close(fd);
                FD_CLR(fd, &read_fds);
            }
        }
    }
}

四、关键注意事项

  1. 集合重用问题

    select返回后,集合会被修改为就绪的fd集合,每次调用前必须重新初始化:

    复制代码
    fd_set tmp_fds = read_fds; // 使用临时集合
  2. 超时时间重置
    timeout参数会被修改为剩余时间,循环调用时需要重新设置:

    复制代码
    struct timeval tv = {5, 0};
    while(1) {
        select(..., &tv);
        tv.tv_sec = 5; // 必须重置
    }
  3. 最大fd限制

    FD_SETSIZE限制(通常1024),超出会导致未定义行为

  4. 性能问题

    每次调用需要从用户态复制整个fd_set到内核态,时间复杂度O(n)

cpp 复制代码
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
int main(int argc, const char *argv[])
{
	if (mkfifo(argv[1],0666) < 0 && errno != EEXIST)
	{
		perror("mkfifo fail");
		return -1;
	}

	printf("mkfifo success\n");

	int fd = open(argv[1], O_RDONLY);
	if (fd < 0)
	{
		perror("open fail");
		return -1;
	}
	char buf[1024] = {0};

	//1.建表
	fd_set readfds;
	FD_ZERO(&readfds);

	//2.添加要关心的fd
	FD_SET(0,&readfds);
	FD_SET(fd,&readfds);

	//3.select函数监控
	fd_set backfds;
		
		struct timeval tv = {5,0};
	while(1)
	{
		backfds = readfds;//每次循环回来拿到的都是最原始数据
		int nfds = fd + 1;//因为另一个是0,所以最大也就是fd
		int ret = select(nfds,&backfds,NULL,NULL,&tv);
		if(ret < 0)
		{
			perror("select fail");
			return -1;
		}
		if(ret > 0)
		{
			for(int i = 0;i < nfds;i++)//也可以是1024,但没必要 
			{
				if(FD_ISSET(i,&backfds))
				{
					if(i == 0)
					{
						fgets(buf,sizeof(buf),stdin);
						if (strncmp(buf,"quit",4) == 0)
							break;
						printf("buf = %s\n",buf);
					}else if(i == fd)
					{
						read(fd,buf,sizeof(buf));
						if (strncmp(buf,"quit",4) == 0)
							break;
						printf("buf = %s\n",buf);
					}
				}
			}
		}
	}
	
	close(fd);
	return 0;
}

可以从客户端读取数据,也可以自身从键盘输入

并发模型对比

模型 实现方式 优点 缺点
多进程 fork() 隔离性好 资源消耗大
多线程 pthread_create() 资源共享高效 同步复杂度高
I/O多路复用 select/poll/epoll 高并发低开销 编程复杂度较高
信号驱动 SIGIO+fcntl 实时性好 信号处理复杂
异步I/O aio_*系列函数 真正的异步操作 系统支持不统一
相关推荐
黄焖鸡能干四碗11 小时前
企业元数据梳理和元数据管理方案(PPT方案)
大数据·运维·网络·分布式·spark
bloglin9999917 小时前
scp、rsync远程文件同步
linux·运维·服务器
克莱因35818 小时前
思科 Cisco 标准ACL
网络·路由
迦南的迦 亚索的索18 小时前
LINUX环境
linux·运维·服务器
yuanjj8818 小时前
linux下调试域格CLM920 NC5等9x07平台模块 QMI拨号
linux·运维·服务器
IMPYLH18 小时前
Linux 的 printenv 命令
linux·运维·服务器·bash
SilentSamsara18 小时前
SSH 远程管理:密钥登录 + 隧道转发,一次性配置好
linux·运维·服务器·ubuntu·centos·ssh
资深数据库专家19 小时前
总账EBS 应用服务器1 的监控分析
java·网络·数据库
2501_9458374319 小时前
OpenClaw:开启 “行动 AI“ 新纪元,从聊天机器人到自主智能体的范式革命
服务器
阿正的梦工坊19 小时前
拦截网络请求:一种更优雅的数据获取方式
网络·okhttp