阻塞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的更多相关文章

  1. UNIX网络编程——非阻塞connect:时间获取客户程序

    #include "unp.h" int connect_nonb(int sockfd, const SA *saptr, socklen_t salen, int nsec) ...

  2. UNIX网络编程-非阻塞connect和非阻塞accept

    1.非阻塞connect 在看了很多资料之后,我自己的理解是:在socket发起一次连接的时候,这个过程需要一段时间来将三次握手的过程走完,如果在网络状况不好或者是其他的一些情况下,这个过程需要比较长 ...

  3. UNIX网络编程——非阻塞accept

    当有一个已完成的连接准备好被accept时,select将作为可读描述符返回该连接的监听套接字.因此,如果我们使用select在某个监听套接字上等待一个外来连接,那就没有必要把监听套接字设置为非阻塞, ...

  4. UNIX网络编程——非阻塞connect: Web客户程序

    非阻塞的connect的实现例子出自Netscape的Web客户程序.客户先建立一个与某个Web服务器的HTTP连接,再获取一个主页.该主页往往含有多个对于其他网页的引用.客户可以使用非阻塞conne ...

  5. UNIX网络编程——非阻塞connect

    当在一个非阻塞的TCP套接字上调用connect时,connect将立即返回一个EINPROGRESS错误,不过已经发起的TCP三次握手继续进行.我们接着使用select检测这个连接或成功或失败的已建 ...

  6. UNIX网络编程——非阻塞式I/O(套接字)

    套接字的默认状态是阻塞的.这就意味着当发出一个不能立即完成的套接字调用时,其进程将被投入睡眠,等待相应的操作完成.可能阻塞的套接字调用可分为以下4类: (1)输入操作,包括read,readv,rec ...

  7. boot asio 非阻塞同步编程---非阻塞的accept和receive。

    boot asio 非阻塞同步编程---非阻塞的accept和receive. 客户端编程: #include<boost/timer.hpp> #include <iostream ...

  8. Linux非阻塞IO(二)网络编程中非阻塞IO与IO复用模型结合

    上文描述了最简易的非阻塞IO,采用的是轮询的方式,这节我们使用IO复用模型.   阻塞IO   过去我们使用IO复用与阻塞IO结合的时候,IO复用模型起到的作用是并发监听多个fd. 以简单的回射服务器 ...

  9. linux网络编程中阻塞和非阻塞socket的区别

    读操作 对于阻塞的socket,当socket的接收缓冲区中没有数据时,read调用会一直阻塞住,直到有数据到来才返 回.当socket缓冲区中的数据量小于期望读取的数据量时,返回实际读取的字节数.当 ...

  10. Python网络编程-IO阻塞与非阻塞及多路复用

    前言 问题:普通套接字实现的服务端的缺陷 一次只能服务一个客户端!                         accept阻塞! 在没有新的套接字来之前,不能处理已经建立连接的套接字的请求 re ...

随机推荐

  1. (自适应手机端)合同模板网站源码 合同范文类网站pbootcms模板

    PbootCMS内核开发的网站模板,该模板适用于合同范文网站.合同模板网站等企业,当然其他行业也可以做,只需要把文字图片换成其他行业的即可: pc+wap,同一个后台,数据即时同步,简单适用!附带测试 ...

  2. 当我老丈人都安装上DeepSeek的时候,我就知道AI元年真的来了!

    关注公众号回复1 获取一线.总监.高管<管理秘籍> 春节期间DeepSeek引爆了朋友圈,甚至连我老丈人都安装了APP,这与两年前OpenAI横空出世很不一样,DeepSeek似乎真的实现 ...

  3. Golang 入门 : Go语言介绍

    简介 Go 语言又称 Golang,由 Google 公司于 2009 年发布,近几年伴随着云计算.微服务.分布式的发展而迅速崛起,跻身主流编程语言之列,和 Java 类似,它是一门静态的.强类型的. ...

  4. OSPF各类LSA

    一.域内路由 路由器将接口宣告进OSPF进程后,形成的链路状态放入1类LSA中,用于描述路由器自身的直连状态. 1. 区域0为骨干区域,非0为非骨干区域. 2. 骨干区域有且只能存在一个. 3. 非骨 ...

  5. 什么是VMware vSphere

    VMware vSphere不是特定的产品或软件.VMware vSphere是整个VMware套件的商业名称.VMware vSphere堆栈包括虚拟化,管理和界面层.VMware vSphere的 ...

  6. BUUCTF---异性相吸(欠编码)

    1.题目 ܟࠄቕ̐员䭜塊噓䑒̈́ɘ䘆呇Ֆ䝗䐒嵊ᐛ asadsasdasdasdasdasdasdasdasdasdqwesqf 2.知识 3.解题 很奇怪,不知道什么加密,借鉴网上参考,得知需将其转化为 ...

  7. 字符串成员方法:截取、替换、切割 及String成员方法小结

    1.截取 subString() subString()方法有两种使用方式: 1.第一种是在括号里只放入一个索引,这时将会从放入的索引为起点,一直截取到末尾 2.第二种是在括号里放入两个索引,分别对应 ...

  8. word突然无法转换latex公式的解决尝试

    正常情况下我在word插入复制的latex公式步骤如下(以\(\mu\neq 10\)为例): 把\(\mu\neq 10\)粘贴到word文档中,选中\(\mu\neq 10\)并同时按下alt和等 ...

  9. dijkstra的封装模版

    /** - swj - * />_____フ | _ _| /`ミ _x ノ / | / ヽ ? / ̄| | | | | ( ̄ヽ__ヽ_)_) \二つ **/ #include <bits ...

  10. sql数据库连接

    前言 作为数据存储的数据库,近年来发展飞快,在早期的程序自我存储到后面的独立出中间件服务,再到集群,不可谓不快了.早期的sql数据库一枝独秀,到后面的Nosql,还有azure安全,五花八门,教人学不 ...