TCP/IP网络编程 --优于select的epoll

网络 网络管理
关于并发服务器中的I/O复用实现方式,前面我们讲过select的方式,但select的性能比较低,并不适合以Web服务器端开发为主流的现代开发环境。因此就有了Linux下的epoll,BSD的kqueue,Solaris的/dev/poll和Windows的IOCP等复用技术。本章就来讲讲Linux下的epoll技术。

关于并发服务器中的I/O复用实现方式,前面我们讲过select的方式,但select的性能比较低,并不适合以Web服务器端开发为主流的现代开发环境。因此就有了Linux下的epoll,BSD的kqueue,Solaris的/dev/poll和Windows的IOCP等复用技术。本章就来讲讲Linux下的epoll技术。

epoll理解及应用

基于select的I/O复用技术速度慢的原因:

1,调用select函数后常见的针对所有文件描述符的循环语句。它每次事件发生需要遍历所有文件描述符,找出发生变化的文件描述符。(以前写的示例没加循环)

2,每次调用select函数时都需要向该函数传递监视对象信息。即每次调用select函数时向操作系统传递监视对象信息,至于为什么要传?是因为我们监视的套接字变化的函数,而套接字是操作系统管理的。(这个才是最耗效率的)

注释:基于这样的原因并不是说select就没用了,在这样的情况下就适合选用select:1,服务端接入者少 2,程序应具有兼容性。

epoll是怎么优化select问题的:

1,每次发生事件它不需要循环遍历所有文件描述符,它把发生变化的文件描述符单独集中到了一起。

2,仅向操作系统传递1次监视对象信息,监视范围或内容发生变化时只通知发生变化的事项。

实现epoll时必要的函数和结构体

函数: 
 
epoll_create:创建保存epoll文件描述符的空间,该函数也会返回文件描述符,所以终止时,也要调用close函数。(创建内存空间) 
 
epoll_ctl:向空间注册,添加或修改文件描述符。(注册监听事件) 
 
epoll_wait:与select函数类似,等待文件描述符发生变化。(监听事件回调) 
 
结构体: 
 
struct epoll_event  
  
{  
  
__uint32_t events;  
  
epoll_data_t data;  
  
}  
  
typedef union epoll_data  
  
{  
  
void *ptr;  
  
int fd;  
  
__uinit32_t u32;  
  
__uint64_t u64;  
  
} epoll_data_t;  
  • 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.

基于epoll的回声服务器端

// 
 
// main.cpp 
 
// hello_server 
 
// 
 
// Created by app05 on 15-10-19. 
 
// Copyright (c) 2015年 app05. All rights reserved. 
 
// 
 
#include 
 
#include 
 
#include 
 
#include 
 
#include 
 
#include 
 
#include 
 
#define BUF_SIZE 100 
 
#define EPOLL_SIZE 50 
 
void error_handling(char *buf); 
 
int main(int argc, const char * argv[]) { 
 
int serv_sock, clnt_sock; 
 
struct sockaddr_in serv_adr, clnt_adr; 
 
socklen_t adr_sz; 
 
int str_len, i; 
 
char buf[BUF_SIZE]; 
 
//类似select的fd_set变量查看监视对象的状态变化,epoll_event结构体将发生变化的文件描述符单独集中到一起 
 
struct epoll_event *ep_events; 
 
struct epoll_event event; 
 
int epfd, event_cnt; 
 
if(argc != 2) 
 

 
printf("Usage: %s \n", argv[0]); 
 
exit(1); 
 

 
serv_sock = socket(PF_INET, SOCK_STREAM, 0); 
 
if(serv_sock == -1) 
 
error_handling("socket() error"); 
 
memset(&serv_adr, 0, sizeof(serv_adr)); 
 
serv_adr.sin_family = AF_INET; 
 
serv_adr.sin_addr.s_addr = htonl(INADDR_ANY); 
 
serv_adr.sin_port = htons(atoi(argv[1])); 
 
if(bind(serv_sock, (struct sockaddr *) &serv_adr, sizeof(serv_adr)) == -1) 
 
error_handling("bind() error"); 
 
if(listen(serv_sock, 5) == -1) 
 
error_handling("listen() error"); 
 
//创建文件描述符的保存空间称为“epoll例程” 
 
epfd = epoll_create(EPOLL_SIZE); 
 
ep_events = malloc(sizeof(struct epoll_event) *EPOLL_SIZE); 
 
//添加读取事件的监视(注册事件) 
 
event.events = EPOLLIN; //读取数据事件 
 
event.data.fd = serv_sock; 
 
epoll_ctl(epdf, EPOLL_CTL_ADD, serv_sock, &event); 
 
while (1) 
 

 
//响应事件,返回发生事件的文件描述符数 
 
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1); //传-1时,一直等待直到事件发生 
 
