Linux高性能服务器编程 阻塞与非阻塞系列问题
1.阻塞IO和非阻塞IO有啥区别?
- 概念定义
- 阻塞 IO(Blocking I/O):
- 当应用程序发起一个 I/O 操作(如读取文件、接收网络数据等)时,在该操作完成之前,应用程序会被阻塞,即线程暂停执行,等待 I/O 操作的完成。例如,在使用
read
函数从一个网络套接字读取数据时,如果没有数据可读,线程会一直等待,直到有数据到达并被成功读取。
- 当应用程序发起一个 I/O 操作(如读取文件、接收网络数据等)时,在该操作完成之前,应用程序会被阻塞,即线程暂停执行,等待 I/O 操作的完成。例如,在使用
- 非阻塞 IO(Non - Blocking I/O):
- 当应用程序发起一个 I/O 操作时,如果操作不能立即完成(例如读取数据时缓冲区没有数据),函数会立即返回一个错误码(如在 Linux 系统中返回
EWOULDBLOCK
或EAGAIN
),而不是阻塞线程等待操作完成。应用程序可以继续执行其他任务,之后可以通过轮询或者其他异步机制来检查 I/O 操作是否可以继续进行。
- 当应用程序发起一个 I/O 操作时,如果操作不能立即完成(例如读取数据时缓冲区没有数据),函数会立即返回一个错误码(如在 Linux 系统中返回
- 阻塞 IO(Blocking I/O):
- 工作原理
- 阻塞 IO:
- 以文件读取为例,当程序调用
read
函数读取文件内容时,操作系统会检查文件系统缓冲区。如果缓冲区中有数据且满足读取要求,就将数据返回给应用程序;如果缓冲区中没有足够的数据,操作系统会将调用read
的线程阻塞,直到有足够的数据被读入缓冲区并可以满足读取要求。在网络通信中,接收数据时如果接收缓冲区为空,发送数据时如果发送缓冲区已满,也会出现类似的阻塞情况。
- 以文件读取为例,当程序调用
- 非阻塞 IO:
- 同样以文件读取为例,当调用非阻塞
read
函数时,如果文件系统缓冲区没有足够的数据满足读取要求,函数会立即返回错误码,而不是等待数据准备好。在网络通信中,当接收数据时缓冲区为空或者发送数据时缓冲区已满,recv
或send
函数也会立即返回错误码。应用程序可以选择暂时忽略这个 I/O 操作,去处理其他事务,然后在合适的时候再次尝试调用 I/O 操作函数来检查是否可以继续进行。
- 同样以文件读取为例,当调用非阻塞
- 阻塞 IO:
- 性能特点
- 阻塞 IO:
- 简单直观,编程模型相对简单。因为应用程序不需要处理复杂的异步 I/O 机制,只需要按照顺序调用 I/O 操作函数,等待操作完成即可。但是,在多任务环境下效率较低。由于线程在 I/O 操作期间被阻塞,不能执行其他任务,这可能会导致资源浪费。例如,在一个多客户端的服务器中,如果服务器使用阻塞 IO 来处理客户端请求,当一个客户端的 I/O 操作(如接收一个大数据块)耗时较长时,服务器在处理这个客户端的过程中不能处理其他客户端的请求,降低了服务器的并发处理能力。
- 非阻塞 IO:
- 能够提高程序的并发处理能力。在多任务环境下,可以同时处理多个 I/O 操作,不会因为某个 I/O 操作未完成而阻塞整个线程。不过,非阻塞 IO 的编程模型相对复杂。开发者需要处理 I/O 操作可能返回的错误码,并且需要通过轮询或者其他异步机制(如
select
、epoll
等)来管理 I/O 操作的进度。例如,在使用非阻塞 IO 实现一个网络应用时,需要编写代码来不断地检查每个 I/O 操作是否有数据可读或者可写,增加了编程的难度和复杂性。
- 能够提高程序的并发处理能力。在多任务环境下,可以同时处理多个 I/O 操作,不会因为某个 I/O 操作未完成而阻塞整个线程。不过,非阻塞 IO 的编程模型相对复杂。开发者需要处理 I/O 操作可能返回的错误码,并且需要通过轮询或者其他异步机制(如
- 阻塞 IO:
- 应用场景
- 阻塞 IO:
- 适用于简单的、顺序执行的程序。例如,简单的命令行工具,只需要读取用户输入、执行一个操作(如文件复制)并输出结果,在这个过程中不需要同时处理其他任务,使用阻塞 IO 可以很方便地实现功能。再比如,简单的客户端 - 服务器通信场景,客户端发送一个请求,然后等待服务器响应,在等待响应的过程中,客户端没有其他任务需要执行,这种情况下使用阻塞 IO 比较合适。
- 非阻塞 IO:
- 适用于需要同时处理多个 I/O 操作的场景,特别是在高性能服务器和网络应用中。例如,在一个支持大量并发客户端连接的服务器中,使用非阻塞 IO 可以让服务器在等待一个客户端的数据到达或者发送缓冲区空闲的过程中,去处理其他客户端的请求,提高服务器的并发性能。同时,在一些对响应时间要求较高的实时系统中,非阻塞 IO 也可以更好地利用系统资源,避免因为 I/O 阻塞而导致系统响应延迟。
- 阻塞 IO:
2.阻塞socket和非阻塞socket有啥区别?
- 阻塞 Socket(Blocking Socket)
- 定义:
- 当一个线程调用阻塞式 Socket 的 I/O 操作(如
recv
接收数据或send
发送数据)时,该线程会被挂起(暂停执行),直到这个 I/O 操作完成。例如,在调用recv
函数从网络接收数据时,如果没有数据到达,线程会一直等待,直到有数据到达并被接收完毕才会继续执行后续代码。
- 当一个线程调用阻塞式 Socket 的 I/O 操作(如
- 工作原理:
- 以接收数据为例,当程序调用
recv
函数读取 Socket 中的数据时,操作系统会检查 Socket 接收缓冲区。如果缓冲区中有数据,就把数据返回给应用程序;如果缓冲区为空,操作系统会将调用recv
的线程阻塞,直到有数据被写入缓冲区。发送数据也是类似的情况,send
函数会一直等待,直到数据全部成功发送到网络或者出现错误。
- 以接收数据为例,当程序调用
- 应用场景:
- 适用于简单的、顺序执行的程序。例如,简单的客户端 - 服务器通信场景,客户端发送一个请求,然后等待服务器响应,在等待响应的过程中,客户端没有其他任务需要执行,这种情况下使用阻塞 Socket 比较合适。比如一个简单的 HTTP 客户端,它发送一个网页请求后,就等待服务器返回网页内容,使用阻塞 Socket 可以很方便地实现这个功能。
- 优点:
- 编程模型相对简单。开发者不需要处理复杂的异步 I/O 机制,只需要按照顺序调用 I/O 操作函数,等待操作完成即可。例如,在简单的文件传输程序中,使用阻塞 Socket 发送文件,只需要循环调用
send
函数,直到文件发送完毕,代码逻辑比较直接。
- 编程模型相对简单。开发者不需要处理复杂的异步 I/O 机制,只需要按照顺序调用 I/O 操作函数,等待操作完成即可。例如,在简单的文件传输程序中,使用阻塞 Socket 发送文件,只需要循环调用
- 缺点:
- 效率较低。因为线程在 I/O 操作期间被阻塞,不能执行其他任务。在多任务环境下,这可能会导致资源浪费。例如,在一个多客户端的服务器中,如果服务器使用阻塞 Socket 来处理客户端请求,当一个客户端的 I/O 操作(如接收一个大数据块)耗时较长时,服务器在处理这个客户端的过程中不能处理其他客户端的请求,降低了服务器的并发处理能力。
- 定义:
- 非阻塞 Socket(Non - Blocking Socket)
- 定义:
- 当一个线程调用非阻塞式 Socket 的 I/O 操作时,如果操作不能立即完成(如接收数据时缓冲区没有数据,或者发送数据时缓冲区已满),函数会立即返回一个错误码(如
EWOULDBLOCK
或EAGAIN
),而不是阻塞线程等待操作完成。线程可以继续执行其他任务,之后可以通过轮询或者其他异步机制来检查 I/O 操作是否可以继续进行。
- 当一个线程调用非阻塞式 Socket 的 I/O 操作时,如果操作不能立即完成(如接收数据时缓冲区没有数据,或者发送数据时缓冲区已满),函数会立即返回一个错误码(如
- 工作原理:
- 同样以接收数据为例,当调用非阻塞
recv
函数时,如果 Socket 接收缓冲区为空,函数不会等待,而是立即返回错误码。线程可以根据这个错误码选择暂时忽略这个 Socket,去处理其他任务,然后在合适的时候再次尝试调用recv
函数来检查是否有数据可读。发送数据时,如果缓冲区已满,send
函数也会立即返回错误码,而不是等待缓冲区有空间。
- 同样以接收数据为例,当调用非阻塞
- 应用场景:
- 适用于需要同时处理多个 I/O 操作的场景,特别是在高性能服务器和网络应用中。例如,在一个支持大量并发客户端连接的服务器中,使用非阻塞 Socket 可以让服务器在等待一个客户端的数据到达或者发送缓冲区空闲的过程中,去处理其他客户端的请求,提高服务器的并发性能。
- 优点:
- 能够提高程序的并发处理能力。在多任务环境下,可以同时处理多个 Socket 的 I/O 操作,不会因为某个 Socket 的 I/O 操作未完成而阻塞整个线程。例如,在一个即时通讯服务器中,使用非阻塞 Socket 可以高效地处理多个用户的消息发送和接收,提高服务器的响应速度和并发处理能力。
- 缺点:
- 编程模型相对复杂。开发者需要处理 I/O 操作可能返回的错误码,并且需要通过轮询或者其他异步机制来管理 I/O 操作的进度。例如,在使用非阻塞 Socket 实现一个网络应用时,需要编写代码来不断地检查每个 Socket 是否有数据可读或者可写,增加了编程的难度和复杂性。
- 定义:
4.非阻塞socket示例
通过轮询来实现的
以下是一个使用 C 语言编写的基于非阻塞套接字的简单服务器端示例代码,它可以同时处理多个客户端连接,展示了非阻塞套接字在网络编程中的应用方式以及如何处理相关的 I/O 操作。
1 |
|
以下是对上述代码的详细解释:
1. 函数和头文件引入
1 |
引入了必要的头文件,用于实现网络套接字相关操作、文件描述符操作、错误处理以及字符串和标准输入输出等功能。
2. set_nonblocking
函数
1 | // 设置套接字为非阻塞模式 |
这个函数用于将给定的套接字文件描述符sockfd
设置为非阻塞模式。它首先通过fcntl
函数获取当前文件描述符的标志位,然后使用按位或操作将O_NONBLOCK
标志添加进去,再通过fcntl
函数重新设置标志位,从而实现将套接字设置为非阻塞的目的。
3. main
函数主体
- 创建服务器套接字并进行基本设置:
1 | int server_socket = socket(AF_INET, SOCK_STREAM, 0); |
- 首先使用
socket
函数创建一个基于 TCP 协议(AF_INET
表示 IPv4 地址族,SOCK_STREAM
表示 TCP 套接字类型)的服务器套接字。 - 通过
setsockopt
函数设置套接字选项,使得端口可以复用,避免地址占用问题。 - 然后将套接字绑定到本地所有可用 IP 地址(
INADDR_ANY
)的8888
端口上,并开始监听客户端连接,允许的最大连接数设置为MAX_CLIENTS
(这里定义为5
)。 - 最后调用
set_nonblocking
函数将服务器套接字设置为非阻塞模式。 - 初始化客户端套接字数组:
1 | // 用于存储客户端套接字的数组 |
创建一个数组用于存储已连接的客户端套接字文件描述符,初始值都设为-1
,表示空闲位置。
- 主循环部分:
1 | fd_set read_fds; |
- 进入一个无限循环,在每次循环中,首先使用
FD_ZERO
函数清空文件描述符集合read_fds
,然后使用FD_SET
函数将服务器套接字添加到这个集合中,表示要监听服务器套接字的可读事件。接着遍历客户端套接字数组,将已连接的客户端套接字也添加到read_fds
集合中,并记录最大的文件描述符值,用于select
函数的参数。 - 使用
select
函数监听这些套接字的可读事件,select
会阻塞等待,直到有套接字可读或者发生错误等情况。如果select
返回-1
表示出现错误,直接跳出循环;如果返回0
表示超时(这里没有设置超时时间,实际是没有超时情况发生),继续下一次循环。 - 处理新客户端连接:
1 | // 检查服务器套接字是否可读,即是否有新客户端连接 |
- 当
select
返回后,通过FD_ISSET
函数检查服务器套接字是否在可读集合中,如果是,则表示有新客户端尝试连接。调用accept
函数接受新连接,获取新客户端的套接字文件描述符。如果accept
返回-1
,并且错误码不是EWOULDBLOCK
或EAGAIN
(这两个错误码在非阻塞模式下表示当前没有新连接可接受,属于正常情况),则打印错误信息;如果成功接受新连接,打印客户端的 IP 地址和端口信息,将新客户端套接字设置为非阻塞模式,然后将其添加到client_sockets
数组的空闲位置中。 - 处理客户端发送的数据:
1 | // 检查客户端套接字是否可读,处理客户端发送的数据 |
- 遍历客户端套接字数组,对于每个已连接且在可读集合中的客户端套接字,使用
read
函数尝试读取客户端发送的数据。如果read
返回-1
,并且错误码不是EWOULDBLOCK
或EAGAIN
(非阻塞模式下表示当前没有数据可读,属于正常情况),则打印错误信息,关闭该客户端套接字并将数组中对应的位置设为-1
;如果read
返回0
,表示客户端关闭了连接,打印相应信息后关闭套接字并设置数组位置为-1
;如果成功读取到数据,在终端打印收到的数据,同时可以在这里添加更复杂的对收到数据的处理逻辑(这里简单地将数据再写回客户端,实现回显功能)。 - 关闭套接字:
1 | // 关闭服务器套接字和所有客户端套接字 |
在程序结束前,关闭服务器套接字以及所有已连接的客户端套接字,释放相关资源。
通过这个示例代码,可以看到非阻塞套接字结合select
机制在服务器端处理多个客户端连接和数据收发时的应用,它能够在不阻塞主线程的情况下,并发地处理多个客户端的情况,提高服务器的并发处理能力。
你可以使用telnet
等工具作为客户端连接到这个服务器进行测试,比如在命令行输入telnet 127.0.0.1 8888
,然后输入一些字符发送给服务器,观察服务器端的输出情况。
请注意,这只是一个简单示例,实际应用中可能需要根据具体需求进一步优化和扩展功能,比如处理更多的网络异常情况、优化数据处理逻辑等。
3.阻塞connect和非阻塞connect有啥区别?
- 阻塞状态方面
- 阻塞 Connect:
- 当执行阻塞式
connect
操作时,调用线程会被阻塞。这意味着线程会暂停执行后续代码,一直等待连接操作完成,即等待 TCP 三次握手(针对 TCP 连接)成功或者出现错误。例如,在客户端与服务器建立连接的过程中,如果服务器响应较慢,比如由于网络拥塞或者服务器负载过高,线程会一直等待,直到握手过程结束或者连接超时。
- 当执行阻塞式
- 非阻塞 Connect:
- 对于非阻塞式
connect
,它不会阻塞调用线程。如果连接不能立即建立,例如服务器没有立刻响应或者网络延迟,connect
函数会立即返回一个错误码(通常是EINPROGRESS
),线程可以继续执行其他任务。比如在图形界面的网络应用中,在等待连接建立的同时,程序可以继续响应用户的其他操作,如更新界面显示或者处理其他已经建立连接的通信。
- 对于非阻塞式
- 阻塞 Connect:
- 编程模型复杂程度
- 阻塞 Connect:
- 编程模型相对简单。开发者只需要像调用普通函数一样调用
connect
,然后等待它返回结果即可。如果返回成功,就可以直接进行后续的数据发送和接收操作;如果返回失败,再根据错误码进行相应处理。这种方式比较符合线性的、顺序执行的编程思维,易于理解和实现。
- 编程模型相对简单。开发者只需要像调用普通函数一样调用
- 非阻塞 Connect:
- 编程模型较为复杂。因为
connect
函数返回后并不代表连接已经建立成功,所以需要额外的机制来检查连接状态。通常会使用select
、epoll
等 I/O 复用技术或者getsockopt
函数来检查套接字状态,以确定连接是否真正建立或者发生错误。这就要求开发者对网络编程的底层机制有更深入的理解,并且要处理好各种可能的情况,代码的逻辑也更加复杂。
- 编程模型较为复杂。因为
- 阻塞 Connect:
- 并发处理能力
- 阻塞 Connect:
- 在并发场景下效率较低。由于每个
connect
操作都会阻塞线程,当需要同时建立多个连接时,只能顺序地逐个进行连接操作。例如,一个程序需要同时连接多个服务器来获取数据,如果使用阻塞connect
,就需要等待一个连接建立完成后才能开始下一个连接的建立,这会大大延长总的连接时间,降低程序的并发性能。
- 在并发场景下效率较低。由于每个
- 非阻塞 Connect:
- 具有较好的并发处理能力。因为它不会阻塞线程,所以可以同时发起多个连接操作。在等待这些连接建立的过程中,程序可以执行其他任务,如处理已经建立连接的通信或者发起新的连接。通过合理使用 I/O 复用机制来检查连接状态,可以高效地管理多个并发连接的建立过程,提高程序在多连接场景下的效率。
- 阻塞 Connect:
- 适用场景
- 阻塞 Connect:
- 适用于简单的、对并发要求不高的网络应用场景。比如简单的命令行工具,只需要与一个服务器建立连接,获取数据后就结束程序。或者在一些简单的客户端 - 服务器通信场景中,客户端发起连接后主要就是等待服务器响应,期间不需要执行其他复杂任务。
- 非阻塞 Connect:
- 适用于对并发性能要求较高的场景。例如高性能的网络服务器或者客户端,需要同时与多个服务器建立连接,或者在连接建立过程中不能让线程被阻塞而影响其他操作的情况。像在网络爬虫程序中,需要同时连接多个网页服务器来抓取数据,使用非阻塞
connect
可以提高效率。
- 适用于对并发性能要求较高的场景。例如高性能的网络服务器或者客户端,需要同时与多个服务器建立连接,或者在连接建立过程中不能让线程被阻塞而影响其他操作的情况。像在网络爬虫程序中,需要同时连接多个网页服务器来抓取数据,使用非阻塞
- 阻塞 Connect:
阻塞 Connect 示例
想象你去一家餐厅吃饭,你告诉服务员你要点餐(相当于发起connect
请求),然后你就坐在那里什么也不做,一直等服务员把菜端上来(也就是等待连接建立完成)。在这个过程中,你不能做其他事情,比如看手机或者和朋友聊天,直到服务员把菜给你(连接成功)或者告诉你餐厅没这个菜了(连接失败)。这就是阻塞connect
,程序就像这个等待上菜的你一样,卡在那里一直等待连接操作完成。
非阻塞 Connect 示例
还是以去餐厅吃饭为例,你告诉服务员你要点餐(发起connect
请求),但是你不会干等着。你可以先去餐厅的休息区看手机、和朋友聊天或者做其他事情(程序继续执行其他任务)。过一会儿,你会去问服务员菜好了没(通过其他机制检查连接是否建立)。如果好了,你就可以开始用餐(进行数据收发等操作);如果没好,你就继续做其他事情,直到菜准备好或者被告知没这个菜了(连接失败)。
4.总结
1.阻塞就是啥也不干就等着,不需要过去的其他操作,就阻塞等待就行
2.非阻塞就是我去看看你来了没,你没来我继续干我自己的,你来了我去处理你
我去看你,我得检查你来了没,没来的话我得判断你没来,你没来之后我还得有其他机制(比如轮询)保证你来了之后我能知道
比如,read函数读取非阻塞socket,读到的内容如果为空,会返回-1并且错误号是EAGAIN或EWOULDBLOCK,这种情况是正常的,之后进程就可以去read或者write别的socket,等再次轮询到它的时候,再次read,如果满足条件,返回值大于0就会开始读了
所以非阻塞比阻塞要难写一些