用 epoll 写一个socket双工通信
NIO 说明
常用的方式有 select、poll、epoll。
select是使用的一种类似位操作的方式,获取到了事件响应就将对应位置为1。缺点就是首先受限于这个标识符长度,因此能监听的数量有限,其次就是每次遍历都需要遍历每一位。
poll 改进了select,不是使用位操作,解决了数量有限的问题,但是依旧遍历需要遍历。
epoll 是使用类似链表的数据结构解决数量有限的问题的,然后是利用回调函数,避免了遍历。
epoll 使用
主要用到的几个函数:
int epoll_create(int size);int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
分别说明:
epoll_create 创建一个 epoll 套接字,之后所有的要监听的套接字都会add到这个套接字上,参数size只需要是一个大于0的整数即可。
epoll_ctl 的op参数标志执行一些操作 EPOLL_CTL_ADD | EPOLL_CTL_MOD | EPOLL_CTL_DEL,分别是增删改套接字,epdf参数就是epoll_create 创建的套接字,然后 fd是对应的文件描述符,event是事件描述。(有一点我不明白的是,event 结构体中其实已经包含了 fd参数,不知道为什么这个地方还需要重新传入)
epoll_wait 监听事件。events 参数用于暂时存放那些响应的事件,timeout代表接受到了消息之后多久返回,-1代表无限等待。
写一个socket通信程序
在计算机网络课程上可能我们第一次接触的程序就是一个简单的socket C/S 程序,但是那个程序只能是 客户端请求->服务端响应的方式,这里我们写一个服务端也能主动发送消息的程序。
主要区别就是在于使用epoll同时监听stdin和socket连接套接字。
server
#include#include #include // for sockaddr_in#include // for socket#include // for socket#include // for printf#include // for exit#include // for bzero#include // inet_ntoa#define LISTENQ 20#define MAXLINE 10000#define SERV_PORT 6666int main(int argc, char **argv){ int listenfd, connfd; struct sockaddr_in cliaddr, servaddr; listenfd = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = htonl(INADDR_ANY); servaddr.sin_port = htons(SERV_PORT); bind(listenfd, (struct sockaddr*) &servaddr, sizeof(servaddr)); listen(listenfd, LISTENQ); int addrSize = sizeof(cliaddr); connfd = accept(listenfd, (struct sockaddr*) &cliaddr, &addrSize); printf("connected from:%s, port:%d \n\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port)); close(listenfd); /* close listening socket */ int epollFd; epollFd = epoll_create(256); struct epoll_event ev,events[20]; ev.data.fd=connfd; ev.events=EPOLLIN|EPOLLET; epoll_ctl(epollFd,EPOLL_CTL_ADD,connfd,&ev); // stdin = 0 ev.data.fd=0; ev.events=EPOLLIN; epoll_ctl(epollFd,EPOLL_CTL_ADD,0,&ev); char sendline[MAXLINE], recvline[MAXLINE]; while(1){ int nfds=epoll_wait(epollFd,events,20,0); for (int i = 0; i < nfds; ++i){ if(events[i].data.fd==connfd){ int n = read(connfd, recvline, MAXLINE); // TODO: add n == 0 fputs(recvline, stdout); }else if(events[i].data.fd==0){ fgets(sendline, MAXLINE, stdin); send(connfd, sendline, strlen(sendline), 0); } } } close(connfd); close(epollFd); return 0;}
client
#include#include #include // for sockaddr_in#include // for socket#include // for socket#include // for printf#include // for exit#include // for bzero#include // inet_ntoa#include #define SERV_PORT 6666#define MAXLINE 10000struct sockaddr_in servaddr, servaddr1;intmain(int argc, char **argv){ int connfd; if (argc != 2){ printf("usage: tcpcli "); exit(0); } connfd = socket(AF_INET, SOCK_STREAM, 0); bzero(&servaddr, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_port = htons(SERV_PORT); inet_pton(AF_INET, argv[1], &servaddr.sin_addr); connect(connfd, (struct sockaddr*) &servaddr, sizeof(servaddr)); int epollFd; epollFd = epoll_create(256); struct epoll_event ev,events[20]; ev.data.fd=connfd; ev.events=EPOLLIN|EPOLLET; epoll_ctl(epollFd,EPOLL_CTL_ADD,connfd,&ev); // stdin = 0 ev.data.fd=0; ev.events=EPOLLIN; epoll_ctl(epollFd,EPOLL_CTL_ADD,0,&ev); char sendline[MAXLINE], recvline[MAXLINE]; while(1){ int nfds=epoll_wait(epollFd,events,20,0); for (int i = 0; i < nfds; ++i){ if(events[i].data.fd==connfd){ int n = read(connfd, recvline, MAXLINE); fputs(recvline, stdout); }else if(events[i].data.fd==0){ fgets(sendline, MAXLINE, stdin); send(connfd, sendline, strlen(sendline), 0); } } } close(connfd); close(epollFd); return 0;}
执行
gcc tcpserv.c -o tcpservgcc tcpcli.c -o tcpcli./tcpserv...# 另开一个窗口./tcpcli 127.0.0.1
就可以在两边都进行输入了。
PS:
关于epoll事件定义
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写; EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来); EPOLLERR:表示对应的文件描述符发生错误; EPOLLHUP:表示对应的文件描述符被挂断; EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里实际使用中只用到了EPOLLIN...... 以后再学习其他的吧。