TCP/IP网络编程之优于select的epoll(二)
基于epoll的回声服务端
在TCP/IP网络编程之优于select的epoll(一)这一章中,我们介绍了epoll的相关函数,接下来给出基于epoll的回声服务端示例。
echo_epollserv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h> #define BUF_SIZE 100
#define EPOLL_SIZE 50
void error_handling(char *buf); int main(int argc, 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]; struct epoll_event *ep_events;
struct epoll_event event;
int epfd, event_cnt; if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
} serv_sock = socket(PF_INET, SOCK_STREAM, 0);
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"); epfd = epoll_create(EPOLL_SIZE);
ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE); event.events = EPOLLIN;
event.data.fd = serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); while (1)
{
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -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) // close request!
{
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); // echo!
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
} void error_handling(char *buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
之前解释过关键代码,而且程序结构与select方式没有区别,故省略代码说明。
条件触发和边缘触发
条件触发的方式中,只要输入缓冲有数据就会一直通知该事件。例如,服务端输入缓冲收到50字节的数据时,服务端操作系统将通知该事件(注册到发生变化的文件描述符)。但服务端读取20字节后还剩30字节的情况下,仍会注册事件。也就是说,条件触发方式中,只要输入缓冲中还剩有数据,就将以事件方式再次注册。而边缘触发中输入缓冲收到数据时仅注册一次事件,即使输入缓冲中还留有数据,也不会再进行注册
接下来通过代码了解条件触发的事件注册方式
echo_EPLTserv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h> #define BUF_SIZE 4
#define EPOLL_SIZE 50
void error_handling(char *buf); int main(int argc, 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]; struct epoll_event *ep_events;
struct epoll_event event;
int epfd, event_cnt; if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
} serv_sock = socket(PF_INET, SOCK_STREAM, 0);
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"); epfd = epoll_create(EPOLL_SIZE);
ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE); event.events = EPOLLIN;
event.data.fd = serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); while (1)
{
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
if (event_cnt == -1)
{
puts("epoll_wait() error");
break;
} puts("return epoll_wait");
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) // close request!
{
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); // echo!
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
} void error_handling(char *buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
上述示例与之前的echo_epollserv.c之间的差异如下:
- 第10行:将调用read函数时使用的缓冲大小缩减为4字节
- 第58行:插入验证epoll_wait函数调用次数的语句
减少缓冲大小是为了阻止服务端一次性读取接收的数据。换言之,调用read函数后,输入缓冲中仍然有数据需要读取。而且会因此注册新的事件并从epoll_wait函数返回时将循环输出“return epoll_wait”字符串。前提是条件触发的工作方式与之前描述一致。接下来观察运行结果
编译echo_EPLTserv.c 并运行
# gcc echo_EPLTserv.c -o echo_EPLTserv
# ./echo_EPLTserv 8500
return epoll_wait
connected client: 5
return epoll_wait
connected client: 6
return epoll_wait
return epoll_wait
return epoll_wait
return epoll_wait
return epoll_wait
closed client: 5
return epoll_wait
return epoll_wait
return epoll_wait
return epoll_wait
return epoll_wait
closed client: 6
运行echo_client ONE:
# ./echo_client 127.0.0.1 8500
Connected...........
Input message(Q to quit): It's my life
Message from server: It's my life
Input message(Q to quit): q
运行echo_client TWO:
# ./echo_client 127.0.0.1 8500
Connected...........
Input message(Q to quit): It's your life
Message from server: It's your life
Input message(Q to quit): q
从运行结果可以看出,每当接收到客户端数据时,都会注册该事件,并且多次调用epoll_wait函数
边缘触发的服务端实现中必知的两点:
- 通过error变量验证错误原因
- 为了完成非阻塞I/O,更改套接字特性
Linux套接字相关函数一般通过返回-1通知发生错误,虽然知道发生了错误,但仅凭这些内容无法得知产生错误的原因。因此,为了在发生错误时提供额外的信息,Linux声明了全局变量:int errno;为了访问该变量,需要引入error.h头文件,因为此头文件中有上述变量的extern声明。另外,每种函数发生错误时,保存到error变量的值都不同。
下面讲解将套接字改为非阻塞方式的方法,Linux提供更改或读取文件属性的如下方法:
#include <fcntl.h>
int fcntl(int filedes, int cmd, ...); //成功时返回cmd参数相关值,失败时返回-1
- filedes:属性更改目标的文件描述符
- cmd:表示函数调用的目的
从上述声明中可以看到,fcntl具有可变参数形式。如果向第二个参数传递F_GETFL,可以获得第一个参数所指的文件描述符属性(int型)。反之,如果传递F_SETFL,可以更改文件描述符属性。若希望将文件(套接字)改为非阻塞模式,需要如下两条语句
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag|O_NONBLOCK);
通过第一条语句获取之前设置的属性信息,通过第二条语句在此基础上添加非阻塞O_NONBLOCK标志。调用read和write函数时,无论是否存在数据,都会形成非阻塞文件(套接字)
echo_EPETserv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h> #define BUF_SIZE 4
#define EPOLL_SIZE 50
void setnonblockingmode(int fd);
void error_handling(char *buf); int main(int argc, 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]; struct epoll_event *ep_events;
struct epoll_event event;
int epfd, event_cnt; if (argc != 2) {
printf("Usage : %s <port>\n", argv[0]);
exit(1);
} serv_sock = socket(PF_INET, SOCK_STREAM, 0);
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"); epfd = epoll_create(EPOLL_SIZE);
ep_events = malloc(sizeof(struct epoll_event) * EPOLL_SIZE); setnonblockingmode(serv_sock);
event.events = EPOLLIN;
event.data.fd = serv_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, serv_sock, &event); while (1)
{
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
if (event_cnt == -1)
{
puts("epoll_wait() error");
break;
} puts("return epoll_wait");
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);
setnonblockingmode(clnt_sock);
event.events = EPOLLIN | EPOLLET;
event.data.fd = clnt_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clnt_sock, &event);
printf("connected client: %d \n", clnt_sock);
}
else
{
while (1)
{
str_len = read(ep_events[i].data.fd, buf, BUF_SIZE);
if (str_len == 0) // close request!
{
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);
break;
}
else if (str_len < 0)
{
if (errno == EAGAIN)
break;
}
else
{
write(ep_events[i].data.fd, buf, str_len); // echo!
}
}
}
}
}
close(serv_sock);
close(epfd);
return 0;
} void setnonblockingmode(int fd)
{
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
} void error_handling(char *buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
- 第11行:为了验证边缘触发的工作方式,将缓冲设置为4个字节
- 第61行:为观察事件发生数而添加的输出字符串的语句
- 第68、69行:第68行将accept函数创建的套接字改为非阻塞模式,第69行向EPOLLIN添加EPOLLET标志,将套接字事件注册方式改为边缘触发
- 第76、78行:之前的条件触发回声服务端中没有while循环,边缘触发方式中,发生事件时需要读取输入缓冲中的所有数据,因此需要循环调用read函数,如第78行所示
- 第86行:read函数返回-1且errno值为EAGAIN时,意味着读取了输入缓冲中的全部数据,因此需要通过break语句跳出第76行的循环
编译echo_EPETserv.c运行
# ./echo_EPETserv 8500
return epoll_wait
connected client: 5
return epoll_wait
return epoll_wait
return epoll_wait
return epoll_wait
closed client: 5
运行客户端程序
# ./echo_client 127.0.0.1 8500
Connected...........
Input message(Q to quit): I like computer programming
Message from server: I like computer programming
Input message(Q to quit): Do you like computer programming?
Message from server: Do you like computer programming?
Input message(Q to quit): Good bye
Message from server: Good bye
Input message(Q to quit): q
上述运行结果中需要注意的是,客户端发送消息次数和服务端epoll_wait函数调用次数。客户端从请求连接到断开连接共发送数据5次,服务端也相应产生5个事件
条件触发和边缘触发孰优孰劣
我们从理论和代码的角度充分理解了条件触发和边缘触发,但仅凭这些还无法理解边缘触发相对于条件触发的优点。边缘触发可以做到分离接收数据和处理时间的时间点。虽然比较简单,但非常准确有力地说明了边缘触发的优点,现在我们来看图1-1
图1-1 理解边缘触发
图1-1的运行流程如下:
- 服务端分别从客户端A、B、C接收数据
- 服务端按照A、B、C的顺序重新组合收到的数据
- 组合的数据将发送给任意主机
为了完成该过程,若能按如下流程运行程序,服务端的实现并不难
- 客户端按照A、B、C的顺序连接服务端,并依序向服务端发送数据
- 需要接收数据的客户端应在客户端A、B、C之前连接到服务端并等待
但现实中可能频繁出现如下这些情况,换言之,如下情况更符合实际
- 客户端C和B向服务端发送数据,但A尚未连接到服务端
- 客户端A、B、C乱序发送数据
- 服务端已收到数据,但要接收数据的目标客户端还未连接到服务端
因此,即使输入缓冲收到数据(注册相应事件),服务端也能决定读取和处理这些数据的时间点,这样就给服务端的实现带来巨大的灵活性。
那么,条件触发中无法区分数据接收和处理吗?并非不可能。但在输入缓冲收到数据的情况下,如果不读取(延迟处理),则每次调用epoll_wait函数时都会产生相应事件。而且事件数也会累加,服务端能承受吗?这在现实中是不合理的
条件触发和边缘触发的区别主要应从服务端实现模型的角度讨论,从实现模型来看,边缘触发更有可能带来高性能,但不能简单地认为“只要使用边缘触发就一定能提高速度”
TCP/IP网络编程之优于select的epoll(二)的更多相关文章
- TCP/IP网络编程之优于select的epoll(一)
epoll的理解及应用 select复用方法由来已久,因此,利用该技术后,无论如何优化程序性能也无法同时接入上百个客户端.这种select方式并不适合以web服务端开发为主流的现代开发环境,所以要学习 ...
- TCP/IP网络编程之多进程服务端(二)
信号处理 本章接上一章TCP/IP网络编程之多进程服务端(一),在上一章中,我们介绍了进程的创建和销毁,以及如何销毁僵尸进程.前面我们讲过,waitpid是非阻塞等待子进程销毁的函数,但有一个不好的缺 ...
- TCP/IP网络编程之进程间通信
进程间通信基本概念 进程间通信意味着两个不同进程间可以交换数据,为了完成这一点,操作系统中应提供两个进程可以同时访问的内存空间.但我们知道,进程具有完全独立的内存结构,就连通过fork函数创建的子进程 ...
- 《TCP/IP网络编程》
<TCP/IP网络编程> 基本信息 作者: (韩)尹圣雨 译者: 金国哲 丛书名: 图灵程序设计丛书 出版社:人民邮电出版社 ISBN:9787115358851 上架时间:2014-6- ...
- 浅谈TCP/IP网络编程中socket的行为
我认为,想要熟练掌握Linux下的TCP/IP网络编程,至少有三个层面的知识需要熟悉: 1. TCP/IP协议(如连接的建立和终止.重传和确认.滑动窗口和拥塞控制等等) 2. Socket I/O系统 ...
- UNIX环境高级编程——TCP/IP网络编程 常用网络信息检索函数
UNIX环境高级编程——TCP/IP网络编程 常用网络信息检索函数 gethostname() getppername() getsockname() gethostbyname() ...
- TCP/IP网络编程系列之四(初级)
TCP/IP网络编程系列之四-基于TCP的服务端/客户端 理解TCP和UDP 根据数据传输方式的不同,基于网络协议的套接字一般分为TCP和UDP套接字.因为TCP套接字是面向连接的,因此又称为基于流的 ...
- TCP/IP网络编程系列之三(初级)
TCP/IP网络编程系列之三-地址族与数据序列 分配给套接字的IP地址和端口 IP是Internet Protocol (网络协议)的简写,是为首发网络数据而分配给计算机的值.端口号并非赋予计算机值, ...
- TCP/IP网络编程系列之二(初级)
套接字类型与协议设置 我们先了解一下创建套接字的那个函数 int socket(int domain,int type,int protocol);成功时返回文件描述符,失败时返回-1.其中,doma ...
随机推荐
- 浅谈WebService的调用
0.前言 前段时间,公司和电信有个合作,产品对接电信的某个平台,使用了WebService接口的调用,实现了业务受理以及单点登录.终于使用到了WebService,楼主还是比较兴奋的,目前功能已经上线 ...
- Django之model基础(查询补充)
学习完简单的单表查询外,是远远不够的,今天我们对查询表记录做一个补充,接下来来看看基于对象的跨表查询.基于双下划线的跨表查询,聚合查询和分组查询,F查询与Q查询. 比如我们有如下一张表,在model中 ...
- eros 修改 android上原生picker的颜色的呢
修改选中颜色和文字颜色 修改文件如下 修改窗口底色
- 树莓派-(一)开箱到点亮一些坑(无屏、无wlan、无直连键鼠)
0x00.前期准备: 材料: 树莓派3b+ 板子 * 1,适配电源 * 1,网线 * 2,sd卡16G * 1,读卡器 * 1 安装时注意,3b+三个散热片贴好.小风扇接线要接对 工具: 0x01. ...
- Callable的简单使用
说起java的线程操作,都会想到Thread和Runable这两个, 这两个类可以实现异步和同步. 在大多数的java开发中, 这两个都是实现异步的线程来使用, 但是现在考虑一种情况: 发出一条线程, ...
- 浏览器兼容圆角Border-radius的问题
圆角css代码:border-radius只有在以下版本的浏览器:Firefox4.0+.Google Chrome 10.0+.Opera 10.5+.IE9+支持border-radius标准语法 ...
- IIS7.5配置自动添加www 及 限制通过IP访问web
IIS7.5配置自动添加www 方法 新建一个站点2(主机名为不带www的站点),将其重定向至带www的URL即可. 注意以下几点 站点2不可与站点1的路径一致,否则会导致站点1也会添加同样的重定向, ...
- HDU5200 数据离线处理
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=5200 ,数据离线处理. 这是BestCoder Round #36的C题,比赛时自己用线段树做,姿势不 ...
- mac上Apache修改目录浏览权限
sudo vim /etc/apache2/httpd.conf <Directory "/Library/WebServer/Documents"> # # Poss ...
- java生成excel文档
要做一个后台自动化,要先预先生成一份文档,以下内容生成了文档 首先下载jxl.jar包,下载地址:http://download.csdn.net/detail/prstaxy/4469935 1.生 ...