zhoulifa 当前离线
空间积分
-
0
信誉积分
-
100
UID
-
8666934
阅读权限
-
20
积分
-
738
帖子
- 164
精华
- 8
可用积分
-
738
专家积分
-
0
在线时间
-
1 小时
注册时间
-
2005-09-06
最后登录
-
2009-10-25
帖子
- 164
主题
- 29
精华
- 8
可用积分
-
738
专家积分
-
0
在线时间
-
1 小时
注册时间
-
2005-09-06
最后登录
-
2009-10-25
|
2楼 [报告] [回复] [引用]
发表于 2006-07-05 23:13:33 | 只看该作者
用gcc tcpforwardport.c -o MyProxy编译此程序后运行效果如下:
./MyProxy 8000 80 172.16.100.218
accepting connections on port 8000
connect from 127.0.0.1
当有用户访问本机的8000端口时,MyProxy程序将把此请求转发到172.16.100.218主机的80端口,即实现了一个http代理。 关于select函数: 其函数原型为: int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 此函数的功能是由内核检测在timeout时间内,是否有readfds,writefds,exceptfds三个句柄集(file descriptors)里的某个句柄(file descriptor)的状态符合寻求,即readfds句柄集里有句柄可读或writefds句柄集里有可写或exceptfds句柄集里有例外发生,任何一个有变化函数就立即返回,返回值为timeout发生状态变化的句柄个数。 n是所有readfds,writefds,exceptfds三个句柄集(file descriptors)里编号最大值加1。比如:要检测两个socket句柄fd1和fd2在timeout时间内是否分别可读和可写就可以这样: 先把两个句柄集(file descriptors)清零: FD_ZERO (&readfds); FD_ZERO (&writefds); 然后把fd1加入读检测集: FD_SET (fd1, &readfds); 然后把fd2加入写检测集: FD_SET (fd2, &writefds); 再给timeout设置值,timeout是这样的一个结构: struct timeval { long tv_sec; /* seconds */ long tv_usec; /* microseconds */ }; 你可以这样赋值: timeout.tv_sec=1; timeout.tv_uec=0; 表示检测在1秒钟内是否有句柄状态发生变化。 如果有句柄发生变化,就可以用FD_ISSET检测各个句柄,比如: FD_ISSET (fd1, &readfds);//检测是否fd1变成可读的了 FD_ISSET (fd2, &writefds);//检测是否fd2变成可写的了 示意程序代码如下:
- /*----------------------示意代码开始--------------------------------------------*/
- fd1 = socket();//创建一个socket
- fd2 = socket();//创建一个socket
- while(1) {
- FD_ZERO (&readfds);
- FD_ZERO (&writefds);
- FD_SET (fd1, &readfds);
- FD_SET (fd2, &writefds);
- timeout.tv_sec=1;
- timeout.tv_uec=0;
- ret = select(fd1>fd2?(fd1+1):(fd2+1), &readfds, &writefds, NULL, &timeout);
- if(ret < 0) {printf("系统错误,select出错,错误代码:%d, 错误信息:%s", errno, strerror(errno));}
- else if(ret == 0) {printf("select超时返回,没有任何句柄状态发生变化!");}
- //有句柄状态发生了变化
- if(FD_ISSET(fd1, &readfds)) {
- fd1有数据可读;
- fd1里的数据被读出来;
- }
- if(FD_ISSET(fd2, &writefds)) {
- fd2可写;
- fd2里发送数据给对方;
- }
- }
- /*----------------------示意代码结束--------------------------------------------*/
复制代码
经常用到的几个自定义函数: 1、开启监听的函数
- /*----------------------源代码代码开始--------------------------------------------*/
- int
- OpenSCPServer(int port, int total, int sendbuflen, int recvbuflen, int blockORnot, int reuseORnot) {
- /*************************关于本函数************************************
- *function_name: OpenSCPServer
- *参数说明:port整数型监听端口号,total整数型监听个数,sendbuflen整数型发送缓冲区大小
- * recvbuflen整数型接收缓冲区大小,blockORnot整数型是否阻塞,reuseORnot整数型是否端口重用
- *purpose: 用来建立一个tcp服务端socket
- *tidied by: zhoulifa([email protected]) 周立发(http://zhoulifa.9999mb.com)
- Linux爱好者 Linux知识传播者 SOHO族 开发者 最擅长C语言
- *date time:2006-07-05 20:00:00
- *Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途
- * 但请遵循GPL
- *Thanks to: Paul Sheer 感谢Paul Sheer在select_tut的man手册里提供了这份源代码
- *Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力
- *Note:要使用此函数需要自定义一个全局变量char errorMessage[1024];并包含GetCurrentTime.h头文件
- *********************************************************************/
- int sockfd = 0, ret = 0, opt = 0, flags=1;
- struct sockaddr_in laddr;
- ret = sockfd = socket(PF_INET, SOCK_STREAM, 0);
- if(ret < 0) {
- sprintf(errorMessage, "OpenTCPServer socket() error! return:%d, errno=%d, errortext:'%s' %s", ret, errno, strerror(errno), GetCurrentTime(0, 0));
- return -1;
- }
- ret = setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, &reuseORnot, sizeof(int));
- if(ret < 0) {
- sprintf(errorMessage, "OpenTCPServer setsockopt() reuse error! return:%d, errno=%d, errortext:'%s' %s", ret, errno, strerror(errno), GetCurrentTime(0, 0));
- return -2;
- }
- ret = setsockopt(sockfd, SOL_SOCKET, SO_RCVBUF, &recvbuflen, sizeof(int));
- if ( ret < 0) {
- sprintf(errorMessage, "OpenTCPServer setsockopt() recvbuf error! return:%d, errno=%d, errortext:'%s' %s", ret, errno, strerror(errno), GetCurrentTime(0, 0));
- return -3;
- }
- ret = setsockopt(sockfd, SOL_SOCKET, SO_SNDBUF, &sendbuflen, sizeof(int));
- if (ret < 0) {
- sprintf(errorMessage, "OpenTCPServer setsockopt() sendbuf error! return:%d, errno=%d, errortext:'%s' %s", ret, errno, strerror(errno), GetCurrentTime(0, 0));
- return -4;
- }
- ioctl(sockfd,FIONBIO,&blockORnot);/*block or not*/
- laddr.sin_family = PF_INET;
- laddr.sin_port = htons(port);
- laddr.sin_addr.s_addr = INADDR_ANY;
- bzero(&(laddr.sin_zero), 8);
- ret = bind(sockfd, (struct sockaddr *)&laddr, sizeof(struct sockaddr));
- if(ret < 0) {
- sprintf(errorMessage, "OpenTCPServer bind() error! return:%d, errno=%d, errortext:'%s' %s", ret, errno, strerror(errno), GetCurrentTime(0, 0));
- close(sockfd);
- return -5;
- }
- ret = listen(sockfd, total);
- if(ret < 0) {
- sprintf(errorMessage, "OpenTCPServer listen() error! return:%d, errno=%d, errortext:'%s' %s", ret, errno, strerror(errno), GetCurrentTime(0, 0));
- close(sockfd);
- return -6;
- }
- sprintf(errorMessage, "OpenTCPServer opened on port.%d(%d) OK, socket(%d), buf(%d:%d)! %s", port, total, sockfd, sendbuflen, recvbuflen, GetCurrentTime(0, 0));
- return sockfd;
- }
- /*----------------------源代码代码结束--------------------------------------------*/
复制代码
2、连接服务器的函数
- /*----------------------源代码代码开始--------------------------------------------*/
- int
- ConnectSCPServer(char * serverip, int serverport, int blockORnot) {
- /*************************关于本函数************************************
- *function_name: ConnectSCPServer
- *参数说明:serverip服务器IP地址或主机名,serverport服务器端口,blockORnot整数型是否阻塞
- *purpose: 用来建立一个tcp客户端socket
- *tidied by: zhoulifa([email protected]) 周立发(http://zhoulifa.9999mb.com)
- Linux爱好者 Linux知识传播者 SOHO族 开发者 最擅长C语言
- *date time:2006-07-05 20:40:00
- *Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途
- * 但请遵循GPL
- *Thanks to: Paul Sheer 感谢Paul Sheer在select_tut的man手册里提供了这份源代码
- *Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力
- *Note:要使用此函数需要自定义一个全局变量char errorMessage[1024];并包含自己编写的GetCurrentTime.h头文件
- *********************************************************************/
- int serversock = 0, ret = 0;
- unsigned long addr;
- struct sockaddr_in sin;
- struct hostent *he;
- if((he=gethostbyname(serverip))== 0) {
- sprintf(errorMessage, "ConnectSCPServer IP address '%s' error! return:-1 %s", serverip, GetCurrentTime(0, 0));
- return -1;
- }
- serversock = socket(PF_INET, SOCK_STREAM, 0);
- if(serversock == -1) {
- sprintf(errorMessage, "ConnectSCPServer socket() error! return:-2, errno=%d, errortext:'%s' %s", errno, strerror(errno), GetCurrentTime(0, 0));
- return -2;
- }
- ioctl(serversock, FIONBIO, &blockORnot); //block or not
- memset((char*)&sin, 0, sizeof(struct sockaddr_in));
- sin.sin_family = PF_INET;
- sin.sin_port = htons(serverport);
- sin.sin_addr = *((struct in_addr *)he->h_addr);
- ret = connect(serversock, (struct sockaddr *)&sin, sizeof(sin));
- if(ret == -1) {
- sprintf(errorMessage, "ConnectSCPServer connect() error! return:-3, errno=%d, errortext:'%s' %s", errno, strerror(errno), GetCurrentTime(0, 0));
- close(serversock);
- return -3;
- }
- return serversock;
- }
- /*----------------------源代码代码结束--------------------------------------------*/
复制代码
3、发送数据函数Send
- /*----------------------源代码代码开始--------------------------------------------*/
- int
- Send(int sock, char * buf, size_t size, int flag, int timeout) {
- /*************************关于本函数************************************
- *function_name: Send
- *参数说明:sock整数型socket,buf待发送的内容,size要发送的大小,flag发送选项,timeout超时时间值
- *purpose: 用来通过一个socket在指定时间内发送数据
- *tidied by: zhoulifa([email protected]) 周立发(http://zhoulifa.9999mb.com)
- Linux爱好者 Linux知识传播者 SOHO族 开发者 最擅长C语言
- *date time:2006-07-05 20:58:00
- *Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途
- * 但请遵循GPL
- *Thanks to: Paul Sheer 感谢Paul Sheer在select_tut的man手册里提供了这份源代码
- *Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力
- *Note:要使用此函数需要自定义一个全局变量char errorMessage[1024];并包含自己编写的GetCurrentTime.h头文件
- *********************************************************************/
- int i = 0, ret = 0, intretry = 0;
- struct timeval tival;
- fd_set writefds;
- int maxfds = 0;
- tival.tv_sec = timeout;
- tival.tv_usec = 0;
- FD_ZERO(&writefds);
- if(sock > 0) {
- FD_SET(sock, &writefds);
- maxfds=((sock > maxfds)?sock:maxfds);
- }
- else {
- sprintf(errorMessage, "Send socket:%d error! return:-2 %s", sock, GetCurrentTime(0, 0));
- return -2;
- }
- ret = select(maxfds + 1, NULL, &writefds, NULL, &tival);
- if(ret <= 0) {
- if(ret < 0) sprintf(errorMessage, "Send socket:%d select() error! return:%d, errno=%d, errortext:'%s' %s", sock, ret, errno, strerror(errno), GetCurrentTime(0, 0));
- else sprintf(errorMessage, "Send socket:%d select timeout(%d)! %s", sock, timeout, GetCurrentTime(0, 0));
- close(sock);
- return -3;
- }
- if(!(FD_ISSET(sock, &writefds))) {
- sprintf(errorMessage, "Send socket:%d not in writefds! %s", sock, GetCurrentTime(0, 0));
- close(sock);
- return -4;
- }
- while(i < size) {
- ret = send(sock, buf + i, size - i, flag);
- if(ret <= 0) {
- sprintf(errorMessage, "Send socket:%d send() error! return:%d, errno=%d, errortext:'%s' %s", sock, ret, errno, strerror(errno), GetCurrentTime(0, 0));
- if (EINTR == errno)
- if(intretry < 10) {intretry++;continue;}
- else sprintf(errorMessage, "Send socket:%d send() error!EINTR 10 times! %s", sock, GetCurrentTime(0, 0));
- close(sock);
- return -1;
- }
- else i += ret;
- }
- sprintf(errorMessage, "Send socket:%d send() OK! %d/%d bytes sent! %s", sock, i, size, GetCurrentTime(0, 0));
- return i;
- }
- /*----------------------源代码代码结束--------------------------------------------*/
复制代码
4、接收数据函数Recv
- /*----------------------源代码代码开始--------------------------------------------*/
- int
- Recv(int sock, char * buf, size_t size, int flag, int timeout) {
- /*************************关于本函数************************************
- *function_name: Recv
- *参数说明:sock整数型socket,buf接收数据的缓冲区,size要接收数据的大小,flag接收选项,timeout超时时间值
- *purpose: 用来从一个socket在指定时间内读取数据
- *tidied by: zhoulifa([email protected]) 周立发(http://zhoulifa.9999mb.com)
- Linux爱好者 Linux知识传播者 SOHO族 开发者 最擅长C语言
- *date time:2006-07-05 21:10:00
- *Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途
- * 但请遵循GPL
- *Thanks to: Paul Sheer 感谢Paul Sheer在select_tut的man手册里提供了这份源代码
- *Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力
- *Note:要使用此函数需要自定义一个全局变量char errorMessage[1024];并包含自己编写的GetCurrentTime.h头文件
- *********************************************************************/
- int i = 0, ret = 0, intretry = 0;
- struct timeval tival;
- fd_set readfds;
- int maxfds = 0;
- tival.tv_sec = timeout;
- tival.tv_usec = 0;
- FD_ZERO(&readfds);
- if(sock > 0) {
- FD_SET(sock, &readfds);
- maxfds=((sock > maxfds)?sock:maxfds);
- }
- else {
- sprintf(errorMessage, "Recv socket:%d error! return:-2 %s", sock, GetCurrentTime(0, 0));
- return -2;
- }
- ret = select(maxfds + 1, &readfds, NULL, NULL, &tival);
- if(ret <= 0) {
- if(ret < 0) sprintf(errorMessage, "Recv socket:%d select() error! return:%d, errno=%d, errortext:'%s' %s", sock, ret, errno, strerror(errno), GetCurrentTime(0, 0));
- else sprintf(errorMessage, "Recv socket:%d select timeout(%d)! %s", sock, timeout, GetCurrentTime(0, 0));
- close(sock);
- return -3;
- }
- if(!(FD_ISSET(sock, &readfds))) {
- sprintf(errorMessage, "Recv socket:%d not in readfds! %s", sock, GetCurrentTime(0, 0));
- close(sock);
- return -4;
- }
- while(i < size) {
- ret = recv(sock, buf + i, size - i, flag);
- if(ret <= 0){
- sprintf(errorMessage, "Recv socket:%d recv() error! return:%d, errno=%d, errortext:'%s' %s", sock, ret, errno, strerror(errno), GetCurrentTime(0, 0));
- if(errno == EINTR)
- if(intretry < 10) {intretry++;continue;}
- else sprintf(errorMessage, "Recv socket:%d recv() error! EINTR 10 times! %s", sock, GetCurrentTime(0, 0));
- close(sock);
- return -1;
- }
- else i += ret;
- }
- sprintf(errorMessage, "Recv socket:%d recv() OK! %d/%d bytes received! %s", sock, i, size, GetCurrentTime(0, 0));
- return i;
- }
复制代码
最后需要说明的是:我这里讲到的源程序并不能实际地作为一个产品程序来用,实际情况下可能会有其它许多工作要做,比如可能要建立共享队列来存放 socket里读到的消息,也可能把发送消息先进行排队然后再调用Send函数。还有,如果不是全数字,在发送前一定要htonl转换为网络字节序,同理接收到后一定要先ntohl由网络字节序转换为主机字节序,否则对方发送过来的0x00000001在你这里可能是0x00010000,因为高低位顺序不同。 |
进入2.6内核时代, select应该进垃圾堆了 高并发服务器用select效率极低, 特别是使用非阻塞IO时更是慢得一蹋糊涂 改用epoll会大大改善 我一个程序监听从8000到18000共计1万个端口, 启动1万个LISTEN 用epoll来阻塞, 系统非常轻松, 完全没有惊群现象 epoll用法比select简单 初始化:创建epoll描述字; 向epoll描述字添加需要响应的套接字, 初始化过程只要一次即可 使用: 等待epoll事件发生, 提取事件的套接字进行相应的读写操作 static int s_epfd;//epoll描述字 {//初始化epoll struct epoll_event ev; //设置epoll s_epfd = epoll_create(65535); {//这个过程可以循环以便加入多个LISTEN套接字进入epoll事件集合 //服务器监听创建 rc = listen();//listen参数这里省略 //加入epoll事件集合 ev.events = EPOLLIN; ev.data.fd = rc; if (epoll_ctl(s_epfd, EPOLL_CTL_ADD, rc, &ev) < 0) { fprintf(stderr, "epoll set insertion error: fd=%d", rc); return(-1); } } } {//epoll事件处理 int i, nfds, sock_new; struct epoll_event events[16384]; for( ; ; ) { //等待epoll事件 nfds = epoll_wait(s_epfd, events, 16384, -1); //处理epoll事件 for(i = 0; i < nfds; i++) { //events[i].data.fd是epoll事件中弹出的套接字 //接收连接 sock_new = accept(events[i].data.fd);//accept其它参数这里省略了 if(0 > sock_new) { fprintf(stderr, "接收客户端连接失败\n"); continue; } } } } |
|
Latitude/Optiplex 老用户征集活动 | 如何使企业中的服务器、数据信息更为安全 | Nginx在工作中的应用讨论 | 立即穿越!参与数据防泄密有奖大讨论!
|
|
|
|
|
zhoulifa 当前离线
空间积分
-
0
信誉积分
-
100
UID
-
8666934
阅读权限
-
20
积分
-
738
帖子
- 164
精华
- 8
可用积分
-
738
专家积分
-
0
在线时间
-
1 小时
注册时间
-
2005-09-06
最后登录
-
2009-10-25
帖子
- 164
主题
- 29
精华
- 8
可用积分
-
738
专家积分
-
0
在线时间
-
1 小时
注册时间
-
2005-09-06
最后登录
-
2009-10-25
|
4楼 [报告] [回复] [引用]
发表于 2006-07-06 08:34:30 | 只看该作者
Very good!又长见识了。 safedead , 不介意收藏你的示意代码吧? |
|
光顾一下周立发的Web log 参加周立发的Linux讨论
Latitude/Optiplex 老用户征集活动 | 如何使企业中的服务器、数据信息更为安全 | Nginx在工作中的应用讨论 | 立即穿越!参与数据防泄密有奖大讨论!
|
|
|
|
|
japonensis 当前离线
空间积分
-
0
信誉积分
-
100
UID
-
20330678
阅读权限
-
20
积分
-
513
帖子
- 839
精华
- 0
可用积分
-
513
专家积分
-
0
在线时间
-
3 小时
注册时间
-
2005-11-02
最后登录
-
2009-12-30
帖子
- 839
主题
- 107
精华
- 0
可用积分
-
513
专家积分
-
0
在线时间
-
3 小时
注册时间
-
2005-11-02
最后登录
-
2009-12-30
|
5楼 [报告] [回复] [引用]
发表于 2006-07-06 09:00:53 | 只看该作者
|
Latitude/Optiplex 老用户征集活动 | 如何使企业中的服务器、数据信息更为安全 | Nginx在工作中的应用讨论 | 立即穿越!参与数据防泄密有奖大讨论!
|
|
|
|
|
zhoulifa 当前离线
空间积分
-
0
信誉积分
-
100
UID
-
8666934
阅读权限
-
20
积分
-
738
帖子
- 164
精华
- 8
可用积分
-
738
专家积分
-
0
在线时间
-
1 小时
注册时间
-
2005-09-06
最后登录
-
2009-10-25
帖子
- 164
主题
- 29
精华
- 8
可用积分
-
738
专家积分
-
0
在线时间
-
1 小时
注册时间
-
2005-09-06
最后登录
-
2009-10-25
|
6楼 [报告] [回复] [引用]
发表于 2006-07-06 22:36:38 | 只看该作者
Linux 2.6内核中提高网络I/O性能的新方法epoll
正如我昨天在“Linux下各类TCP网络服务器的实现源代码”(http://zhoulifa.bokee.com/5345930.html)一文中提到的那样,I/O多路复用技术在比较多的TCP网络服务器中有使用,即比较多的用到select函数。 感谢chinaunix.net上朋友safedead(http://bbs.chinaunix.net/viewpro.php?uid=407631)提醒,我今天仔细研究了一下,证实了在2.6内核中的新的I/O技术epoll。 1、为什么select是落后的? 首先,在Linux内核中,select所用到的FD_SET是有限的,即内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数,在我用的2.6.15-25-386内核中,该值是1024,搜索内核源代码得到: include/linux/posix_types.h:#define __FD_SETSIZE 1024 也就是说,如果想要同时检测1025个句柄的可读状态是不可能用select实现的。或者同时检测1025个句柄的可写状态也是不可能的。 其次,内核中实现select是用轮询方法,即每次检测都会遍历所有FD_SET中的句柄,显然,select函数执行时间与FD_SET中的句柄个数有一个比例关系,即select要检测的句柄数越多就会越费时。 当然,在前文中我并没有提及poll方法,事实上用select的朋友一定也试过poll,我个人觉得select和poll大同小异,个人偏好于用select而已。 /************关于本文档******************************************** *filename: Linux 2.6内核中提高网络I/O性能的新方法epoll *purpose: 补充“Linux下各类TCP网络服务器的实现源代码”一文的不足之处 *wrote by: zhoulifa([email protected]) 周立发(http://zhoulifa.9999mb.com) Linux爱好者 Linux知识传播者 SOHO族 开发者 最擅长C语言 *date time:2006-07-06 22:30:00 *Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途 * 但请遵循GPL *Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力 *********************************************************************/ 2、2.6内核中提高I/O性能的新方法epoll epoll是什么?按照man手册的说法:是为处理大批量句柄而作了改进的poll。要使用epoll只需要这三个系统调用:epoll_create(2), epoll_ctl(2), epoll_wait(2)。 当然,这不是2.6内核才有的,它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44) 以下文章转自滕昱的Web Log http://mechgouki.spaces.msn.com/blog/PersonalSpace.aspx
/*********************************引用开始******************************/
Linux2.6内核epoll介绍---我的blog 2005/3/30
[作者]:滕昱,2005/3/30,0.1版本
[版权声明]:此文档遵循GNU自由文档许可证(GNU Free Documentation License).任何人可以自由复制,分发,修改,不过如果方便,请注明出处和作者
(1)导言:
首先,我强烈建议大家阅读Richard Stevens著作《TCP/IP Illustracted Volume 1,2,3》和《UNIX Network Programming Volume 1,2》。虽然他离开我们大家已经5年多了,但是他的书依然是进入网络编程的最直接的道路。其中的3卷的《TCP/IP Illustracted》卷1是必读-如果你不了解tcp协议各个选项的详细定义,你就失去了优化程序重要的一个手段。卷2,3可以选读一下。比如卷2 讲解的是4.4BSD内核TCP/IP协议栈实现----这个版本的协议栈几乎影响了现在所有的主流os,但是因为年代久远,内容不一定那么vogue. 在这里我多推荐一本《The Linux Networking Architecture--Design and Implementation of Network Protocols in the Linux Kernel》,以2.4内核讲解Linux TCP/IP实现,相当不错.作为一个现实世界中的实现,很多时候你必须作很多权衡,这时候参考一个久经考验的系统更有实际意义。举个例子,linux内核中sk_buff结构为了追求速度和安全,牺牲了部分内存,所以在发送TCP包的时候,无论应用层数据多大,sk_buff最小也有272的字节.
其实对于socket应用层程序来说,《UNIX Network Programming Volume 1》意义更大一点.2003年的时候,这本书出了最新的第3版本,不过主要还是修订第2版本。其中第6章《I/O Multiplexing》是最重要的。Stevens给出了网络IO的基本模型。在这里最重要的莫过于select模型和Asynchronous I/O模型.从理论上说,AIO似乎是最高效的,你的IO操作可以立即返回,然后等待os告诉你IO操作完成。但是一直以来,如何实现就没有一个完美的方案。最著名的windows完成端口实现的AIO,实际上也是内部用线程池实现的罢了,最后的结果是IO有个线程池,你应用也需要一个线程池...... 很多文档其实已经指出了这带来的线程context-switch带来的代价。
在linux 平台上,关于网络AIO一直是改动最多的地方,2.4的年代就有很多AIO内核patch,最著名的应该算是SGI那个。但是一直到2.6内核发布,网络模块的AIO一直没有进入稳定内核版本(大部分都是使用用户线程模拟方法,在使用了NPTL的linux上面其实和windows的完成端口基本上差不多了)。2.6内核所支持的AIO特指磁盘的AIO---支持io_submit(),io_getevents()以及对Direct IO的支持(就是绕过VFS系统buffer直接写硬盘,对于流服务器在内存平稳性上有相当帮助)。
所以,剩下的select模型基本上就是我们在linux上面的唯一选择,其实,如果加上no-block socket的配置,可以完成一个"伪"AIO的实现,只不过推动力在于你而不是os而已。不过传统的select/poll函数有着一些无法忍受的缺点,所以改进一直是2.4-2.5开发版本内核的任务,包括/dev/poll,realtime signal等等。最终,Davide Libenzi开发的epoll进入2.6内核成为正式的解决方案
(2)epoll的优点
<1>支持一个进程打开大数目的socket描述符(FD)
select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
<2>IO效率不随FD数目增加而线性下降
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是"活跃"的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有"活跃"的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll并不比select/poll有什么效率,相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了。
<3>使用mmap加速内核与用户空间的消息传递。
这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。而如果你想我一样从2.5内核就关注epoll的话,一定不会忘记手工 mmap这一步的。
<4>内核微调
这一点其实不算epoll的优点了,而是整个linux平台的优点。也许你可以怀疑linux平台,但是你无法回避linux平台赋予你微调内核的能力。比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么可以在运行时期动态调整这个内存pool(skb_head_pool)的大小--- 通过echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函数的第2个参数(TCP完成3次握手的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的NAPI网卡驱动架构。
(3)epoll的使用
令人高兴的是,2.6内核的epoll比其2.5开发版本的/dev/epoll简洁了许多,所以,大部分情况下,强大的东西往往是简单的。唯一有点麻烦是epoll有2种工作方式:LT和ET。
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.
ET (edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。
epoll只有epoll_create,epoll_ctl,epoll_wait 3个系统调用,具体用法请参考http://www.xmailserver.org/linux-patches/nio-improve.html ,
在http://www.kegel.com/rn/也有一个完整的例子,大家一看就知道如何使用了
(4)Leader/follower模式线程pool实现,以及和epoll的配合
.....未完成,主要是要避免过多的epoll_ctl调用,以及尝试使用EPOLLONESHOT加速......
(5)benchmark
.......未完成
/*********************************引用结束******************************/
3、epoll的使用方法 这是epoll的man手册提供的一个例子,这段代码假设一个非阻塞的socket监听listener被建立并且一个epoll句柄kdpfd已经提前用epoll_create建立了:
- struct epoll_event ev, *events;
- for(;;) {
- nfds = epoll_wait(kdpfd, events, maxevents, -1);/*wait for an I/O event. All notes here added by zhoulifa(http://zhoulifa.bokee.com) on 2006-7-6 22:10:00*/
- for(n = 0; n < nfds; ++n) {
- if(events[n].data.fd == listener) {/*if listen socket has an I/O, accept the new connect*/
- client = accept(listener, (struct sockaddr *) &local,
- &addrlen);
- if(client < 0){
- perror("accept");
- continue;
- }
- setnonblocking(client);
- ev.events = EPOLLIN | EPOLLET;/*EPOLLIN-available for read*/
- ev.data.fd = client;
- if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) {/*add the new socket into the epoll file descriptors*/
- fprintf(stderr, "epoll set insertion error: fd=%d\n",
- client);
- return -1;
- }
- }
- else
- do_use_fd(events[n].data.fd);/*read from a socket which has data come*/
- }
- }
复制代码
4、epoll使用方法示意代码 以下代码由chinaunix.net上BBS用户safedead(http://bbs.chinaunix.net/viewpro.php?uid=407631)提供:
- static int s_epfd;//epoll描述字
- {//初始化epoll
- struct epoll_event ev;
- //设置epoll
- s_epfd = epoll_create(65535);
- {//这个过程可以循环以便加入多个LISTEN套接字进入epoll事件集合
- //服务器监听创建
- rc = listen();//listen参数这里省略
- //加入epoll事件集合
- ev.events = EPOLLIN;
- ev.data.fd = rc;
- if (epoll_ctl(s_epfd, EPOLL_CTL_ADD, rc, &ev) < 0) {
- fprintf(stderr, "epoll set insertion error: fd=%d", rc);
- return(-1);
- }
- }
- }
- {//epoll事件处理
- int i, nfds, sock_new;
- struct epoll_event events[16384];
- for( ; ; ) {
- //等待epoll事件
- nfds = epoll_wait(s_epfd, events, 16384, -1);
- //处理epoll事件
- for(i = 0; i < nfds; i++) {
- //events[i].data.fd是epoll事件中弹出的套接字
- //接收连接
- sock_new = accept(events[i].data.fd);//accept其它参数这里省略了
- if(0 > sock_new) {
- fprintf(stderr, "接收客户端连接失败\n");
- continue;
- }
- }
- }
- }
复制代码
对照safedead和前面的一份代码,我想大家一定是明白了的。 |
|
|