I/O复用select

在前面的博客中写过TCP协议的服务端的编程流程

但是有一个小问题,就是每一次发送数据都要建立一次三次握手,多次发送和接受数据可能很多时间都浪费在三次握手这个过程中。

所以当一个客户端和服务端建立连接后,完成整个交互过程(和服务器存在多次的收发数据)之后,再断开连接,能够很好的提高效率。

I/O复用使得程序能同时监听多个文件描述符。在下面这几种情况下需要使用I/O复用技术:

  • TCP服务器同时要处理监听套接字和连接套接字
  • 服务器要同时处理TCP请求和UDP请求
  • 程序要同时处理多个套接字
  • 客户端程序要同时处理用户输入和网络连接
  • 服务器要同时监听多个端口

IO复用对于服务器的意义:

一个线程通过记录I/O流的状态来同时管理多个I/O,可以提高服务器的吞吐能力。

select API

1
2
3
4
#include <sys/select.h>
int select(int maxfd,fd_set *readfds,fd_set *writefds,fd_set *exceptfds.struct timeval *timeout);
//在线修改,每次调用select之前,都必须重新设置三个结构体变量,
//用户程序必须通过某种机制记录所有的文件描述符
1
2
3
4
5
maxfd:设置为所有监听的文件描述符的最大值+1
readfds:可读事件类型
writefds:可写事件类型
exceptfds:异常事件类型
timeout:指定一个书简,定时事件。(指定为NULL,则表示永久阻塞)

fd_set结构

fd_set包含一个32个元素的long int类型的数组:32*32=1024 bit

使用每一个比特位记录一个文件描述符,文件描述符的值在位移上表现。

select优点:

select模型是Windows sockets中最常见的IO模型。它利用select函数实现IO 管理。通过对select函数的调用,应用程序可以判断套接字是否存在数据、能否向该套接字写入据。

​ 如:在调用recv函数之前,先调用select函数,如果系统没有可读数据那么select函数就会阻塞在这里。当系统存在可读或可写数据时,select函数返回,就可以调用recv函数接 收数据了。

可以看出使用select模型,需要两次调用函数。第一次调用select函数第二次socket API。使用该模式的好处是:可以等待多个套接字。

select缺点:
(1)每次调⽤用select,都需要把fd集合从⽤用户态拷贝到内核态,这个开销在fd很多时会很⼤大
(2)同时每次调⽤用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很⼤大
(3)select⽀支持的⽂文件描述符数量太⼩小了,默认是1024

直接上代码

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>

#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>

#include<sys/select.h>
typedef struct node
{
int fd;
struct node *next;
}node;
node head;
void initlist()
{
head.next=NULL;
}
void insertlist_to_tail(int fd)
{
node *p=&head;
while(p->next!=NULL)
{
p=p->next;
}
node *s=(node*)malloc(sizeof(node));
s->fd=fd;
s->next=NULL;
p->next=s;
}
void delete_node(int _fd)
{
node *p=&head;
while(p!=NULL)
{
if(p->next!=NULL&&p->next->fd==_fd)
{
node *q=p->next;
p->next=q->next;
free(q);
break;
}
p=p->next;
}
}
void destorylink()
{
node *p=&head;
while(p->next!=NULL)
{
node *q=p->next;
p->next=q->next;
free(q);
}
}
//socket() bind() listen()
int initsocket()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
if(sockfd==-1)
return -1;
struct sockaddr_in ser;
memset(&ser,0,sizeof(ser));
ser.sin_family=AF_INET;
ser.sin_port=htons(6000);
ser.sin_addr.s_addr = inet_addr("127.0.0.1");

int res=bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));
if(res==-1)
return -1;
res=listen(sockfd,5);
if(res==-1)
return -1;
return sockfd;
}
//1.select只能关注三种事件类型:可读 可写 异常
//2.通过fd_set结构
//3.在线修改-->每次调用select都需要重新设置fd_set结构体变量
//4.用户程序需要记录所有的文件描述符-->单链表

//将用户程序记录的所有文件描述符设置到fds上,并记录最大的文件描述符返回
int setfds(fd_set *fds)
{
FD_ZERO(fds);
int maxfd=-1;
node *p=head.next;
while(p!=NULL)
{
FD_SET(p->fd,fds);
if(p->fd>maxfd)
maxfd=p->fd;
p=p->next;
}
return maxfd;
}
void dealfinishevent(fd_set *fds,int sockfd)
{
node* p=head.next;
while(p!=NULL)
{
if(FD_ISSET(p->fd,fds))
{
if(p->fd==sockfd)//有新的客户端连接
{
struct sockaddr_in cli;
socklen_t len=sizeof(cli);
int c=accept(sockfd,(struct sockaddr*)&cli,&len);
if(c<0)
continue;
insertlist_to_tail(c);
}
else //连接套接字,此客户端连接套接字有数据到达
{
//接受数据
//send
char buff[128]={0};
int n=recv(p->fd,buff,127,0);
if(n<=0)//出错或者客户端关闭
{
close(p->fd);
delete_node(p->fd);
continue;
}
printf("%d: %s\n",p->fd,buff);
send(p->fd,"OK",2,0);
}
}
p=p->next;
}
}
int main()
{
initlist();
int sockfd=initsocket();
assert(sockfd!=-1);
//将sockfd添加到用户链表中
insertlist_to_tail(sockfd);
fd_set readfds;
while(1)
{
int maxfd=setfds(&readfds);
int n=select(maxfd+1,&readfds,NULL,NULL,NULL);
if(n<=0)
{
printf("select error\n");
continue;
}
dealfinishevent(&readfds,sockfd);
}
close(sockfd);
//销毁用户链表
destorylink();
exit(0);
}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>

#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>

int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
assert(sockfd!=-1);

struct sockaddr_in ser;
memset(&ser,0,sizeof(ser));
ser.sin_family=AF_INET;
ser.sin_port=htons(6000);
ser.sin_addr.s_addr=inet_addr("127.0.0.1");

int res=connect(sockfd,(struct sockaddr*)&ser,sizeof(ser));
assert(res!=-1);

while(1)
{
printf("please input: ");
char buff[128]={0};
fgets(buff,127,stdin);
if(strncmp(buff,"end",3)==0)
break;

send(sockfd,buff,strlen(buff)-1,0);

char result[128]={0};
int n=recv(sockfd,result,127,0);
if(n<=0)
break;
printf("result: %s\n",result);
}
close(sockfd);
}

通信测试