参考书籍:《TCP/IP》网络编程,作者【韩】尹圣雨
测试环境:Ubuntu 10.10
GCC版本:4.4.5
一、回顾基于select的I/O复用技术
1.基于select的I/O复用技术速度慢的主要原因
* 调用select函数后针对所有文件描述符的循环语句
* 每次调用select函数时都需要向该函数传递监视对象信息(更耗时:调用select函数时向操作系统传递监视对象信息)
2.select的优点——兼容性好(大部分操作系统都支持select函数)
3.select适用条件——服务器端接入者少和程序应具有兼容性
二、基于epoll的I/O复用技术
1.epoll函数具有如下优点:
* 无需编写以监视状态变化为目的的针对所有文件描述符的循环语句
* 调用对应于select函数的epoll_wait函数时无需每次传递监视对象信息
2.epoll相关函数及结构体
* epoll_create函数(由操作系统管理)——创建保存epoll文件描述符的空间
头文件:#include <sys/epoll.h>
函数功能:创建保存epoll文件描述符的空间
返回值:成功时返回epoll文件描述符,失败时返回-1
函数原型:int epoll_create(int size);
参数:
size——Linux2.6.8之后的内核将完全忽略传入epoll_create函数的size参数,因为内核会根据情况调整epoll例程的大小
* epoll_event、epoll_data结构体
struct epoll_event
{
__unit32_t events; //epoll事件
epoll_data_t data; //用户数据
};
typedef union epoll_data
{
void* ptr; //指定与fd相关的用户数据
int fd; //指定事件所从属的目标文件描述符
__uint32_t u32;
__uint64_t u64;
}epoll_data_t;
注意:fd和ptr不能同时使用(联合体),建议在ptr指定的数据中包含fd
* epoll_ctl函数——注册监视对象文件描述符
头文件:#include <sys/epoll.h>
函数功能:注册监视对象文件描述符
返回值:成功时返回0,失败时返回-1
函数原型:int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
参数:
epfd——用于注册监视对象的epoll例称的文件描述符
op——用于指定监视对象的添加、删除或更改等操作
fd——需要注册的监视对象文件描述符
event——监视对象的事件类型
op指定的操作类型有如下3种:
* EPOLL_CTL_ADD:将文件描述符注册到epoll例程
* EPOLL_CTL_DEL:从epoll例程中删除文件描述符(epoll_ctl第四个参数为NULL,但Linux2.6.9之前不行)
* EPOLL_CTL_MOD:更改注册的文件描述符的关注事件发生情况
epoll_event结构体使用示例:
struct epoll_event event;
……
event.events = EPOLLIN; //发生需要读取数据的情况(事件)时
event.data.fd = sockfd;
event_ctl(epfd, EPOLL_CTL_ADD, sockfd, &event);
……
epoll_event成员events中可以保存的常量及所指的事件类型:
EPOLLIN:需要读取数据的情况
EPOLLOUT:输出缓冲为空,可以立即发送数据的情况
EPOLLPRI:收到OOB数据的情况
EPOLLRDHUP:断开连接或半关闭的情况,这在边缘触发方式下非常有用
EPOLLERR:发生错误的情况
EPOLLET:以边缘触发的方式得到事件通知
EPOLLONESHOT:发生一次事件后,相应文件描述符不再收到事件通知。因此需要向
epoll_ctl函数的第二个参数传递EPOLL_CTL_MOD,再次设置事件。
* epoll_wait函数——等待文件描述符发生变化
头文件:#include <sys/epoll.h>
函数功能:等待文件描述符发生变化
返回值:成功时返回发生事件的文件描述符,失败时返回-1
函数原型:int epoll_wait(int epfd, struct epoll_event* event, int maxevent, int timeout);
参数:
epfd——表示事件发生监视范围的epoll例程的文件描述符
events——保存发生事件的文件描述符集合的结构体地址值(需要动态分配内存)
maxevents——第二个参数中可以保存的最大事件数
timeout——以1/1000秒为单位的等待事件,传递-1时,一直等待直到发生事件
调用方式如下:
int event_cnt;
struct epoll_event* ep_events;
……
ep_events = malloc(sizeof(struct epoll_event)*EPOLL_SIZE); //EPOLL_SIZE是宏常量
……
event_cnt = epoll_wait(epfd, ep_events, EPOLL_SIZE, -1);
……
3.基于epoll的回声服务器端
#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 errorHandling(const char* buf);
int main(int argc, char* argv[])
{
int servSock, clientSock;
struct sockaddr_in servAddr, clientAddr;
socklen_t clientAddrSize;
int strLen, i;
char buf[BUF_SIZE];
struct epoll_event* epollEvents;
struct epoll_event event;
int epfd, eventCount;
if(2 != argc)
{
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
servSock = socket(PF_INET, SOCK_STREAM, 0);
if(-1 == servSock)
errorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(atoi(argv[1]));
if(-1 == bind(servSock, (struct sockaddr*)&servAddr, sizeof(servAddr)))
errorHandling("bind() error");
if(-1 == listen(servSock, 5))
errorHandling("listen() error");
epfd = epoll_create(EPOLL_SIZE);
epollEvents = (struct epoll_event*)malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
event.events = EPOLLIN;
event.data.fd = servSock;
epoll_ctl(epfd, EPOLL_CTL_ADD, servSock, &event);
while(1)
{
eventCount = epoll_wait(epfd, epollEvents, EPOLL_SIZE, -1);
if(-1 == eventCount)
{
puts("epoll_wait() error");
break;
}
for(i = 0; i < eventCount; i++)
{
if(epollEvents[i].data.fd == servSock)
{
clientAddrSize = sizeof(clientAddr);
clientSock = accept(servSock, (struct sockaddr*)&clientAddr, &clientAddrSize);
if(-1 == clientSock)
errorHandling("clientSock() error");
event.events = EPOLLIN;
event.data.fd = clientSock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientSock, &event);
printf("connected client: %d\n", clientSock);
}
else
{
strLen = read(epollEvents[i].data.fd, buf, BUF_SIZE);
if(0 == strLen) //close request!
{
epoll_ctl(epfd, EPOLL_CTL_DEL, epollEvents[i].data.fd, NULL);
close(epollEvents[i].data.fd);
printf("closed client:%d\n", epollEvents[i].data.fd);
}
else
{
write(epollEvents[i].data.fd, buf, strLen); //echo!
}
}
}
}
close(servSock);
close(epfd);
return 0;
}
void errorHandling(const char *buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void errorHandling(const char* message);
int main(int argc, char* argv[])
{
int sock = -1;
char message[BUF_SIZE] = {0};
int clntDataLen = -1;
struct sockaddr_in servAddr;
if(argc != 3)
{
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if(-1 == sock)
errorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr(argv[1]);
servAddr.sin_port = htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&servAddr, sizeof(servAddr)))
errorHandling("connect() error");
else
puts("Connected............");
while(1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
write(sock, message, strlen(message));
clntDataLen = read(sock, message, BUF_SIZE);
message[clntDataLen] = 0;
printf("Message from server: %s\n", message);
}
close(sock);
return 0;
}
void errorHandling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
测试:
服务端:gcc echoEpollServer.c -o echoEpollServer.out
connected client: 5
connected client: 6
closed client:6
closed client:5
客户端1:gcc echoClient.c -o echoClient.out
Connected............
Input message(Q to quit): 123
Message from server: 123
Input message(Q to quit): hello~
Message from server: hello~
Input message(Q to quit): Q
客户端2:gcc echoClient.c -o echoClient.out
Connected............
Input message(Q to quit): abc
Message from server: abc
Input message(Q to quit): Q
三、条件触发和边缘触发
1. 条件触发和边缘触发区别
* 条件触发——epoll默认工作模式。当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。如果不处理,下一次调用epoll_wait时还会再次向应用程序通告此事件。
* 边缘触发——当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,否则后续的epoll_wait调用将不再向应用程序通知这一事件。
2. 证明:epoll默认触发方式是条件触发
服务器端:
#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 4
#define EPOLL_SIZE 50
void errorHandling(const char* buf);
int main(int argc, char* argv[])
{
int servSock, clientSock;
struct sockaddr_in servAddr, clientAddr;
socklen_t clientAddrSize;
int strLen, i;
char buf[BUF_SIZE];
struct epoll_event* epollEvents;
struct epoll_event event;
int epfd, eventCount;
if(2 != argc)
{
printf("Usage: %s <port>\n", argv[0]);
exit(1);
}
servSock = socket(PF_INET, SOCK_STREAM, 0);
if(-1 == servSock)
errorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(atoi(argv[1]));
if(-1 == bind(servSock, (struct sockaddr*)&servAddr, sizeof(servAddr)))
errorHandling("bind() error");
if(-1 == listen(servSock, 5))
errorHandling("listen() error");
epfd = epoll_create(EPOLL_SIZE);
epollEvents = (struct epoll_event*)malloc(sizeof(struct epoll_event)*EPOLL_SIZE);
event.events = EPOLLIN;
event.data.fd = servSock;
epoll_ctl(epfd, EPOLL_CTL_ADD, servSock, &event);
while(1)
{
eventCount = epoll_wait(epfd, epollEvents, EPOLL_SIZE, -1);
printf("epoll_wait finish!\n");
if(-1 == eventCount)
{
puts("epoll_wait() error");
break;
}
for(i = 0; i < eventCount; i++)
{
if(epollEvents[i].data.fd == servSock)
{
clientAddrSize = sizeof(clientAddr);
clientSock = accept(servSock, (struct sockaddr*)&clientAddr, &clientAddrSize);
if(-1 == clientSock)
errorHandling("clientSock() error");
event.events = EPOLLIN;
event.data.fd = clientSock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientSock, &event);
printf("connected client: %d\n", clientSock);
}
else
{
strLen = read(epollEvents[i].data.fd, buf, BUF_SIZE);
if(0 == strLen) //close request!
{
epoll_ctl(epfd, EPOLL_CTL_DEL, epollEvents[i].data.fd, NULL);
close(epollEvents[i].data.fd);
printf("closed client:%d\n", epollEvents[i].data.fd);
}
else
{
write(epollEvents[i].data.fd, buf, strLen); //echo!
}
}
}
}
close(servSock);
close(epfd);
return 0;
}
void errorHandling(const char *buf)
{
fputs(buf, stderr);
fputc('\n', stderr);
exit(1);
}
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#define BUF_SIZE 1024
void errorHandling(const char* message);
int main(int argc, char* argv[])
{
int sock = -1;
char message[BUF_SIZE] = {0};
int clntDataLen = -1;
struct sockaddr_in servAddr;
if(argc != 3)
{
printf("Usage: %s <IP> <port>\n", argv[0]);
exit(1);
}
sock = socket(PF_INET, SOCK_STREAM, 0);
if(-1 == sock)
errorHandling("socket() error");
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = inet_addr(argv[1]);
servAddr.sin_port = htons(atoi(argv[2]));
if(connect(sock, (struct sockaddr*)&servAddr, sizeof(servAddr)))
errorHandling("connect() error");
else
puts("Connected............");
while(1)
{
fputs("Input message(Q to quit): ", stdout);
fgets(message, BUF_SIZE, stdin);
if(!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
break;
write(sock, message, strlen(message));
clntDataLen = read(sock, message, BUF_SIZE);
message[clntDataLen] = 0;
printf("Message from server: %s\n", message);
}
close(sock);
return 0;
}
void errorHandling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
测试:
服务器端:gcc server.c -o server.out,运行:./server.out 9190
epoll_wait finish!
connected client: 5
epoll_wait finish!
connected client: 6
epoll_wait finish!
epoll_wait finish!
epoll_wait finish!
epoll_wait finish!
epoll_wait finish!
epoll_wait finish!
epoll_wait finish!
epoll_wait finish!
closed client:5
epoll_wait finish!
epoll_wait finish!
closed client:6
客户端1:gcc client.c -o client.out,运行:./client.out 127.0.0.1 9190
Connected............
Input message(Q to quit): Hello Server~
Message from server: Hell
Input message(Q to quit): Q
客户端2:运行:./client.out 127.0.0.1 9190
Connected............
Input message(Q to quit): hello~
Message from server: hello~
Input message(Q to quit): a
Message from server: a
Input message(Q to quit): Q
3.边缘触发
* 实现边缘触发的必知内容
a. 通过errno变量验证错误原因
b. 为了完成非阻塞I/O,更改套接字特性
头文件:#include <fcntl.h>
函数功能:改变已打开的文件描述符性质
返回值:成功时返回cmd参数相关值,失败时返回-1
函数原型:int fcntl(int filedes, int cmd,...);
参数:
filedes——属性更改目标的文件描述符
cmd——表示函数调用的目的
* 边缘触发示例
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#define BUF_SIZE 4
#define EPOLL_SIZE 50
void setnonblockingmode(int fd);
void errorHangling(const char* message);
int main(int argc, char* argv[])
{
int servSock, clientSock;
struct sockaddr_in servAddr, clientAddr;
socklen_t clientAddrSize;
int strLen, i;
char buf[BUF_SIZE];
struct epoll_event* epollEvents;
struct epoll_event event;
int epfd, eventCount;
if(2 != argc)
{
printf("Usage:%s <port>\n", argv[0]);
exit(1);
}
servSock = socket(PF_INET, SOCK_STREAM, 0);
memset(&servAddr, 0, sizeof(servAddr));
servAddr.sin_family = AF_INET;
servAddr.sin_addr.s_addr = htonl(INADDR_ANY);
servAddr.sin_port = htons(atoi(argv[1]));
if(-1 == bind(servSock, (struct sockaddr*)&servAddr, sizeof(servAddr)))
errorHangling("bind() error");
if(-1 == listen(servSock, 5))
errorHangling("listen() error");
epfd = epoll_create(EPOLL_SIZE);
epollEvents = (struct epoll_event*)malloc(sizeof(struct epoll_event) * EPOLL_SIZE);
setnonblockingmode(servSock);
event.events = EPOLLIN;
event.data.fd = servSock;
epoll_ctl(epfd, EPOLL_CTL_ADD, servSock, &event);
while(1)
{
eventCount = epoll_wait(epfd, epollEvents, EPOLL_SIZE, -1);
if(-1 == eventCount)
{
puts("epoll_wait() error");
break;
}
puts("return epoll_wait");
for(i = 0; i < eventCount; i++)
{
if(epollEvents[i].data.fd == servSock)
{
clientAddrSize = sizeof(clientSock);
clientSock = accept(servSock, (struct sockaddr*)&clientAddr, &clientAddrSize);
setnonblockingmode(clientSock);
event.events = EPOLLIN | EPOLLET;
event.data.fd = clientSock;
epoll_ctl(epfd, EPOLL_CTL_ADD, clientSock, &event);
printf("connected client: %d\n", clientSock);
}
else
{
while(1) //循环是为了从缓冲区读取全部数据
{
strLen = read(epollEvents[i].data.fd, buf, BUF_SIZE);
if(0 == strLen) //close request!
{
epoll_ctl(epfd, EPOLL_CTL_DEL, epollEvents[i].data.fd, NULL);
close(epollEvents[i].data.fd);
printf("closed client: %d\n", epollEvents[i].data.fd);
break;
}
else if(strLen < 0)
{
if(errno == EAGAIN)
break;
}
else
{
write(epollEvents[i].data.fd, buf, strLen);
}
}
}
}
}
close(servSock);
close(epfd);
return 0;
}
void setnonblockingmode(int fd)
{
int flag = fcntl(fd, F_GETFL, 0);
fcntl(fd, F_SETFL, flag | O_NONBLOCK);
}
void errorHangling(const char *message)
{
fputs(message, stderr);
fputc('\n', stderr);
exit(1);
}
客户端代码同上。
测试:
服务器端:gcc epollEPETserver.c -o epollEPETserver.out,运行:./epollEPETserver.out 9190
return epoll_wait
connected client: 5
return epoll_wait
return epoll_wait
return epoll_wait
return epoll_wait
closed client: 5
客户端:
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
分析:客户端从连接到断开连接,总共发送5次数据,服务器端产生了5个事件。
边缘触发的优势:可以分离接收数据和处理数据的时间点!