网络编程:非阻塞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 ...
随机推荐
- Golang 实现本地持久化缓存
// Copyright (c) 2024 LiuShuKu // Project Name : balance // Author : liushuku@yeah.net package cache ...
- css的度量单位:px、em、rem、vh、vw、vmin、vmax、百分比
css的度量单位 px,像素数量,适用于比较固定的场景,比如边框宽度,分割线宽度 em em:是描述相对于应用在当前元素的字体尺寸,所以它也是相对长度单位.一般浏览器字体大小默认为16px,则2em ...
- linux下配置ip为动态获取
点击查看代码 在Linux系统中配置网络接口以动态获取IP地址,通常需要使用DHCP(Dynamic Host Configuration Protocol).大多数现代Linux发行版都默认支持这个 ...
- 多版本Java 配置记录
来自 https://blog.csdn.net/zdl177/article/details/105246997 起因是为了启动MC 目录结构 Java总目录下放置多个jdk目录(jdk16.0.2 ...
- nginx 简单实践:负载均衡【nginx 实践系列之四】
〇.前言 本文为 nginx 简单实践系列文章之三,主要简单实践了负载均衡,仅供参考. 关于 Nginx 基础,以及安装和配置详解,可以参考博主过往文章: https://www.cnblogs.co ...
- centos7 docker卸载老版本并升级到最新稳定版本
一.前言 docker的版本分为社区版docker-ce和企业版dokcer-ee社,区版是免费提供给个人开发者和小型团体使用的,企业版会提供额外的收费服务,比如经过官方测试认证过的基础设施.容器.插 ...
- 万字长文详解SIFT特征提取
本文对 SIFT 算法进行了详细梳理.SIFT即尺度不变特征变换(Scale-Invariant Feature Transform),是一种用于检测和描述图像局部特征的算法.该算法对图像的尺度和旋转 ...
- dotnet 源代码生成器分析器入门
本文将带领大家入门 dotnet 的 SourceGenerator 源代码生成器技术,期待大家阅读完本文能够看懂理解和编写源代码生成器和分析器 恭喜你看到了本文,进入到 C# dotnet 的深水区 ...
- netstat 与 ss 比较
一.netstat 命令 1. 核心功能 显示网络连接.路由表.接口统计等信息. 支持TCP.UDP.UNIX域套接字等协议. 可查看进程与端口的关联. 2. 常用语法示例 查看所有活动连接 nets ...
- 要命的DRG成本核算!
改开几十年的三座大山之一,鬼见愁的问题.会随着DRG的推进而让平头老百姓过上翻身做主的日子呢!? 对于DRG,首先医保部门需要了解病种成本状况,确定给你结算多少银子.第二,咱们医院靠医保吃饭,那就更需 ...