网络编程:非阻塞I/O
阻塞VS非阻塞
阻塞I/O
:应用程序会被挂起,等待内核完成操作,实际上,内核所做的事情是将CPU时间切换给其他有需要的进程,网络应用程序在这种情况下是得不到CPU时间做该做的事情的。
非阻塞I/O
:当应用程序调用非阻塞I/O完成某个操作,内核立即返回,不会把CPU时间切换给某个其他进程,应用程序在返回后,可以得到足够的CPU时间继续完成其他事情。
几个I/O模型,再加上进程、线程模型并构成了整个网络编程的知识核心。
按照使用场景,非阻塞 I/O 可以被用到读操作、写操作、接收连接操作和发起连接操作上。
非阻塞I/O
读操作
如果套接字对应的接收缓冲区没有数据可读,在非阻塞情况下 read 调用会立即返回,一般返回 EWOULDBLOCK 或 EAGAIN 出错信息。在这种情况下,出错信息是需要小心处理,比如后面再次调用 read 操作,而不是直接作为错误直接返回。这就好像去书店买书没买到离开一样,需要不断进行又一次轮询处理。
写操作
在阻塞 I/O 情况下,write 函数返回的字节数,和输入的参数总是一样的。
在非阻塞 I/O 的情况下,如果套接字的发送缓冲区已达到了极限,不能容纳更多的字节,那么操作系统内核会尽最大可能从应用程序拷贝数据到发送缓冲区中,并立即从 write 等函数调用中返回。
write 等函数是可以同时作用到阻塞 I/O 和非阻塞 I/O 上的,为了复用一个函数,处理非阻塞和阻塞 I/O 多种情况,设计出了写入返回值,并用这个返回值表示实际写入的数据大小。
非阻塞 I/O 需要这样:拷贝→返回→再拷贝→再返回。
而阻塞 I/O 需要这样:拷贝→直到所有数据拷贝至发送缓冲区完成→返回。
read 和 write 在阻塞模式和非阻塞模式下的不同行为特性:
关于 read 和 write 还有几个结论,你需要把握住:
1、read 总是在接收缓冲区有数据时就立即返回,不是等到应用程序给定的数据充满才返回。当接收缓冲区为空时,阻塞模式会等待,非阻塞模式立即返回 -1,并有 EWOULDBLOCK 或 EAGAIN 错误。
2、和 read 不同,阻塞模式下,write 只有在发送缓冲区足以容纳应用程序的输出字节时才返回;而非阻塞模式下,则是能写入多少就写入多少,并返回实际写入的字节数。
3、阻塞模式下的 write 有个特例, 就是对方主动关闭了套接字,这个时候 write 调用会立即返回,并通过返回值告诉应用程序实际写入的字节数,如果再次对这样的套接字进行 write 操作,就会返回失败。失败是通过返回值 -1 来通知到应用程序的。
accept
当 accept 和 I/O 多路复用 select、poll 等一起配合使用时,如果在监听套接字上触发事件,说明有连接建立完成,此时调用 accept 肯定可以返回已连接套接字。这样看来,似乎把监听套接字设置为非阻塞,没有任何好处。
为了说明这个问题,构建一个客户端程序,其中最关键的是,一旦连接建立,设置 SO_LINGER
套接字选项,把 l_onoff
标志设置为 1,把l_linger
时间设置为 0。这样,连接被关闭时,TCP 套接字上将会发送一个 RST。
struct linger ling;
ling.l_onoff = 1;
ling.l_linger = 0;
setsockopt(socket_fd, SOL_SOCKET, SO_LINGER, &ling, sizeof(ling));
close(socket_fd);
服务器端使用 select I/O 多路复用,不过,监听套接字仍然是 blocking 的。如果监听套接字上有事件发生,休眠 5 秒,以便模拟高并发场景下的情形。
if (FD_ISSET(listen_fd, &readset)) {
printf("listening socket readable\n");
sleep(5);
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listen_fd, (struct sockaddr *) &ss, &slen);
这里的休眠时间非常关键,这样,在监听套接字上有可读事件发生时,并没有马上调用 accept。由于客户端发生了 RST 分节,该连接被接收端内核从自己的已完成队列中删除了,此时再调用 accept,由于没有已完成连接(假设没有其他已完成连接),accept 一直阻塞,更为严重的是,该线程再也没有机会对其他 I/O 事件进行分发,相当于该服务器无法对其他 I/O 进行服务。
如果我们将监听套接字设为非阻塞,上述的情形就不会再发生。只不过对于 accept 的返回值,需要正确地处理各种看似异常的错误,例如忽略 EWOULDBLOCK、EAGAIN 等。
这个例子给我们的启发是,一定要将监听套接字设置为非阻塞的
connect
在非阻塞 TCP 套接字上调用 connect 函数,会立即返回一个 EINPROGRESS 错误。TCP 三次握手会正常进行,应用程序可以继续做其他初始化的事情。当该连接建立成功或者失败时,通过 I/O 多路复用 select、poll 等可以进行连接的状态检测。
非阻塞 I/O + select 多路复用
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define MAX_LINE 1024
#define FD_INIT_SIZE 5
#define SERV_PORT 43211
#define LISTENQ 1024
struct Buffer
{
/* data */
int connect_fd; //连接字
char buffer[MAX_LINE]; //实际缓冲
size_t writeIndex; // 缓冲写入位置
size_t readIndex; // 缓冲读取位置
int readable; //是否可读
};
//分配一个Buffer对象,初始化writeIndex和readIndex
struct Buffer *alloc_Buffer()
{
struct Buffer *buffer = malloc(sizeof(struct Buffer));
if(!buffer)
{
return NULL;
}
buffer->connect_fd = 0;
buffer->writeIndex = buffer->readIndex = buffer->readable = 0;
return buffer;
}
//释放Buffer对象
void free_Buffer(struct Buffer *buffer)
{
free(buffer);
}
char rot13_char(char c) {
if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
return c + 13;
else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
return c - 13;
else
return c;
}
int tcp_nonblocking_server_listen(int port)
{
int listenfd;
listenfd = socket(AF_INET,SOCK_STREAM, 0);
make_nonblocking(listenfd);
struct sockaddr_in server_addr;
bzero(&server_addr, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(port);
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));
int rt1 = bind(listenfd, (struct sockaddr *)&server_addr, sizeof(server_addr));
if(rt1 < 0)
{
perror("bind failed");
return -1;
}
int rt2 = listen(listenfd, LISTENQ);
if(rt2 < 0)
{
perror("listen failed");
return -1;
}
signal(SIGPIPE, SIG_IGN);
return listenfd;
}
int make_nonblocking(int fd)
{
fcntl(fd, F_SETFL, O_NONBLOCK);
}
//这里从fd套接字读取数据,数据先读取到本地buf数组中,再逐个拷贝到buffer对象缓冲中
int onSocketRead(int fd, struct Buffer *buffer)
{
char buf[1024];
int i;
ssize_t result;
while(1)
{
result = recv(fd, buf, sizeof(buf), 0);
if(result <= 0)
{
break;
}
//按char对每个字节进行拷贝,每个字节都会先调用rot13_char来完成编码,之后拷贝到buffer对象的缓冲中
//其中writeIndex标志了缓冲中写的位置
for(i = 0; i< result; ++i)
{
if(buffer->writeIndex < sizeof(buffer->buffer))
{
buffer->buffer[buffer->writeIndex++] = rot13_char(buf[i]);
}
//如果读取了回车符,则认为client端发送结束,此时可以把编码后的数据回送给客户端
if(buf[i] == '\n')
{
buffer->readable = 1;//缓冲区可读
}
}
}
if(result == 0)
{
return 1;
}
else if(result < 0)
{
if(errno == EAGAIN)
{
return 0;
}
return -1;
}
return 0;
}
//从buffer对象的readIndex开始读,一直读到writeIndex的位置,这段区间是有效数据
int onSocketWrite(int fd, struct Buffer *buffer)
{
while(buffer->readIndex < buffer->writeIndex)
{
ssize_t result = send(fd, buffer->buffer + buffer->readIndex ,
buffer->writeIndex - buffer->readIndex, 0);
if(result < 0)
{
if(errno == EAGAIN)
return 0;
return -1;
}
buffer->readIndex += result;
}
//readIndex已经追上writeIndex,说明有效发送区间已经全部读完,将readIndex和writeIndex设置为0
if(buffer->readIndex == buffer->writeIndex)
{
buffer->readIndex = buffer->writeIndex = 0;
}
//缓冲数据已经全部读完,不需要再读
buffer->readable = 0;
return 0;
}
int main(int argc, char *argv[])
{
int listen_fd;
int i, maxfd;
struct Buffer *buffer[FD_INIT_SIZE];
for(i = 0;i < FD_INIT_SIZE; i++)
{
buffer[i] = alloc_Buffer();
}
listen_fd = tcp_nonblocking_server_listen(SERV_PORT);
fd_set readset, writeset, exset;
FD_ZERO(&readset);
FD_ZERO(&writeset);
FD_ZERO(&exset);
while(1)
{
maxfd = listen_fd;
//listen 加入 readset
FD_SET(listen_fd, &readset);
for(i = 0; i < FD_INIT_SIZE; ++i)
{
if(buffer[i]->connect_fd > 0)
{
if(buffer[i]->connect_fd > maxfd)
{
maxfd = buffer[i]->connect_fd;
}
FD_SET(buffer[i]->connect_fd, &readset);
if(buffer[i]->readable)
{
FD_SET(buffer[i]->connect_fd, &writeset);
}
}
}
if(select(maxfd + 1, &readset, &writeset, &exset, NULL) < 0)
{
perror("select error");
return -1;
}
if(FD_ISSET(listen_fd, &readset))
{
printf("listening socket readable\n");
sleep(5);
struct sockaddr_storage ss;
socklen_t slen = sizeof(ss);
int fd = accept(listen_fd, (struct sockaddr *)&ss, &slen);
printf("fd:%d\n",fd);
if(fd < 0)
{
perror("accept failed");
return -1;
}
else if(fd > FD_INIT_SIZE)
{
perror("too many connections");
close(fd);
return -1;
}
else
{
make_nonblocking(fd);
printf("buffer[%d]->connect_fd :%d\n",fd, buffer[fd]->connect_fd);
if(buffer[fd]->connect_fd == 0)
{
printf("1111666666111111\n");
buffer[fd]->connect_fd = fd;
}
else
{
perror("too many connections");
return -1;
}
}
}
for(i = 0; i < maxfd+1; i++)
{
int r = 0;
if(i == listen_fd)
{
continue;
}
if(FD_ISSET(i, &readset))
{
r = onSocketRead(i, buffer[i]);
}
if(r == 0 && FD_ISSET(i, &writeset))
{
r = onSocketWrite(i, buffer[i]);
}
if(r)
{
buffer[i]->connect_fd = 0;
close(i);
}
}
}
}
调用 fcntl 将监听套接字设置为非阻塞
fcntl(fd, F_SETFL, O_NONBLOCK);
运行结果:
小结
非阻塞 I/O 可以使用在 read、write、accept、connect 等多种不同的场景,在非阻塞 I/O 下,使用轮询的方式引起 CPU 占用率高,所以一般将非阻塞 I/O 和 I/O 多路复用技术 select、poll 等搭配使用,在非阻塞 I/O 事件发生时,再调用对应事件的处理函数。这种方式,极大地提高了程序的健壮性和稳定性,是 Linux 下高性能网络编程的首选。
网络编程:非阻塞I/O的更多相关文章
- UNIX网络编程——非阻塞connect:时间获取客户程序
#include "unp.h" int connect_nonb(int sockfd, const SA *saptr, socklen_t salen, int nsec) ...
- UNIX网络编程-非阻塞connect和非阻塞accept
1.非阻塞connect 在看了很多资料之后,我自己的理解是:在socket发起一次连接的时候,这个过程需要一段时间来将三次握手的过程走完,如果在网络状况不好或者是其他的一些情况下,这个过程需要比较长 ...
- UNIX网络编程——非阻塞accept
当有一个已完成的连接准备好被accept时,select将作为可读描述符返回该连接的监听套接字.因此,如果我们使用select在某个监听套接字上等待一个外来连接,那就没有必要把监听套接字设置为非阻塞, ...
- UNIX网络编程——非阻塞connect: Web客户程序
非阻塞的connect的实现例子出自Netscape的Web客户程序.客户先建立一个与某个Web服务器的HTTP连接,再获取一个主页.该主页往往含有多个对于其他网页的引用.客户可以使用非阻塞conne ...
- UNIX网络编程——非阻塞connect
当在一个非阻塞的TCP套接字上调用connect时,connect将立即返回一个EINPROGRESS错误,不过已经发起的TCP三次握手继续进行.我们接着使用select检测这个连接或成功或失败的已建 ...
- UNIX网络编程——非阻塞式I/O(套接字)
套接字的默认状态是阻塞的.这就意味着当发出一个不能立即完成的套接字调用时,其进程将被投入睡眠,等待相应的操作完成.可能阻塞的套接字调用可分为以下4类: (1)输入操作,包括read,readv,rec ...
- boot asio 非阻塞同步编程---非阻塞的accept和receive。
boot asio 非阻塞同步编程---非阻塞的accept和receive. 客户端编程: #include<boost/timer.hpp> #include <iostream ...
- Linux非阻塞IO(二)网络编程中非阻塞IO与IO复用模型结合
上文描述了最简易的非阻塞IO,采用的是轮询的方式,这节我们使用IO复用模型. 阻塞IO 过去我们使用IO复用与阻塞IO结合的时候,IO复用模型起到的作用是并发监听多个fd. 以简单的回射服务器 ...
- linux网络编程中阻塞和非阻塞socket的区别
读操作 对于阻塞的socket,当socket的接收缓冲区中没有数据时,read调用会一直阻塞住,直到有数据到来才返 回.当socket缓冲区中的数据量小于期望读取的数据量时,返回实际读取的字节数.当 ...
- Python网络编程-IO阻塞与非阻塞及多路复用
前言 问题:普通套接字实现的服务端的缺陷 一次只能服务一个客户端! accept阻塞! 在没有新的套接字来之前,不能处理已经建立连接的套接字的请求 re ...
随机推荐
- 给Typecho加上心知天气-网页天气插件
给你的博客添加个知心天气的天气预报,代码看下面 <!-- 知心天气--> <div id="tp-weather-widget" class="navb ...
- CentOS7安装部署ClickHouse(单机版&&集群部署)
1.1 什么是ClickHouse ClickHouse 是俄罗斯的Yandex于2016年开源的列式存储数据库(DBMS),主要用于在线分析处理查询(OLAP),能够使用SQL查询实时生成分析数据报 ...
- burpsuite激活
激活burpsuite--教程 点击Start 文件,把三个框都选上 点击RUN,会自动启动,复制一下那个证书 粘贴刚刚复制的密钥,点击下一个即可 这里点击手动激活,复制请求,粘贴到刚刚那个激活程序的 ...
- 帝国CMS下iframe标签无法引入视频,ueditor编辑器中html标签无法显示问题,设置ueditor默认行高为1.75
问题描述: 1.帝国cms后台添加优酷视频,使用到iframe,富文本编辑器中使用iframe引入视频后检查发现html代码未出现iframe字样,排查后发现为ueditor限制过滤了部分html代码 ...
- C++ open()和read()函数使用详解
对于Framework工程师来说,必要C或者C++编程能力是必须的,像对设备节点的操作是最基本的操作,那么我们便会用到open和read函数.open()函数用于打开文件,而read()函数用于从打开 ...
- 【Matlab函数】提取inp文件中的节点、单元数据并保留集合信息
功能 提取hypermesh2020(其他版本也可以)中的节点.单元信息,并保留elem set信息. 返回的是一个结构体 输入输出参数 输入: inp文件路径,如:'example.inp'.&qu ...
- python 字典使用
整理很好的文章 文章复制链接: https://mp.weixin.qq.com/s/Aj65A-uuTaARW3vvYTxvzQ 1.检查键是否存在于字典中 def key_in_dict(d, k ...
- 使用react-native-drawer,跟着官网配置仍报错,Error: [Reanimated] `valueUnpacker` is not a worklet, js engine: hermes
在使用react-native-drawer组件时,编译项目报错 试了许多的方法,最后通过在一篇博客中找到解决方法https://blog.csdn.net/lxyoucan/article/deta ...
- VulnHub2018_DeRPnStiNK靶机渗透练习
据说该靶机有四个flag 扫描 扫描附近主机arp-scan -l 扫主目录 扫端口 nmap -sS -sV -n -T4 -p- 192.168.xx.xx 结果如下 Starting Nmap ...
- 微信 dat 文件还
荐
前言 以微信 PC 端为例 某个小姐姐/小哥哥通过微信发给你的图片会在以下目录以 .dat 后缀的格式出现 C:\Users\taadis\Documents\WeChat Files\taadis\ ...