下面是关于高级IO我总结的一篇文章: https://blog.csdn.net/qq_37941471/article/details/80952057 可以了解一下 五种IO模型 以及 它们之间的关系;当然还有IO多路转接的其他实现方式:poll epoll 以及三者之间的对比
select
一 . IO多路转接的作用—提高效率:
在数据通信过程中,分为两部分:
1. 是等待数据到达内核;
2. 是将数据从内核拷贝到用户区。
而往往在实际应用中,等待的时间往往比拷贝的时间多,所以我们如果想要提高效率;
必然就是要将等的时间减少(在一定的时间内,减少等待的比重)
1. 这个时候,IO多路转接就是解决这个问题的:一次监视多个文件描述符
在IO多路转接中,由于一次等待多个文件描述符,
在单位时内就绪事件发生的概率就越大,所以等的比重就会越小。
2. 而这里我们会有一个问题:监视的文件描述符返回条件是什么?
答: 1. 监视的文件描述符都有自己要关注的事件(读/写/异常事件
2. 返回条件就是:我们所监视(关心)的文件描述符的事件至少一个已经就绪
3. IO多路转接的实现方式:
1. select
2. poll
3. epoll等
当然还有好多,我们也可以说这三个都是:就绪事件通知机制
具体下面我们来讲一下select:
二. select函数:
1. select的函数原型
int select(
int nfds, fd_set
*readfds, fd_set
*writefds,\
fd_set
*exceptfds, struct timeval
*timeout);
2. 参数说明:
1. nfds :表示的是等待的文件描述符中的最大的那个
+1;
用于限定操作系统遍历的区间(只关心的那部分,其他的就不会去遍历,减少了开销)
2. fd_set:该结构实际是一个位图;
因为文件描述符其实是数组下标,也就是从
0开始的整数;
所以对于位图的每一个比特位表示的是一个文件描述符的状态;
如果是
1表示关心该文件描述符上的事件,是
0,表示不关心该文件描述符上的事件。
而具体关心文件描述符上的什么事件,则由中间三个参数决定。
3. readfds :表示的是需要等待的读事件的文件描述符集;
4. writefds :表示的是需要等待的写事件的文件描述符集;
5. exceptfds :表示的是需要等待的异常事件的文件描述符集;
注意:以上三个位图参数都是输入输出型参数 :
1.作为输入参数:告诉系统我要关心的文件描述符的哪些事件
2.作为输出参数:关心的文件描述符中哪些事件就绪
6. timeout:用于设置
select阻塞等待的时间。
取值如下:
1. NULL:表示
select阻塞等待,关心的多个文件描述符上没有时间发生时,
进程会一直阻塞在
select的函数调用处。
如果至少有一个文件描述符上有事件发生,则
select返回。
eg:
select(max_fd
+1,
&rfds,
NULL,
NULL,
NULL)
2. 0:表示
select非阻塞等待,只是用于检测等待事件的状态。
当调用该函数时,不管有无事件发生,该函数都会立即返回,
进程不会挂起等待事件的发生。
eg :
struct timeval timeout
= {
0,
0};
select(max_fd
+1,
&rfds,
NULL,
NULL,
&timeout)
3.特定的时间值:表示
select只会阻塞等待一定的时间;
在该时间段内,如果有事件发生,则
select返回,进程结束阻塞。
如果达到规定的时间,还没有事件发生,此时
select将会超时返回。
eg :
struct timeval timeout
= {
5,
0};
select(max_fd
+1,
&rfds,
NULL,
NULL,
&timeout)
timeval的结构如下:
struct timeval {
long tv_sec;
long tv_usec;
};
3. 返回值的含义:
1. > 0 :满足就绪条件的事件个数
2. 0 : 在规定的时间内没有事件发生(超出timeout设置的时间)
3. -1 :错误
原因由errno标识;此时中间三个参数的值变得不可预测。
4. select模型—理解select的执行过程
*** 关键在于理解fd_set
假设fd_set长度为1字节,即8个bit位,一个bit位可以表示一个文件描述符;
则1字节长的fd_set最大可以对应8个文件描述符(fd)
步骤:
(1)执行fd_set
set; FD_ZERO(&
set);则
set用位表示是0000,0000。
(2)若fd=5,执行FD_SET(fd,&set);后
set变为0001,0000(第5位置为1)
(3)若再加入fd=2,fd=1,则set变为0001,0011
(4)执⾏行select(6,&set,0,0,0)阻塞等待
(5)若fd=1,fd=2上都发生可读事件,则select返回,此时set变为0000,0011。
注意:没有事件发生的fd=5被清空
1. 所以,在select之前需要将关心的文件描述符集合保存起来。这样便可设置下一次需要关心的
文件描述符集,同时select返回时也可由此判断哪些文件描述符上的事件就绪了。这里,可以
提供一个数组fdArray[]来保存关心的文件描述符。将文件描述符作为数组元素保存在数组中。
也就是说,fdArray[]数组来表示连接的客户端,该数组大小表示最多可以连接的数量
int fdArray[sizeof(fd_set)*8];//第三方数组
sizeof(fd_set)*8 就是最多能够连接的数量
2. fd_set:该结构实际是一个位图;
因为文件描述符其实是数组下标,也就是从0开始的整数;
所以对于位图的每一个比特位表示的是一个文件描述符的状态;
如果是1表示关心该文件描述符上的事件,是0,表示不关心该文件描述符上的事件。
5. socket就绪条件:
一. 读就绪:
1. 当对内核中的数据进行读取时,如果接收缓冲区中的字节数,大于等于低水位
SO_RCVLOWAT,此时就说明读就绪,在对该文件描述符调用
read等进行读取时,
不会阻塞,且返回值大于
0;
2. 在TCP通信中,如果服务器的
socket上有新的连接到达时,客户端会发送SYN数据包
给服务器,说明读就绪。此时在调用
accept接收新连接时,不会阻塞。
而且会返回新的文件描述符与客户端进行通信;
3. TCP通信中,如果对端关闭连接,此时会发送FIN数据包。也说明读就绪,
此时在调用
read对文件描述符进行读取时,会返回
0;
4. 当
socket上有未处理的错误时,也说明读就绪,
在对文件描述符进行
read读取时,会返回-
1;
二. 写就绪:
1. 在
socket内核中,如果发送缓冲区中的可用字节数大于等于低水位
标记SO_SNDLOWAT时,说明写就绪,此时调用
write进行写操作时,
不会阻塞,且返回值大于
0;
2. 当一方要进行写操作(即关心的是写事件),而对端将文件描述符关闭,
此时写就绪。调用
write时会触发SIGPIPE信号;
3. 当
socket使用非阻塞
connect连接成功或失败之后,写就绪;
4. 当
socket上有未读取的错误时,写就绪,此时
write进行写时,会返回-
1;
三. 异常就绪:当
socket上收到带外数据时,异常事件就绪。
6. select的优缺点:
优点 :
1. 不需要fork或者pthread_create就可以实现一对多的通信,简化了进程线程的使用
也就是没有多进程和多线程的一些缺点
2. 同时等待多个文件描述符,效率相对较高
缺点 :
1. 代码编写复杂,维护起来较麻烦
2. 每次调用select,都需要把fd集合从用户态拷贝到内核态,
在fd很多的情况下,循环次数多;开销大
3. 每次调用select都需要在内核遍历传递进来的所有fd,开销比较大
4. 能够接收的文件描述符有上限
select支持的文件描述符数量过小,默认是1024
实现 select版本的TCP服务器:
Makefile :
.PHONY:select_server clean
select_server:select_server
.c
gcc -o $@ $^
clean:
rm -rf select_server
select_server.c :
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int fdArray[
sizeof(fd_set)*
8];
int startup(
int port )
{
int sock = socket(AF_INET,SOCK_STREAM,
0);
if( sock <
0 )
{
perror(
"socket fail...\n");
exit(
2);
}
int opt =
1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,
sizeof(opt));
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_addr.s_addr = htonl(INADDR_ANY);
local.sin_port = htons(port);
if( bind(sock,(
struct sockaddr *)&local,
sizeof(local)) <
0 )
{
perror(
"bind fail...\n");
exit(
3);
}
if( listen(sock,
5) <
0 )
{
perror(
"listen fail...\n");
exit(
4);
}
return sock;
}
int main(
int argc,
char* argv[] )
{
if( argc !=
2 )
{
printf(
"Usage:%s port\n ",argv[
0]);
return 1;
}
int listen_sock = startup(atoi(argv[
1]));
fdArray[
0] = listen_sock ;
int num =
sizeof(fdArray)/
sizeof(fdArray[
0]);
printf(
"%d\n",num);
int i =
1;
for( ; i < num; i++ )
{
fdArray[i] = -
1;
}
while(
1)
{
fd_set rfds;
FD_ZERO(&rfds);
int max_fd = fdArray[
0];
int i =
0;
for( ; i < num; i++ )
{
if( fdArray[i] >=
0 )
{
FD_SET(fdArray[i],&rfds);
if( fdArray[i] > max_fd )
{
max_fd = fdArray[i];
}
}
}
struct timeval timeout = {
5,
0};
switch( select(max_fd+
1,&rfds,NULL,NULL,&timeout) )
{
case 0:
printf(
"timeout...\n");
case -
1:
printf(
"select fail...\n");
default:
{
int i =
0;
for( ;i < num; i++ )
{
if( fdArray[i] == -
1 )
{
continue;
}
if( fdArray[i] == listen_sock && FD_ISSET( fdArray[i],&rfds ) )
{
struct sockaddr_in client;
socklen_t len =
sizeof(client);
int new_sock = accept(listen_sock,(
struct sockaddr *)&client,&len);
if(new_sock <
0)
{
perror(
"accept fail...\n ");
continue;
}
int i =
0;
for( ; i < num; i++ )
{
if( fdArray[i] == -
1 )
break;
}
if( i < num )
{
fdArray[i] = new_sock;
}
else
{
close(new_sock);
}
printf(
"get a new link!,[%s:%d]\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));
continue;
}
if( FD_ISSET( fdArray[i],&rfds ) )
{
char buf[
1024];
ssize_t s = read(fdArray[i],buf,
sizeof(buf)-
1);
if( s <
0 )
{
printf(
"read fail...\n");
close(fdArray[i]);
fdArray[i] = -
1;
}
else if( s ==
0 )
{
printf(
"client quit...\n");
close(fdArray[i]);
fdArray[i] = -
1;
}
else
{
buf[s] =
0;
printf(
"client# %s\n",buf);
}
}
}
}
break;
}
}
return 0;
}
测试代码 :
这里我们用telnet客户端程序来测试,同样的我们也可以写一个自己的客户端程序,同TCP客户端程序;下面是我的TCP程序代码,可以参考: https://blog.csdn.net/qq_37941471/article/details/80738319 另外如果telnet还没有安装,没有用过的可以看一下这篇文章: 终端下telnet的安装及其应用:https://blog.csdn.net/qq_37941471/article/details/80787368
1. 参数timeout 的值为特定的时间值:
struct timeval timeout = {
5,
0};
switch( select(max_fd+
1,&rfds,
NULL,
NULL,&timeout) )
这段代码也就是上面的代码,另外: 特定的时间值 : 如果在指定的时间内没有事件产生,select将超时返回。 并且这里的select的返回值为0时,表示词状态改变前已经超过了timeout的时间 输出:timeout…
1. 首先我们先运行服务器端的代码,如果先运行客户端,会发生连接失败:
原因很简单:这是一个TCP程序,TCP面向连接
先运行客户端:
运行客户端:
2. 运行客户端:
2. 参数timeout 的值为0:
struct timeval timeout = {
0,
0};
switch( select(max_fd+
1,&rfds,
NULL,
NULL,&timeout) )
将上面的代码修改如上:
0:表示select非阻塞等待,只是用于检测等待事件的状态。当调用该函数时,不管有无事件发生,该函数都会立即返回,进程不会挂起等待事件的发生。
服务器端运行结果:
3. 参数timeout 的值为NULL:
switch( select(max_fd+
1,&rfds,
NULL,
NULL,
NULL) )
将上面的代码修改如上:直接在select()函数中把timeout参数置为NULL
NULL:表示select阻塞等待,关心的多个文件描述符上没有时间发生时,进程会一直阻塞在select的函数调用处。如果至少有一个文件描述符上有事件发生,则select返回。
2. 运行服务器端:
一直阻塞:
2. 运行客户端: