博客文章

epoll和socket的一点点儿爱恨情仇

作者: andy.      时间: 2016-08-31 17:35:43

维基百科的东西就不翻译,然后再贴上来了。直接切入正题。epoll很像select模型,比IOCP简单多了(也许开始学的Windows网络编程的原因)。也不扯远了~正题。

int epoll_create(int size);

创建epoll的文件描述符。size,这里我们说的是socket的,那么就是指的是要监听的socket的数目。返回的就是创建的epoll的描述符。

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

第一个参数是上面创建的epoll,第二个参数是操作的方式,增加还是删除还是修改,一个宏,具体参考下面:

EPOLL_CTL_ADD(添加)
       Register the target file descriptor fd on the epoll instance referred toby the file descriptor epfd and  associate  the  event  event  with  the internal file linked to fd.
 
EPOLL_CTL_MOD(修改)
       Change the event event associated with the target file descriptor fd.
 
EPOLL_CTL_DEL(删除)
       Remove  (deregister)  the  target  file  descriptor  fd  from  the epoll instance referred to by epfd.  The event is ignored and can be NULL (but see BUGS below).

我们这儿是socket编程,那么第三个参数就是要操作的socket,第四个参数要监听的事件,参考如下:

EPOLLIN     //读
EPOLLOUT    //写
EPOLLPRI    //紧急
EPOLLERR    //错误
EPOLLHUP    //关闭了
EPOLLET     //将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT//只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。

我们关心一下读写错误就行了。

int epoll_wait(int epfd, struct epoll_event *events,
                      int maxevents, int timeout);

第一个参数epoll的描述符,第二个参数就是用来存放发生的事件集合的,第三个参数,最多放多少,和上面创建的事件集合数量相同就行了。第四个参数超时返回,填-1好了,没事件发生就一直让其阻塞好了。这儿有几个事件发生就返回几,同时发生事件的套接字存放在events中

编程的模型流程大概如下:

1、 创建监听套接字并监听端口。

2、 设置这个监听套接字为非阻塞状态(默认的工作模式两种状态都行)。

3、 创建epoll_event、接收发生状态的事件集合和epoll句柄。其中epoll_event用于为套接字注册事件,事件集合用于调用wait后,存放发生的事件。

4、 将监听套接字与创建的epoll句柄进行关联。

5、 死循环,调用epoll_wait,当有事件发生的时候就返回,发生的事件存在上面创建的事件集合当中。然后我们遍历发生的事件,判断是什么事件,做出相应的处理就行了。

总体和select模型非常像,比较简单,没得回调函数。比Windows下面的IOCP简单多了,但是据说效率和IOCP差不多。

看代码,按照上面的流程构造主函数:

int main(int argc, char* argv[]){
        int port = 8080;
        int listen_socket = listen_st(port);//创建监听套接字,监听并返回监听套接字。

        struct epoll_event event, events[1024];//事件和存放wait后发生的事件。
        int epoll_fd = epoll_create(1024);//创建epoll。

        set_no_blocking(listen_socket);//设置套接字为非阻塞状态。
        event.data.fd = listen_socket;//下面这点儿为监听套接字加到epoll里面。
        event.events = EPOLLIN | EPOLLERR | EPOLLHUP;
        if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_socket, &event) == -1)
                process_error("Add to poll error:");

        int st = 0;
        while(1){
                int nfds = epoll_wait(epoll_fd, events, 1024, -1);//等待有事件发生。
                printf("epoll_wait complete.nfds:%d\n", nfds);
                if(nfds == -1)
                        process_error("epoll_wait error:");

                int i = 0;
                for(int i = 0; i < nfds; i++){
                        if(events[i].data.fd < 0)
                                continue;

                        if(events[i].data.fd == listen_socket){//发生事件的套接字和监听套接字相同,肯定就是有连接了。
                                st = accept_socket(listen_socket);
                                set_no_blocking(st);
                                event.data.fd = st;
                                event.events = EPOLLIN | EPOLLERR | EPOLLHUP;
                                epoll_ctl(epoll_fd, EPOLL_CTL_ADD, st, &event);
                                continue;
                        }

                        if(events[i].events & EPOLLIN){
                                st = events[i].data.fd;
                                if(recv_socket(st) <= 0){
                                        close(st);
                                        events[i].data.fd = -1;
                                }
                        }

                        if(events[i].events & EPOLLERR){
                                close(events[i].data.fd);
                                events[i].data.fd = -1;
                        }
                        if(events[i].events & EPOLLHUP){
                                close(events[i].data.fd);
                                events[i].data.fd = -1;
                        }
                }
        }

        close(epoll_fd);
}

上面因为实参、形参的原因,所以接收的套接字都放在st中没有问题。因为当我们把事件的套接字置为-1、关闭套接字后系统会自动从epoll中移除该套接字,所以我们并没有调用epoll_ctl进行移除。

下面是创建监听、处理接收消息等等的函数:

void process_error(char * msg){
        process_error(msg);
        exit(-1);
}

int recv_socket(int socket){
        char buffer[1024];


        memset(buffer, 0, sizeof(1024));
        ssize_t result = recv(socket, buffer, sizeof(buffer), 0);
        if(result <= 0){
                perror("recv error:");
                return -1;
        }
        result = send(socket, buffer, result, 0);//回显
        if(result <= 0){
                perror("send error:");
                return -1;
        }
        return result;
}

void set_no_blocking(int socket){//设置为非阻塞状态
        int opts = fcntl(socket, F_GETFL);
        if(opts < 0){
                process_error("fcntl get current status error:");
                exit(-1);
        }

        opts = opts | O_NONBLOCK;
        if(fcntl(socket, F_GETFL) < 0){
                process_error("fcntl set current status error:");
                exit(-1);
        }
}

int accept_socket(int listen_socket){
        int client_socket;
        struct sockaddr_in client_addr;
        int length = sizeof(client_addr);
        if((client_socket = accept(listen_socket, (struct sockaddr*)&client_addr, &length)) == -1)
                process_error("accept error:");
        else
                printf("accept new connection from:%s:%d\n", inet_ntoa(client_addr.sin_addr), client_addr.sin_port);
        return client_socket;
}

int listen_st(int port){
        int st = socket(AF_INET, SOCK_STREAM, 0);
        int on = 1;
        if(setsockopt(st, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1)
                process_error("setsocketopt error:");

        struct sockaddr_in addr;
        memset(&addr, 0, sizeof(addr));
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.s_addr = inet_addr("192.168.1.201");

        if(bind(st, (struct sockaddr*)&addr, sizeof(addr)) == -1)
                process_error("bind errror:");

        if(listen(st, 10) == -1)
                process_error("listen error:");

        return st;
}

我们可以通过getpeername和getsockname获得远程和自身的地址。下面进行测试,测试的一个客户端由PiggyXP(小猪)提供,上次学习IOCP的时候发现的(我标注了出处的,应该没问题吧)。先运行服务端:

blob.png

用上面的并发的程序运行:

blob.png

服务器当前状态:

blob.png

用原来写的windows的一个通讯客户端连上去发送消息并可以看到服务器的状态:
blob.pngblob.png

关闭并发连接,再用客户端发送消息,观察:

blob.png

运行的还是蛮正常的。好了~epoll的介绍就完了,ET、LT模式等等都没有介绍。我们这儿就把默认的工作模式介绍了一下,感觉够了。