Linux网络编程 --- IO复用之Select系统调用详解

I/O复用技术使得程序能同时监听多个文件描述符。

通常网络程序在下列情况下需要使用I/O复用技术。

  1. 客户端程序要同时处理多个socket。
  2. 客户端程序要同时处理用户输入和网络连接。
  3. TCP服务器要同时处理监听socket和连接socket。
  4. 服务器要同时处理TCP请求和UDP请求。
  5. 服务器要同时监听多个端口,或者处理多种服务。

 Linux下I/O复用技术使用select、poll和epoll来实现。select和poll在Windows下也有,而Linux下独有的是epoll方法。

-------------------------------------------------------------------------------------------------------------------------------------------------------------------------

SELECT API

select系统调用的用途:在一段时间内,监听用户感兴趣的文件描述符上的可读,可写和异常等事件

select系统调用原型:

#include<sys/select.h>
int select(int nfds,fd_set* readfds,fd_set* writefds,
           fd_set* exceptfds,struct timeval* timeout);

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

2、readfds、writefds和exceptfds这三个参数分别指向可读,可写和异常等事件对应的文件描述符的集合。应用程序调用select函数时,通过这3个参数传入自己感兴趣的文件描述符。select调用返回时,内核将修改它们来通知应用程序哪些文件描述符已经就绪。这三个参数都是fd_set结构指针类型。

fd_set结构体定义如下:

#include<typesizes.h>
#define __FD_SETSIZE 1024

#include<sys/select.h>
#define	FD_SETSIZE		__FD_SETSIZE
typedef long int __fd_mask;   

#undef	__NFDBITS
#define __NFDBITS	(8 * (int) sizeof (__fd_mask))

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;

fd_set结构体仅包含一个整型数组,该数组的每个元素的每一位(bit)标记一个文件描述符。fd_set能容纳的文件描述符数由FD_SETSIZE指定,同时也就限制了select能同时处理的文件描述符的总数。

由于位操作过于繁琐,我们应使用下面的一系列宏来访问fd_set结构体中的位:

#include<sys/select.h>
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是否被设置

3、timeout参数用来设置select函数的超时时间。它是一个timeval结构类型的指针,采用指针参数是因为内核将修改它以告诉应用程序select等待了多久。不过我们不能完全信任select调用返回后的timeout值,比如调用失败时timeout值是不确定的

timeval结构体定义如下:

struct timeval
{
    long tv_sec;//秒
    long tv_usec;//微妙
};

由以上定义可见,select给我们提供了一个微妙级的定时方式。

  • 如果给timeout变量的tv_sec成员和tv_usec成员都传递0,则select立即返回。
  • 如果给timeout传递NULL,则select将一直阻塞,直到某个文件描述符就绪。

4、select返回值

  • select成功时返回就绪(可读、可写和异常)文件描述符的总数。
  • 如果在超时时间内没有任何文件描述符就绪。select将返回0。
  • 失败时返回-1并设置errno。如果在select等待期间,程序接收到信号,则select立即返回-1,并设置errno为EINTR。

5、文件描述符就绪条件
哪些情况下文件描述符可以被认为是可读、可写或者出现异常,对于select的使用非常关键。

在网络编程中,下列情况下socket可读:

  • socket内核接收缓存区中的字节数大于或等于其低水位标记SO_RCVLOWAT。此时可以无阻塞地读该socket,并且读操作返回的字节数大于0。
  • socket通信对方关闭连接。此时对该socket读操作将返回0
  • 监听socket上有新的连接请求
  • socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。

下列情况下socket可写:

  • socket内核发送缓冲区中的可用字节数大于或等于其低水位标记SO_SNDLOWAT。此时我们可以无阻塞地写该socket,并且写操作返回的字节数大于0。
  • socket写操作被关闭对写操作被关闭的socket执行写操作将触发一个SIGPIPE信号
  • socket使用非阻塞connect连接成功或者失败(超时)之后
  • socket上有未处理的错误。此时我们可以使用getsockopt来读取和清除该错误。

网络程序中,select能处理的异常情况只有一种:socket上接收到带外数据。

6、标准输入实现select调用程序示例:

#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <stdlib.h>
#include <sys/time.h>
#include <sys/select.h>


#define STDIN 0 //标准输入,文件描述符为0

int main()
{
	int fd = STDIN;
	fd_set fdset;

	while (1)
	{
		FD_ZERO(&fdset); //clear		
		FD_SET(fd, &fdset); //添加描述符
		
		struct timeval tv = { 5, 0 };//设置超时时间

		int n = select(fd + 1, &fdset, NULL, NULL, &tv);
		if (n == -1)//调用失败
		{
			perror("select error");
		}
		else if (n == 0)//超时
		{
			printf("time out\n");
		}
		else
		{
			if (FD_ISSET(fd, &fdset))
			{
				char buffer[128] = { 0 };				
				int res = read(fd, buffer, 127);//从标准输入stdin读取数据到buffer中
				printf("read(%d) = %s\n", res, buffer);
			}
		}
	}
}

如果键盘输入,它就会从标准输入读取数据并将其打印在屏幕上,如果在超时时间5s之内没有任何的输入则会打印time out,表示超时。

