本文主要是网络编程——多路复用IO中的epoll的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows windows11 Ubuntu Ubuntu16.04的64位版本 VMware® Workstation 16 Pro 16.2.3 build-19376536 SecureCRT Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 开发板 正点原子 i.MX6ULL Linux阿尔法开发板 uboot NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) linux内核 linux-4.15(NXP官方提供) STM32开发板 正点原子战舰V3(STM32F103ZET6)
点击查看本文参考资料
点击查看相关文件下载
一、epoll
简介 epoll
是在2.6
内核中提出的,是之前的select
和poll
的增强版本。相对于select
和poll
来说,epoll
更加灵活,没有描述符限制,它所支持的文件描述符上限是整个系统可以打开的文件数目。
epoll
使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,并且每个fd
上边都会有一个callback
函数(回调函数),只有活跃的socket
才会去主动调用这个回调函数,这属于一种通知机制 ,就是当事件发生的时候,则主动通知;通知机制的反面,就是轮询机制。
我们主要会用到以下三个函数,这里提前说一下,后边介绍原理的话可能会用到:
1 2 3 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
通过以上三个函数把原先的select/poll
调用分成了3
个部分:
(1)调用epoll_create()
建立一个epoll
对象(在epoll
文件系统中为这个句柄对象分配资源);
(2)调用epoll_ct()l
向epoll
对象中添加需要连接的socket
套接字;
(3)调用epoll_wait()
收集发生的事件的连接。
这样做的好处?
假设现在有100
万个客户端同时与一个服务器进程保持着TCP
连接。而每一时刻,通常只有几百或者几千个TCP
连接是活跃的,这种情况下如何实现高并发?
在select/poll
时代,服务器进程每次都把这100
万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大。对于select
来说,在FD_SETSIZE
为1024
的情况下,则我们至少需要创建1k
个进程才能实现100
万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select
模型的服务器程序,要达到100
万级别的并发访问,是一个很难完成的任务。poll
与select
其实是很类似的,它也是难以完成这样的高并发连接。
而我们使用epoll
的话,只需要在进程启动时建立一个epoll
对象,然后在需要的时候向这个epoll
对象中添加或者删除连接。同时,epoll_wait
的效率也非常高,因为调用epoll_wait
时,并不会一次性向操作系统复制这100
万个连接的句柄数据,内核也不需要去遍历全部的连接。
二、epoll
数据结构 关于epoll
的源码我是找了一下,但是没有找到,也可能是自己查找的方式不对吧,但是这个嘛暂时不太重要,后边再补充吧。不过我在github
上找到了一个项目,包含了linux
内核源码:linux/eventpoll.c at master · torvalds/linux (github.com)
先来看一张网上找到的一张关于epoll
数据结构的图,不过我修改成自己可以理解的样子啦(这里会提到红黑树,前边学习数据结构的时候好像没有了解这一种树,后边会在数据结构的笔记中补充):
当某一进程调用epoll_create()
创建epoll
对象的时候,Linux
内核会创建一个eventpoll
结构体,这个结构体中有两个成员与epoll
的使用方式密切相关,这两个成员是rdllist
和rbr
,这两个成员的类型如下所示:
1 2 3 4 5 6 7 8 9 10 struct eventpoll { ... struct list_head rdlist ; ... struct rb_root_cached rbr ; ... };
点击查看 eventpoll 详细定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 struct eventpoll { struct mutex mtx ; wait_queue_head_t wq; wait_queue_head_t poll_wait; struct list_head rdllist ; rwlock_t lock; struct rb_root_cached rbr ; struct epitem *ovflist ; struct wakeup_source *ws ; struct user_struct *user ; struct file *file ; u64 gen; struct hlist_head refs ; #ifdef CONFIG_NET_RX_BUSY_POLL unsigned int napi_id; #endif #ifdef CONFIG_DEBUG_LOCK_ALLOC u8 nests; #endif };
每一个epoll
对象都会有一个独立的eventpoll
结构体,用于存放通过epoll_ctl
方法向epoll
对象中添加进来的事件。这些事件都会挂载在红黑树中,这样的话,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lg(n)
,其中n
为树的高度)。
所有添加到epoll
中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback
,它会将发生的事件添加到rdlist
双链表中。
在epoll
中,对于每一个事件,都会建立一个epitem
结构体,其中有两个成员:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 struct epitem { union { struct rb_node rbn ; struct rcu_head rcu ; }; struct list_head rdllink ; ... };
rbn
:该成员表示挂载到eventpoll
的红黑树节点,这些节点存储着我们添加到epoll
的所有事件。
rdllink
:该成员表示挂载到eventpoll.rdllist
的节点,这些节点存储着已经准备好的事件。
点击查看 epitem 详细定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 struct epitem { union { struct rb_node rbn ; struct rcu_head rcu ; }; struct list_head rdllink ; struct epitem *next ; struct epoll_filefd ffd ; struct eppoll_entry *pwqlist ; struct eventpoll *ep ; struct hlist_node fllink ; struct wakeup_source __rcu *ws ; struct epoll_event event ; };
当调用epoll_wait
检查是否有事件发生时,只需要检查eventpoll
对象中的rdlist
双链表中是否有epitem
元素即可。如果rdlist
不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。
总结下来就是,通过红黑树和双链表数据结构,并结合回调机制,使epoll更加高效。
三、相关API
1. epoll_create()
1.1 函数说明 在linux
下可以使用man 2 epoll_create
命令查看该函数的帮助手册。
1 2 3 4 5 #include <sys/epoll.h> int epoll_create (int size) ;
【函数说明】 该函数用于创建一个epoll
句柄,也就是新创建一个epoll
对象。
【函数参数】
size
:int
类型,在初始epoll_create()
实现中,size
参数告知内核调用者希望添加到epoll
对象的文件描述符的数量。从 Linux
内核 2.6.8
版本起,不再需要这个参数了(内核不需要该参数就可以动态地调整所需数据结构的大小),但是size
仍然必须大于零,以便在旧内核上运行新的epoll
应用程序时确保向后兼容性。
【返回值】 int
类型,成功将返回一个epoll
句柄,这其实就是一个文件描述符(非负整数)。如果发生错误,则返回-1
,并设置errno
来表示错误类型。
【使用格式】 一般情况下基本使用格式如下:
1 2 3 4 5 6 7 #include <sys/epoll.h> #define MAX_EVENTS 500 int epollFd = -1 ;epollFd = epoll_create(MAX_EVENTS);
【注意事项】 当我们创建好epoll
句柄后,它就会占用一个文件描述符,当我们使用完毕后,需要调用close
来关闭这个epoll
句柄,否则可能会导致fd
不足。
1.2 使用实例 暂无
2. epoll_ctl()
2.1 函数说明 在linux
下可以使用man 2 epoll_ctl
命令查看该函数的帮助手册。
1 2 3 4 5 #include <sys/epoll.h> int epoll_ctl (int epfd, int op, int fd, struct epoll_event *event) ;
【函数说明】 该函数用于控制 epoll
对象,主要涉及 epoll
红黑树上节点的一些操作,比如添加节点,删除节点,修改节点事件。
【函数参数】
epfd
:int
类型,通过 epoll_create()
创建的 epoll
对象句柄。
op
:int
类型,表示动作类型,是对红黑树的操作,添加节点、删除节点、修改节点事件。
点击查看 op 可取的值
EPOLL_CTL_ADD 注册新的 fd 到 epoll 对象中,相当于往红黑树添加一个节点。 EPOLL_CTL_MOD 修改已注册的 fd 的监听事件,相当于把红黑树上监听的 socket 对应的监听事件做修改。 EPOLL_CTL_DEL 从 epoll 对象中删除一个 fd,相当于取消监听 socket 的事件。
fd
:int
类型,需要添加监听的 socket
描述符,可以是监听套接字,也可以是与客户端通讯的通讯套接字。
event
:struct epoll_event
类型结构体指针变量,表示事件的信息。
点击查看 event 参数详细说明
我们在使用man
手册的时候,下边会有该参数类型的说明:
1 2 3 4 5 6 7 8 9 10 11 12 13 typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t ; struct epoll_event { uint32_t events; epoll_data_t data; };
【成员说明】
events
:uint32_t
类型,代表要监听的 epoll
事件类型,有读事件,写事件等。 events取值 含义 EPOLLIN 表示对应的文件描述符可读。例如,如果客户端发送消息过来,代表服务器收到了可读事件。 EPOLLOUT 表示对应的文件描述符可写。如果 fd 对应的发数据内核缓冲区不为满,只要监听了写事件,就会触发可写事件。 EPOLLPRI 表示对应的文件描述符有紧急数据可读(带外数据)。 EPOLLERR 表示对应的文件描述符发生错误。 EPOLLHUP 表示对应的文件描述符被挂起。 EPOLLET 将对应的文件描述符设置为边缘触发(edge-triggered),这是相对于水平触发(level-triggered)而言的。 EPOLLRDHUP 监听套接字关闭或半关闭事件,Linux 内核 2.6.17 后可用。
data
:epoll_data_t
类型,这是一个联合体类型,它可以在我们调用 epoll_ctl
给 fd
添加或者修改描述符监听的事件时附带一些数据。最典型的用法就是每个通讯套接字会对应内存中的一块数据区,这块数据区一般存放着一些连接相关的信息,比如对端的 IP
,端口等。当我们要添加该通讯套接字监听事件时就可以把这块内存的地址赋值给 ptr
,这样当我们调用 epoll_wait
时也可以取出这些信息。
【返回值】 int
类型,成功返回0
;失败返回-1
,并设置errno
来表示错误类型。
【使用格式】 一般情况基本使用格式如下:
1 2 3 4 5 6 7 8 9 10 #include <sys/epoll.h> struct epoll_event event ; epfd = epoll_create(1 ); event.data.fd = socket_fd; event.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, socket_fd, &event);
【注意事项】 none
2.2 使用实例 暂无
3. epoll_wait()
3.1 函数说明 在linux
下可以使用man 2 epoll_wait
命令查看该函数的帮助手册。
1 2 3 4 5 #include <sys/epoll.h> int epoll_wait (int epfd, struct epoll_event *events, int maxevents, int timeout) ;
【函数说明】 该函数用于等待事件发生,返回事件集合,也就是获取内核的事件通知。其实就是遍历双向链表,把双向链表里的节点数据拷贝出来,拷贝完毕后就从双向链表移除。
【函数参数】
epfd
:int
类型,通过 epoll_create()
创建的 epoll
对象句柄。
events
:struct epoll_event
类型结构体指针变量,这是一个传出参数,用于获取从内核传出的已经符合用户要求的事件集合,一般这里我们会定义一个struct epoll_event
结构体数组,并将数组首地址赋值给该变量。
maxevents
:int
类型,代表可以存放的事件个数,就是每次可以处理的最大事件数,也就是events
数组的大小。注意这个值要大于0
,并且在以前的时候不能大于epoll_create()
中参数size
的大小,后来epoll_create()
忽略size
参数后,好像就没有这个限制了。
timeout
:int
类型,表示超时时间,单位为毫秒(ms
)。
点击查看 timeout 取值说明
timeout = 0 立即返回 timeout = -1 阻塞等待 timeout > 0 超时后返回
【返回值】 int
类型,可能会有。以下三种情况:
返回-1
:表示发生错误,并会设置errno
表示错误类型。
返回0
:超时时间timeout
到了,但是监听的事件中依然没有符合我们要求的事件发生。
返回正整数:表示符合我们要求的事件发生了,返回值代表这些事件的个数。
【使用格式】 一般情况基本使用格式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <sys/epoll.h> epret = epoll_wait(epfd, events, 20 , -1 ); if (epret < 0 ){ perror("epoll_wait error" ); continue ; } for (i = 0 ; i < epret; i++) { if (events[i].data.fd == socket_fd) { ... } else { ... } }
【注意事项】 none
3.2 使用实例 暂无
4. epoll_pwait()
4.1 函数说明 在linux
下可以使用man 2 epoll_pwait
命令查看该函数的帮助手册。
1 2 3 4 5 #include <sys/epoll.h> int epoll_pwait (int epfd, struct epoll_event *events, int maxevents, int timeout, const sigset_t *sigmask) ;
【函数说明】 该函数与epoll_wait()
一样,只是多了一个信号掩码的参数,是一个防止信号干扰的增强型 epoll_wait()
函数。
【函数参数】
epfd
:int
类型,通过 epoll_create()
创建的 epoll
对象句柄。
events
:struct epoll_event
类型结构体指针变量,这是一个传出参数,用于获取从内核传出的已经符合用户要求的事件集合。
maxevents
:int
类型,代表可以存放的事件个数,就是每次可以处理的最大事件数,也就是events
数组的大小。注意这个值要大于0
,并且不能大于epoll_create()
中参数size
的大小。
timeout
:int
类型,表示超时时间,单位为毫秒(ms
)。
点击查看 timeout 取值说明
timeout = 0 立即返回 timeout = -1 阻塞等待 timeout > 0 超时后返回
sigmask
:sigset_t
类型,表示信号掩码,指定一个需要屏蔽的信号集,当该参数为NULL
时,该函数就等价于epoll_wait()
。
【返回值】 int
类型,可能会有。以下三种情况:
返回-1
:表示发生错误,并会设置errno
表示错误类型。
返回0
:超时时间timeout
到了,但是监听的事件中依然没有符合我们要求的事件发生。
返回正整数:表示符合我们要求的事件发生了,返回值代表这些事件的个数。
【使用格式】 none
【注意事项】 none
4.2 使用实例 暂无
四、工作模式 epoll
对文件描述符的操作有两种模式:LT
(level trigger
)和ET
(edge trigger
),其中LT
模式是默认模式。
LT
(Level Triggered
),水平触发模式。该模式为epoll
的默认工作模式,它同时支持阻塞和非阻塞的socket
。在该种模式下,当epoll_wait
检测到描述符事件发生并将此事件通知进程,进程可以不立即处理该事件,当下次调用epoll_wait
时,会再次响应进程并通知此事件,直到该事件被进程处理。在后边的例子中,当数据不被处理的话,可能会导致epoll_wait()
函数不再阻塞,从而一直通知进程有新数据到达。
ET
(Edge Triggered
),边缘触发模式。该模式是一种高速处理模式,当且仅当状态发生变化时进程才会获得通知。当epoll_wait
检测到描述符事件发生并将此事件通知进程,进程必须立即处理该事件,如果不处理,或者只是处理了一部分的数据,并没有全部处理,那么下次调用epoll_wait
时,不会再次响应进程并通知此事件。需要注意的是,每个使用ET
模式的文件描述符都应该是非阻塞的,如果文件描述符是阻塞的,那么读或写操作将会因为没有后续的事件而一直处于阻塞状态(饥渴状态)。
ET模式在很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。但是需要注意 epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
五、使用实例 下边的程序中,我只在server
中使用了epoll
,客户端的程序使用的还是select
。
1. 基本使用实例 1.1 server
服务器端 点击查看实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <strings.h> #include <arpa/inet.h> #include <netinet/in.h> #include <unistd.h> #include <string.h> #include <sys/epoll.h> void usage (char *str) ; int main (int argc, char *argv[]) { int port = -1 ; if (argc != 3 ) { usage(argv[0 ]); exit (-1 ); } port = atoi(argv[2 ]); if (port < 5000 ) { usage(argv[0 ]); exit (-1 ); } int socket_fd = -1 ; if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0 )) < 0 ) { perror ("socket" ); exit (-1 ); } int b_reuse = 1 ; setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int )); struct sockaddr_in sin ; bzero (&sin , sizeof (sin )); sin .sin_family = AF_INET; sin .sin_port = htons(port); if (inet_pton(AF_INET, argv[1 ], (void *)&sin .sin_addr) != 1 ) { perror ("inet_pton" ); exit (-1 ); } if (bind(socket_fd, (struct sockaddr *)&sin , sizeof (sin )) < 0 ) { perror("bind" ); exit (-1 ); } if (listen(socket_fd, 5 ) < 0 ) { perror("listen" ); exit (-1 ); } printf ("Server starting....OK!\n" ); int client_fd = -1 ; struct sockaddr_in cin ; socklen_t addrlen = sizeof (cin ); char ipv4_addr[16 ]; int ret = -1 ; char buf[BUFSIZ]; char replay[BUFSIZ]; int epfd, epret, i; struct epoll_event event ; struct epoll_event events [20]; epfd = epoll_create(1 ); event.data.fd = socket_fd; event.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, socket_fd, &event); while (1 ) { epret = epoll_wait(epfd, events, 20 , -1 ); if (epret < 0 ) { perror("epoll_wait error" ); continue ; } for (i = 0 ; i < epret; i++) { if (events[i].data.fd == socket_fd) { if ((client_fd = accept(socket_fd, (struct sockaddr *)&cin , &addrlen)) < 0 ) { perror("accept" ); exit (-1 ); } event.data.fd = client_fd; event.events = EPOLLIN | EPOLLET; epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &event); if (!inet_ntop(AF_INET, (void *)&cin .sin_addr, ipv4_addr, sizeof (cin ))) { perror ("inet_ntop" ); exit (-1 ); } printf ("Clinet(%s:%d) is connected successfully![client_fd=%d]\n" , ipv4_addr, ntohs(cin .sin_port), client_fd); } else { bzero(buf, BUFSIZ); bzero(replay, BUFSIZ); do { ret = read(events[i].data.fd, buf, BUFSIZ - 1 ); }while (ret < 0 && EINTR == errno); if (ret < 0 ) { perror("read" ); exit (-1 ); } if (!ret) { close(events[i].data.fd); printf ("closed client: %d \n" , events[i].data.fd); epoll_ctl(epfd, EPOLL_CTL_DEL, events[i].data.fd, &event); } else { printf ("Receive data[client_fd=%d]: %s" , events[i].data.fd, buf); strcat (replay, buf); ret = send(events[i].data.fd, replay, strlen (replay), 0 ); if (ret < 0 ) { perror("send" ); continue ; } } } } } close(socket_fd); return 0 ; } void usage (char *str) { printf ("\n%s serv_ip serv_port" , str); printf ("\n\t serv_ip: server ip address" ); printf ("\n\t serv_port: server port(>5000)\n\n" ); printf ("\n\t Attention: The IP address must be the IP address of the local nic or 0.0.0.0 \n\n" ); }
1.2 client
客户端 点击查看实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <sys/epoll.h> void usage (char *str) ; int main (int argc, char *argv[]) { int port = -1 ; int portClient = -1 ; if (argc != 4 ) { usage(argv[0 ]); exit (-1 ); } port = atoi(argv[2 ]); portClient = atoi(argv[3 ]); if (port < 5000 || portClient < 5000 || (port == portClient)) { usage(argv[0 ]); exit (-1 ); } int socket_fd = -1 ; if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0 )) < 0 ) { perror ("socket" ); exit (-1 ); } int b_reuse = 1 ; setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int )); struct sockaddr_in sin ; bzero (&sin , sizeof (sin )); sin .sin_family = AF_INET; sin .sin_port = htons(port); if (inet_pton(AF_INET, argv[1 ], (void *)&sin .sin_addr) != 1 ) { perror ("inet_pton" ); exit (-1 ); } struct sockaddr_in sinClient ; bzero(&sinClient, sizeof (sinClient)); sinClient.sin_family = AF_INET; sinClient.sin_port = htons(portClient); if (inet_pton(AF_INET, argv[1 ], (void *)&sinClient.sin_addr) != 1 ) { perror ("inet_pton" ); exit (-1 ); } if (bind(socket_fd, (struct sockaddr *)&sinClient, sizeof (sinClient)) < 0 ) { perror("bind" ); exit (-1 ); } if (connect(socket_fd, (struct sockaddr *)&sin , sizeof (sin )) < 0 ) { perror("connect" ); exit (-1 ); } printf ("Client staring...OK!\n" ); int ret = -1 ; char buf[BUFSIZ]; char replay[BUFSIZ]; fd_set rset; int maxfd = -1 ; struct timeval tout ; tout.tv_sec = 5 ; tout.tv_usec = 0 ; while (1 ) { FD_ZERO(&rset); FD_SET(0 , &rset); FD_SET(socket_fd, &rset); maxfd = socket_fd; ret = select(maxfd + 1 , &rset, NULL , NULL , &tout); if (ret < 0 ) { perror("select" ); continue ; } if (FD_ISSET(0 , &rset)) { bzero(buf, BUFSIZ); do { ret = read(0 , buf, BUFSIZ - 1 ); }while (ret < 0 && EINTR == errno); if (ret < 0 ) { perror ("read() from stdin" ); continue ; } if (!ret) continue ; if (write(socket_fd, buf, strlen (buf)) < 0 ) { perror ("write to socket error" ); continue ; } if (!strncasecmp(buf, "quit" , strlen ("quit" ))) { printf ("Client is exiting!\n" ); break ; } } if (FD_ISSET(socket_fd, &rset)) { bzero (replay, BUFSIZ); do { ret = recv(socket_fd, replay, BUFSIZ, 0 ); }while (ret < 0 && EINTR == errno); if (ret < 0 ) { perror("recv" ); continue ; } if (ret == 0 ) break ; printf ("server replay:%s" , replay); if (!strncasecmp(buf, "quit" , strlen ("quit" ))) { printf ("Sender Client is exiting... ...!\n" ); break ; } } } close(socket_fd); return 0 ; } void usage (char *str) { printf ("\n%s serv_ip serv_port" , str); printf ("\n\t serv_ip: server ip address" ); printf ("\n\t serv_port: server port(>5000)\n\n" ); printf ("\n\t client_port: client portClient(>5000 && !=port )\n\n" ); }
1.3 Makefile
由于需要生成两个可执行程序,自己输命令有些繁琐,这里使用make
来进行。
点击查看 Makefile 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 CC = gcc DEBUG = -g -O2 -Wall CFLAGS += $(DEBUG) TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}} all : $(TARGET_LIST) %.o : %.c $(CC) $(CFLAGS) -c $< -o $@ .PHONY : all clean clean_o clean_outclean : clean_o clean_out @rm -vf $(TARGET_LIST) clean_o : @rm -vf *.o clean_out : @rm -vf *.out
1.4 测试结果 我们执行以下命令编译链接程序,生成两个可执行文件:
然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序server
和客户端程序client
:
1 2 gcc -g -O2 -Wall client.c -o client gcc -g -O2 -Wall server.c -o server
对于服务器端,我们执行以下命令启动服务器进程:
1 ./server 0.0.0.0 5001 # 允许监听所有网卡IP及端口
对于客户端,我们执行以下命令启动客户端进程:
1 2 ./client 192.168.10.101 5001 5002 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5002 ./client 192.168.10.101 5001 5003 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5003
然后我们就会看到如下现象:
可以发现我们发送的数据,在客户端都被打印了出来,说明客户端与服务器端可以进行正常通信。
2. 工作模式测试 下边的例子依然只是修改了服务器的代码,我们在启动服务器的时候选择epoll
工作模式,并删除服务器端读取数据部分的代码。
2.1 server
服务器端 点击查看实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <sys/types.h> #include <sys/socket.h> #include <strings.h> #include <arpa/inet.h> #include <netinet/in.h> #include <unistd.h> #include <string.h> #include <sys/epoll.h> void usage (char *str) ; int main (int argc, char *argv[]) { int port = -1 ; if (argc != 3 ) { usage(argv[0 ]); exit (-1 ); } port = atoi(argv[2 ]); if (port < 5000 ) { usage(argv[0 ]); exit (-1 ); } int socket_fd = -1 ; if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0 )) < 0 ) { perror ("socket" ); exit (-1 ); } int b_reuse = 1 ; setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int )); struct sockaddr_in sin ; bzero (&sin , sizeof (sin )); sin .sin_family = AF_INET; sin .sin_port = htons(port); if (inet_pton(AF_INET, argv[1 ], (void *)&sin .sin_addr) != 1 ) { perror ("inet_pton" ); exit (-1 ); } if (bind(socket_fd, (struct sockaddr *)&sin , sizeof (sin )) < 0 ) { perror("bind" ); exit (-1 ); } if (listen(socket_fd, 5 ) < 0 ) { perror("listen" ); exit (-1 ); } int mode = 0 ; printf ("please select epoll mode(1:LT 2:ET):" ); scanf ("%d" , &mode); printf ("Server starting...OK, epoll mode is %d(1:LT 2:ET)!\n" , mode); int client_fd = -1 ; struct sockaddr_in cin ; socklen_t addrlen = sizeof (cin ); char ipv4_addr[16 ]; int count = 0 ; int epfd, epret, i; struct epoll_event event ; struct epoll_event events [20]; epfd = epoll_create(1 ); event.data.fd = socket_fd; event.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, socket_fd, &event); while (1 ) { epret = epoll_wait(epfd, events, 20 , -1 ); if (epret < 0 ) { perror("epoll_wait error" ); continue ; } for (i = 0 ; i < epret; i++) { if (events[i].data.fd == socket_fd) { if ((client_fd = accept(socket_fd, (struct sockaddr *)&cin , &addrlen)) < 0 ) { perror("accept" ); exit (-1 ); } event.data.fd = client_fd; if (mode == 2 ) event.events = EPOLLIN | EPOLLET; else event.events = EPOLLIN; epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &event); if (!inet_ntop(AF_INET, (void *)&cin .sin_addr, ipv4_addr, sizeof (cin ))) { perror ("inet_ntop" ); exit (-1 ); } printf ("Clinet(%s:%d) is connected successfully![client_fd=%d]\n" , ipv4_addr, ntohs(cin .sin_port), client_fd); } else { count++; printf ("new data arrive!count = %d\n" ,count); if (count > 10 ) exit (0 ); } } } close(socket_fd); return 0 ; } void usage (char *str) { printf ("\n%s serv_ip serv_port" , str); printf ("\n\t serv_ip: server ip address" ); printf ("\n\t serv_port: server port(>5000)\n\n" ); printf ("\n\t Attention: The IP address must be the IP address of the local nic or 0.0.0.0 \n\n" ); }
2.2 client
客户端 点击查看实例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 #include <stdio.h> #include <stdlib.h> #include <errno.h> #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <string.h> #include <sys/epoll.h> void usage (char *str) ; int main (int argc, char *argv[]) { int port = -1 ; int portClient = -1 ; if (argc != 4 ) { usage(argv[0 ]); exit (-1 ); } port = atoi(argv[2 ]); portClient = atoi(argv[3 ]); if (port < 5000 || portClient < 5000 || (port == portClient)) { usage(argv[0 ]); exit (-1 ); } int socket_fd = -1 ; if ((socket_fd = socket(AF_INET, SOCK_STREAM, 0 )) < 0 ) { perror ("socket" ); exit (-1 ); } int b_reuse = 1 ; setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &b_reuse, sizeof (int )); struct sockaddr_in sin ; bzero (&sin , sizeof (sin )); sin .sin_family = AF_INET; sin .sin_port = htons(port); if (inet_pton(AF_INET, argv[1 ], (void *)&sin .sin_addr) != 1 ) { perror ("inet_pton" ); exit (-1 ); } struct sockaddr_in sinClient ; bzero(&sinClient, sizeof (sinClient)); sinClient.sin_family = AF_INET; sinClient.sin_port = htons(portClient); if (inet_pton(AF_INET, argv[1 ], (void *)&sinClient.sin_addr) != 1 ) { perror ("inet_pton" ); exit (-1 ); } if (bind(socket_fd, (struct sockaddr *)&sinClient, sizeof (sinClient)) < 0 ) { perror("bind" ); exit (-1 ); } if (connect(socket_fd, (struct sockaddr *)&sin , sizeof (sin )) < 0 ) { perror("connect" ); exit (-1 ); } printf ("Client staring...OK!\n" ); int ret = -1 ; char buf[BUFSIZ]; char replay[BUFSIZ]; fd_set rset; int maxfd = -1 ; struct timeval tout ; tout.tv_sec = 5 ; tout.tv_usec = 0 ; while (1 ) { FD_ZERO(&rset); FD_SET(0 , &rset); FD_SET(socket_fd, &rset); maxfd = socket_fd; ret = select(maxfd + 1 , &rset, NULL , NULL , &tout); if (ret < 0 ) { perror("select" ); continue ; } if (FD_ISSET(0 , &rset)) { bzero(buf, BUFSIZ); do { ret = read(0 , buf, BUFSIZ - 1 ); }while (ret < 0 && EINTR == errno); if (ret < 0 ) { perror ("read() from stdin" ); continue ; } if (!ret) continue ; if (write(socket_fd, buf, strlen (buf)) < 0 ) { perror ("write to socket error" ); continue ; } if (!strncasecmp(buf, "quit" , strlen ("quit" ))) { printf ("Client is exiting!\n" ); break ; } } if (FD_ISSET(socket_fd, &rset)) { bzero (replay, BUFSIZ); do { ret = recv(socket_fd, replay, BUFSIZ, 0 ); }while (ret < 0 && EINTR == errno); if (ret < 0 ) { perror("recv" ); continue ; } if (ret == 0 ) break ; printf ("server replay:%s" , replay); if (!strncasecmp(buf, "quit" , strlen ("quit" ))) { printf ("Sender Client is exiting... ...!\n" ); break ; } } } close(socket_fd); return 0 ; } void usage (char *str) { printf ("\n%s serv_ip serv_port" , str); printf ("\n\t serv_ip: server ip address" ); printf ("\n\t serv_port: server port(>5000)\n\n" ); printf ("\n\t client_port: client portClient(>5000 && !=port )\n\n" ); }
2.3 Makefile
由于需要生成两个可执行程序,自己输命令有些繁琐,这里使用make
来进行。
点击查看 Makefile 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 CC = gcc DEBUG = -g -O2 -Wall CFLAGS += $(DEBUG) TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}} all : $(TARGET_LIST) %.o : %.c $(CC) $(CFLAGS) -c $< -o $@ .PHONY : all clean clean_o clean_outclean : clean_o clean_out @rm -vf $(TARGET_LIST) clean_o : @rm -vf *.o clean_out : @rm -vf *.out
2.4 测试结果 我们执行以下命令编译链接程序,生成两个可执行文件:
然后会有如下提示,并生成两个可执行文件吗,分别为服务器端程序server
和客户端程序client
:
1 2 gcc -g -O2 -Wall client.c -o client gcc -g -O2 -Wall server.c -o server
对于客户端,我们执行以下命令启动客户端进程:
1 ./client 192.168.10.101 5001 5002 # 连接到本地一块网卡的IP,并设置客户端向外发送数据的端口为5002
对于服务器端,我们执行以下命令启动服务器进程:
1 ./server 0.0.0.0 5001 # 允许监听所有网卡IP及端口
然后会得到以下提示:
1 please select epoll mode(1:LT 2:ET):
我们选择输入1
并按下enter
后,服务器端中用于通信的文件描述符将会使用默认的工作模式,也就是LT
模式,此时我们来看一下现象:
可以发现我们发送的数据,并没有被读取,而服务器端会一直提醒有数据到达,提醒11
次后,程序退出,这是因为我设置了超过10
次就结束进程,否则会一直打印一直打印,这也说明了,LT
模式下,不处理数据的话,内核会一直通知进行,并且epoll_wait()
函数也不再阻塞了。
对于服务器端,我们执行以下命令启动服务器进程:
1 ./server 0.0.0.0 5001 # 允许监听所有网卡IP及端口
然后会得到以下提示:
1 please select epoll mode(1:LT 2:ET):
我们选择输入2
并按下enter
后,服务器端中用于通信的文件描述符将会使用ET
模式,此时我们来看一下现象:
可以发现我们发送的数据,并没有被读取,然后我们发送一次就提示一次,即便数据没有被处理。