1.同步阻塞迭代模型
//创建socket
int sockfd = socket(...);
//准备通信地址
//绑定
bind(...);
//监听
listen(...);
for(;;)
{
//等待连接
accept(...);
//接收客户端的请求
recv(...);
//响应请求
send(...);
}
缺点:同步阻塞模型使用单进程,在accept、recv、send时都可能会发生阻塞,一次只能服务一个客户端,效率低。
- 若没有客户端发来连接请求,进程会阻塞在accept处,不能进行其它操作。
- recv函数用于接收客户端传送过来的信息,若客户端一直不发送消息,则进程会阻塞在这里。
- send函数用于向客户端发送信息,也可能发生阻塞。例如:客户端接受数据异常缓慢,导致写缓冲区满,数据迟迟发送不出。
优点:实现简单、占用资源少
2.多进程并发模型
多进程并发模型在同步阻塞迭代模型的基础上进行了改进,解决了一次只能服务一个客户端的问题。在接收到客户端的连接请求时,用fork函数为每一个客户端提供一个进程来处理客户端请求。
//创建socket
int sockfd = socket(...);
//准备要绑定的结构体addr
//绑定
bind(...);
//监听
listen(...);
for(;;)
{
//等待连接
accept(...);
//创建子进程服务
if(0 == fork())
{
//接收客户端的请求
recv(...);
//响应请求
send(...);
//关闭socket
close(cli_fd);
}
}
多进程并发模型通过创建子进程处理多并发的问题,看似有多个子进程同时工作,实际上只有一个CPU在处理,CPU轮流为每个进程服务一定时间,切换进程的过程中也会耗费一些时间。
3.多路复用模型
I/O多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦
某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相
应的读写操作。
优点:不需要频繁地创建、销毁进程/线程,从而达到节约内存资源、时间资源,也能避免进程之间的竞争、等待。 缺点:单个客户端的任务不能耗时太长,否则其他客户端就会感知到。 适合并发量高、任务量短小的情景,例如:Web服务器 目前支持I/O多路复用的系统调用有 select,pselect,poll,epoll
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
typedef struct sockaddr* SP;
int main(int argc,const char* argv[])
{
int svr_fd = socket(AF_INET,SOCK_STREAM,0);
if(0 > svr_fd)
{
perror("socket");
return EXIT_FAILURE;
}
struct sockaddr_in addr = {};
addr.sin_family = AF_INET;
addr.sin_port = htons(7788);
addr.sin_addr.s_addr = INADDR_ANY;
socklen_t addrlen = sizeof(addr);
if(bind(svr_fd,(SP)&addr,addrlen))
{
perror("bind");
return EXIT_FAILURE;
}
if(listen(svr_fd,10))
{
perror("listen");
return EXIT_FAILURE;
}
fd_set reads;
FD_ZERO(&reads);
FD_SET(svr_fd,&reads);
struct timeval timeout = {5,500};
int max_fd = svr_fd;
char buf[4096] = {};
for(;;)
{
fd_set reads_copy = reads;
int ret = select(max_fd+1,&reads_copy,NULL,NULL,&timeout);
if(0 < ret)
{
if(FD_ISSET(svr_fd,&reads_copy))
{
int cli_fd = accept(svr_fd,(SP)&addr,&addrlen);
if(0 > cli_fd)
{
perror("accept");
}
else
{
FD_SET(cli_fd,&reads);
if(cli_fd > max_fd)
{
max_fd = cli_fd;
}
}
}
for(int fd=3; fd <= max_fd; fd++)
{
if(FD_ISSET(fd,&reads_copy) && fd != svr_fd)
{
int ret = recv(fd,buf,sizeof(buf),0);
if(0 > ret)
{
printf("客户端%d退出\n",fd);
FD_CLR(fd,&reads);
continue;
}
printf("recv:%s bits:%d\n",buf,ret);
strcpy(buf,"return");
ret = send(fd,buf,strlen(fd,buf,strlen(buf)+1,0));
if(0 > ret)
{
printf("客户端%d退出\n",fd);
FD_CLR(fd,&reads);
continue;
}
}
}
}
}
}
- select
优点 | 缺点 |
---|
它是最早的多路复用的函数,几乎所有的操作系统都支持,程序的兼容性高 | 单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024。 | | 每次调用select时都需要重新向它传递被监控者合集 | | 调用结束后若想要知道哪些文件描述符发生了相关操作,需要把所有的被监控的文件描述符都测试一遍 |
- pselect
与select大致相同
区别 |
---|
超时时间的类型不同,pselect精度更高 | pselect的timeout既是输入也是输出,可以返回剩余的时间,但是select只是输入 | pselect监听时可以通过sigmask参数设置想要监听时屏蔽的信号,可以保证pselect的监听不被信号干扰 |
- poll
与select区别 |
---|
它没有最大连接数的限制,原因是它是基于链表来存储的 | 调用结束后若想要知道哪些文件描述符发生了相关操作,只需要把所有的被监控的文件描述符都测试一遍 |
- epoll
|