LT和ET的
我们在做epoll网络编程的时候,可以选择LT(水平触发)或者ET(边缘触发)。
epoll默认就是水平触发,水平触发也不需要什么特别的设置。所以这里主要研究一下边缘触发怎么弄。
边缘触发(edge-triggered)
什么是边缘触发
关于定义的解释,网上有很多。
我的理解就是,有事件的时候,比如可读了,epoll_wait会触发一次,即使读了一次以后,缓冲区中还有数据,也不会再次触发。
问题:
- 如何实现边缘触发
- 边缘触发如何使用recv & send
如何实现边缘触发
初始化函数,和LT模式没有区别。
//Description: Ubuntu 16.04.6 LTS
//Release: 16.04
#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <netinet/in.h>
#include <errno.h>
#include <arpa/inet.h>
#include <string.h>
#include <fcntl.h> //fctnl()
#define BUFFER_LENGTH 128
#define ARRAY_LENGTH 1024
typedef struct connections
{
int fd;
char rbuffer[BUFFER_LENGTH];
int rbuff_index;
//char wbuffer[128];
} connections_t;
int InitServer(int* listenfd, int* epfd)
{
*listenfd = socket(AF_INET,SOCK_STREAM,0);
if (-1 == *listenfd) {
perror("socket");
return -1;
}
struct sockaddr_in svraddr;
svraddr.sin_family = AF_INET;
svraddr.sin_addr.s_addr = htonl(INADDR_ANY);
svraddr.sin_port = htons(2048);
socklen_t len = sizeof(svraddr);
if(-1 == bind(*listenfd,(struct sockaddr*)&svraddr,len)) {
perror("bind");
return -1;
}
if(-1 == listen(*listenfd,10)) {
perror("listen");
return -1;
}
*epfd = epoll_create(1);
if(-1 == *epfd) {
perror("epoll_create");
return -1;
}
return 0;
}
int InitConnListItem(int fd, connections_t *connlist)
{
if(fd < ARRAY_LENGTH) {
connlist[fd].fd = fd;
connlist[fd].rbuff_index = 0;
memset(connlist[fd].rbuffer, 0x00 ,ARRAY_LENGTH);
} else {
printf("error:fd out of range.");
return -1;
}
return 0;
}
使用fcntl
把fd设置为非阻塞的函数
int SetNonBlockFD(int fd)
{
int oldflag = fcntl(fd,F_GETFL);
int newflag = fcntl(fd,F_SETFL, oldflag | O_NONBLOCK);
if(newflag == -1)
return -1;
return oldflag;
}
main函数
这里只写了个recv。利用recv的调用次数来看是否实现了ET。
int main()
{
int listenfd, epfd;
if(InitServer(&listenfd, &epfd) < 0)
return -1;
connections_t connlist[ARRAY_LENGTH] = { 0x00 };
struct epoll_event events[ARRAY_LENGTH] = { 0x00 };
struct epoll_event ev;
ev.data.fd = listenfd;
ev.events = EPOLLIN;
ev.events |= EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
int oldflag = SetNonBlockFD(listenfd);
if(oldflag < 0)
return -1;
if(InitConnListItem(listenfd,connlist) < 0)
return -1;
while(1) {
int nready = epoll_wait(epfd,events,ARRAY_LENGTH,-1);
if(nready > 0)
{
int i = 0;
for(i = 0; i < nready; i++) {
int eventfd = events[i].data.fd;
if(events[i].events & EPOLLIN) {
if(eventfd == listenfd) {
struct sockaddr acceptaddr;
socklen_t len = sizeof(acceptaddr);
int acceptfd = accept(listenfd,&acceptaddr,&len);
if(accept < 0) {
perror("accept.");
continue;
}
printf("accept fd:%d\n",acceptfd);
int oldflag = SetNonBlockFD(acceptfd);
if (oldflag < 0)
return -1;
InitConnListItem(acceptfd,connlist);
ev.data.fd = acceptfd;
ev.events = EPOLLIN;
ev.events |= EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, acceptfd, &ev);
}
else {
#define READ_LENGTH 1
int count = recv(eventfd, connlist[eventfd].rbuffer, READ_LENGTH, 0);
printf("recv, fd:%d,count:%d,msg:%s\n",eventfd, count, connlist[eventfd].rbuffer);
}
}
if(events[i].events & EPOLLOUT) {
printf("EPOLLOUT\n");
}
}
}
}
return 0;
}
测试:
使用net assistant来模拟客户端,向server发送一个长36Byte的字符串。
根据mainloop中recv处的限定,recv一次只会读1个字节。
如果实现了ET,recv在一定时间内只会调用一次,读一个字节。
如果实现的还是LT,recv会连续调用36次,直到把缓冲区读完。
可以看到,只recv了一次,读了一个"0",所以实现了ET。
总结&注意
实现ET的代码时的总结:
- 把listenfd的
ev.events |= EPOLLET;
和int oldflag = SetNonBlockFD(listenfd);
注释掉,
还是可以实现recv的边缘触发,所以listenfd的设置,对于acceptfd的边缘触发不是必要条件。 - 在mainloop中新增以下代码:
for(i = 0; i < nready; i++) {
int eventfd = events[i].data.fd;
if(events[i].events & EPOLLET) //新增
printf("fd:%d is EPOLLET\n",eventfd); //新增
else //新增
printf("fd:%d is LT\n",eventfd); //新增
虽然把listenfd和acceptfd的ev.events
都设置为了|=EPOLLET
,但是以上新增的代码,输出一直都是“is LT”,
所以没法用 if(events[i].events & EPOLLET)
这样的方式判断fd是否是ET。
↑此时已经执行了步骤1,fd 3 (listenfd) 是LT可以理解,fd 5(acceptfd)还是 LT就无法理解了。
这个原因我没搜到。在这里先记录一下,看后续有没有机会调查。
3. if(events[i].events & EPOLLIN) { }
的部分,InitConnListItem(acceptfd, connlist);
, epoll_ctl(epfd, EPOLL_CTL_ADD, acceptfd, &ev);
, int oldflag = SetNonBlockFD(acceptfd);
的执行顺序对ET的实现没有影响。之前我在一份LT代码的基础上改ET,一直不成功,我以为是这三个函数的顺序问题。重写代码后,成功实现ET,调换了这三个的顺序,发现对ET没有影响。
4. 把acceptld的 ev.events |= EPOLLET;
注释掉,recv会恢复成水平触发(此时仍是非阻塞的)。
边缘触发如何使用recv & send
recv
使用边缘触发的时候,伴随的是fd的非阻塞。需要一次性读完所有缓冲区内的数据。
需要在一个非阻塞的recv循环中读完:
while (1)
{
int count = recv(eventfd, connlist[eventfd].rbuffer, READ_LENGTH, 0);
if (count <= 0)
{
if(count == 0) {
perror("et recv return 0:");
epoll_ctl(epfd,EPOLL_CTL_DEL,eventfd,NULL);
close(eventfd);
break;
}
if(count == -1) {
perror("et recv return -1:");
if(errno == EAGAIN) {
printf("errno is EAGAIN,read end.\n");
}
else if(errno == EINTR) {
printf("errno is EINTR,read end.\n");
}
else {
printf("errno is not EAGAIN,read error.\n");
epoll_ctl(epfd,EPOLL_CTL_DEL,eventfd,NULL);
close(eventfd);
}
break;
}
}
printf("recv, fd:%d,count:%d,msg:%s,time:%s\n", eventfd, count, connlist[eventfd].rbuffer, __TIME__);
}
recv的返回值
- 非阻塞情况下:
recv return -1 且 errno == EINTR || EAGAIN (EWOULDBLOCK):读完成。(perror:Resource temporarily unavailable)
recv return -1 且 errno != EINTR || EAGAIN (EWOULDBLOCK):出现错误。
recv return 0 :客户端已断开连接。 - 阻塞情况下:
除了EAGAIN这个errno,别的相同。
返回值参考的文章:linux socket编程中的recv和send的返回值介绍及其含义。
send
在上面的代码中,留了一个EPOLLOUT的响应代码:
if(events[i].events & EPOLLOUT) {
printf("EPOLLOUT\n");
}
根据recv的经验,如果是LT模式,当可写时,就会一直触发EPOLLOUT ,当ET时,就只会触发一次EPOLLOUT。
把accepted的events设置为EPOLLOUT:
ev.data.fd = acceptfd;
events = EPOLLOUT;
测试一下LT模式下的EPOLLOUT执行情况:
不停地触发EPOLLOUT。
测试一下ET模式下的EPOLLOUT执行情况:
只触发一次EPOLLOUT。
水平触发(level-trggered)
epoll默认就是是LT的。
当触发条件满足时,会一直触发。
比如读缓冲区中有数据,recv一次没有读完,那缓冲区中还剩余数据,就会再次触发EPOLLIN,可以继续recv。
写一段代码测试一下,代码在 [水平触发完整代码],这里只说一下结果。
用net assistant作为客户端,客户端向服务器发送一条长度为26个字符的消息。
“0123456789abcdefghijklmnopqrstuvwxyz”
每次epoll_wait()前会打印“epoll_wait”。
每次recv后,会打印收到的长度,和完整的fd rbuffer的长度。限定每次最多读10个字符。recv后会把epoll_event改写成EPOLLOUT。
每次send后,会打印send的长度。send完成后会把epoll_event改写成EPOLLIN。
结果如下:
实际上我只在net assistant进行了一次消息发送。
可以看到自动触发了4次recv(),读完了所有发过去的字符。
而且在recv中,我插入了EPOLLOUT事件,在recv和send来回切换时,还是可以正确读到度缓冲区中的字符的。
LT必须经过wpoll_wati才能再次触发recv。
水平触发代码
这里错误处理弄的不好,后面需要专门总结一下返回值的处理。
这段代码不看也罢。
//Description: Ubuntu 16.04.6 LTS
//Release: 16.04
#include <stdio.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <netinet/in.h>
#include <errno.h>
#include <arpa/inet.h>
#include <string.h>
#define BUFFER_LENGTH 128
typedef struct connections
{
int fd;
char rbuffer[BUFFER_LENGTH];
int rbuff_index;
//char wbuffer[128];
} connections_t;
int main()
{
printf("epoll_event size=%ld\n",sizeof(struct epoll_event));
int listenfd = socket(AF_INET,SOCK_STREAM,0);
if (-1 == listenfd) {
perror("socket");
return -1;
}
struct sockaddr_in svraddr;
svraddr.sin_family = AF_INET;
svraddr.sin_addr.s_addr = htonl(INADDR_ANY);
svraddr.sin_port = htons(2048);
socklen_t len = sizeof(svraddr);
if(-1 == bind(listenfd,(struct sockaddr*)&svraddr,len)) {
perror("bind");
return -1;
}
if(-1 == listen(listenfd,10)) {
perror("listen");
return -1;
}
int epfd = epoll_create(1);
if(-1 == epfd) {
perror("epoll_create");
return -1;
}
connections_t connlist[1024] = { 0x00 };
struct epoll_event events[1024] = { 0x00 };
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listenfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
connlist[listenfd].fd = listenfd;
short iserror = 0;
while(1) {
printf("epoll_wait\n");
int nready = epoll_wait(epfd, events, 1024, -1);
for(int i = 0; i < nready; ++i) {
int eventfd = events[i].data.fd;
if (events[i].events & EPOLLIN)
{
if (eventfd == listenfd) {
struct sockaddr cliaddr;
socklen_t len = sizeof(cliaddr);
int acceptfd = accept(eventfd,(struct sockaddr*)&cliaddr,&len);
printf("accept,fd:%d\n",acceptfd);
ev.events = EPOLLIN;
ev.data.fd = acceptfd;
epoll_ctl(epfd,EPOLL_CTL_ADD,acceptfd,&ev);
connlist[acceptfd].fd = acceptfd;
connlist[acceptfd].rbuff_index = 0;
memset(connlist[acceptfd].rbuffer,0x00,BUFFER_LENGTH);
}
else {
char *buff = connlist[eventfd].rbuffer;
int *index = &connlist[eventfd].rbuff_index;
int recv_length = 10;
int msg_count = recv(eventfd, buff + (*index), recv_length, 0);
if(msg_count == 0) {
perror("recv");
printf("disconnect %d\n",eventfd);
epoll_ctl(epfd, EPOLL_CTL_DEL, eventfd, NULL);
close(eventfd);
continue;
}
if(msg_count == -1) {
perror("recv");
printf("recv error fd:%d\n",eventfd);
iserror = 1;
break;
}
(*index) += msg_count;
printf("recv,count:%d,rbuffer:%s\n", msg_count, buff);
ev.events = EPOLLOUT;
ev.data.fd = eventfd;
epoll_ctl(epfd,EPOLL_CTL_MOD,eventfd,&ev);
}
}
if(events[i].events & EPOLLOUT) {
char *send_msg = connlist[eventfd].rbuffer ;
int send_msg_length = connlist[eventfd].rbuff_index;
usleep(100000);
int msg_count = send(eventfd,send_msg, send_msg_length, 0);
printf("send,count:%d,msg=%s\n", msg_count, send_msg);
ev.events = EPOLLIN;
ev.data.fd = eventfd;
epoll_ctl(epfd, EPOLL_CTL_MOD, eventfd, &ev);
}
}
if(iserror == 1)
break;
}
close(epfd);
close(listenfd);
return 0;
}