一、select模型
1.函数参数含义
int select(int maxfd,fd_set *rset,fd_set *wset,fd_set *eset,struct timeval *timeout);
将需要检测的fd_set集合(参数二、三、四)拷贝到内核,如果有io就绪清空fd_set集合,重新设置fd_set集合,在拷贝到用户层。
参数一:判断最大fd的值 方便内部循环判断fd是否io就绪
参数二:文件描述符可读
参数三:文件描述符可写
参数四:文件描述符异常
参数五:多久轮询一次 NULL:阻塞 0:立即返回不阻塞进程
返回值:大于0表示有io就绪事件
代码实现:
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
seraddr.sin_port = htons(2048);
if(-1==bind(sockfd,(struct sockaddr*)&seraddr,sizeof(struct sockaddr)))
{
perror("bind");
return -1;
}
printf("----bind\n");
listen(sockfd,10);
fd_set rfds,rset;// 一个设置 一个读判断
FD_ZERO(&rfds);
FD_SET(sockfd,&rfds);
int maxfd = sockfd;
while(1)
{
rset = rfds;
int nready = select(maxfd+1,&rset,NULL,NULL,NULL);//第五个参数为空表示一直等待
if(FD_ISSET(sockfd,&rset))//判断是否有连接
{
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd,(struct sockaddr*)&clientaddr,&len);
printf("accept\n");
FD_SET(clientfd,&rfds);
if (clientfd > maxfd) maxfd = clientfd;
}
int i=0;
for(i=sockfd+1;i<=maxfd;i++)
{
if(FD_ISSET(i,&rset))//读事件
{
char buffer[128] = {0};
int count = recv(i,buffer,sizeof(buffer),0);
if(count == 0){
FD_CLR(i,&rfds);
close(i);
}
if(count > 0)
printf("sockfd: %d,count: %d,buffer:%s\n",sockfd,count,buffer);
}
}
}
select缺点
1.参数较多 不利于维护理解
2.对io的数量是有限制的 linux1024个,windows64个。
3.每次都需要拷贝待检测的io集合
4.每次需要遍历io集合,而返回就绪集合
二、poll模型
1.函数参数含义
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数一:struct pollfd{
int fd; 文件描述
short events;//带入的参数 POLLIN \ POLLOUT…
short revents;//返回的信息 POLLIN \ POLLOUT…
}
参数二:最大fd的值 方便内部循环判断fd是否io就绪
参数三:多久轮询一次 。NULL:阻塞 0:立即返回不阻塞进程
/*struct pollfd{
int fd;
short events;//带入的参数
short revents;//返回的信息
}*/
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
seraddr.sin_port = htons(2048);
if(-1==bind(sockfd,(struct sockaddr*)&seraddr,sizeof(struct sockaddr)))
{
perror("bind");
return -1;
}
printf("----bind\n");
listen(sockfd,10);
struct pollfd fds[1024] = {0};
fds[sockfd].fd=sockfd;
fds[sockfd].events = POLLIN;
int maxfd = sockfd;//遍历次数少一些 fd按递增的
while(1)
{
int nready = poll(fds,maxfd+1,-1);
if(fds[sockfd].revents & POLLIN)
{
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd,(struct sockaddr*)&clientaddr,&len);
printf("accept\n");
fds[clientfd].fd = clientfd;
fds[clientfd].events = POLLIN;
if (clientfd > maxfd) maxfd = clientfd;
}
int i = 0;
for (i = sockfd+1;i <= maxfd;i ++) {
if (fds[i].revents & POLLIN) {
char buffer[128] = {0};
int count = recv(i,buffer,sizeof(buffer),0);
if (count == 0) {
fds[i].fd = -1;
fds[i].events = 0;
close(i);
break;
}
if(count > 0)
printf("sockfd: %d,count: %d,buffer:%s\n",sockfd,count,buffer);
}
}
}
poll 与select类似也是采用轮询的方式,poll对文件描述符没有1024个限制,使用链表存储fd
三、epoll模型
1.函数含义
int epoll_create(int size);//参数没有意义 大于0就行
创建一个新的epoll实例并返回一个引用该实例的文件描述符,会创建一个红黑树和io就绪队列。
epoll_crl(int epfd,int op,int fd,struct epoll_event *event);
添加修改删除特定文件描述符到epoll集中(红黑树)。
参数一:epoll_create创建的fd返回值
参数二:EPOLL_CTL_ADD 添加fd到红黑树
EPOLL_CTL_DEL 删除红黑树上fd的值
EPOLL_CTL_MOD 修改红黑树上fd的值
参数三:具体的网络fd,scokfd,clientfd
参数四: typedef union epoll_data {
void ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;//保存触发事件的某个文件描述符相关的数据
struct epoll_event {
__uint32_t events; / epoll event / EPOLLIN读 EPOLLOUT写 EPOLLLT水平触发 EPOLLET边缘触发
epoll_data_t data; / User data variable */
};
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待I/O事件,如果当前没有可用的事件,则阻塞调用线程。
参数一:epoll_create创建的fd返回值
参数二:接口的返回参数,epoll把发生的事件的集合从内核复制到 events数组中
参数三:与参数二预分配的数组的大小是相等的
参数四:表示在没有检测到事件发生时最多等待的时间,超时时间(>=0),单位是毫秒ms,-1表示阻塞,0表示不阻塞
返回值:成功返回需要处理的事件数目。失败返回0,表示等待超时。
//面向io处理: 通过listenfd进行accept 通过clientfd进行读写
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <pthread.h>
#include <sys/poll.h>
#include <sys/epoll.h>
int main()
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
if(sockfd < 0)
{
perror("socket");
return -1;
}
struct sockaddr_in seraddr;
seraddr.sin_family = AF_INET;
seraddr.sin_addr.s_addr = htonl(INADDR_ANY);
seraddr.sin_port = htons(2048);
if(-1==bind(sockfd,(struct sockaddr*)&seraddr,sizeof(struct sockaddr)))
{
perror("bind");
return -1;
}
printf("----bind\n");
listen(sockfd,10);
int epfd = epoll_create(1);
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&ev);
struct epoll_event events[1024] = {0};
while(1)
{
int nready = epoll_wait(epfd,events,1024,-1);
if (nready < 0) continue;
int i = 0;
for(i = 0;i < nready;i++)
{
int connfd = events[i].data.fd;
if(sockfd == connfd) //处理listenfd 面向io分类
{
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd,(struct sockaddr*)&clientaddr,&len);
printf("accept\n");
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev);
}
else if(events[i].events & EPOLLIN) //处理clientfd
{
char buffer[128] = {0};
int count = recv(connfd,buffer,sizeof(buffer),0);
if(count == 0)
{
epoll_ctl(epfd,EPOLL_CTL_DEL,connfd,NULL);
close(i);
}
if(count > 0)
printf("sockfd: %d,count: %d,buffer:%s\n",sockfd,count,buffer);
continue;
}
}
}
}
2.epoll水平触发LT(默认触发方式): io缓冲区有数据或有空间写就会一直触发, 不断的通过epoll_wait()返回
测试:服务器每次接受10个字节,客户端每次发送32字节
while(1)
{
int nready = epoll_wait(epfd,events,1024,-1);
printf("epoll_wait\n");
if (nready < 0) continue;
int i = 0;
for(i = 0;i < nready;i++)
{
int connfd = events[i].data.fd;
if(sockfd == connfd) //处理listenfd 面向io分类
{
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd,(struct sockaddr*)&clientaddr,&len);
printf("accept\n");
ev.events = EPOLLIN;
ev.data.fd = clientfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev);
}
else if(events[i].events & EPOLLIN) //处理clientfd
{
char buffer[10] = {0};//每次接受10个数据
int count = recv(connfd,buffer,sizeof(buffer),0);
if(count == 0)
{
epoll_ctl(epfd,EPOLL_CTL_DEL,connfd,NULL);
close(i);
}
if(count > 0)
printf("sockfd: %d,count: %d,buffer:%s\n",sockfd,count,buffer);
continue;
}
}
}
3.epoll边缘触发ET: io缓冲区状态变化就会触发,读:来数据就触发一次,写:写缓冲区从满到有空间才会触发一次
测试:服务器每次接受10个字节,客户端每次发送32字节
while(1)
{
int nready = epoll_wait(epfd,events,1024,-1);
if (nready < 0) continue;
int i = 0;
for(i = 0;i < nready;i++)
{
int connfd = events[i].data.fd;
if(sockfd == connfd)
{
struct sockaddr_in clientaddr;
socklen_t len = sizeof(clientaddr);
int clientfd = accept(sockfd,(struct sockaddr*)&clientaddr,&len);
printf("accept\n");
ev.events = EPOLLIN | EPOLLET;//设置边缘触发
ev.data.fd = clientfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev);
}
else if(events[i].events & EPOLLIN)
{
char buffer[10] = {0};
int count = recv(connfd,buffer,sizeof(buffer),0);
if(count == 0)
{
epoll_ctl(epfd,EPOLL_CTL_DEL,connfd,NULL);
close(i);
}
if(count > 0)
printf("sockfd: %d,count: %d,buffer:%s\n",sockfd,count,buffer);
continue;
}
}
}
点击再次发送,继续读出10个字节
通过循环读取解决读不完整情况
while((count = recv(connfd,buffer,sizeof(buffer),0)) > 0)
{
if(count == 0)
{
epoll_ctl(epfd,EPOLL_CTL_DEL,connfd,NULL);
close(i);
}
else if(count < 0)
{
if (errno == EAGAIN) {
break;
}
epoll_ctl(epfd,EPOLL_CTL_DEL,connfd,NULL);
close(i);
}
else if(count > 0)
{
printf("sockfd: %d,count: %d,buffer:%s\n",sockfd,count,buffer);
memset(buffer,0,sizeof(buffer));
}
continue;
}
4.LT与ET应用场景
tcp协议有粘包的过程,通常在业务数据包首部加个长度或者加分隔符(redis)
以定义包长为例,读两次,先读长度,在读内容
short length = 0;
recv(fd,&length ,2,0);
length = ntohs(length);
recv(fd,buffer,length,0);
redis协议格式如下:
*3\r\n
$3\r\n //长度
SET\r\n //内容
$6\r\n //长度
mykey\r\n
$5\r\n //长度
12345\r\n
水平触发:适合上述情况
边缘触发:一次性数据读不完的情况 比如发送一个大文件
5.一次发送一个大文件(超过读写缓冲区,读写缓冲区都设置为16384字节)
#写缓冲区大小
$ sysctl net.ipv4.tcp_wmem
net.ipv4.tcp_wmem = 4096 16384 4194304
#更改写缓冲区大小
$ sysctl -w net.ipv4.tcp_wmem="4096 16384 8388608"
#读缓冲区大小
sysctl net.ipv4.tcp_rmem
net.ipv4.tcp_rmem = 4096 131072 6291456
#更改发送缓冲区大小
$ sysctl -w net.ipv4.tcp_rmem="4096 16384 8388608"
ET模式下,发送47411字节数据,只调用一次epoll_wait
LT模式下,发送47411字节数据,会多次调用epoll_wait