select模型
在单进程和单线程的C/S模型中,server会阻塞在accept和recv,而accept和recv二者有着相似的原理,即二者都在listen:
accept通过serverfd阻塞等待来自client的读事件,当收到读事件之后,accept则会唤醒完成连接
对于recv,其通过clientfd阻塞等待来自client的数据,当收到数据之后,recv则会唤醒读取数据
由于二者的特性,就导致了不能连接多个client。
通过引入select模型,可以将二者所监听的读事件都加入到一个集合中,由内核负责listen
代码
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<ctype.h>
5 #include<sys/types.h>
6 #include<sys/socket.h>
7 #include<strings.h>
8 #include<sys/select.h>
9 #include<errno.h>
10 #include<arpa/inet.h>
11 #define DEFPORT 8000
12 int main(void)
13 {
14 int sfd,cfd;//serverfd和clientfd
15 int maxfd;//监听集合的最大值
16 int clientsock[1024];//client监听集合
17 int i,j,nReady,len,datalen;
18 char buf[1024];//读取数据缓冲区
19 bzero(buf,sizeof(buf));//缓冲区置0
20 struct sockaddr_in server_sock,client_sock;
21 fd_set rset,allset;//监听集合
22 bzero(&server_sock,sizeof(server_sock));
23 server_sock.sin_family = AF_INET;//IPV4
24 server_sock.sin_port = htons(DEFPORT);//htons:本机序转网络序,16位short,端口号8000
25 server_sock.sin_addr.s_addr = htonl(INADDR_ANY);//本机任意ip
26 sfd = socket(AF_INET,SOCK_STREAM,0);
27 bind(sfd,(struct sockaddr*)&server_sock,sizeof(server_sock));
28 listen(sfd,128);
29
30 maxfd = sfd;//开始将sfd作为最大值,因为sfd是监听集合第一个元素
31 for(i=0;i<1024;i++)//将client监听集合所有元素初始化为-1,方便后续操作
32 clientsock[i] = -1;
33 FD_ZERO(&allset);//将监听集合元素置0
34 FD_SET(sfd,&allset);//将allset的第sfd位元素置为1
35 while(1)//此时设置一直监听,退出条件为监听集合数超过1024
36 {
37 rset = allset;//此时需要用rset来代替,是一个传入传出参数
38 nReady = select(maxfd+1,&rset,NULL,NULL,NULL);//nReady为监听到的读事件数量
39 while(nReady)//每次循环处理一个读事件
40 {
41 if(FD_ISSET(sfd,&rset))//如果此时读事件是sfd相应,即accept接收到client的连接请求
42 {
43 len = sizeof(client_sock);
44 cfd = accept(sfd,(struct sockaddr*)&client_sock,&len);//连接成功,返回cfd
45 for(i = 0; i < 1024; i++)
46 if(clientsock[i]==-1)//将cfd保存
47 {
48 clientsock[i] = cfd;
49 break;
50 }
51 if(i==1024)//此时i==1024,即达到select处理上线,退出,正常的话不应该退出
52 perror("client too much\n");
53 FD_SET(cfd,&allset);//将cfd加入监听集合
54 FD_CLR(sfd,&rset);//将sfd从rset集合中删除,否则下次遍历时仍会进入当前if,会阻塞在accept
55 if(cfd>maxfd)//更新最大值位置
56 maxfd = cfd;
57 }
58 else//此时监听事件为recv的读事件,即接收到数据请求
59 {
60 for(i = 0; i < 1024; i++)//遍历找到对应读事件的cfd
61 {
62 if(clientsock[i]!=-1)
63 {
64 if(FD_ISSET(clientsock[i],&rset))
65 {
66 if((datalen = recv(clientsock[i],buf,sizeof(buf) ,0)>0))//接收到数据
67 {//业务处理
68 printf("len=%d\n",len);
69 for(j = 0; j < datalen; j++)
70 buf[j] = toupper(buf[j]);
71 printf("len=%d\n",datalen);
72 send(clientsock[i],buf,len,0);
73 bzero(buf,sizeof(buf));
74 }
75 if(len==0)//返回0,客户端断开连接
76 {//<0出错 =0断开连接 >0接收数据大小
77 FD_CLR(clientsock[i],&allset);//将断开连接的cfd从监听集合中删除
78 FD_CLR(clientsock[i],&rset);
79 close(cfd);//关闭
80 clientsock[i] = -1;//将对应位置置为-1
}
80 FD_CLR(clientsock[i],&rset);//每次处理完当前recv读事件,需要将其从rset集合中删除,否则下次仍会执行
81 break;//处理完当前的recv,break
82 }
83 }
84 }
85 }
86 --nReady;//每次将nReady--
87 }
88 }
89 return 0;
90 }
设置rset和allset的原因
rset作为一个传入传出参数,当传入时,可以将监听集合利用select模型进行监听,传出时作为收到读事件的集合,如果只有一个allset,没有rset,那么会出现错误:
1.如果当client中有一个异常断开,而此时又有一个新的client连接,则会占用原来的clientsock的位置,但是此时循环的次数为nReady次数,那么此时则不会处理allset中最后一个读事件请求,因为循环次数固定,而循环内处理数量为nReady+1,则会出现错误。
2.当处理完accept的读事件之后,需要将其从rset集合中删除,否则当下次判断FD_ISSET(sfd,&rset)时,仍会判断为真,会进入if语句等待阻塞等待accept,出现错误。
同理,当处理recv的读事件时,每次处理完之后需要将对应clientsock[i]从监听rset中删除,否则会出现和上述一样的错误。
3.暂时就想到这种情况,后续遇到再补充
4.api直接参考manpage