1.I/O复用
I/O复用能使程序同时监听多个文件描述符,网络程序在下列情况下需要用到I/O技术
(1)非阻塞connect 技术:客户端程序要处理多个socket
(2)聊天室程序:客户端程序要同时处理用户输入和网络连接
(3)TCP服务器要同时监听socket 和连接socket
(4)回射服务器:服务器要同时处理TCP请求和UDP请求
(5)xinetd服务器:服务器要同时监听多个端口,或者处理多种服务
1.1select 系统调用
用途:在一段指定时间内,监听用户感兴趣的文件描述符的可读,可写,异常的事件。
1.2 参数分析
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
第一个参数指定被监听的文件描述符的总数:通常被设置为select监听的所以文件描述符的最大值+1,因为文件描述符是从0开始的。
第二,三,四个参数指向可读,可写,异常等事件对应的文件描述符集合
应用程序调用select 函数时,通过这三个参数传入自己感兴趣的文件描述符,
select 调用返回时,内核将修改他们来通知应用程序哪些文件描述符已经就绪,这三个参数都是fd_set 结构指针类型的数据
fd_set 结构体仅包含一个整形数组,该数组的每个元素的每一位标记一个文件描述符,fd_set 能容纳的文件描述符的数量由FD_SETSIZE指定,这样就限制了select 能同时处理的文件描述符的总量
通过一系列宏来访问fd_set结构体中的位
void FD_CLR(int fd, fd_set *set);清除fd
int FD_ISSET(int fd, fd_set *set);判断fd 是否被设置
void FD_SET(int fd, fd_set *set);设置
void FD_ZERO(fd_set *set);清除所有位在
第五个参数用来设置select 的超时时间。是一个timeval指针,采用指针参数是因为内核将修改它以告诉应用程序select
等待了多久。
struct timeval
{
long tv_sec; /* seconds */秒
long tv_usec; /* microseconds */微秒
};
如果给timeout 变量的两个成员都传递0,select 将立即返回0,如果给timeout 变量传NULL,select 将一直阻塞,直到某个文件描述就绪。
3.返回值
select 成功返回就绪的文件描述符的个数,
如果在超时时间内没有任何文件描述符就绪,select 将返回0
select 失败时返回-1并设置errno
如果select 在等待期间,程序接收到信号,则select立即返回-1,并设置errno 为EINTR
4.文件描述符的就绪条件
socket可读:
(1)socket 内核接收缓存区中字节数大于或者等于其低水位SO_RCVLOWAT,此时我们可以无阻塞地读
socket,并且读操作返回的字节数大于0
(2)socket 通信的对方关闭连接。此时对该socket的读操作将返回0
(3)监听socket 上有新的连接请求
(4)socket 上有未处理的错误,此时我们可以用getsockopt 来读取和清除该错误
socket 可写
(1)socket内核接收缓冲区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT,此时我们可以无阻塞
的写该socket,并且写操作返回的字节数大于0
(2)socket的写操作被关闭。对写操作被关闭的socket写操作将触发一个SIGPIPE信号
(3)socket 使用非阻塞connect连接成功或者失败之后
(4)socket上有未处理的错误,此时我们可以用getsockopt 来读取和清除该错误
异常:
select 上接收到带外数据:
关于什么是带外数据:
带外数据(out of band data):是指连接双方中的一方有重要事情,想要迅速的通知对方
这种通知在已经排队等待发送任何普通(有时称为带内数据)之前发送
带外数据设计比带内数据有更高的优先级
带外数据是映射到现有连接中的,而不是在客户端与服务区之间创建一个新的连接
带外数据在正常数据流之外能够及时的通知到应用程序有异常事件发生。
socket 上接收到的普通数据和带外数据都将由socket 返回,但socket 处于不同的就绪状态
前者属于可读状态,后者属于异常状态
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<assert.h>
#include<errno.h>
#include<unistd.h>
#include<string.h>
#include<fcntl.h>
#include<stdlib.h>
int main(int argc, char *argv[])
{
if(argc <= 2)
{
printf("useage: %s ip_address port_number\n",basename(argv[0]));
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
int ret = 0;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(listenfd >= 0);
ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(listenfd, 5);
assert(ret != -1);
struct sockaddr_in client_address;
socklen_t client_addrlenth = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr*)&client_address,&client_addrlenth);
if(connfd < 0)
{
printf("errno is %d\n",errno);
close(listenfd);
}
char buf[1024];
fd_set read_fds;
fd_set exception_fds;
FD_ZERO(&read_fds);
FD_ZERO(&exception_fds);
while(1)
{
memset(buf, '\0',sizeof(buf));
FD_SET(connfd, &read_fds);
FD_SET(connfd, &exception_fds);
ret = select(connfd+1, &read_fds, NULL, &exception_fds, NULL);
if(ret < 0)
{
printf("selection faiure\n");
break;
}
//接收普通数据
if(FD_ISSET(connfd, &read_fds))
{
ret = recv(connfd, buf, sizeof(buf)-1, 0);
if(ret <= 0)
{
break;
}
printf("get %d bytes of nomal data: %s\n", ret, buf);
}
//接收带外数据
else if(FD_ISSET(connfd, &exception_fds))
{
ret = recv(connfd, buf, sizeof(buf)-1, MSG_OOB);
if(ret <= 0)
{
break;
}
printf("get %d bytes of nomal data:%s\n", ret, buf);
}
close(connfd);
close(listenfd);
return 0;
}
basename:文件的最后一个路径
2.poll系统调用
poll与select 调用相似,也是在一定时间内轮询一定数量的文件描述符,以测试其中是否有就绪者
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <poll.h>
int ppoll(struct pollfd *fds, nfds_t nfds,
const struct timespec *timeout_ts, const sigset_t *sigmask);
1)fds参数是一个pollfd 结构类型的数组,指定我们感兴趣的文件描述符上发生可读,可写,异常等情况
struct pollfd {
int fd; /* file descriptor */文件描述符
short events; /* requested events */注册的事件
short revents; /* returned events */实际发生的事件
};
fd成员指定文件描述符,events成员告诉poll 监听fd 上的哪些事件,它是一系列事件的按位或,revents成员由内核修改,通知应用程序fd 上发生了哪些事件。
通常,应用程序需要根据recv调用的返回值来区分socket上接收到的是有效数据还是对方关闭连接的请求
并作出相应的处理。但是后来GNU为poll 系统调用增加了一个POLLRHUP事件
POLLRHUP 事件,它在socket 上接收到的对方关闭连接的请求后触发,为区分上面两种方式提供了一种更简单的方式。
但在使用POLLRDHUP 事件时,我们需要在代码的最开始定义_GNU_SOURCE
2)nfds 是nfds_t 类型的,nfds参数指定被监听事件集合fds 的大小
nfds_t typedef unsigned long int nfds_t;
3)注意了最后一个参数和select 不一样,它是int 型的,指定poll的超时时间,单位是毫秒
timeout 为 -1 时,poll 调用将永远阻塞,直到有某个事件发生;
timeout 为0时,立即返回。
返回值与select 含义相同。
3.epoll系统调用
epoll是Linux特有的I/O复用函数,它的实现和使用上与select,poll 有很大的差异,
epoll是使用一组函数来完成任务,而不是单个函数。
epoll 把用户关心的文件描述符上的事件放在内核里的一个事件表中,从而无需像select和poll那样每次
调用都要重复传入文件描述符集或者事件集。
1)但是epoll调用需要使用一个额外的文件描述符,来唯一标示内核中的事件表,这个文件描述符由epoll_create 来创建
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_create1(int flags);
size 参数现在不起作用,只是给内核一个提示,告诉它事件表需要多大。该函数返回的文件描述符将作用于其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表
2)epoll_ctl操作内核事件表
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
fd 参数是要操作的文件描述符,op 是指定操作类型。
EPOLL_CTL_ADD 往事件表中注册fd事件
EPOLL_CTL_MOD 修改fd 上的事件
EPOLL_CTL_DEL 删除fd 上的注册事件
event 参数指定事件
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
其中events 成员描述事件类型。epoll 支持的事件类型和poll 基本相同,表示epoll事件类型的宏是在poll对应的宏上加E
比如epoll的数据可读事件为EPOLLIN 但是epoll由两个额外的事件类型----EPOLLET 和 EPOLLONESHOT
data成员用来存储用户数据,其类型:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
epoll_data_t 是一个联合体,其中fd 指定事件所从属的目标文件描述符。ptr 可用来指定与fd 相关的用户数据,但是epoll_data_t是一个联合体,两个不能同时使用,因此要将文件描述符与用户数据关联起来,只能使用其他手段,比如放弃使用epoll_data_t 的
fd 成员,而在ptr指定的用户数据中包含fd
调用成功返回0,调用失败返回-1,并设置error
3)epoll_wait 函数
epoll 系统调用的主要接口是epoll_wait 函数,
作用:它在一段时间内等待一组文件描述符上的事件
原型:
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
timeout 与poll 接口的timeout 参数相同 maxevents 参数指定最多能监听多少个事件,必须大于0
epoll_wait 函数如果监测到事件,就将所有就绪的事件从内核事件表中复制到它的第二个参数events指定的
数组中,这个数组只用于输出内核检测到的就绪事件,提高应用程序索引就绪文件描述符的效率
4.LT模式和ET模式
epoll从对文件描述符的操作有两种:LT 电平触发模式 ET 边沿触发模式
LT模式是默认的工作模式,在这种模式下,这种模式下epoll相当于一个效率比较高的poll.
当往epoll内核事件表中注册一个文件描述符 上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。ET模式是epoll的高效工作模式
LT:当epoll_wait 检测到其上有事件发生并将此事件通知给应用程序后,应用程序可以不立即处理该事件,这样当应用程序
下一次调用epoll_wait函数时,epoll_wait 还会再次向应用程序通知此事件,直到该事件被处理。
ET:当epoll_wait 检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait调用将不再向应用程序通知这一事件。
所以ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,因此效率比电平触发高。
下面是服务器程序:
#include<stdio.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<stdio.h>
#include<unistd.h>
#include<errno.h>
#include<string.h>
#include<fcntl.h>
#include<stdlib.h>
#include<sys/epoll.h>
#include<pthread.h>
#define MAX_EVEVT_NUMBER 1024
#define BUFFER_SIZE 10
//将文件描述符设置成非阻塞的
int setnonblocking(int fd)
{
int old_option = fcntl(fd, F_GETFL);
int new_option =old_option | O_NONBLOCK;
fcntl(fd, F_SETFL,new_option);
return old_option;
}
//将文件描述符fd 上的EPOLLIN 注册到epollfd 指定的epoll内核事件表中,参数enable_et 指定是否对fd 启用ET模式
void addfd(int epollfd, int fd, bool enable_et)
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN;
if(enable_et)
{
event.events |= EPOLLET;
}
epoll_ctl(epollfd,EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd);
}
void lt(epoll_event *events, int number, int epollfd, int listenfd)
{
char buf[BUFFER_SIZE];
for(int i=0; i< number; ++i)
{
int sockfd = events[i].data.fd;
if(sockfd == listenfd)
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);
addfd(epollfd, connfd, false);//禁用ET模式
}
else if(events[i].events & EPOLLIN)
{
printf("event trigger once\n");
memset(buf, '\0',BUFFER_SIZE);
int ret = recv(sockfd, buf, BUFFER_SIZE, 0);
if(ret <= 0)
{
close(sockfd);
continue;
}
printf("get %d bytes of content: %s\n", ret, buf);
}
else
{
printf("something else happen\n");
}
}
}
void et(epoll_event *events, int number, int epollfd, int listenfd)
{
char buf[BUFFER_SIZE];
for(int i=0; i<number; ++i)
{
int sockfd = events[i].data.fd;
if(sockfd == listenfd)
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);
addfd(epollfd, connfd, true);
}
else if(events[i].events | EPOLLIN)
{
printf("event trigger once\n");
while(1)
{
memset(buf, '\0',BUFFER_SIZE);
int ret = recv(sockfd, buf, BUFFER_SIZE, 0);
if(ret < 0)
{
if((errno == EAGAIN)||(errno == EWOULDBLOCK))
{
printf("read later\n");
break;
}
close(sockfd);
break;
}
else if(ret == 0)
{
close(sockfd);
}
else
{
printf("get %d bytes of content : %s\n", ret, buf);
}
}
}
else
{
printf("something else happen \n");
}
}
}
int main(int argc, char *argv[])
{
if(argc <= 2)
{
printf("usage: %s ip_address port_number\n", basename(argv[0]));
return 1;
}
const char *ip = argv[1];
int port = atoi(argv[2]);
int ret = 0;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
inet_pton(AF_INET, ip, &address.sin_addr);
address.sin_port = htons(port);
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(listenfd >= 0);
ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(listenfd, 5);
assert(ret != -1);
epoll_event events[MAX_EVEVT_NUMBER];
int epollfd = epoll_create(5);
assert(epollfd != -1);
addfd(epollfd, listenfd, true);
while(1)
{
int ret = epoll_wait(epollfd, events, MAX_EVEVT_NUMBER, -1);
if( ret < 0)
{
printf("epoll failure\n");
break;
}
//lt(events, ret, epollfd, listenfd);
et(events, ret, epollfd, listenfd);
}
close(listenfd);
return 0;
}
给一个客户端程序:
#include<stdio.h>
#include"../utili.h"
#include<string.h>
int main()
{
int sockCli = socket(AF_INET, SOCK_STREAM, 0);
if(sockCli == -1)
{
perror("socket");
exit(1);
}
struct sockaddr_in addrSer;
addrSer.sin_family = AF_INET;
addrSer.sin_port = htons(SERVER_PORT);
addrSer.sin_addr.s_addr = inet_addr(SERVER_IP);
socklen_t len = sizeof(struct sockaddr);
int res = connect(sockCli, (struct sockaddr*)&addrSer, len);
if(res == -1)
{
perror("connect");
close(sockCli);
exit(1);
}
char recv_buffer[MAX_BUFFER_SIZE];
char send_buffer[MAX_BUFFER_SIZE];
while(1)
{
memset(send_buffer, 0, sizeof(send_buffer));
memset(recv_buffer, 0, sizeof(recv_buffer));
char *str = "Server OverLoad";
printf("msg:>");
scanf("%s",send_buffer);
send(sockCli, send_buffer, strlen(send_buffer)+1, 0);
recv(sockCli,recv_buffer, strlen(recv_buffer)+1, 0);
printf("From server of myself msg:%s\n",recv_buffer);
}
close(sockCli);
return 0;
}
结果显示:这是ET模式下的结果:
这是LT工作模式下的结果: