首页 > 代码库 > 0729------Linux网络编程----------使用 select 、poll 和 epoll 模型 编写客户端程序
0729------Linux网络编程----------使用 select 、poll 和 epoll 模型 编写客户端程序
1.select 模型
1.1 select 函数原型如下,其中 nfds 表示的描述符的最大值加1(因为这里是左闭右开区间),中间三个参数分别表示要监听的不同类型描述符的集合,timeout用来表示轮询的时间间隔,这里用NULL表示无限等待。
1.2 使用 select函数编写客户端程序的一般步骤:
a)初始化参数,包括初始化监听集合read_set并添加fd,以及初始化监听的最大描述符 maxfd 和select的返回值 nready;
b)将read_set 赋值给 ready_set,因为每次执行select函数监听结合都会改变,read_set用来保存原始的集合;
c)执行 select调用,检查返回值;
d)依次检查fd,分别执行不同的操作;
1.3 程序示例。
#include "def.h"#include <sys/select.h>/* * 使用select模型 编写客户端程序 * */void do_service(int peerfd);int main(int argc, const char *argv[]){ //1.创建 socket int peerfd = socket(AF_INET, SOCK_STREAM, 0); if(peerfd == -1) ERR_EXIT("socket"); //2. conncet struct sockaddr_in peeraddr; peeraddr.sin_family = AF_INET; peeraddr.sin_addr.s_addr = inet_addr("127.0.0.1"); peeraddr.sin_port = htons(9999); if(connect(peerfd, (struct sockaddr*)&peeraddr, sizeof(peeraddr)) == -1) ERR_EXIT("connect"); do_service(peerfd); close(peerfd); return 0;}void do_service(int peerfd){ char sendbuf[1024] = {0}; char recvbuf[1024] = {0}; //1.初始化数组,添加要监听的描述符 fd_set read_set, ready_set; FD_ZERO(&read_set); FD_SET(STDIN_FILENO, &read_set); FD_SET(peerfd, &read_set); int maxfd = STDIN_FILENO > peerfd ? STDIN_FILENO : peerfd; int nready; //接收select的返回值 // 2.执行监听 while(1){ ready_set = read_set; nready = select(maxfd + 1, &ready_set, NULL, NULL, NULL); if(nready == -1){ if(errno == EINTR) continue; ERR_EXIT("select"); } else if(nready == 0) continue; // 3.依次检查每个fd是否在集合中 if(FD_ISSET(STDIN_FILENO, &ready_set)){ if(fgets(sendbuf, sizeof(sendbuf), stdin) == NULL){ shutdown(peerfd, SHUT_WR); //从监听集合中移除该fd FD_CLR(STDIN_FILENO, &read_set); } writen(peerfd, sendbuf, strlen(sendbuf)); } if(FD_ISSET(peerfd, &ready_set)){ int ret = readline(peerfd, recvbuf, sizeof(recvbuf)); if(ret == -1) ERR_EXIT("readline"); else if(ret == 0){ close(peerfd); printf("server closed\n"); break; } printf("recv data: %s", recvbuf); } }}
2.poll模型
2.1 poll 函数
2.1.1 poll函数原型如下,第一个参数为要监听的描述符数组,第二个是数组的长度,第三个表示轮询的间隔(负值表示无限等待)。
2.1.2 poll的第一个参数结构体如下,fd即为要监听的描述符,events表示等待的事件,我们一般使用POLLIN,revents是一个传出参数,我们用它来判断等待的事件是否发生。
2.2 使用 poll 编写客户端程序的一般步骤:
a)准备数组(即poll函数的第一个参数),填入相应的 fd 和 events,定义maxi(数组下标的最大值)和nready(接收poll的返回值)变量;
b)进入while(1)循环,执行 poll 函数,检查其返回值;
c)通过revents字段,检查两个fd;
2.3 程序示例。
#include "def.h"/* * 使用poll模型 编写客户端程序 * */void do_service(int peerfd);int main(int argc, const char *argv[]){ //1.创建 socket int peerfd = socket(AF_INET, SOCK_STREAM, 0); if(peerfd == -1) ERR_EXIT("socket"); //2. conncet struct sockaddr_in peeraddr; peeraddr.sin_family = AF_INET; peeraddr.sin_addr.s_addr = inet_addr("127.0.0.1"); peeraddr.sin_port = htons(9999); if(connect(peerfd, (struct sockaddr*)&peeraddr, sizeof(peeraddr)) == -1) ERR_EXIT("connect"); do_service(peerfd); close(peerfd); return 0;}void do_service(int peerfd){ char sendbuf[1024] = {0}; char recvbuf[1024] = {0}; // 1.初始化描述符数组 struct pollfd fds[2]; fds[0].fd = STDIN_FILENO;//fileno(stdin); //? fds[0].events = POLLIN; fds[1].fd = peerfd; fds[1].events = POLLIN; int maxi = 1; //数组的最大下标 int nready; // 接收poll的返回值 // 2.执行poll,检查返回值 while(1){ nready = poll(fds, maxi + 1, -1); //负值表示永久等待 if(nready == -1){ if(errno == EINTR) continue; else ERR_EXIT("poll"); } else if(nready == 0) continue; // 3.依次检查 if(fds[0].revents & POLLIN){ // 位操作 if(fgets(sendbuf, sizeof(sendbuf), stdin) == NULL){ shutdown(peerfd, SHUT_WR); //关闭写端 仍然可从套接字中读数据 fds[0].fd = -1; //不再监听 STDIN_FILENO } else writen(peerfd, sendbuf, strlen(sendbuf)); } if(fds[1].revents & POLLIN){ int ret = readline(peerfd, recvbuf, 1024); if(ret == -1){ ERR_EXIT("readline"); } else if(ret == 0){// 对端关闭了连接 printf("server closed\n"); close(peerfd); break; } printf("recv data: %s", recvbuf); } }}
3.epoll模型
3.1 epoll模型常用的函数。
3.1.1 epoll_create 函数,用来创建一个epoll句柄,参数为句柄的大小,即要监听的描述符的个数。例如,我们要监听STDIN_FILENO 和 peerfd ,那么传入size的值就为2.
3.1.2 epoll_ctl 函数,根据第二个参数的不同,执行不同的操作,常用的有往句柄里注册一个描述符和从中移除一个描述符,此时第二个参数分别为 EPOLL_CTL_ADD 和 EPOLL_CTL_DEL,注意这里每一个新添加(或者移除等)的 fd 都和一个 struct epoll_event结构体相关联。该结构体如下:
3.1.3 epoll_wait 函数,用来等待准备好的fd。返回准备好的的fd的个数。
3.2 epoll 函数编写客户端程序的一般步骤:
a)使用 epoll_create 函数创建 epollfd 句柄;
b)使用 epoll_ctl 函数往epollfd中注册需要监听的描述符;
c)准备一个数组 events(传出参数,epoll_wait的第三个参数),接收需要处理的事件;
d)进入while(1)循环, 执行 epoll_wait 函数,根据返回值 nready(准备好的fd个数)依次遍历events数组,进行处理。
3.3 程序示例。
#include "def.h"#include <sys/epoll.h>/* * 使用epoll模型 编写客户端程序 * */void do_service(int peerfd);int main(int argc, const char *argv[]){ //1.创建 socket int peerfd = socket(AF_INET, SOCK_STREAM, 0); if(peerfd == -1) ERR_EXIT("socket"); //2. conncet struct sockaddr_in peeraddr; peeraddr.sin_family = AF_INET; peeraddr.sin_addr.s_addr = inet_addr("127.0.0.1"); peeraddr.sin_port = htons(9999); if(connect(peerfd, (struct sockaddr*)&peeraddr, sizeof(peeraddr)) == -1) ERR_EXIT("connect"); do_service(peerfd); close(peerfd); return 0;}void do_service(int peerfd){ char sendbuf[1024] = {0}; char recvbuf[1024] = {0}; //1.生成一个epoll句柄 大小为2 因为要监听STDIN_FILENO 和 peerfd int epollfd = epoll_create(2); if(epollfd == -1) ERR_EXIT("epoll_create"); //2.往epollfd 中注册要监听的fd struct epoll_event ev; ev.events = POLLIN; ev.data.fd = STDIN_FILENO; if(epoll_ctl(epollfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev) == -1) ERR_EXIT("epoll_ctl"); ev.events = POLLIN; ev.data.fd = peerfd; if(epoll_ctl(epollfd, EPOLL_CTL_ADD, peerfd, &ev) == -1) ERR_EXIT("epoll_ctl"); //3.准备一个数组 struct epoll_event events[2];//存放准备好的fd信息 int nready; //接收epoll_wait的返回值 while(1){ nready = epoll_wait(epollfd, events, 2, -1); // -1无限等待 if(nready == -1){ if(errno == EINTR) continue; else ERR_EXIT("epoll_wait"); } else if(nready == 0) continue; //4.开始检查每个fd int i; for(i = 0; i < nready; i++){ int fd = events[i].data.fd; if(fd == STDIN_FILENO){ if(fgets(sendbuf, sizeof(sendbuf), stdin) == NULL){ shutdown(peerfd, SHUT_WR); //移除这个fd struct epoll_event ev; ev.data.fd = STDIN_FILENO; if(epoll_ctl(epollfd, EPOLL_CTL_DEL, STDIN_FILENO, &ev) == -1) ERR_EXIT("epoll_ctl"); } else{ writen(peerfd, sendbuf, strlen(sendbuf)); } } if(fd == peerfd){ int ret = readline(peerfd, recvbuf, 1024); if(ret == -1) ERR_EXIT("readline"); else if(ret == 0){ close(peerfd); printf("server closed\n"); exit(EXIT_SUCCESS); } printf("recv data: %s", recvbuf); } } }}
4.总结(转自 http://www.cnblogs.com/bigwangdi/p/3182958.html)
a)select 函数监视的文件描述符分3类,分别是writefds、readfds、和exceptfds。调用后select函数会阻塞,直到有描述副就绪(有数据 可读、可写、或者有except),或者超时(timeout指定等待时间,如果立即返回设为null即可),函数返回。当select函数返回后,可以通过遍历fdset,来找到就绪的描述符。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点。select的一个缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,可以通过修改宏定义甚至重新编译内核的方式提升这一限制,但是这样也会造成效率的降低。
b) pollfd 结构体包含了要监视的event和函数返回后的event,不再使用select“参数-值”传递的方式。同时,pollfd并没有最大数量限制(但是数量过大后性能也是会下降)。 和select函数一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
从上面看,select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
c) epoll模型主要是epoll_create,epoll_ctl和epoll_wait三个函数。epoll_create函数创建epoll文件描述符,参数size并不是限制了epoll所能监听的描述符最大个数,只是对内核初始分配内部数据结构的一个建议。返回是epoll描述符。-1表示创建失败。epoll_ctl 控制对指定描述符fd执行op操作,event是与fd关联的监听事件。op操作有三种:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。epoll_wait 等待epfd上的io事件,最多返回maxevents个事件。
在 select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一 个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait() 时便得到通知。
epoll的优点主要是一下几个方面:
1. 监视的描述符数量不受限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。select的最大缺点就是进程打开的fd是有数量限制的。这对于连接数量比较大的服务器来说根本不能满足。虽然也可以选择多进程的解决方案( Apache就是这样实现的),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。
2. IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数。
3.支持电平触发和边沿触发(只告诉进程哪些文件描述符刚刚变为就绪状态,它只说一遍,如果我们没有采取行动,那么它将不会再次告知,这种方式称为边缘触发)两种方式,理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
4.mmap加速内核与用户空间的信息传递。epoll是通过内核于用户空间mmap同一块内存,避免了无畏的内存拷贝。
5.附录(其他代码)
#ifndef __DEF_H__#define __DEF_H__#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <errno.h>#include <sys/types.h>#include <sys/socket.h>#include <netinet/in.h>#include <arpa/inet.h>#include <poll.h>#include <signal.h>#define ERR_EXIT(m) do { perror(m); exit(EXIT_FAILURE); }while(0)ssize_t readn(int fd, void *usrbuf, size_t n);ssize_t writen(int fd, void *usrbuf, size_t n);ssize_t readline(int sockfd, void *usrbuf, size_t maxline);ssize_t readpeek(int sockfd, void *usrbuf, size_t n);#endif#include "def.h"/* * 封装后的read类函数 * */ssize_t readn(int fd, void *usrbuf, size_t n){ size_t nleft = n; char *bufptr = usrbuf; ssize_t nread; while(nleft > 0){ nread = read(fd, bufptr, nleft); if(nread == -1){ if(errno == EINTR) continue; else return -1; } else if(nread == 0) break; bufptr += nread; nleft -= nread; } return (n - nleft);}ssize_t writen(int fd, void *usrbuf, size_t n){ size_t nleft = n; char *bufptr = usrbuf; size_t nwrite; while(nleft > 0){ nwrite = write(fd, bufptr, nleft); if(nwrite <= 0){ if(errno == EINTR) nwrite = 0; else return -1; } bufptr += nwrite; nleft -= nwrite; } return n;}// recv_peek完成 一次 正确的读取过程ssize_t recv_peek(int sockfd, void *usrbuf, size_t n){// 使用 recv函数的 MSG_PEEK标志 预读取数据 int nread; //接收recv函数的返回值 while(1){//使用while循环是因为被中断时候还要continue; //recv只成功调用一次 nread = recv(sockfd, usrbuf, n, MSG_PEEK); if(nread < 0){ if(errno == EINTR) continue; else return -1; } break; } return nread;}ssize_t readline(int sockfd, void *usrbuf, size_t maxline){ char *bufptr = usrbuf; size_t nleft = maxline - 1; ssize_t ret; ssize_t nread; size_t total = 0; // while(nleft > 0){ //预览数据 并没有把数据从缓冲区取走 ret = recv(sockfd, bufptr, nleft, MSG_PEEK); if(ret <= 0) //这里读取的字节不够不是错误 return ret; int i; nread = ret; for(i = 0; i < nread; i++){ if(bufptr[i] == ‘\n‘){ // 遇到\n时截断 ret = readn(sockfd, bufptr, i + 1); if(ret != i+1) // 没有读够 错误 return -1; total += ret; // 如果一次预读取都不含\n 此时就用用total保存一已读取的字节数 bufptr += ret; *bufptr = 0; return total; } } //没有发现\n 全部接收 ret = readn(sockfd, bufptr, nread); if(ret != nread) return -1; total += nread; nleft -= nread; bufptr += nread; } *bufptr = 0; return maxline - 1;}#include "def.h"int listenfd_init(); //返回一个处于监听状态的套接字描述符void do_service(int peerfd); // 处理客户端的请求int main(int argc, const char *argv[]){ if(signal(SIGPIPE, SIG_IGN) == SIG_ERR) ERR_EXIT("signal"); int listenfd = listenfd_init(); // 生成一个套接字并使其处于监听状态 struct sockaddr_in peeraddr; int len = sizeof(peeraddr); int peerfd; if((peerfd = accept(listenfd, (struct sockaddr*)&peeraddr, &len)) == -1) //接受一个TCP连接请求 ERR_EXIT("accpet"); do_service(peerfd); // 处理请求 close(peerfd); close(listenfd); return 0;}int listenfd_init(){ int listenfd = socket(AF_INET, SOCK_STREAM, 0); int on = 1; if(setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) ERR_EXIT("setsockopt"); struct sockaddr_in serveraddr; serveraddr.sin_family = AF_INET; serveraddr.sin_addr.s_addr = htonl(INADDR_ANY); serveraddr.sin_port = htons(9999); if(listenfd == -1) ERR_EXIT("socket"); if(bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) ERR_EXIT("bind"); if(listen(listenfd, 10) == -1) ERR_EXIT("listen"); return listenfd;}void do_service(int peerfd){ char recvbuf[1024] = {0}; int ret; while(1){ ret = readline(peerfd, recvbuf, 1024); if(ret <= 0) break; printf("recv data : %s", recvbuf); writen(peerfd, recvbuf, strlen(recvbuf)); }}