在不使用I/O复用的技术下,如果要同时监听多个fd上的事件得使用多个线程(或进程,下同),每个线程单独负责一个fd上事件。而I/O复用技术可以同时监听多个fd,即在一个线程里监听多个fd的事件。另外I/O本身是阻塞的,在它返回后,如果要实现并发,还是得使用多进程或多线程的变成手段。
Linux下实现的I/O复用的系统调用主要有select、poll和epoll。
1、select系统调用
API
select系统调用的用途是在一段指定时间内,监听用户感兴趣的文件描述符上的可读、可写和异常等事件。
#include <sys/select.h>
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
1)nfds参数指定被监听的文件描述符总数。
2)readfds、writefds、exceptfds分别是可读、可写和异常事件对应的fd集合。fd结构体如下
#include <typesizes.h>
#define __FD_SETSIZE 1024
#include <sys/select.h>
typedef long int __fd_mask;
#undef __NFDBITS
/* It's easier to assume 8-bit bytes than to get CHAR_BIT. */
#define __NFDBITS (8 * (int) sizeof (__fd_mask))
/* fd_set for select and pselect. */
typedef struct
{
#ifdef __USE_XOPEN
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->fds_bits)
#else
__fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS];
# define __FDS_BITS(set) ((set)->__fds_bits)
#endif
} fd_set;
/* Access macros for `fd_set'. */
#define FD_SET(fd, fdsetp) __FD_SET (fd, fdsetp) //设置fdsetp的位fd
#define FD_CLR(fd, fdsetp) __FD_CLR (fd, fdsetp) //清除fdsetp的位fd
#define FD_ISSET(fd, fdsetp) __FD_ISSET (fd, fdsetp) //测试fdsetp的位fd是否被设置
#define FD_ZERO(fdsetp) __FD_ZERO (fdsetp) //清除所有位
可以看到,fd_set结构体包含一个整型数组,数组的每个元素的每一位标记一个fd,fd_set能容纳的fd数量由__FD_SETSIZE指定,这就限制了select能同时处理的fd的总量。可以通过上面定义的宏来访问fd_set结构体中的位。
3)timeou参数用来设置select函数的超时时间,timeval结构体如下
struct timeval
{
long tv_sec; //秒数
long tv_usec; //微秒数
};
如果给timeout变量的tv_sec和tv_usec都传递0,则select立即返回,传递NULL,select就一直阻塞,直到有fd就绪。
select系统调用成功返回就绪的fd总数数量,超时返回0,失败返回-1,并设置errno。
fd的就绪条件
在网络编程中,下列情况下socket可读:
- socket内核接收缓冲区中的字节数大于等于低水位标记
- socket通信对方关闭连接,此时socket的读操作返回0
- 正在监听的socket上有新的连接请求
- socket上有未处理的错误,此时可以使用getsockopt来读取和清除该错误
下列情况下可写
- socket内核发送缓存区中的可用字节数大于等于低水位标记
- socket的写操作被关闭。对写操作别关闭的socket执行写操作将触发一个SIGPIPE信号
- socket使用非阻塞connect连接成功或失败(超时)后
- socket上有未处理的错误,此时可以使用getsockopt来读取和清除该错误
网络程序中,select能处理的异常情况只有一种,socket上接收到带外数据。
select的缺点
- 可同时处理的fd总量有限制,默认1024
- 每次调用都需要重新传入几个fd集合(涉及到用户态到内核态的拷贝)
- 返回后需要遍历整个fd集合来找到就绪的fd
2、poll系统调用
poll与select类似,也是在指定时间内轮询一定数量的fd,以测试其中是否有就绪者。
#include <poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
struct pollfd
{
int fd;
short events; //注册的事件
short revents; //实际发生的事件,由内核填充
}
l
typedef unsigned long int nfds_t;
1)fds是pollfd类型的数组,pollfd中events和revents是一系列事件的按位或,events由poll的调用用户填充,revents在事件发生后由内核填充。
2)nfds参数指定被监听事件集合fds的大小
3)timeout为-1时,poll一直阻塞直到某fd就绪,为0立即返回。
poll系统调用的返回值含义与select相同。
与select相比,poll只解决了select的第一个缺点,后面两个依然存在。
3、epoll系列系统调用
select和poll都是一个函数了事,而epoll有三个。epoll会把用户关心的fd上的事件放到内核中的一个事件表里,从而无须像select和poll那样每次调用都要重复传入fd集合和事件集。
三个系统调回用
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctrl(int epfd, int op, int fd, struct epoll_event* event);
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
struct epoll_event
{
__uint32_t events; //epoll事件
epoll_data_t data; //用户数据
};
typedef union epoll_data
{
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
epoll_create创建一个内核事件表,成功返回表示这个事件表的fd,参数size现在并不起作用,只是给内核一个提示,告诉他事件表需要多大。
epoll_ctrl用户来操作上面创建的事件表,成功返回0,失败返回-1并设置errno。fd是要操作的文件描述符,op是操作类型,有以下3种:
- EPOLL_CTL_ADD 往事件表种注册fd上的事件
- EPOLL_CTL_MOD 修改fd上的注册事件
- EPOLL_CTL_DEL 删除fd上的注册事件
event参数指定事件。
epoll_wait在一段超时时间内等待一组fd上的事件。成功返回就绪的fd的个数,失败返回-1,并设置errno。maxevents指定最多监听多少事件,必须大于0。events中保存已经由内核准备好的就绪的事件列表。这个列表里的都是已就绪的事件,而不像select和poll那样。
可以看出epoll将select的三个缺点都解决了。
LT和ET模式
epoll对文件描述符的操作有两种模式:LT(Level Trigger)水平触发和ET(Edge Trigger)边沿触发。
LT模式下的epoll,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理,当下次调用epoll_wait时,epoll_wait还会再次通知,直到事件被处理。
ET模式下的epoll则不然,它在检测到其上有事件发生并将此事件通知应用程序后,不管应用程序是不是立即处理,在下次调用epoll_wait时都将不会再通知此事件,除非有新的事件到来。
EPOLLONESHOT事件
在多线程编程中,如果一个线程在读取完某个socket上的数据后开始处理数据,而此时该socket上又触发新的事件,此时系统选择另一个线程来处理,这样同时有两个线程处理这个socket,这是不被期望的。
在注册了EPOLLONESHOT事件的fd,操作系统最大触发其上注册的一个可读、可写或异常事件,且只触发一次,除非使用epoll_ctl重置该fd上的EPOLLONESHOT事件。
4、I/O复用的高级应用
非阻塞connect(使用select)
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdlib.h>
#include <assert.h>
#include <time.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <string.h>
#include <unistd.h>
#define BUFFER_SIZE 1023
int setnonbolcking(int fd)
{
int old = fcntl(fd, F_GETFL);
int newo = old | O_NONBLOCK;
fcntl(fd, F_SETFL, newo);
return old;
}
//超时连接函数,成功返回已经处于连接的socket,失败返回-1
int unblock_connect(const char* ip, int port, int time)
{
int ret = 0;
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
address.sin_port = htons(port);
inet_pton(AF_INET, ip, &address.sin_addr);
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
assert(sockfd > 0);
int fdopt = setnonbolcking(sockfd);
ret = connect(sockfd, (struct sockaddr*)&address, sizeof(address));
if (ret == 0)
{
//连接成功 恢复sockfd的属性,立即返回
printf("connnect with server immediately\n");
fcntl(sockfd, F_SETFL, fdopt);
return sockfd;
}
else if (errno != EINPROGRESS)
{
//连接失败
printf("unlock connect not suppotr\n");
return -1;
}
//正在连接中
fd_set readfds;
fd_set writefds;
struct timeval timeout;
//监听写事件 这是因为connect的man手册中讲到,使用非阻塞的connect时,可以通过监听sockfd的可写事件,当select、
//poll等函数返回后,利用getsockopt来读取错误码,错误码为0表示连接建立成功,否则失败
FD_ZERO(&readfds);
FD_SET(sockfd, &writefds);
timeout.tv_sec = time;
timeout.tv_usec = 0;
ret = select(sockfd + 1, NULL, &writefds, NULL, &timeout);
if (ret <= 0)
{
//超时或出错
printf("connection timeout\n");
close(sockfd);
return -1;
}
if (!FD_ISSET(sockfd, &writefds))
{
printf("no events on sockfd found\n");
close(sockfd);
return -1;
}
int error = 0;
socklen_t length = sizeof(error);
//调用getsockopt来获取并清除sockfd上的错误
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &length) < 0)
{
printf("get socket option faild\n");
close(sockfd);
return -1;
}
if (error != 0)
{
printf("connection faild after select with error:%d\n", error);
close(sockfd);
return -1;
}
//连接成功
printf("connection ready after select with the socket:%d", sockfd);
fcntl(sockfd, F_SETFL, fdopt);
return sockfd;
}
int main(int argc, char* argv[])
{
if (argc <= 2)
{
printf("usage: %s ip port\n", argv[0]);
return 1;
}
int sockfd = unblock_connect(argv[1], atoi(argv[2]), 10);
if (sockfd < 0)
{
return 1;
}
close(sockfd);
return 0;
}
聊天室(poll)
client
#define _GNU_SOURCE 1
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <poll.h>
#include <fcntl.h>
#include <assert.h>
#define BUFFER_SIZE 64
int main(int argc, char* argv[])
{
if (argc <=2)
{
printf("usage: %s ip port\n", argv[0]);
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in server_address;
bzero(&server_address, sizeof(server_address));
server_address.sin_family = AF_INET;
server_address.sin_port = htons(port);
inet_pton(AF_INET, ip, &server_address.sin_addr);
int sockfd = socket(PF_INET, SOCK_STREAM, 0);
assert(sockfd >= 0);
if (connect(sockfd, (struct sockaddr*)&server_address, sizeof(server_address)) < 0)
{
printf("connection failed\n");
close(sockfd);
return 1;
}
pollfd fds[2];
/*
struct pollfd
{
int fd;
short events; //注册的事件
short revents; //实际发生的事件,由内核填充
};
*/
//注册文件描述符0(标准输入)和文件描述符sockfd上的可读事件
fds[0].fd = 0;
fds[0].events = POLLIN;
fds[0].revents = 0;
fds[1].fd = sockfd;
fds[1].events = POLLIN | POLLRDHUP;
fds[1].revents = 0;
char read_buf[BUFFER_SIZE];
int pipefd[2];
int ret = pipe(pipefd);
assert(ret != -1);
while (1)
{
ret = poll(fds, 2, -1);
if (ret < 0)
{
printf("pool failure\n");
break;
}
if (fds[1].revents & POLLRDHUP)
{
//对方关闭了连接
printf("server close connection\n");
break;
}
else if (fds[1].revents & POLLIN)
{
//sockfd上有可读事件
memset(read_buf, '\0', BUFFER_SIZE);
recv(fds[1].fd, read_buf, BUFFER_SIZE - 1, 0);
printf("receive msg: %s\n", read_buf);
}
if (fds[0].revents & POLLIN)
{
//客户输入
//使用splice将用户输入的数据直接写到sockfd上(零拷贝)
//将标准输入里的数据写入管道
ret = splice(0, NULL, pipefd[1], NULL, 32768, SPLICE_F_MOVE | SPLICE_F_MORE);
//将管道里的数据写入sockfd
ret = splice(pipefd[0], NULL, sockfd, NULL, 32768, SPLICE_F_MOVE | SPLICE_F_MORE);
}
}
close(sockfd);
return 0;
}
server
#define _GNU_SOURCE 1
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <poll.h>
#include <fcntl.h>
#include <assert.h>
#include <errno.h>
#define USER_LIMIT 5 //最大用户数
#define BUFFER_SIZE 64 //读缓冲区大小
#define FD_LIMIT 65535 //文件描述符数量限制
/*
客户数据:客户端socket地址
待写到客户端的数据的位置,即客户端写缓冲区的位置
从客户端读入的数据
*/
struct client_data
{
sockaddr_in address;
char* write_buf;
char buf[BUFFER_SIZE];
};
int setnonblocking(int fd)
{
int oldopt = fcntl(fd, F_GETFL);
int newopt = oldopt | O_NONBLOCK;
fcntl(fd, F_SETFL, newopt);
return oldopt;
}
int main(int argc, char* argv[])
{
if (argc <= 2)
{
printf("usage: %s ip port\n", argv[0]);
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
address.sin_port = htons(port);
inet_pton(AF_INET, ip, &address.sin_addr);
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(listenfd >= 0);
int ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(listenfd, 5);
printf("listen ret %d %d %s\n", ret, errno, strerror(errno));
assert(ret != -1);
/* 创建users数组,分配FD_LIMIT个client_data对象。可以预期,每个可能的socket连接都可以
* 获得这样一个对象
*/
client_data* users = new client_data[FD_LIMIT];
pollfd fds[USER_LIMIT+1];
int user_counter = 0;
for (int i = 0; i <= USER_LIMIT; ++i)
{
fds[0].fd = -1;
fds[0].events = 0;
}
fds[0].fd = listenfd;
fds[0].events = POLLIN | POLLERR;
fds[0].revents = 0;
while (1)
{
ret = poll(fds, user_counter + 1, -1);
if (ret < 0)
{
printf("poll failure\n");
break;
}
for(int i = 0; i < user_counter + 1; ++i)
{
//新连接
if ((fds[i].fd == listenfd) && (fds[i].revents & POLLIN))
{
struct sockaddr_in client_address;
socklen_t client_length = sizeof(client_address);
int connfd = accept(fds[i].fd, (struct sockaddr*)&client_address, &client_length);
if (connfd < 0)
{
printf("accept failure %d\n", errno);
continue;
}
//可接受的连接已满
if (user_counter >= USER_LIMIT)
{
const char* info = "too many users\n";
printf("%s", info);
send(connfd, info, strlen(info), 0);
close(connfd);
continue;
}
user_counter++;
users[connfd].address = client_address;
setnonblocking(connfd);
fds[user_counter].fd = connfd;
fds[user_counter].events = POLLIN | POLLRDHUP | POLLERR;
fds[user_counter].revents = 0;
//保存点分十进制ip地址
char ipaddr[INET_ADDRSTRLEN];
memset(ipaddr, '\0', INET_ADDRSTRLEN);
printf("comes a new user %s:%d, now %d\n",
inet_ntop(AF_INET, &client_address.sin_addr, ipaddr, sizeof(ipaddr)),
ntohs(client_address.sin_port),
user_counter);
}
else if (fds[i].revents & POLLERR)
{
//客户端连接出错
printf("get an error from %d ", fds[i].fd);
char errors[100];
memset(errors, '\0', 100);
socklen_t length = sizeof(errors);
if (getsockopt(fds[i].fd, SOL_SOCKET, SO_ERROR, &errors, &length) < 0)
{
printf("\n getsocket option failed\n");
continue;
}
printf("error %s\n", errors);
}
else if (fds[i].revents & POLLRDHUP)
{
//客户端关闭连接
//作者这里的本意应该是将fds中最后一个的数据覆盖到当前数据,但是没明白
//为什么要处理users 感觉应该将user[fds[i].fd]重置
users[fds[i].fd] = users[fds[user_counter].fd];
close(fds[i].fd);
fds[i] = fds[user_counter];
i--;
user_counter--;
printf("a client left\n");
}
else if (fds[i].revents & POLLIN)
{
//有客户发消息了
int connfd = fds[i].fd;
memset(users[connfd].buf, '\0', BUFFER_SIZE);
//这里如果客户端发的消息长度超过BUFFER_SIZE-1,则在下一次循环中还会读取
ret = recv(connfd, users[connfd].buf, BUFFER_SIZE - 1, 0);
printf("get %d bytes msg from client %d, msg:%s\n", ret, connfd, users[connfd].buf);
if (ret < 0)
{
//读操作出错
if (errno != EAGAIN)
{
close(connfd);
users[fds[i].fd] = users[fds[user_counter].fd];
fds[i] = fds[user_counter];
i--;
user_counter--;
}
}
else if (ret == 0)
{
//客户端关闭连接?
}
else
{
//正确接受到消息,通知其他socket准备写数据
for (int j = 1; j <= user_counter; ++j)
{
if (fds[j].fd == connfd)
{
continue;
}
//注销读事件 添加写事件
fds[j].events |= ~POLLIN;
fds[j].events |= POLLOUT;
//记录待写数据来源
users[fds[j].fd].write_buf = users[connfd].buf;
}
}
}
else if (fds[i].revents & POLLOUT)
{
//可以写了
int connfd = fds[i].fd;
//安全判断
if (!users[connfd].write_buf)
{
continue;
}
//向该客户端发送数据
ret = send(connfd, users[connfd].write_buf, strlen(users[connfd].write_buf), 0);
users[connfd].write_buf = NULL;
//数据写完重新注册可读事件
fds[i].events |= ~POLLOUT;
fds[i].events |= POLLIN;
}
}
}
delete[] users;
close(listenfd);
return 0;
}
同时处理tcp请求和udp请求的回射服务器(epoll)
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>
#include <fcntl.h>
#include <assert.h>
#include <errno.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_EVENT_NUMBER 1024
#define TCP_BUFFER_SIZE 512
#define UDP_BUFFER_SIZE 1024
int setnonblocking(int fd)
{
int oldopt = fcntl(fd, F_GETFL);
int newopt = oldopt | O_NONBLOCK;
fcntl(fd, F_SETFL, newopt);
return oldopt;
}
void addfd(int epollfd, int fd)
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET; //ET模式
epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event);
setnonblocking(fd);
}
int main(int argc, char* argv[])
{
if (argc <= 2)
{
printf("usage: %s ip port\n", argv[0]);
return 1;
}
const char* ip = argv[1];
int port = atoi(argv[2]);
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
address.sin_port = htons(port);
inet_pton(AF_INET, ip, &address.sin_addr);
//TCP
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
assert(listenfd >= 0);
int ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
ret = listen(listenfd, 5);
assert(ret != -1);
//UDP
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
address.sin_port = htons(port);
inet_pton(AF_INET, ip, &address.sin_addr);
int udpfd = socket(PF_INET, SOCK_DGRAM, 0);
assert(udpfd >= 0);
ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));
assert(ret != -1);
epoll_event events[MAX_EVENT_NUMBER];
int epollfd = epoll_create(5);
assert(epollfd != -1);
addfd(epollfd, listenfd);
addfd(epollfd, udpfd);
while (1)
{
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if (number < 0)
{
printf("epoll failure\n");
break;
}
for (int i = 0; i < number; ++i)
{
int sockfd = events[i].data.fd;
if (sockfd == listenfd)
{
//新的TCP连接
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
int connfd = accept(sockfd, (struct sockaddr*)&client_address, &client_addrlength);
addfd(epollfd, connfd);
}
else if (sockfd == udpfd)
{
//udp端口有数据
char buf[UDP_BUFFER_SIZE];
memset(buf, '\0', UDP_BUFFER_SIZE);
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
ret = recvfrom(sockfd, buf, UDP_BUFFER_SIZE - 1, 0, (struct sockaddr*)&client_address, &client_addrlength);
if (ret > 0)
{
sendto(sockfd, buf, UDP_BUFFER_SIZE - 1, 0, (struct sockaddr*)&client_address, client_addrlength);
}
}
else if (events[i].events & EPOLLIN)
{
//tcp连接有数据
char buf[TCP_BUFFER_SIZE];
//因为是ET模式 所以需要循环读取数据
while (1)
{
memset(buf, '\0', TCP_BUFFER_SIZE);
ret = recv(sockfd, buf, TCP_BUFFER_SIZE - 1, 0);
if (ret < 0)
{
if (errno == EAGAIN || errno == EWOULDBLOCK)
{
break;
}
close(sockfd);
break;
}
else if (ret == 0)
{
//对端关闭连接
close(sockfd);
break;
}
else
{
send(sockfd, buf, ret, 0);
}
}
}
else
{
printf("something else happened\n");
}
}
}
close(listenfd);
return 0;
}