Linux下的socket编程实践(七) I/O多路复用技术之select模型
2018-01-22 17:16
1336 查看
在进入今天的select模型的主题之前,我们先来简单了解一下五种I/O模型:
(1)阻塞I/O(默认采用这种方式)
在服务端socket编程中,我们常见的accpet函数、recv函数都是采取的阻塞形式。以recv为例: 当上层应用App调用recv系统调用时,如果对等方没有发送数据(Linux内核缓冲区中没有数据),上层应用Application1将阻塞;当对等方发送了数据,Linux内核recv端缓冲区数据到达,内核会把数据copy给用户空间。然后上层应用App解除阻塞,执行下一步操作。
(2)非阻塞I/O(不推荐)
上层应用如果应用非阻塞模式, 会循环调用recv函数,接受数据。若缓冲区没有数据,上层应用不会阻塞,recv返回值为-1,错误码是EWOULDBLOCK(图中的标记有误)。上层应用程序不断轮询有没有数据到来。造成上层应用忙等待。大量消耗CPU。因此非阻塞模式很少直接用。应用范围小,一般和IO复用配合使用。
(3)信号驱动I/O模型(不经常使用)
上层应用建立SIGIO信号处理程序。当缓冲区有数据到来,内核会发送信号告诉上层应用App; 当上层应用App接收到信号后,调用recv函数,因缓冲区有数据,recv函数一般不会阻塞。但是这种用于模型用的比较少,属于典型的“拉模式(上层应用被动的去Linux内核空间中拉数据)”。即:上层应用App,需要调用recv函数把数据拉进来,会有时间延迟,我们无法避免在延迟时,又有新的信号的产生,这也是他的缺陷。
(4)异步I/O(不常用)
上层应用调用aio_read函数,同时提交一个应用层的缓冲区buf;调用完毕后,不会阻塞。上层应用程序App可以继续其他任务; 当TCP/IP协议缓冲区有数据时,Linux主动的把内核数据copy到用户空间。然后再给上层应用App发送信号;告诉App数据到来,需要处理!
异步IO属于典型的“推模式”, 是效率最高的一种模式,上层应用程序App有异步处理的能力(在Linux内核的支持下,处理其他任务的同时,也可支持IO通讯, 与Windows平台下的完成端口作用类似IOCP)。
(5)I/O复用的select模型(本篇的重点)
试想如果你遇到下面的问题会怎么处理?
1)server除了要对外响应client的服务外,还要能够接受标准输入的命令来进行管理。
假如使用上述阻塞方式,在单线程中,accept调用和read调用必定有先后顺序,而它们都是阻塞的。比如先调用accept,后调用
read,那么如果没有客户请求时,服务器会一直阻塞在accept,没有机会调用read,也就不能响应标准输入的命令。
2) server要对外提供大量的client请求服务。
假如使用阻塞方式,在单线程中,由于accept和recev都是阻塞式的,那么当一个client被服务器accept后,它可能在send发送消息时阻塞,因此服务器就会阻塞在recev调用。即时此时有其他的client进行connect,也无法进行响应。
这时就需要select来解决啦!select实现的是一个管理者的功能: 用select来管理多个IO, 一旦其中的一个IO或者多个IO检测到我们所感兴趣的事件, select就返回, 返回值就是检测到的事件个数, 并且由第2~4个参数返回那些IO发送了事件, 这样我们就可以遍历这些事件, 进而处理这些事件。
有人说,我用多线程不就可以了吗?但是在UNIX平台下多进程模型擅长处理并发长连接,但却不适用于连接频繁产生和关闭的情形。当然select并不是最高效的,有着O(N)的时间复杂度,关于更高效的epoll我将在后面的博客中继续讲解,欢迎大家关注,╰( ̄▽ ̄)╮
[cpp] view
plain copy
#include <sys/select.h>
#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);
nfds: is the highest-numbered file descriptor in any of the three sets,plus 1[读,写,异常集合中的最大文件描述符+1].
fd_set[四个宏用来对fd_set进行操作]
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);
timeout[从调用开始到select返回前,会经历的最大等待时间, 注意此处是指的是相对时间]
[cpp] view
plain copy
/timeval结构:
struct timeval
{
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
//一些调用使用3个空的set, n为0, 一个非空的timeout来达到较为精确的sleep.
Linux中, select函数改变了timeout值,用来指示还剩下的时间,但很多实现并不改timeout。
为了较好的可移植性,timeout在循环中一般常被重新赋初值。
Timeout取值:
timeout== NULL
无限等待,被信号打断时返回-1, errno 设置成 EINTR
timeout->tv_sec == 0 && tvptr->tv_usec == 0
不等待立即返回
timeout->tv_sec != 0 || tvptr->tv_usec != 0
等待特定时间长度, 超时返回0
注:关于select用来设置超时时间的用法可以参考我的另外一篇博客 http://blog.csdn.net/nk_test/article/details/49050379
返回值:
如果成功,返回所有sets中描述符的个数;如果超时,返回0;如果出错,返回-1.
下面是使用select改进服务器端和客户端的程序,解决了上述提出的两个问题:
服务器端:
[cpp] view
plain copy
/*示例1: 用select来改进echo回声服务器的client端的echoClient函数
使得可以在单进程的情况下同时监听多个文件描述符;
*/
void echoClient(int sockfd)
{
char buf[512];
fd_set rset;
//确保标准输入不会被重定向
int fd_stdin = fileno(stdin);
int maxfd = fd_stdin > sockfd ? fd_stdin : sockfd;
while (true)
{
FD_ZERO(&rset);
//监视两个I/O
FD_SET(fd_stdin, &rset);
FD_SET(sockfd, &rset);
int nReady = select(maxfd+1, &rset, NULL, NULL, NULL); //不需要的置NULL
if (nReady == -1)
err_exit("select error");
else if (nReady == 0)
continue;
/** nReady > 0: 检测到了可读事件 **/
if (FD_ISSET(fd_stdin, &rset))
{
memset(buf, 0, sizeof(buf));
if (fgets(buf, sizeof(buf), stdin) == NULL)
break;
if (writen(sockfd, buf, strlen(buf)) == -1)
err_exit("write socket error");
}
if (FD_ISSET(sockfd, &rset))
{
memset(buf, 0, sizeof(buf));
int readBytes = readline(sockfd, buf, sizeof(buf));
if (readBytes == 0)
{
cerr << "server connect closed..." << endl;
exit(EXIT_FAILURE);
}
else if (readBytes == -1)
err_exit("read-line socket error");
cout << buf;
}
}
}
客户端:
[cpp] view
plain copy
/*示例2: 用select来改进echo回射服务器的server端的接受连接与处理连接部分的代码:
使得可以在单进程的情况下处理多客户连接, 对于单核的CPU来说, 单进程使用select处理连接与监听套接字其效率不一定就会比多进程/多线程性能差;
*/
struct sockaddr_in clientAddr;
socklen_t addrLen;
int maxfd = listenfd;
fd_set rset;
fd_set allset;
FD_ZERO(&rset);
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
int client[FD_SETSIZE]; //用于保存已连接的客户端套接字
for (int i = 0; i < FD_SETSIZE; ++i)
client[i] = -1;
int maxi = 0; //用于保存最大的不空闲的位置, 用于select返回之后遍历数组
while (true)
{
rset = allset;
int nReady = select(maxfd+1, &rset, NULL, NULL, NULL);
if (nReady == -1)
{
if (errno == EINTR)
continue;
err_exit("select error");
}
//nReady == 0表示超时, 但是此处是一定不会发生的
else if (nReady == 0)
continue;
if (FD_ISSET(listenfd, &rset))
{
addrLen = sizeof(clientAddr);
int connfd = accept(listenfd, (struct sockaddr *)&clientAddr, &addrLen);
if (connfd == -1)
err_exit("accept error");
int i;
for (i = 0; i < FD_SETSIZE; ++i)
{
if (client[i] < 0)
{
client[i] = connfd;
if (i > maxi)
maxi = i;
break;
}
}
if (i == FD_SETSIZE)
{
cerr << "too many clients" << endl;
exit(EXIT_FAILURE);
}
//打印客户IP地址与端口号
cout << "Client information: " << inet_ntoa(clientAddr.sin_addr)
<< ", " << ntohs(clientAddr.sin_port) << endl;
//将连接套接口放入allset, 并更新maxfd
FD_SET(connfd, &allset);
if (connfd > maxfd)
maxfd = connfd;
if (--nReady <= 0)
continue;
}
/**如果是已连接套接口发生了可读事件**/
for (int i = 0; i <= maxi; ++i)
if ((client[i] != -1) && FD_ISSET(client[i], &rset))
{
char buf[512] = {0};
int readBytes = readline(client[i], buf, sizeof(buf));
if (readBytes == -1)
err_exit("readline error");
else if (readBytes == 0)
{
cerr << "client connect closed..." << endl;
FD_CLR(client[i], &allset);
close(client[i]);
client[i] = -1;
}
//注意此处: Server从Client获取数据之后并没有立即回射回去,
// 而是等待四秒钟之后再进行回射
sleep(4);
cout << buf;
if (writen(client[i], buf, readBytes) == -1)
err_exit("writen error");
if (--nReady <= 0)
break;
}
}
版权声明:本文为博主原创文章,未经博主允许不得转载。
本文已收录于以下专栏:
Programming
int the Linux environment
(1)阻塞I/O(默认采用这种方式)
在服务端socket编程中,我们常见的accpet函数、recv函数都是采取的阻塞形式。以recv为例: 当上层应用App调用recv系统调用时,如果对等方没有发送数据(Linux内核缓冲区中没有数据),上层应用Application1将阻塞;当对等方发送了数据,Linux内核recv端缓冲区数据到达,内核会把数据copy给用户空间。然后上层应用App解除阻塞,执行下一步操作。
(2)非阻塞I/O(不推荐)
上层应用如果应用非阻塞模式, 会循环调用recv函数,接受数据。若缓冲区没有数据,上层应用不会阻塞,recv返回值为-1,错误码是EWOULDBLOCK(图中的标记有误)。上层应用程序不断轮询有没有数据到来。造成上层应用忙等待。大量消耗CPU。因此非阻塞模式很少直接用。应用范围小,一般和IO复用配合使用。
(3)信号驱动I/O模型(不经常使用)
上层应用建立SIGIO信号处理程序。当缓冲区有数据到来,内核会发送信号告诉上层应用App; 当上层应用App接收到信号后,调用recv函数,因缓冲区有数据,recv函数一般不会阻塞。但是这种用于模型用的比较少,属于典型的“拉模式(上层应用被动的去Linux内核空间中拉数据)”。即:上层应用App,需要调用recv函数把数据拉进来,会有时间延迟,我们无法避免在延迟时,又有新的信号的产生,这也是他的缺陷。
(4)异步I/O(不常用)
上层应用调用aio_read函数,同时提交一个应用层的缓冲区buf;调用完毕后,不会阻塞。上层应用程序App可以继续其他任务; 当TCP/IP协议缓冲区有数据时,Linux主动的把内核数据copy到用户空间。然后再给上层应用App发送信号;告诉App数据到来,需要处理!
异步IO属于典型的“推模式”, 是效率最高的一种模式,上层应用程序App有异步处理的能力(在Linux内核的支持下,处理其他任务的同时,也可支持IO通讯, 与Windows平台下的完成端口作用类似IOCP)。
(5)I/O复用的select模型(本篇的重点)
试想如果你遇到下面的问题会怎么处理?
1)server除了要对外响应client的服务外,还要能够接受标准输入的命令来进行管理。
假如使用上述阻塞方式,在单线程中,accept调用和read调用必定有先后顺序,而它们都是阻塞的。比如先调用accept,后调用
read,那么如果没有客户请求时,服务器会一直阻塞在accept,没有机会调用read,也就不能响应标准输入的命令。
2) server要对外提供大量的client请求服务。
假如使用阻塞方式,在单线程中,由于accept和recev都是阻塞式的,那么当一个client被服务器accept后,它可能在send发送消息时阻塞,因此服务器就会阻塞在recev调用。即时此时有其他的client进行connect,也无法进行响应。
这时就需要select来解决啦!select实现的是一个管理者的功能: 用select来管理多个IO, 一旦其中的一个IO或者多个IO检测到我们所感兴趣的事件, select就返回, 返回值就是检测到的事件个数, 并且由第2~4个参数返回那些IO发送了事件, 这样我们就可以遍历这些事件, 进而处理这些事件。
有人说,我用多线程不就可以了吗?但是在UNIX平台下多进程模型擅长处理并发长连接,但却不适用于连接频繁产生和关闭的情形。当然select并不是最高效的,有着O(N)的时间复杂度,关于更高效的epoll我将在后面的博客中继续讲解,欢迎大家关注,╰( ̄▽ ̄)╮
[cpp] view
plain copy
#include <sys/select.h>
#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);
nfds: is the highest-numbered file descriptor in any of the three sets,plus 1[读,写,异常集合中的最大文件描述符+1].
fd_set[四个宏用来对fd_set进行操作]
FD_CLR(int fd, fd_set *set);
FD_ISSET(int fd, fd_set *set);
FD_SET(int fd, fd_set *set);
FD_ZERO(fd_set *set);
timeout[从调用开始到select返回前,会经历的最大等待时间, 注意此处是指的是相对时间]
[cpp] view
plain copy
/timeval结构:
struct timeval
{
long tv_sec; /* seconds */
long tv_usec; /* microseconds */
};
//一些调用使用3个空的set, n为0, 一个非空的timeout来达到较为精确的sleep.
Linux中, select函数改变了timeout值,用来指示还剩下的时间,但很多实现并不改timeout。
为了较好的可移植性,timeout在循环中一般常被重新赋初值。
Timeout取值:
timeout== NULL
无限等待,被信号打断时返回-1, errno 设置成 EINTR
timeout->tv_sec == 0 && tvptr->tv_usec == 0
不等待立即返回
timeout->tv_sec != 0 || tvptr->tv_usec != 0
等待特定时间长度, 超时返回0
注:关于select用来设置超时时间的用法可以参考我的另外一篇博客 http://blog.csdn.net/nk_test/article/details/49050379
返回值:
如果成功,返回所有sets中描述符的个数;如果超时,返回0;如果出错,返回-1.
下面是使用select改进服务器端和客户端的程序,解决了上述提出的两个问题:
服务器端:
[cpp] view
plain copy
/*示例1: 用select来改进echo回声服务器的client端的echoClient函数
使得可以在单进程的情况下同时监听多个文件描述符;
*/
void echoClient(int sockfd)
{
char buf[512];
fd_set rset;
//确保标准输入不会被重定向
int fd_stdin = fileno(stdin);
int maxfd = fd_stdin > sockfd ? fd_stdin : sockfd;
while (true)
{
FD_ZERO(&rset);
//监视两个I/O
FD_SET(fd_stdin, &rset);
FD_SET(sockfd, &rset);
int nReady = select(maxfd+1, &rset, NULL, NULL, NULL); //不需要的置NULL
if (nReady == -1)
err_exit("select error");
else if (nReady == 0)
continue;
/** nReady > 0: 检测到了可读事件 **/
if (FD_ISSET(fd_stdin, &rset))
{
memset(buf, 0, sizeof(buf));
if (fgets(buf, sizeof(buf), stdin) == NULL)
break;
if (writen(sockfd, buf, strlen(buf)) == -1)
err_exit("write socket error");
}
if (FD_ISSET(sockfd, &rset))
{
memset(buf, 0, sizeof(buf));
int readBytes = readline(sockfd, buf, sizeof(buf));
if (readBytes == 0)
{
cerr << "server connect closed..." << endl;
exit(EXIT_FAILURE);
}
else if (readBytes == -1)
err_exit("read-line socket error");
cout << buf;
}
}
}
客户端:
[cpp] view
plain copy
/*示例2: 用select来改进echo回射服务器的server端的接受连接与处理连接部分的代码:
使得可以在单进程的情况下处理多客户连接, 对于单核的CPU来说, 单进程使用select处理连接与监听套接字其效率不一定就会比多进程/多线程性能差;
*/
struct sockaddr_in clientAddr;
socklen_t addrLen;
int maxfd = listenfd;
fd_set rset;
fd_set allset;
FD_ZERO(&rset);
FD_ZERO(&allset);
FD_SET(listenfd, &allset);
int client[FD_SETSIZE]; //用于保存已连接的客户端套接字
for (int i = 0; i < FD_SETSIZE; ++i)
client[i] = -1;
int maxi = 0; //用于保存最大的不空闲的位置, 用于select返回之后遍历数组
while (true)
{
rset = allset;
int nReady = select(maxfd+1, &rset, NULL, NULL, NULL);
if (nReady == -1)
{
if (errno == EINTR)
continue;
err_exit("select error");
}
//nReady == 0表示超时, 但是此处是一定不会发生的
else if (nReady == 0)
continue;
if (FD_ISSET(listenfd, &rset))
{
addrLen = sizeof(clientAddr);
int connfd = accept(listenfd, (struct sockaddr *)&clientAddr, &addrLen);
if (connfd == -1)
err_exit("accept error");
int i;
for (i = 0; i < FD_SETSIZE; ++i)
{
if (client[i] < 0)
{
client[i] = connfd;
if (i > maxi)
maxi = i;
break;
}
}
if (i == FD_SETSIZE)
{
cerr << "too many clients" << endl;
exit(EXIT_FAILURE);
}
//打印客户IP地址与端口号
cout << "Client information: " << inet_ntoa(clientAddr.sin_addr)
<< ", " << ntohs(clientAddr.sin_port) << endl;
//将连接套接口放入allset, 并更新maxfd
FD_SET(connfd, &allset);
if (connfd > maxfd)
maxfd = connfd;
if (--nReady <= 0)
continue;
}
/**如果是已连接套接口发生了可读事件**/
for (int i = 0; i <= maxi; ++i)
if ((client[i] != -1) && FD_ISSET(client[i], &rset))
{
char buf[512] = {0};
int readBytes = readline(client[i], buf, sizeof(buf));
if (readBytes == -1)
err_exit("readline error");
else if (readBytes == 0)
{
cerr << "client connect closed..." << endl;
FD_CLR(client[i], &allset);
close(client[i]);
client[i] = -1;
}
//注意此处: Server从Client获取数据之后并没有立即回射回去,
// 而是等待四秒钟之后再进行回射
sleep(4);
cout << buf;
if (writen(client[i], buf, readBytes) == -1)
err_exit("writen error");
if (--nReady <= 0)
break;
}
}
版权声明:本文为博主原创文章,未经博主允许不得转载。
本文已收录于以下专栏:
Programming
int the Linux environment
相关文章推荐
- Linux下的socket编程实践(七) I/O多路复用技术之select模型
- Linux网络通信编程(套接字模型TCP\UDP与IO多路复用模型select\poll\epoll)
- Linux网络通信编程(套接字模型TCP\UDP与IO多路复用模型select\poll\epoll)
- linux socket编程之多路复用select的例子
- 【Socket】linux网络多路复用IO技术
- Unix Socket编程--I/O复用之select模型
- socket编程:多路复用I/O服务端客户端之select
- Linux下的socket编程实践(八) Select的限制和poll(并发的初步知识)
- Socket编程实践 -- Select I/O复用
- Socket编程实践(7) --Select-I/O复用
- Linux下的socket编程实践(八) Select的限制和poll(并发的初步知识)
- Socket编程实践(8) --Select-I/O复用
- linux网络编程-----几种服务器模型及io多路复用函数
- TCP IO复用 select并发服务端 Linux socket编程入门(3)
- linux基础编程:IO模型:阻塞/非阻塞/IO复用 同步/异步 Select/Epoll/AIO
- linux下多路复用模型之Select模型
- socket编程之select多路复用
- linux下多路复用模型之Select模型
- linux基础编程:IO模型:阻塞/非阻塞/IO复用 同步/异步 Select/Epoll/AIO
- 【Socket】linux网络多路复用IO技术