在写TCP的基本通信流程时,由于accept()函数的性质,在单执行流的程序里无法实现多人个持续通信,因此引入了多进程和多线程的方法,但这种方法并不利于并发。由此引入了多路转接IO。
原理:阻塞监视(等待)多个文件描述符的就绪状态,只要有至少一个文件文件描述符就绪,立马拷贝。
int select(int nfds, fd_set *readfds, fd_set *writefds,\
fd_set *exceptfds, struct timeval *timeout);
nfds:最大文件描述符的值+1;
fd_set *readfds:读事件集合(接收缓冲区就绪了,就是来东西了)
fd_set *writefds:写事件集合(发送缓冲区有空闲了)
fd_set *exceptfds:异常事件集合(套接字描述符异常关闭了)
struct timeval *timeout :
阻塞:NULL
非阻塞:0{秒,微秒}
return :返回就绪个数
失败:-1
struct fd_set
typedef long int __fd_mask;
/* It's easier to assume 8-bit bytes than to get CHAR_BIT. */
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
#define __FDELT(d) ((d) / __NFDBITS)
#define __FDMASK(d) ((__fd_mask) 1 << ((d) % __NFDBITS))
/* fd_set for select and pselect. */
typedef struct
{
/* XPG4.2 requires this member name. Otherwise avoid the name
from the global namespace. */
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
/* Maximum number of file descriptors in `fd_set'. */
#define FD_SETSIZE __FD_SETSIZE //__FD_SETSIZE等于1024
可以看出 __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];就是一个可以融纳1024个bit位的位图。
/* Access macros for `fd_set'. */
#define FD_SET(fd, fdsetp) __FD_SET (fd, fdsetp)//加入
#define FD_CLR(fd, fdsetp) __FD_CLR (fd, fdsetp)//去除
#define FD_ISSET(fd, fdsetp) __FD_ISSET (fd, fdsetp)//判断fd是否在此集合中
#define FD_ZERO(fdsetp) __FD_ZERO (fdsetp)//清空
#include
#include
#include
#include
#include
#include
#include
int main()
{
//新建fd
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd<0)
{
perror("fd\n");
}
//绑定
struct sockaddr_in addr;
addr.sin_port = htons(9997);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr=inet_addr("0.0.0.0");
if(bind(fd,(sockaddr*)&addr,sizeof(addr))<0)
{
perror("bind");
}
//listen
if(listen(fd,5)<0)
{
perror("listen\n");
}
//添加事件结构
fd_set read_;
FD_ZERO(&read_);
FD_SET(fd,&read_);
int nfds = fd+1;
while(1)
{
fd_set tmp = read_;
int fs = select(nfds,&tmp,NULL,NULL,0);
if(fs < 0)
{
//错误
continue;
}
if(fs == 0) continue;
//开始遍历
for(int i = 0;i < nfds; i++)
{
if(FD_ISSET(i,&tmp))//文件描述符 i 有响应
{
if(i==fd)//侦听套接字有反应
{
struct sockaddr_in add;
socklen_t len = sizeof(add);
int newfd = accept(fd,(sockaddr*)&add,&len);
if(newfd<0)
{
perror("accept");
continue;
}
FD_SET(newfd,&read_);
std::cout<<"accept\n";
nfds = newfd+1>nfds?newfd +1:nfds;
continue;
}
/newfd 有情况
char buf[1024]={0};
ssize_t n = recv(i,buf,1024,0);
if(n<0) continue;
else if(n==0)
{
FD_CLR(i,&read_);
close(i);
continue;
}
printf("%s\n",buf);
std::cout<<"re send:";
memset(buf,0,1024);
std::cin>>buf;
send(i,buf,1024,0);
}
}
}
close(fd);
return 0;
}
原理:
每创建一个epoll,内核都创建一个eventpoll结构体。可以利用epoll_ctl函数对齐操作。
在epoll中,每个事件都有一个epitem结构体。每添加一个事件都会挂载在红黑树中,且每个添加进来的事件都会和网卡建立回调,若有相应会将这个事件放到一个双向链表中,epoll_wait()只需要检查这个双向链表是否为空,不为空就将这些事件赋值給用户态。
struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}
//创建一个epoll
int epoll_create(int size);
size>0 已经弃用
return epoll的操作句柄
//操作epoll
int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event);
opfd:epoll 操作句柄
op:
EPOLL_CTL_ADD: 添加一个文件描述符到epoll中
EPOLL_CTL_MOD:修改...
EPOLL_CTL_DEL:删除..
fd:待处理的操作句柄
event:fd对应的事件结构
//epoll等待
int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout)
events:是事件结构的结构体数组,不能为空,内核将数据赋值进去。
maxevents:一次最多获取的事件结构数量。
timeout:>0 超时事件 ;=0 非阻塞;<0 阻塞;
return :就绪的fd个数;=0 超时;<0 错误;
struct epoll_event{
uint32_t events;
epoll_data_t data;
}
events:关心的事件:
EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭);
EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要再次把这个socket加入到EPOLL队列里
typedef union epoll_data{
void* ptr;
int fd;//方便用户使用
uint32_t u32;
uint64_t u64;
}epoll_data_s;
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
//新建fd
int fd = socket(AF_INET,SOCK_STREAM,0);
if(fd<0)
{
perror("fd\n");
}
//绑定
struct sockaddr_in addr;
addr.sin_port = htons(9997);
addr.sin_family = AF_INET;
addr.sin_addr.s_addr=inet_addr("0.0.0.0");
if(bind(fd,(sockaddr*)&addr,sizeof(addr))<0)
{
perror("bind");
}
//listen
if(listen(fd,5)<0)
{
perror("listen\n");
}
//建立epoll操作句柄
int epfd = epoll_create(9);
if(epfd<0)
{
printf("create\n");
}
//给侦听套接字准备一份事件结够
epoll_event listen_fd;
listen_fd.events = EPOLLIN;
listen_fd.data.fd = fd;
//把fd添加到epoll
epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&listen_fd);
while(1)
{
struct epoll_event se[9];
int nfd = epoll_wait(epfd,se,9,-1);
if(nfd<0) {perror("wait\n");break;}
if(nfd == 0) continue;
for(int i = 0; i < nfd; i++)
{
if(se[i].data.fd==fd)//侦听
{
int newfd = accept(fd,NULL,NULL);
if(newfd<0) continue;
struct epoll_event ee;
ee.events=EPOLLIN;
ee.data.fd = newfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,newfd,&ee);
continue;
}
recv
char buf[1024]={0};
ssize_t n = recv(se[i].data.fd,buf,1024,0);
if(n<0) continue;
else if(n==0)
{
epoll_ctl(epfd,EPOLL_CTL_DEL, se[i].data.fd,NULL);
close(se[i].data.fd);
continue;
}
printf("%s\n",buf);
std::cout<<"re send:";
memset(buf,0,1024);
std::cin>>buf;
send(se[i].data.fd,buf,1024,0);
}
}
close(fd);
close(epfd);
return 0;
}
也叫条件触发,是默认的触发方式,只要缓冲区不空就返回读就绪,只要缓冲区不满就返回写就绪。
读事件
- 缓冲区由空变为不空时(不可变可读)
- 有新数据到达时
- 缓冲区有数据可读且对应的放fd的events修改位EPOLLIN时。
写事件:
- 缓冲区由满变为空时,(不可写变可写时)
- 有数据被发送时
- 缓冲区有空间且对应的放fd的events修改位EPOLLOUT时。
1. 边沿触发只通知一次的问题:
因为阻塞IO不保证一次处理完,如果这次每有处理完,而且后来再没有相应的事件触发,那么就再也无法处理了。
因此:必须使用非阻塞IO:
2. 循环读取无法确定退出条件问题:
对于 recv ,write ,read, send
阻塞:<0 出错; =0 连接关闭; >0 处理的数据量
非阻塞:<0 :erron = EINTR||EWOULDBLOCK||EAGAIN 都是连接正常的
EINTR(操作信号被中断)
EWOULDBLOCK||EAGAIN (缓冲区空/满)