一:TCP通信的CS模型
将TCP通信的CS(服务端、客户端)模型按照解答电话的过程做一个介绍
Server | Client | ||
Socket() | 买电话 | Socket() | 买电话 |
bind() | 绑定电话卡 | bind() | 绑定电话卡 |
listen() | 监听 | ||
accept() | 接电话 | Connect() | 打电话 |
send()/recv() | 接发消息 | send()/recv() | 接发消息 |
close() | 挂电话 | close() | 挂电话 |
二:io多路复用
网络编程多路复用主要技术点select、poll、epoll;其中select和poll底层实现是一致的。
常用宏定义:
FD_CLR(inr fd, fd_set *fdset);用来清除描述符集合fdset中的描述符fd
FD_ISSET(int fd,f d_set *fdset);用来检测描述符集合fdset中的描述符fd是否发生了变化
FD_SET(int fd, fd_set *fdset);用来将描述符fd添加到描述符集合fdset中
FD_ZERO(fd_set *fdset);用来清除描述符集合fdset
1:select、pool
函数原型及参数解析
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
//nfds:文件描述符的范围,因为文件描述符从0开始(0、1、2被标准输入、标准输出、标准错误占用),所以这里的参数为最大文件描述符+1
/*
readfds:用来监控可读类型文件描述符的集合
writefds:用来监控可写类型文件描述符的集合
exceptfds:监控错误文件类型文件描述符的集合,判断io是否出错
timeout:超时时间
NULL:函数阻塞,直到监控的文件描述符集合中某个文件描述符发生变化为止;
0:函数不阻塞,无论是否有文件描述符发生变化,都会返回继续执行后续操作;
>0:超时时间,在超时时间内阻塞
*/
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
/*
fds:监听的文件描述符数组
struct pollfd {
int fd; //待监听的文件描述符
short events; //待监听的文件描述符对应的监听事件,取值为:POLLIN、POLLOUT、POLLERR
short revents; //返回的事件信息,传入时,给0。如果满足对应事件的话,返回非0-->POLLIN、POLLOUT、POLLERR
};
nfds:要监控的文件描述符范围,最大文件描述符+1
timeout:超时时间
-1:阻塞等待,有事件之前,永远等待;
0:不阻塞,立即返回
>0:超时时长。单位:毫秒;
*/
select、pool底层分析:
select\poll将需要监控的集合copy到内核当中,内核会轮循监控的集合直到有文件描述符发生变化返回。
select缺点:
- select函数参数多;
- 需要将整个reset集合拷贝到内核,reset是个位图,会有无效拷贝;
- 对io的数量有限制,限制在1024(位图);
- 每次要遍历集合,而返回就绪集合。
poll缺点:
因为poll和select的底层实现是一样的,所以其缺点是一致的;
但底层中select使用位图,而poll使用链表,所以对io的数量没有限制。
select示例
poll示例
2:epoll
API及参数分析
int epoll_create(int size);
/*
这里的size无效,只要>0即可
返回值:epoll模型的文件描述符,也可以说是底层红黑树根节点的值
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
epfd: epoll模型的返回文件描述符
op: 操作类型
EPOLL_CTL_ADD:向epfd中添加一个待监听的文件描述符fd(将fd添加到红黑树中)
EPOLL_CTL_MOD:修改已经存在于epfd中的文件描述符fd的事件
EPOLL_CTL_DEL:从epfd中删除一个监听的文件描述符fd。
fd: 需要监听的文件描述符
event: 需要监听的事件
events:表示需要监听的事件类型,可以是EPOLLIN(可读)、EPOLLOUT(可写)等等组合。
data:用户数据,在事件触发时会被返回给调用者。
返回值:0:成功 -1:失败
*/
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
/*
epfd: epoll模型的返回文件描述符
events: 用来接收返回的事件,一般都是一个数组,数组长度大于等于maxevents。
maxevents: 期望监听的最大事件数量
timeout: 超时时间,同select中的timeout
返回值:
成功:有事件发生的IO个数
0: 无事件发生或超时
-1: 发生错误
*/
底层分析
从底层来看,epoll_create创建了一个epoll模型,epoll模型由红黑树来维护;其中rbr为红黑树的根节点节点,红黑树的每个节点表示一个文件描述符,其中根节点文件描述符符为4(0-3被标准输入、输出、错误,以及要监听的socketfd占用);
epoll_ctl函数将需要监听的文件描述符加入到红黑树中,当监听的某个 socket 有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用 epoll_wait 函数时,只会返回有事件发生的文件描述符的个数;
epoll示例
epoll相较于select和poll的优点
- epoll不需要像select和poll一样将数据集合copy到内核,内核轮询监听的文件集合,再将文件集合copy到用户,减少了内核态和用户态的交互;
- epoll也没有select的io数量限制
- 底层使用红黑树维护,效率更高
水平触发EPOLLLT和边沿触发EPOLLET
epoll默认是LT模式。
水平触发:
只要缓冲区有数据,水平触发会一直读取,直到缓冲区数据为空;
边沿触发:
只触发一次。
使用epoll水平触发模式,当socket可写时,会不停触发socket可写事件,如何处理?
1:需要向 socket 写数据的时候才把 socket 加入 epoll ,等待可写事件。接受到可写事件后,调用 write 或者 send 发送数据。当所有数据都写完后,把 socket 移出 epoll。
这种方式的缺点是,即使发送很少的数据,也要把 socket 加入 epoll,写完后在移出 epoll,有一定操作代价。
2:开始不把 socket 加入 epoll,需要向 socket 写数据的时候,直接调用 write 或者 send 发送数据。如果返回 EAGAIN,把 socket 加入 epoll,在 epoll 的驱动下写数据,全部数据发送完毕后,再移出 epoll。
这种方式的优点是:数据不多的时候可以避免 epoll 的事件处理,提高效率。