网络编程-I/O模型和服务器模型,select()并发服务器

服务器模型

在网络程序里面,通常都是一个服务器处理多个客户机。
为了处理多个客户机的请求, 服务器端的程序有不同的处理方式。
目前最常用的2种服务器模型:

循环服务器

循环服务器在同一个时刻只能响应一个客户端的请求 。

eg:TCP服务器
TCP服务器端运行后等待客户端的连接请求。
TCP服务器接受一个客户端的连接后开始处理,完成了客户的所有请求后断开连接。
TCP循环服务器一次只能处理一个客户端的请求。
只有在当前客户的所有请求都完成后,服务器才能处理下一个客户的连接/服务请求。
如果某个客户端一直占用服务器资源,那么其它的客户端都不能被处理。TCP服务器一般很少采用循环服务器模型。

并发服务器

并发服务器在同一个时刻可以响应多个客户端的请求 。

eg:TCP服务器
为了弥补TCP循环服务器的缺陷,人们又设计了并发服务器的模型。
并发服务器的设计思想是服务器接受客户端的连接请求后,创建子进程(或者线程)来为客户端服务 。
TCP并发服务器可以避免TCP循环服务器中客户端独占服务器的情况。
为了响应客户机的请求,服务器要创建子进程来处理。 如果有多个客户端的话,服务器端需要创建多个子进程。过多的子进程会影响服务器端的运行效率。

I/O模型

I/O模型分类

(1)阻塞I/O:最常用、最简单、效率最低(释放占用的CPU进入休眠状态)

(2)非阻塞I/O:可防止进程阻塞在I/O操作上,需要不停轮询,效率最低,一直占用CPU,消耗CPU的资源。最不建议使用

(3)I/O 多路复用:select,选择接收指定的多个I/O,允许同时对多个I/O进行控制

(4)异步通知I/O:epoll,有被通知的意味,一种异步通信模型,是最高效的一种模型。

阻塞I/O

阻塞I/O是最普遍使用的I/O 模式,大部分程序使用的都是阻塞模式的I/O 。
默认情况下,套接字建立后所处于的模式就是阻塞I/O 模式。
很多读写函数在调用过程中会发生阻塞:
1)读操作: read、recv、recvfrom,缓冲区有数据就不阻塞,没有数据时就阻塞等待。
2)写操作: write、send、sendto,缓冲区没有写满就不会阻塞,但是缓存区写满了,就会阻塞等待。
3)其他操作:accept、connect,如果都接收不到连接的话都会阻塞等待。

非阻塞I/O的实现

fcntl()函数
NAME
		操作文件描述符
       fcntl - manipulate file descriptor

SYNOPSIS
       #include <unistd.h>
       #include <fcntl.h>

       int fcntl(int fd, int cmd, long arg);
       

参数:
(1)fd:文件描述符
(2)cmd:操作命令
cmd操作命令:

	F_DUPFD  复制文件描述符
	F_GETFD  读取文件描述符
	F_SETFD  设置文件描述符
	F_GETFL  获取文件状态
	F_SETFL  设置文件状态

(3)arg:供命令使用的参数

返回值

RETURN VALUE
       For a successful call, the return value depends on the operation:
       
       F_DUPFD  		The new descriptor.
       F_GETFD 			Value of file descriptor flags.
       F_GETFL  		Value of file status flags.
       F_GETLEASE		Type of lease held on file descriptor.
       F_GETOWN			Value of descriptor owner.
       F_GETSIG 		Value of signal sent when read or write becomes possible,	
       					or zero for traditional SIGIO behavior.
	   F_GETPIPE_SZ		The pipe capacity.
     
       All other commands Zero.
       
	 On error, -1 is returned, and errno is set appropriately.
	 

fcntl()函数:当你一开始建立一个套接字描述符的时候,系统内核将其设置为阻塞IO模式。
可以使用函数fcntl()设置一个套接字的标志为 O_NONBLOCK 来实现非阻塞。

int flag;
flag = fcntl(sockfd, F_GETFL, 0);//flag得到sockfd原来的阻塞属性
flag |= O_NONBLOCK;//把不阻塞属性写到flag里面
fcntl(sockfd, F_SETFL, flag);//设置sockfd的属性

多路复用I/O

应用程序中同时处理多路输入输出流,若采用阻塞模式,将得不到预期的目的。若采用非阻塞模式,对多个输入进行轮询,但又太浪费CPU时间。若设置多个进程,分别处理一条数据通路,将新产生进程间的同步与通信问题,使程序变得更加复杂。进程(fork子进程)和线程的并发执行,由于占用资源太多,任务切换时系统繁重。让一个进程同时接收并处理多个客户端请求的方法就是使用I/O多路复用。

