Windows下完成端口移植Linux下的epoll
先来说说Windows下的完成端口。完成端口号称是Windows下面最复杂的异步IO操作。但是如果你想开发出具有高性能的、支持大量连接的网络服务程序的话,就必须将它拿下。这里假设你已经对完成端口有一定的了解了。
hIocp = CreateIoCompletionPort(
INVALID_HANDLE_VALUE,
NULL,
(ULONG_PTR)0,
0);
if (hIocp == NULL) {
// Error
}
以下代码创建了一个套接字,并把它和前面创建的完成端口关联起来:
s = socket(AF_INET, SOCK_STREAM, 0);
if (s == INVALID_SOCKET) {
// Error
if (CreateIoCompletionPort((HANDLE)s,
hIocp,
(ULONG_PTR)0,
0) == NULL)
{
// Error
}
...
HANDLE CompletionPort, // handle to completion port
LPDWORD lpNumberOfBytes, // bytes transferred
PULONG_PTR lpCompletionKey, // file completion key
LPOVERLAPPED *lpOverlapped, // buffer
DWORD dwMilliseconds // optional timeout value
);
DWORD dwBufferCount, LPDWORD lpNumberOfBytesRecvd,
LPINT lpFlags, LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE
l pCompletionRoutine );
SOCKET s,
LPWSABUF lpBuffers,
DWORD dwBufferCount,
LPDWORD lpNumberOfBytesSent,
int iFlags,
LPWSAOVERLAPPED lpOverlapped,
LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);
在上一篇中,我们主要讨论了Windows下关于完成端口的一些知识。对应于完成端口,Linux下面在2.5.44内核中有了epoll,这个是为处理大批量句柄而引进的。
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET:将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要
#include <sys/socket.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <string.h>
#include <errno.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define MAXSIZE 64000
#define MAXEPS 256
//#define EVENTS 100
#define LISTENQ 32
#define SERV_PORT 8000
int setnonblock(int sock)
{
int flags = fcntl(sock, F_GETFL, 0);
if(-1 == flags) {
perror("fcntl(sock, F_GETFL)");
return -1;
}
flags |= O_NONBLOCK;
if(-1 == fcntl(sock, F_SETFL, flags)) {
perror("fcntl(sock, F_SETFT, flags)");
return -2;
}
return 0;
}
int main(int argc, char *argv[])
{
int i, maxi, listenfd, connfd, sockfd, epfd, nfds;
ssize_t n;
char buf[MAXSIZE];
socklen_t clilen;
struct epoll_event ev, events[20];
epfd = epoll_create(MAXEPS);
struct sockaddr_in clientaddr;
struct sockaddr_in serveraddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
setnonblock(listenfd);
ev.data.fd = listenfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);
memset(&serveraddr, 0, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
/*
char *local_addr = "10.0.2.15";
inet_aton(local_addr, &(serveraddr.sin_addr));
*/
serveraddr.sin_addr.s_addr = htonl(INADDR_ANY);
serveraddr.sin_port = htons(SERV_PORT);
if(bind(listenfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr)) != 0) {
perror("bind failed");
return -1;
}
if(listen(listenfd, LISTENQ) != 0) {
perror("listen failed");
return -2;
}
maxi = 0;
printf("began to accept.../n");
for(;;) {
nfds = epoll_wait(epfd, events, 32, 10000);
if(-1 == m_nfds) {
if(EINTR == errno) {
continue;
}
return -1;
}
for(i=0; i<nfds; ++i) {
if(events[i].data.fd == listenfd) {
connfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clilen);
if(connfd < 0) {
perror("accept failed");
return -3;
}
printf("accepted../n");
setnonblock(connfd);
ev.data.fd = connfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);
}
else if(events[i].events & EPOLLIN) {
if((sockfd = events[i].data.fd) < 0)
continue;
if((n = read(sockfd, buf, MAXSIZE)) < 0) {
if(errno == ECONNRESET) {
close(sockfd);
events[i].data.fd = -1;
} else {
perror("read failed");
}
}
else if(0 == n) {
close(sockfd);
events[i].data.fd = -1;
}
printf("Read the buf: %s/n", buf);
ev.data.fd = sockfd;
ev.events = EPOLLOUT | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
}
else if(events[i].events & EPOLLOUT) {
sockfd = events[i].data.fd;
char *sndbuf = "I get your message!";
write(sockfd,sndbuf, 10);
ev.data.fd = sockfd;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
}
}
}
return 0;
}
epoll为什么这么快
epoll是多路复用IO(I/O Multiplexing)中的一种方式,但是仅用于linux2.6以上内核,在开始讨论这个问题之前,先来解释一下为什么需要多路复用IO.
以一个生活中的例子来解释.
假设你在大学中读书,要等待一个朋友来访,而这个朋友只知道你在A号楼,但是不知道你具体住在哪里,于是你们约好了在A号楼门口见面.
如果你使用的阻塞IO模型来处理这个问题,那么你就只能一直守候在A号楼门口等待朋友的到来,在这段时间里你不能做别的事情,不难知道,这种方式的效率是低下的.
现在时代变化了,开始使用多路复用IO模型来处理这个问题.你告诉你的朋友来了A号楼找楼管大妈,让她告诉你该怎么走.这里的楼管大妈扮演的就是多路复用IO的角色.
进一步解释select和epoll模型的差异.
select版大妈做的是如下的事情:比如同学甲的朋友来了,select版大妈比较笨,她带着朋友挨个房间进行查询谁是同学甲,你等的朋友来了,于是在实际的代码中,select版大妈做的是以下的事情:
int n = select( & readset,NULL,NULL, 100 );
for ( int i = 0 ; n > 0 ; ++ i)
{
if (FD_ISSET(fdarray[i], & readset))
{
do_something(fdarray[i]);
--n;
}
}
epoll版大妈就比较先进了,她记下了同学甲的信息,比如说他的房间号,那么等同学甲的朋友到来时,只需要告诉该朋友同学甲在哪个房间即可,不用自己亲自带着人满大楼的找人了.于是epoll版大妈做的事情可以用如下的代码表示:
for (i = 0 ;i < n; ++ i)
{
do_something(events[n]);
}
在epoll中,关键的数据结构epoll_event定义如下:
void * ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
别小看了这些效率的提高,在一个大规模并发的服务器中,轮询IO是最耗时间的操作之一.再回到那个例子中,如果每到来一个朋友楼管大妈都要全楼的查询同学,那么处理的效率必然就低下了,过不久楼底就有不少的人了.
对比最早给出的阻塞IO的处理模型, 可以看到采用了多路复用IO之后, 程序可以自由的进行自己除了IO操作之外的工作, 只有到IO状态发生变化的时候由多路复用IO进行通知, 然后再采取相应的操作, 而不用一直阻塞等待IO状态发生变化了.
从上面的分析也可以看出,epoll比select的提高实际上是一个用空间换时间思想的具体应用.
本文详细介绍了Windows下的完成端口与Linux下的epoll机制,对比了它们在处理大量并发连接时的不同之处。完成端口通过重叠I/O机制提高了效率,而epoll则通过更为高效的事件通知机制实现了对大量文件描述符的支持。

487

被折叠的 条评论
为什么被折叠?