if(event_cnt == -1) 
 

 
puts("epoll_wait() error"); 
 
break
 

 
//服务端套接字和客服端套接字 
 
for (i = 0; i < event_cnt; i++) { 
 
if(ep_events[i].data.fd == serv_sock)//服务端与客服端建立连接 
 

 
adr_sz = sizeof(clnt_adr); 
 
clnt_sock = accept(serv_sock, (struct sockaddr *)&clnt_adr, &adr_sz); 
 
event.events = EPOLLIN; 
 
event.data.fd = clnt_sock; 
 
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event); 
 
printf("connected client: %d \n", clnt_sock); 
 

 
else //连接之后传递数据 
 

 
str_len = read(ep_events[i].data.fd, buf, BUF_SIZE); 
 
if(str_len == 0) 
 

 
//删除事件 
 
epoll_ctl(epfd, EPOLL_CTL_DEL, ep_events[i].data.fd, NULL); 
 
close(ep_events[i].data.fd); 
 
printf("closed client: %d \n", ep_events[i].data.fd); 
 

 
else 
 

 
write(ep_events[i].data.fd, buf, str_len); 
 

 

 

 

 
close(serv_sock); 
 
close(epfd); 
 
return 0; 
 

 
void error_handling(char *message) 
 

 
fputs(message, stderr); 
 
fputc('\n', stderr); 
 
exit(1); 
 
}
  • 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.
  • 180.
  • 181.
  • 182.
  • 183.
  • 184.
  • 185.
  • 186.
  • 187.
  • 188.
  • 189.
  • 190.
  • 191.
  • 192.
  • 193.

条件触发和边缘触发

什么是条件触发和边缘触发?它们是指事件响应的方式,epoll默认是条件触发的方式。条件触发是指:只要输入缓冲中有数据就会一直通知该事件,循环响应epoll_wait。而边缘触发是指:输入缓冲收到数据时仅注册1次该事件,即使输入缓冲中还留有数据,也不会再进行注册,只响应一次。

边缘触发相对条件触发的优点:可以分离接收数据和处理数据的时间点,从实现模型的角度看,边缘触发更有可能带来高性能。

将上面epoll实例改为边缘触发:

1,首先改写 event.events = EPOLLIN | EPOLLET; (EPOLLIN:读取数据事件 EPOLLET:边缘触发方式)

2,边缘触发只响应一次接收数据事件,所以要一次性全部读取输入缓冲中的数据,那么就需要判断什么时候数据读取完了?Linux声明了一个全局的变量:int errno; (error.h中),它能记录发生错误时提供额外的信息。这里就可以用它来判断是否读取完数据:

str_len = read(...); 
 
if(str_len < 0) 
 

 
if(errno == EAGAIN) //读取输入缓冲中的全部数据的标志 
 
break
 
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

3,边缘触发方式下,以阻塞方式工作的read&write有可能会引起服务端的长时间停顿。所以边缘触发一定要采用非阻塞的套接字数据传输形式。那么怎么将套接字的read,write数据传输形式修改为非阻塞模式呢?

//fd套接字文件描述符,将此套接字数据传输模式修改为非阻塞

void setnonblockingmode(int fd) 
 

 
int flag = fcntl(fd, F_GETFL,0); //得到套接字原来属性 
 
fcntl(fd, F_SETFL, flag | O_NONBLOCK);//在原有属性基础上设置添加非阻塞模式 
 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
责任编辑:何妍 来源: CSDN博客
相关推荐

2019-09-18 20:07:06

AndroidTCP协议

2015-04-24 09:48:59

TCPsocketsocket编程

2015-10-19 09:34:42

TCPIP网络协议

2019-11-08 14:47:49

TCPIP网络

2015-10-16 09:33:26

TCPIP网络协议

2019-04-08 08:44:10

TCPIP网络协议

2009-04-09 10:11:00

TCPIP通讯

2010-09-09 16:28:19

2025-02-06 09:47:33

2015-10-27 09:40:31

TCPIP网络协议

2010-09-09 16:21:32

TCP IP网络协议

2012-09-24 15:13:50

C#网络协议TCP

2023-06-01 07:55:56

TCP/IP网络模型

2013-10-28 09:24:34

SDN软件定义网络TCP

2022-02-22 08:55:29

SelectPoll/ Epoll

2012-12-03 11:50:44

TCPIP网络流量

2021-05-31 06:50:47

SelectPoll系统

2025-01-07 00:07:17

2019-07-16 11:06:09

TCP四次挥手半关闭

2021-03-17 09:51:31

网络编程TCP网络协议
点赞
收藏

51CTO技术栈公众号