基本思想:先构造一张有关描述符的表,然后调用一个函数。当这些文件描述符中的一个或多个已准备好进行I/O时函数才返回。函数返回时告诉进程那个描述符已就绪,可以进行I/O操作。
即把每次连接进来的客户端,accept每次返回生成的client_fd,不再覆盖client_fd,而是把socket文件描述符,每次记录下来形成一张表。监视指定的这张表中有哪些sockfd已经准备好,如果准备好了,就调用select处理。

select()函数

select函数也是阻塞的,select不仅仅只适用于网络编程的fd,同时也可用于监听别的文件的fd。

SYNOPSIS
       /* According to POSIX.1-2001 */
       #include <sys/select.h>

       /* According to earlier standards */
       #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);
       

一、参数
(1)nfds:所有监控的文件描述符中最大的那一个加1。
待测试的描述集总是从0, 1, 2, …开始的。 所以假如你要检测的描述符为8, 9, 10, 那么系统实际也要从0, 1, 2 …7开始, 此时真正待测试的描述符的个数为11个, 也就是max(8,9,10)+ 1。
(2)
readfds(所有要读的文件文件描述符的集合) 、
writefds(所有要的写文件文件描述符的集合 )、
exceptfds(异常通知的文件描述符的集合)的数据类型都是fd_set结构体。
long int 型32个元素的数组组成,数组总共有 32 x 4 x 8 = 1024 (位)。

设计机制:由于在一个程序中,最多打开1024个进程,因此也最多能生成1024个文件描述符fd,除去
默认的:0,1,2,…3个文件描述符。1024个文件描述符fd正好对应fd_set类型的1024位,如果指定
监听范围则把生成的文件描述符fd相对于fd_set类型的对应位置1。

		typedef struct 
		{
			long int _fds_bits[1024/32 = 32];
		}fd_set;

(3)timeout:超时设置.
NULL:一直阻塞,直到有文件描述符就绪或出错
时间值为0:仅仅检测文件描述符集的状态,然后立即返回
时间值不为0:在指定时间内,如果没有事件发生,则超时返回

二、返回值
返回有事件到来时描述文件的数量‐包含在三个返回的描述符集(即在readfds、writefds、exceptfds中设置位的总数),如果超时在任何有事情之前过期,那么哪个值可能是零发生了。

设置文件描述符表

在这里插入图片描述
通过一系列的宏对文件描述符表进行置位操作。
fd_set set;
(1)文件描述符表初始化清零:FD_ZERO(&set);
(2)选择监听指定文件描述符表:FD_SET( fd,&set);
(3)清除对应的文件描述符:FD_CLR( fd, &set);

FD_ZERO :从fdset中清除所有的文件描述符
FD_SET :将fd加入到fdset
FD_ISSET :判断fd是否在fdset集合中

FD_CLR :将fd从fdset里面清除

参数格式:
void FD_ZERO(fd_set *fdset)
void FD_SET(int fd,fd_set *fdset)
int FD_ISSET(int fd,fd_set *fdset)
void FD_CLR(int fd,fd_set *fdset)

select并发程序

