linux网络编程十三:I/O复用select

最近在看《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);

nfds参数指定被监听的文件描述符的总数,通常设置为select监听的所有文件描述中最大值+1,因为文件描述符是从0开始的。

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是否被设置

timeout参数设置select的超时时间。这是timeval结构指针,用来告诉内核select等待多久。

struct timeval

{

        long tv_sec;       \\秒数

        long tv_usec;     \\微秒

}

如果都为0,则select立即返回。如果给timeval传NULL,则一直阻塞。


调用成功时返回就绪文件描述符的总数。如果在超时时间内没有任何文件描述符就绪,返回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;
}





你可能感兴趣的:(linux网络编程)