在网络程序里面,通常都是一个服务器处理多个客户机。为了处理多个客户机的请求,服务器端的程序有不同的处理方式。
阻塞I/O模式是最普遍使用的I/O模式,大部分程序使用的都是阻塞模式的I/O
缺省情况下,套接字建立后所处于的模式就是阻塞I/O模式
例:
read(); recv(); recvfrom(); write(); send(); accept(); connect();
- fcntl() 函数:操作文件描述符
当一开始建立一个套接字描述符的时候,系统内核将其设置为阻塞IO模式。
使用函数fcntl() 设置一个套接字的标志为O_NONBLOCK来实现非阻塞。
#include#include int fcntl(int fd, int cmd, ... /* arg */ ); 参数: fd:文件描述符 cmd:命令 返回值: /* eg. */ int flag = fcntl(fd, F_GETFL, 0); //获得属性 flag = flag | O_NONBLOCK; //修改属性,添加非阻塞 fcnl(fd, F_SETFL, flag); //设置属性
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
应用程序中同时处理多路输入输出流:
- 若设置多个进程,分别处理一条数据通路,将新产生进程间的同步与通信问题,使程序变得更加复杂;
- 若采用阻塞模式,将得不到预期目的;
- 若采用非阻塞模式,对多个输入进行轮询,又太浪费CPU世间
比较好的方法,使用 IO多路复用
基本思想
先构造一张有关描述符的表fd_set,
然后函数select,让内核帮助上层用户循环检测是否有可操作的文件描述符,如果有则告诉应用程序去操作
用于探测多个文件句柄的状态变化.只适用于“短作业”处理
#include
#include
#include
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
参数:
nfds: 最大文件描述符+1
readfds: 读事件的表(要读的文件描述符集合)
writefds: 写事件的表
exceptfds: 异常事件的表
timeout: 超时检测
NULL: 一直阻塞,直到文件描述符就绪或出错
0: 仅检测文件描述符状态,然后立即返回
不为0: 在指定时间内,若没有事件发生,则超时返回
返回值:
成功返回准备好的文件描述符个数,失败返回 -1;
select函数调用完成后,fd_set变量将发生变化。除了发生了改变的文件描述符外,其他原来为 1 的变为 0
相关辅助函数:
void FD_CLR(int fd, fd_set *set);//删除表中一个fd
int FD_ISSET(int fd, fd_set *set);//检测是否准备好,是 返回1 ,否返回 0
void FD_SET(int fd, fd_set *set);//加入fd到表
void FD_ZERO(fd_set *set);//清空表,将fd_set变量所指的位全部初始化为 0
- 内核使用轮询的方式来检查文件描述符集合中的描述符是否就绪,文件描述符越多,消耗的时间资源越多
- 文件描述符集合使用的数组,有大小限制 0-1024
- 每次文件描述符集合更新时,重新拷贝到内核中
struct pollfd {
int fd; /* file descriptor 文件描述符*/
short events; /* requested events 请求事件(读、写、异常)*/
short revents; /* returned events 对请求事件的反馈*/
};
/*eg.*/
struct pollfd fds[];
fds[0].fd;
fds[0].events;
//POLLIN: 读事件
//POLLOUT: 写事件
fds[0].revents;
#include
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数:
fds: 文件描述符表
nfds: 文件描述符表中文件描述符个数
timeout:超时检测
-1 阻塞,直到文件描述符准备好
返回值:
成功返回准备好的文件描述符的个数,失败返回 -1;
相关函数:epoll_create(); epoll_ctl(); epoll_wait();
#include
int epoll_create(int size);
功能:
创建一个树
参数:
size:树的结点
返回值:
成功返回 红黑树描述符epfd ,失败返回 -1;
-------------相关数据结构--------------------------------------
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
--------------------------------------------------------------
#include
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:
控制 epoll
参数:
epfd: epoll_create()的返回值
op: 指令
EPOLL_CTL_ADD: 添加文件描述符
EPOLL_CTL_DEL: 删除文件描述符
fd: 准备好的文件描述符
event: 结构体
/* eg. */
struct epoll_event ev;
ev.events; //事件 EPOLLIN 读事件;EPOLLOUT 写事件;
ev.data.fd;//文件描述符
返回值:
成功返回 0,失败返回 -1;
#include
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
功能:等待文件描述符就绪
参数:
epfd: epoll_create()返回值
events: 结构体数组
struct epoll_event evs[SIZE];
maxevents: SIZE 结构体数组最大值
timeout: 超时检测
-1: 阻塞,等待
返回值:
成功返回准备好的文件描述符的个数 ,失败返回 -1;
在网络程序里面,通常都是一个服务器处理多个客户机。为了处理多个客户机的请求,服务器端的程序有不同的处理方式。
循环服务器在同一时刻只能相应一个客户端的请求。
TCP服务器一般很少采用循环服务器模型。如果某个客户端一直占用服务器资源,那么其他的客户端都不能被处理。
除非UDP服务器在处理某个客户端的请求时所用时间比较长,UDP服务器实际上很少使用并发服务器模型。
并发服务器在同一个时刻可以相应多个客户端的请求。
多进程并发服务器:
- 基本原理:
每连接一个客户端,创建一个子进程,子进程负责处理connfd(客户请求),父进程处理sockfd(连接请求)。
- 细节处理:
回收子进程资源
多线程并发服务器:
- 基本原理:
每连接一个客户端,创建一个子线程,子线程负责处理connfd(客户请求),主线程处理sockfd(连接请求)。
- 注意:子线程结束时,进行资源回收
pthread_self 获取线程自身id