select并发程序设计:一个进程,2个套接口,并发处理客户端连接和数据传输
思路:先创建一个服务器套接字创建函数,创建一个文件描述符表rdset并清零,FD_SET把每一次生成的sockfd都放入文件描述符表中让select监听是否有事件到来,如果有事件到来,通过for(;;)遍历每一个描述符FD_ISSET()来确认是哪一个sockfd到来,进入accpet连接,并将返回用于和客户端连接的cfd加入到select监听表中,判断是否maxfd+1,因为每一次select()会清空表因此需要创建一个临时文件描述符表用于保存之前rdset的状态,继续轮询监听。

  1 #include <stdio.h>
  2 #include <sys/types.h>
  3 #include <sys/socket.h>                                                                                                                                             
  4 #include <netinet/in.h>
  5 #include <arpa/inet.h>
  6 #include <string.h>
  7 #include <errno.h>
  8 #include <unistd.h>
  9 #include <stdlib.h>
 10 #include <sys/select.h>
 11 
 12 #define NUM 32
 13 
 14 
 15 int socket_server_init(unsigned short port, char * addr)//把socket的创建封装成一个函数。
 16 {
 17     int sockfd;
 18     int ret_bind, ret_listen;
 19     struct sockaddr_in server_addr;
 20     socklen_t len = sizeof(server_addr);
 21 
 22     printf("port = %d, addr  = %s\n", port, addr);
 23 
 24     sockfd = socket(AF_INET, SOCK_STREAM, 0);
 25     if(sockfd < 0)
 26     {
 27         perror("socket");
 28         return -1;
 29     }
 30 
 31     server_addr.sin_family = AF_INET;
 32     server_addr.sin_port   = htons(port);
 33     server_addr.sin_addr.s_addr = inet_addr(addr);
 34 
 35     ret_bind = bind(sockfd, (struct sockaddr *)&server_addr, len);
 36     if(ret_bind == -1)
 37     {
 38         perror("bind");
 39         return -1;
 40     }
 41                                                                                                                                                                     
 42     ret_listen = listen(sockfd, 5);
 43     if(ret_listen == -1)
 44     {
 45         perror("listen");
 46         return -1;
 47     }
 48     return sockfd;
 49 
 50 }
 51 
 52 int main(int argc, const char *argv[])
 53 {
 54     int i;
 55     int sfd, sfd1;
 56     int maxfd;
 57     int ret;
 58     struct sockaddr_in client_addr;
 59     socklen_t len = sizeof(client_addr);
 60     int cfd;
 61     char buf[NUM];
 62     int ret_recv;
 63 
 64     sfd = socket_server_init(12345, "192.168.2.160");
 65     if(sfd < 0)
 66     {
 67         perror("socket create");
 68         return -1;
 69     }
 70     printf("sockfd = %d\n", sfd);
 71 
 72     sfd1 = socket_server_init(54321, "192.168.2.160");
 73     if(sfd1 < 0)
 74     {
 75         perror("socket create");
 76         return -1;
 77     }                                                                                                                                                               
 78     printf("sockfd = %d\n", sfd1);
 79 
 80     fd_set rdset, tmpset;//创建一个rdset表,和一个临时记录当时rdset状态的描述符表
 81     FD_ZERO(&rdset);//清零
 82     FD_SET(sfd, &rdset);
 83     FD_SET(sfd1, &rdset);
 84     maxfd = sfd1 + 1;
 85 
 86     while(1)//轮询select
 87     {
 88         tmpset = rdset;//因为每次select时,tmpset表都会被清零,需重新赋值
 89         memset(buf, 0, NUM);//缓冲区每次清零buf
 90         ret = select(maxfd, &tmpset, NULL, NULL, NULL);//select阻塞等待监听指定的fd
 91         if(ret == -1)
 92         {
 93             perror("select");
 94             return -1;
 95         }
 96 
 97         if(ret > 0)//ret > 0:表示有事件到来
 98         {
 99             for(i = 3; i < maxfd; i++)//遍历每一个(除去默认的std 0,1,2)fd=3 -> maxfd-1
100             {
					/两个套接口的有连接事件的到来
101                 if((i == sfd) || (i == sfd1))/
102                 {
						//确认是哪一个套接口sfd有事件到来,并关联生成与之相对应的cfd
103                     if(FD_ISSET(i, &tmpset))
104                     {
105                         {
106                             cfd = accept(i, (struct sockaddr *)&client_addr, &len);
107                             if(cfd < 0)
108                             {
109                                 perror("accept");
110                                 return -1;
111                             }
								//打印连接的客户端信息
112                             printf("i connected client_%d = %s:%d\n",\
		 cfd, inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
113 				//把生成的与客户端数据传输用的cfd也加载到rdset描述符表中,加入select的监听队列中
114                             FD_SET(cfd, &rdset);
					//加载到rdset表中,select最大监听maxfd同时也应 +1
115                             maxfd = (cfd >= maxfd)?(cfd + 1):maxfd;
116                         }                                                                                                                                           
117                     }
118                 }

119 				//cfd有数据传输的事件到来
120                 else if(FD_ISSET(i, &tmpset))//确定是哪一个cfd有事件
121                 {
122                     ret_recv = recv(i, buf, NUM, 0);//读取buf
123                     if(ret_recv > 0)
124                     {
125                          printf("recv from client_%d: %s\n", i, buf);
126                     }
127                     else if(ret_recv == 0)//客户端断开连接
128                     {
129                         printf("cfd = %d connect shutdown\n", i);
130                         close(i);//关闭fd,因为总共才能打开1024个fd,节省使用
131                         FD_CLR(i, &rdset);//把rdset表中相应的监听位清零
							//如果是最靠后的fd关闭,要把maxfd-1
132                         maxfd = (i == maxfd)?(i - 1):maxfd;
133                         continue;//继续下一层循环
134                     }
135                     else
136                     {
137                         perror("recv fail");
138                     }
139                 }
140             }
141 
142         }
143     }
144     return 0;
145 }
146 
147                                                                                                                                                                     
~     

运行结果显示:
linux@ubuntu:~/test$ ./a.out 

port = 12345, addr  = 192.168.2.160
sockfd = 3
port = 54321, addr  = 192.168.2.160
sockfd = 4
i connected client_5 = 192.168.2.105:58348
i connected client_6 = 192.168.2.105:58351
i connected client_7 = 192.168.2.105:58352
i connected client_8 = 192.168.2.105:58353
i connected client_9 = 192.168.2.105:58354
recv from client_9: hello world
recv from client_6: hello
recv from client_9:  world
recv from client_5: how are you
recv from client_8: linux




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值