用 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 <sys/epoll.h>
#include <fcntl.h>
#include <netinet/in.h> // for sockaddr_in
#include <sys/types.h> // for socket
#include <sys/socket.h> // for socket
#include <stdio.h> // for printf
#include <stdlib.h> // for exit
#include <string.h> // for bzero
#include <arpa/inet.h> // inet_ntoa
#define LISTENQ 20
#define MAXLINE 10000
#define SERV_PORT 6666
int 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 <sys/epoll.h>
#include <fcntl.h>
#include <netinet/in.h> // for sockaddr_in
#include <sys/types.h> // for socket
#include <sys/socket.h> // for socket
#include <stdio.h> // for printf
#include <stdlib.h> // for exit
#include <string.h> // for bzero
#include <arpa/inet.h> // inet_ntoa
#include <unistd.h>
#define SERV_PORT 6666
#define MAXLINE 10000
struct sockaddr_in servaddr, servaddr1;
int
main(int argc, char **argv)
{
int connfd;
if (argc != 2){
printf("usage: tcpcli <IPaddress>");
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 tcpserv
gcc 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...... 以后再学习其他的吧。