这篇博客介绍使用epoll函数实现并发服务器。
服务端代码:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_EVENTS 10
#define LISTENQ 20
#define PORT 5000 //8080
//设置socket连接为非阻塞模式
void setnonblocking (int fd)
{
int opts;
opts = fcntl (fd, F_GETFL);
if (opts < 0)
{
perror ("fcntl(F_GETFL)\n");
exit (1);
}
opts = (opts | O_NONBLOCK);
if (fcntl (fd, F_SETFL, opts) < 0)
{
perror ("fcntl(F_SETFL)\n");
exit (1);
}
}
int main ()
{
struct epoll_event ev, events[MAX_EVENTS];
int listenfd, connfd, nfds, epfd, sockfd, i, nread, n;
struct sockaddr_in local, remote;
socklen_t addrlen;
char buf[BUFSIZ];
//创建listen socket
if ((listenfd = socket (AF_INET, SOCK_STREAM, 0)) < 0)
{
perror ("sockfd\n");
exit (1);
}
setnonblocking (listenfd);
bzero (&local, sizeof (local));
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl (INADDR_ANY);;
local.sin_port = htons (PORT);
if (bind (listenfd, (struct sockaddr *) &local, sizeof (local)) < 0)
{
perror ("bind\n");
exit (1);
}
listen (listenfd, LISTENQ);
epfd = epoll_create (MAX_EVENTS);
if (epfd == -1)
{
perror ("epoll_create");
exit (EXIT_FAILURE);
}
ev.events = EPOLLIN;
ev.data.fd = listenfd;
if (epoll_ctl (epfd, EPOLL_CTL_ADD, listenfd, &ev) == -1)
{
perror ("epoll_ctl: listen_sock");
exit (EXIT_FAILURE);
}
for (;;)
{
nfds = epoll_wait (epfd, events, MAX_EVENTS, -1);
if (nfds == -1)
{
perror ("epoll_wait error");
exit (EXIT_FAILURE);
}
for (i = 0; i < nfds; ++i)
{
sockfd = events[i].data.fd;
if (sockfd == listenfd)
{
while ((connfd = accept (listenfd, (struct sockaddr *) &remote, &addrlen)) > 0)
{
char *ipaddr = inet_ntoa (remote.sin_addr);
printf("accept a connection from [%s]\n", ipaddr);
setnonblocking (connfd); //设置连接socket为非阻塞
ev.events = EPOLLIN | EPOLLET; //边沿触发要求套接字为非阻塞模式;水平触发可以是阻塞或非阻塞模式
ev.data.fd = connfd;
if (epoll_ctl (epfd, EPOLL_CTL_ADD, connfd, &ev) == -1)
{
perror ("epoll_ctl: add");
exit (EXIT_FAILURE);
}
}
if (connfd == -1)
{
if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR)
perror ("accept");
}
continue;
}
if (events[i].events & EPOLLIN)
{
n = 0;
while ((nread = read (sockfd, buf + n, BUFSIZ - 1)) > 0)
{
n += nread;
}
if (nread == -1 && errno != EAGAIN)
{
perror ("read error");
}
printf("recv from client data [%s]\n", buf);
ev.data.fd = sockfd;
ev.events = events[i].events | EPOLLOUT;
if (epoll_ctl (epfd, EPOLL_CTL_MOD, sockfd, &ev) == -1)
{
perror ("epoll_ctl: mod");
}
}
if (events[i].events & EPOLLOUT)
{
snprintf (buf, sizeof(buf), "HTTP/1.1 200 OK\r\nContent-Length: %d\r\n\r\nHello World", 11);
int nwrite, data_size = strlen (buf);
n = data_size;
while (n > 0)
{
nwrite = write (sockfd, buf + data_size - n, n);
if (nwrite < n)
{
if (nwrite == -1 && errno != EAGAIN)
{
perror ("write error");
}
break;
}
n -= nwrite;
}
printf("send to client data [%s]\n", buf);
close (sockfd);
events[i].data.fd = -1;
}
}
}
close (epfd);
close (listenfd);
return 0;
}
相关函数:
int epoll_create
int epoll_create(int size)
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
自从Linux 2.6.8开始,size参数被忽略,但是依然要大于0。
int epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epoll的事件注册函数,它不同与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。
int epoll_wait
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents表示每次能处理的最大事件数,告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
客户端代码:
//compile: g++ -g epoll_client.cpp -o epoll_client
//run: ./epoll_client
//
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;
#define PORT 5000
int main(int argc, char* argv[])
{
int sockfd, on = 1;
char buffer[512] = {0};
struct sockaddr_in servaddr;
memset(&servaddr, 0, sizeof(servaddr));
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0){
cout << "create socket fail" << endl;
return -1;
}
cout << "succeed to create client socket fd " << sockfd << endl;
setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
cout << "set socket reuse by etsockopt" << endl;
servaddr.sin_port = htons((short)PORT);
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr("127.0.0.1"); //此处更改epoll服务器地址
if(connect(sockfd, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0){
cout << "connect error" << endl;
return -1;
}
cout << "succeed to connect epoll server " << endl;
char target[] = "The Author: [email protected]";
memcpy(buffer, target, strlen(target));
while(1)
{
int wlen = send(sockfd, buffer, strlen(buffer), 0);
if(wlen <= 0)
cout << " send data to server fail " << strerror(errno) << endl;
cout << "send data to server on success, data: [" << buffer << "]"<< endl;
memset(buffer, 0, sizeof(buffer));
int rlen = recv(sockfd, buffer, sizeof(buffer), 0);
if(rlen <= 0)
cout << " receive data from server fail " << strerror(errno) << endl;
cout << "receive data from server on success, data: [" << buffer << "]" << endl;
}
return 0;
}
Level_triggered(水平触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!!!
Edge_triggered(边缘触发):当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!!!
阻塞IO:当你去读一个阻塞的文件描述符时,如果在该文件描述符上没有数据可读,那么它会一直阻塞(通俗一点就是一直卡在调用函数那里),直到有数据可读。当你去写一个阻塞的文件描述符时,如果在该文件描述符上没有空间(通常是缓冲区)可写,那么它会一直阻塞,直到有空间可写。以上的读和写我们统一指在某个文件描述符进行的操作,不单单指真正的读数据,写数据,还包括接收连接accept(),发起连接connect()等操作...
非阻塞IO:当你去读写一个非阻塞的文件描述符时,不管可不可以读写,它都会立即返回,返回成功说明读写操作完成了,返回失败会设置相应errno状态码,根据这个errno可以进一步执行其他处理。它不会像阻塞IO那样,卡在那里不动!!!