I/O模型和服务器模型
服务器模型
在网络程序里面,通常都是一个服务器处理多个客户机。
为了处理多个客户机的请求, 服务器端的程序有不同的处理方式。
目前最常用的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