最近在看《linux高性能服务器编程》,在此做个日记,以激励自己,同时分享于有需要的朋友。
I/O复用使得程序能够同时监听多个文件描述符,对提高程序的性能至关重要。
通常,网络程序在下列情况下需要使用I/O复用技术:
A. 客户端程序要同时处理多个socket。
B. 客户端程序要同时处理用户输入和网络连接。比如:聊天室
C. TCP服务器要同时监听socket和连接socket。
D. 服务器要同时处理TCP请求和UDP请求。
E. 服务器要同时监听多个端口, 或者处理多种服务。比如:xinetd服务
虽然I/O复用能同时监听多个文件描述符,但它本身是阴塞的。并且当多个文件描述符同时就绪时,如果不采取额外的措施,程序就只能顺序依次处理其中的每个文件描述符,这使得服务器程序看起来像是串行工作。如果要实现并发,只能使用多进程或多线程。
linux下实现I/O复用的系统调用有select、poll、epoll,今天我们讨论select。
1. select函数
#include
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
readfds, writefds, exceptfds参数分别指可读,可写和异常文件描述符。select调用返回时,内核将修改它们来通知程序哪些文件描述符已经就绪。它们都是fd_set结构指针,它仅包含一个整型数组,该数组的每个元素的每一位(bit)标记一个文件描述符。fd_set能容纳的最大文件描述符数量由FD_SETSIZE指定,这就限制了select能同时处理的文件描述符的总量。
我们使用一系列宏来访问fd_set结构体:
#include
FD_ZERO(fd_set *fdset); //清除fdset所有位
FD_SET(int fd, fd_set *fdset); //设置fdset的位fd
FD_CLR(int fd, fd_set *fdset); //清除fdset的位fd
int FD_ISSET(int fd, fd_set *fdset); //测试fdset的位fd是否被设置
struct timeval
{
long tv_sec; \\秒数
long tv_usec; \\微秒
}
调用成功时返回就绪文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,返回0。失败时返回-1,并设置errno。如果在select等待期间,程序收到信号,则立即返回-1,并设置errno为EINTR。
2. 代码,用select同时接收普通数据和外带数据
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
int main(int argc, char **argv)
{
if (argc != 3) {
fprintf(stderr, "Usage: %s ip port\n", basename(argv[0]));
return 1;
}
const char *ip = argv[1];
int port = atoi(argv[2]);
printf("ip is %s and port is %d\n", ip, port);
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 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_addrlength = sizeof(client_address);
int connfd = accept(listenfd, (struct sockaddr*)&client_address, &client_addrlength);
if (connfd < 0) {
printf("error: %s\n", strerror(errno));
close(listenfd);
}
char remote_addr[INET_ADDRSTRLEN];
printf("connect with ip: %s and port: %d\n",
inet_ntop(AF_INET, &client_address.sin_addr, remote_addr, INET_ADDRSTRLEN),
ntohs(client_address.sin_port));
char buf[1024];
fd_set read_fds;
fd_set exception_fds;
FD_ZERO(&read_fds);
FD_ZERO(&exception_fds);
//设置接收处带数据
int reuseaddr = 1;
setsockopt(connfd, SOL_SOCKET, SO_OOBINLINE, &reuseaddr, sizeof(reuseaddr));
while (1) {
memset(buf, '\0', sizeof(buf));
//每次调用select前都要重新设置,因为事件发生后,文件描述符集合会被内核修改
FD_SET(connfd, &read_fds);
FD_SET(connfd, &exception_fds);
ret = select(connfd + 1, &read_fds, NULL, &exception_fds, NULL);
printf("select one\n");
if (ret < 0) {
printf("selection failed\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 normal 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 oob data: %s\n", ret, buf);
}
}
close(connfd);
close(listenfd);
return 0;
}