7、服务器端程序示例:

服务器可以让select调用同时检查监听套接字和客户的连接套接字。一旦select调用指示有活动发生,就可以用FD_ISSET来遍历所有可能的文件描述符,以检查是哪个上面有活动发生。
 

1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<stdlib.h>
5 #include<assert.h>
6 #include<sys/socket.h>
7 #include<netinet/in.h>
8 #include<arpa/inet.h>
9 #include<sys/select.h>
10 #include<sys/time.h>
11 
12 #define MAXFD 10
13 
14 void fds_add(int fds[],int fd)
15 {
16     int i = 0;
17     for(;i<MAXFD;++i)
18     {
19         if(fds[i] == -1)
20         {
21             fds[i] = fd;//如果当前文件描述符的值为-1,表示未用过,将当前连接的socket赋给它
22             break;
23         }
24     }
25 }
26 
27 void fds_del(int fds[],int fd)
28 {
29     int i = 0;
30     for(;i<MAXFD;++i)
31     {
32         if(fds[i] == fd)
33         {
34             fds[i] = -1;
35             break;
36         }
37     }
38 }
39  int main()
40 {
41     int sockfd = socket(AF_INET,SOCK_STREAM,0);//监听套接字
42     assert(sockfd != -1);
43 
44     struct sockaddr_in saddr;
45     memset(&saddr,0,sizeof(saddr));
46     saddr.sin_family = AF_INET;
47     saddr.sin_port = htons(6000);
48     saddr.sin_addr.s_addr = inet_addr("127.0.0.1");
49 
50     int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));//命名socket
51     assert(res != -1);
52 
53     listen(sockfd,5);
54 
55     fd_set fdset;//listen read symbol
56 
57     int fds[MAXFD];//array of collect symbols
58 
59     int i = 0;
60     for(;i<MAXFD;++i)
61     {
62         fds[i] = -1;//将收集的文件标识符数组中的每一位都置为-1
63     }
64 
65     fds_add(fds,sockfd);//将当前连接服务器端的socket标识符加入数组

66     while(1)
67     {
68         FD_ZERO(&fdset);//清除fdset的所有位
69 
70         int maxfd = -1;
71         int i = 0;
72         for(;i<MAXFD;i++)
73         {
74             if(fds[i] == -1)
75             {
76                 continue;//当前位未被赋值sockfd,结束本次循环
77             }
78 
79             FD_SET(fds[i],&fdset);//设置fdset的fds[i]位
80 
81             if(fds[i] > maxfd)    //当前连接服务器端的socket地址 大于 -1
82             {
83                 maxfd = fds[i];//将当前连接服务器端的sockfd赋值给maxfd
84             }
85         }
86 
87         struct timeval tv = {5,0};//定义超时时间结构体变量
88 
89         int n = select(maxfd+1,&fdset,NULL,NULL,&tv);//select系统调用
90         if(n == -1)//失败返回-1
91         {
92             perror("select error");
93         }
94         else if( n == 0)//超时时间内没有任何文件描述符就绪
95         {
96             printf("time out\n");
97         }
98         else   //有n个数据就绪
99         {
100             for(i = 0;i<MAXFD;++i)
101             {
102                 if(fds[i] == -1)
103                 {
104                     continue;//当前位未被赋值sockfd,结束本次循环
105                 }
106 
107                 if(FD_ISSET(fds[i],&fdset))//测试fdset的fds[i]位是否被设置
108                 {
109                     if(fds[i] == sockfd)//fds[i] 是当前socket的返回值
110                     {
111                         //accept
112                         struct sockaddr_in caddr;//定义结构体变量
113                         int len  = sizeof(caddr);
114 
115                         int c = accept(sockfd,(struct sockaddr*)&caddr,&len);//连接套接字,从listen的监听队列中接受一个连接,accept成功会返回一个新的连接socket
116                         if(c<0)
117                         {
118                             continue;
119                         }
120 
121                         printf("accept c = %d\n",c);
122 
123                         fds_add(fds,c);//将当前建立好连接返回的新socket加入数组
124                     }
125                     else
126                     {
127                         //recv 接收数据
128                         char buff[128] = {0};
129                         int res = recv(fds[i],buff,127,0); //读取sockfd的数据
130                         if(res <= 0)
131                         {
132                             close(fds[i]);      //关闭socket或者写为close(sockfd);
133                             fds_del(fds,fds[i]);//从数组中删除
134                             printf("one client over\n");
135                         }
136                         else
137                         {
138                             printf("recv(%d) = %s\n",fds[i],buff);//打印接收到的数据
139                             send(fds[i],"ok",2,0);//发送反馈信息
140                         }
141                     }
142                 }
143             }
144         }
145 
146     }
147 
148 }

 

可以根据Linux下多个终端连接服务器运行结果可以看出,如果当前有客户端连接服务器并发送信息时,如果终端成功接收则反馈自己接收成功的信息;如果在超时时间5s之内没有任何的客户端连接则会打印time out,表示超时。这样select就实现了多客户访问。

猜你喜欢

转载自blog.csdn.net/Disremembrance/article/details/89421184