给群里写的,简单的EPOLLET 例子:
下面2个服务器例子 ,一个没有处理EPOLLOUT, 另一个处理了;
第一个例子只用来展示EPOLLET 的特点
第2个例子是一个echo服务器, 处理了EPOLLOUT,
如果要看EPOLLOUT的直接往下拉到第个例子, 主要还是看 errno == EAGAIN 这些地方
在ET模式下:
socket 一般情况下需要非阻塞的, 与EAGAIN 这个错误 配合使用.
如果是默认的阻塞socket, 不是不可以,而是你需要做额外的事, 每一次的read/write , 你都需要去getsockopt( sock,SO_ERROR,...)
直到获取 EAGAIN 算是正常读/写. 否则, 你完蛋了. 这个socket将不再收到 epoll 的触发条件了
对于EPOLLIN : 如果状态改变了[ 比如 从无到有],那么只要输入缓冲区可读就会触发
对于EPOLLOUT: 如果状态改变了[比如 从满到不满],只要输出缓冲区可写就会触发;
EPOLLOUT那么具体什么时间点触发呢
1. EPOLL_CTL_ADD或EPOLL_CTL_MOD 时 , 如果输出缓冲区状态改变了
2. 不论注册方式是 EPOLLIN | EPOLLOUT | EPOLLET 还是 EPOLLOUT | EPOLLET 只要含EPOLLOUT
只要状态改变就能触发.
状态改变到底是什么? 简单理解:
EPOLLOUT: 满->不满
EPOLLIN : 空->不空
有没有发现 , 都跟边缘有关
3.对于 send / write 需要依靠 EPOLLOUT 触发才调用吗 ? 什么时候需要 注册上 EPOLLOUT ?
不需要. 如果要 send / write 那么就直接调用, 如果返回值 > 0 , 证明数据已经复制到发送缓冲区中.一切正常.
如果 send / write 返回 < 0 且 errno == EAGAIN . 此时说明发送缓冲区满了. 那么需要把 剩余的字节保存起来,
然后注册上 EPOLLOUT , 直到epoll_wait 返回 , 说明发送缓冲区可写, 再把 之前保存起来的数据 发送,
如果此时 write 返回 > 0 那就把EPOLLOUT 取消掉.
简单来说 : 1. 直接发送 2. 看返回值, 没发送完才挂上EPOLLOUT 3. 发送完就把EPOLLOUT 取消掉
在LT模式下: 与select的触发条件一致
EPOLLIN: 可读就一直触发
EPOLLOUT:可写就一直触发
边缘的意思是只对客户端数据到达的次数感兴趣;
不会因为此次缓冲区没读完继续调用epoll_wait再次返回 <- 这种情况是select, poll 和 epoll的默认情况;
比如客户端发送了10个字节, 服务器端epoll_wait 返回,然后read读取数据,但由自定义缓冲区太小或输入缓冲区太小或其他网络原因,read函数现在只读了4个字节,那么还有6个字节在缓冲区内,这6个字节只能
等到下一次客户端再次发送数据时才有机会读到(而对于条件触发比如select和默认的epoll 会再次返回), 因此对于EPOLLET只能
使用nonblock去读取,才能完全读完输入缓冲区;
下面的第一个服务器代码中也同样让自定义接受缓冲区变小来显示epollET的特点;
边缘模式主要减少epoll_wait的返回次数,即减少内存拷贝;
一般的条件触发例如 select 和 没使用EPOLLET的epoll 都是只要接受缓冲区有数据就返回;
因此对于epollet 需要使用nonblock的socket, 否则无法简单的完整读取缓冲区内所有数据;
对监听sock和客户端sock 都使用了nonblock ;另外注意注意 不要对监听sock设置EPOLLET ,可能会丢失客户端连接;
除非用 while(1) accpet()... if(errno == EAGAIN) break;
util.h
这个只是包含了一些头文件 , 内部的函数在服务器代码中都没使用, 省的看起来麻烦
#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <sys/un.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/time.h>
#include <sys/epoll.h>
#include <signal.h>
#define EPOLL_SIZE 128
#define PORT 9988
#define BACKLOG 10
#define MAXLINE 4096
#define SA struct sockaddr
ssize_t readn(int fd , void *ptr, size_t n)
{
size_t left = n;
char * p = (char *)ptr;
int nread = 0;
while (left > 0){
if((nread = read(fd,p,left)) < 0){
if(errno == EINTR)
nread = 0;
else
return -1;
}
else if(nread == 0)
break;
left -= nread;
p += nread;
}
return n - left;
}
ssize_t writen(int fd , void * ptr , size_t n)
{
size_t left = n;
char * p = (char*)ptr;
int nwrite = 0;
while( left > 0){
if((nwrite = write(fd,p,left)) <= 0){
if(errno == EINTR && nwrite < 0)
nwrite = 0;
else
return -1;
}
left -= nwrite;
p += nwrite;
}
return n - left;
}
int conn_timeout(int sockfd , const struct sockaddr_in * sin, socklen_t socklen, int secs)
{
int old_flag = fcntl(sockfd,F_GETFL,0);
if( fcntl(sockfd,F_SETFL,old_flag|O_NONBLOCK) < 0){
perror("fcntl failed");
return -1;
}
int ret = 0;
if((ret = connect(sockfd,(SA*)&sin,socklen)) < 0){
if(errno != EINPROGRESS){
perror("connect error");
return -1;
}
}
if(0 == ret)
goto done;
fd_set rset,wset;
FD_ZERO(&rset);
FD_SET(sockfd,&rset);
wset = rset;
struct timeval timeout;
timeout.tv_sec= secs;
timeout.tv_usec = 0;
ret = select(sockfd+1,&rset,&wset,NULL,secs?&timeout:NULL);
if(ret < 0){
perror("select error");
return -1;
}
else if( 0 == ret){
close(sockfd);
return -1;
}
if(!(FD_ISSET(sockfd,&rset) || FD_ISSET(sockfd,&wset))){
return -1;
}
done:
fcntl(sockfd,F_SETFL,old_flag);
return 0;
}
第一个例子 : 只是用来展示 EPOLLET 特性的,因此 BUFF_SIZE = 4 特别小 , 如果要用telnet来测试,
尽量每次发送 4 字节以上数据
#include "util.h"
#define BUFF_SIZE 4
// 设置非阻塞
static int setnonblock(int fd){
int flag = fcntl(fd,F_GETFL,0);
return fcntl(fd,F_SETFL,flag|O_NONBLOCK);
}
//每个套接字关联的信息
typedef struct _ev_data
{
int fd; //套接字
struct sockaddr_in addr; //地址信息
} ev_data;
int main(int argc, char ** argv) {
int listensock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr, cli_addr;
socklen_t socklen = sizeof(serv_addr);
memset(&serv_addr, 0, socklen);
memset(&cli_addr, 0, socklen);
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(PORT);
serv_addr.sin_family = AF_INET;
if (bind(listensock, (SA *) &serv_addr, sizeof(serv_addr)) < 0) {
perror("bind");
return 0;
}
if (listen(listensock, BACKLOG) < 0) {
perror("listen");
return 0;
}
//epoll_size = 128, 此大小看操作系统
int epfd = epoll_create(EPOLL_SIZE);
if (epfd < 0) {
perror("epoll_create");
return 0;
}
setnonblock(listensock); //这行可以省略
struct epoll_event ev = {0, {0}};
ev.events = EPOLLIN ;
//创建一块内存,用于存放listenfd 的信息,对于客户端套接字也同样
ev_data * data = malloc(sizeof(ev_data));
data->fd = listensock;
data->addr = serv_addr;
ev.data.ptr = data;
//把此事件与套接字关联
epoll_ctl(epfd,EPOLL_CTL_ADD , listensock , &ev);
int nready = -1 , len = -1;
//需要自己创建一个event数组,epoll_wait返回时将把触发事件的socket 复制到这里
struct epoll_event * evts = malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
// 很小的缓冲区
char buff[BUFF_SIZE] ={0};
while(1)
{
//返回后触发事件的socket相关信息将被复制到 evts 数组
nready = epoll_wait(epfd,evts,EPOLL_SIZE, -1);
printf("epoll_wait return : %d\n" , nready);
for( int i =0 ; i < nready ; ++i){
//如果是输入事件
if( evts[i].events & EPOLLIN ){
//如果是监听套接字发生输入事件
if( ( (ev_data * )(evts[i].data.ptr) )->fd == listensock){
socklen = sizeof(cli_addr);
int clt_fd = accept(listensock,(SA*)&cli_addr,&socklen);
if( clt_fd < 0){
puts("accept failed");
continue;
}
setnonblock(clt_fd);//nonblock 别忘记了
ev.events = EPOLLIN | EPOLLET; // 设置边缘模式
ev_data * data = malloc(sizeof(ev_data));
data->addr = cli_addr;
data->fd = clt_fd;
ev.data.ptr = data;
epoll_ctl(epfd,EPOLL_CTL_ADD,clt_fd, &ev); //加入这个epoll
}
else{
// 如果是有数据来了
ev_data * pData = (ev_data * )(evts[i].data.ptr);
//由于是非阻塞,因此while
while(1) {
read_again:
len = read(pData->fd, buff, BUFF_SIZE - 1);
printf("read from ip:%s, port:%d\n" ,
inet_ntoa(pData->addr.sin_addr),
ntohs(pData->addr.sin_port));
//如果出错了
if (len < 0){
if(errno == EINTR)
goto read_again;
else if(errno == EAGAIN){
puts("read error , no more data!");
break;
}
else{
perror("read error , close socket . ");
epoll_ctl(epfd,EPOLL_CTL_DEL,
pData->fd,NULL);
close(pData->fd);
free(pData);
break;
}
}
//对端关闭
else if( 0 == len){
printf("socket : %d closed!\n" , pData->fd);
epoll_ctl(epfd,EPOLL_CTL_DEL,
pData->fd,NULL);
close(pData->fd);
free(pData);
break;
}
else{
buff[len] = 0;
printf("write buff:%s\n" , buff);
write(pData->fd,buff,len);
}
}
}
} else{
printf("epoll_wait returned , somethingelse happened nready:%d\n",
nready);
}
}
}
}
第2个例子: 处理EPOLLOUT的情况
EPOLLOUT 总的来说就是只要EPOLLIN被触发了, 并且输出缓冲区不满 也触发一次, 或者输出缓冲区由满到不满时也触发;
#define BUFF_SIZE 1024
#define MAX_EVENTS 1024
static int setnonblock(int fd , int nonblock){
int flag = fcntl(fd,F_GETFL,0);
if(nonblock)
return fcntl(fd,F_SETFL,flag|O_NONBLOCK);
else
return fcntl(fd,F_SETFL,flag&~O_NONBLOCK);
}
//每个套接字关联的信息
typedef struct _ev_data
{
int fd; //socket
char * buffer; //接受缓冲区
int nread; //读了N个字节
} ev_data;
void free_event(ev_data * pEv){ //释放内存用的
if(!pEv)
return;
if(pEv->buffer){
free(pEv->buffer);
}
free(pEv);
}
int main(int argc, char ** argv) {
signal(SIGPIPE, SIG_IGN);
int listensock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in serv_addr, cli_addr;
socklen_t socklen = sizeof(serv_addr);
memset(&serv_addr, 0, socklen);
memset(&cli_addr, 0, socklen);
serv_addr.sin_addr.s_addr = INADDR_ANY;
serv_addr.sin_port = htons(PORT);
serv_addr.sin_family = AF_INET;
int on = 1;
setsockopt(listensock,SOL_SOCKET,SO_REUSEADDR,&on,sizeof(int));
if (bind(listensock, (SA *) &serv_addr, sizeof(serv_addr)) < 0) {
perror("bind");
return 0;
}
if (listen(listensock, BACKLOG) < 0) {
perror("listen");
return 0;
}
/* 以上都是初始化 没什么好说的*/
//也可以用 epoll_create1() ,MAX_EVENTS这个常量没什么用
int epfd = epoll_create(MAX_EVENTS);
if (epfd < 0) {
perror("epoll_create");
return 0;
}
setnonblock(listensock,1);
struct epoll_event ev = {0, {0}};
ev.events = EPOLLIN ; //输入事件
ev_data * data = malloc(sizeof(ev_data));
data->fd = listensock;
data->buffer = NULL;
//放入ptr中
ev.data.ptr = data;
//加到epoll
epoll_ctl(epfd,EPOLL_CTL_ADD , listensock , &ev);
int nready = -1 , len = -1;
//存放返回事件用的
struct epoll_event * evts = calloc(MAX_EVENTS, sizeof(struct epoll_event));
while(1)
{
nready = epoll_wait(epfd,evts,MAX_EVENTS, -1);
printf("\nepoll_wait return : %d\n" , nready);
if(nready < 0){
perror("epoll_wait");
break;
}
for( int i =0 ; i < nready ; ++i){
printf("evts[%d].events = %d\n" , i,evts[i].events);
//如果是这2个出错事件, 就把套接字给关了
if( ( evts[i].events & EPOLLERR ) ||
evts[i].events & EPOLLHUP){
printf("i=%d, occurs err, events:%d\n",
i, evts[i].events);
ev_data * pData = (ev_data * )(evts[i].data.ptr);
if(pData){
evts[i].data.ptr = NULL;
close(pData->fd);
free_event(pData);
}
continue;
}
//如果是输入事件
if( evts[i].events & EPOLLIN ){
if( !evts[i].data.ptr){
puts("ptr is empty");
continue;
}
//如果是监听套接字发生输入事件
if( ( (ev_data * )(evts[i].data.ptr) )->fd == listensock){
socklen = sizeof(cli_addr);
/*
如果监听sock 也是EPOLLET的话, 需要while(1) accept
*/
int clt_fd = accept(listensock,(SA*)&cli_addr,&socklen);
if( clt_fd < 0){
puts("accept failed");
continue;
}
setnonblock(clt_fd,1);//nonblock
ev.events = EPOLLIN | EPOLLOUT |EPOLLET; // 注册2个事件
ev_data * data = malloc(sizeof(ev_data));
data->fd = clt_fd;
data->nread = 0;
data->buffer = malloc(BUFF_SIZE); //创建一个接受缓冲区
ev.data.ptr = data;
epoll_ctl(epfd,EPOLL_CTL_ADD,clt_fd, &ev);
puts("\taccept success");
}
else{
// 如果是有数据来了
printf("\tdata is comming, u can read !\n");
ev_data * pData = (ev_data * )(evts[i].data.ptr);
if(!pData){
printf("read error ! ptr is empty\n");
continue;
}
//下面接受数据
int left = BUFF_SIZE - pData->nread;
char * pbuff = pData->buffer + pData->nread;
while(left > 0 )
{
len = read(pData->fd, pbuff,left);
if (len < 0){
if(errno == EINTR)
len = 0;
//sock输入缓冲区空了
else if(errno == EAGAIN || errno == EWOULDBLOCK){
printf("read error , no more data! len:%d\n" , pData->nread);
break;
}
else{
//出错了
perror("read error , close socket . ");
epoll_ctl(epfd,EPOLL_CTL_DEL,
pData->fd,NULL);
evts[i].data.ptr = NULL;
close(pData->fd);
free_event(pData);
break;
}
}
else if( 0 == len){
//客户端断开了
printf("socket : %d closed!\n" , pData->fd);
//EPOLL_CTL_DEL 这行可以不写, close() 将把epoll中的fd移除
epoll_ctl(epfd,EPOLL_CTL_DEL,
pData->fd,NULL);
evts[i].data.ptr = NULL;
close(pData->fd);
free_event(pData);
break;
}
pbuff += len;
left -= len;
pData->nread += len;
}
printf("-> nread: %d\n" , pData->nread);
}
}
if(evts[i].events & EPOLLOUT){
// 如果有输出事件
printf("\t****u can write now!! !\n");
ev_data * pData = (ev_data * )(evts[i].data.ptr);
/*
这里做判断是, EPOLLIN也将附带着触发一次EPOLLOUT,如果sock的输入
缓冲区不满的话
*/
if(!pData){
puts("oh no ... socket is closed");
continue;
}
char * p = pData->buffer;
int nwrite = 0;
int left = pData->nread;
//如果接受缓冲区没数据,就直接返回了
if(0 == left){
puts("buff is empty!");
continue;
}
int write_bytes = 0;
while( left > 0){
write_again:
nwrite = write(pData->fd,p,left);
if(nwrite < 0){
if(errno == EINTR)
goto write_again;
//如果输出缓冲区满了
else if( EAGAIN == errno || EWOULDBLOCK == errno){
perror("sock buff is full , write error :");
break;
}
else {
perror("other write error :");
close(pData->fd);
free_event(pData);
pData = NULL;
break;
}
}
p+= nwrite;
left -= nwrite;
write_bytes += nwrite;
}
//这里主要是为了echo 客户端做的
if( write_bytes > 0 && pData){
memcpy(pData->buffer,pData->buffer + write_bytes ,pData->nread - write_bytes);
pData->nread -= write_bytes;
}
printf("break on write , write_bytes : %d left bytes:%d\n" , write_bytes,
pData->nread);
}
}
}
}