• 网络编程:socket的阻塞模式和非阻塞模式


    前言

    在阻塞和非阻塞模式下,常讨论的具有不同行为表现的socket函数一般有connect、accept、send和recv。在讨论这四个函数前,首先要了解阻塞和非阻塞模式的概念。
    阻塞是指当某个函数执行成功的条件当前不满足时,该函数会阻塞当前执行线程,程序执行流在超过时间到达或执行成功的条件满足后恢复继续执行。非阻塞模式相反,即使某个函数执行成功的条件当前不满足,该函数也不会阻塞当前执行线程,而是立即返回,继续执行程序流。

    在这里插入图片描述

    在这里插入图片描述

    socket的阻塞模式和非阻塞模式

    1. 如何将socket设置为非阻塞模式

    在Linux上,可以使用 fcntl函数或ioctl函数给创建的socket增加 O_NONBLOCK标志来将socket设置为非阻塞模式。
    示例:

    int oldSocketFlag = fcntl(sockfd, F_GETFL, 0);
    int newSocketFlag = oldSocketFlag | O_NONBLOCK;
    fcntl(sockfd, F_SETFL, newSocketFlag);
    
    • 1
    • 2
    • 3

    也可以直接在socket创建时将其设置为非阻塞模式:

    socket:

    int socket(int domain, int type, int protocol);
    
    • 1

    给type参数增加一个SOCK_NONBLOCK标志即可:

    int s = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, IPPROTO_TCP);
    
    • 1

    不仅如此,在Linux上利用accept函数返回的代表与客户端通信的socket也提供了一个扩展函数accept4,直接将accept函数返回的socket设置为非阻塞:

    int accept(int sockfd, struct sockaddr* addr, socklen_t* addrlen);
    int accept4(int sockfd, struct sockaddr* addr, socklen_t* addrlen, int flags);
    
    • 1
    • 2

    只要将accept4的最后一个参数flags设置为SOCK_NONBLOCK即可。

    2.send和recv函数在阻塞和非阻塞模式下的表现

    send函数在本质上并不是向网络上发送数据,而是将应用层发送缓冲区的数据拷贝到内核缓冲区中,至于数据什么时候会从网卡缓冲区中真正的发到网络中,要根据TCP/IP协议栈的行为来确定。

    如果socket设置了TCP_NODELAY选项(禁用nagle算法),存放到内核缓冲区的数据就会被立即发送出去;反之,一次放入内核缓冲区的数据包如果太小,系统会在多个小数据包凑成足够大的数据包后才会发出去。

    recv函数本质上并不是从网络上收取数据,而是将内核缓冲区中的数据拷贝到应用程序的缓冲区中。
    拷贝完成后会将内核缓冲区中的该部分数据移除。

    图示:

    在这里插入图片描述

    1. 当socket是阻塞模式时,继续调用send/recv函数,程序会阻塞在send/recv调用处。
    2. 当socket是非阻塞模式时,继续调用send/recv函数,send/recv函数不会阻塞程序执行流,而是立即出错并返回,会得到一个相关的错误码,在Linux上错误码为 EWOULDBLOCK或EAGAIN,在Windows上为WSAEWOULDBLOCK。

    3.非阻塞模式下send和recv函数的返回值总结

    返回值n返回值含义
    大于0成功发送(send)或接收(recv)n字节
    0对端关闭连接
    小于0(-1)出错、信号被中断、对端TCP窗口太小导致数据发送不出去或当前网卡缓冲区已无数据可接收

    逐一介绍三种情况:

    1. 返回值大于0。在这种情形下,一定要判断send函数的返回值是不是我们期望发送的字节数,而不是简单的判断其返回值大于0.

    示例:

    int n = send(socket, buf, buf_length, 0);
    if (n > 0)
    {
    	printf("send data successfully!\n");
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    虽然返回值n大于0,但在实际情况下,由于对端的TCP可能因为缺少一部分字节就满了,所以n的值可能为(0, buf_length]。当 0 < n < buf_length时,虽然send函数调用成功,但在业务上并不算正确,因为有部分数据并没有发送出去。
    所以,建议要么在返回值n等于buf_length时才认为正确,要么在一个循环中调用send函数,如果数据一次性发送不完,则记录偏移量,下一次从偏移量处接着发送,直到全部发送完为止:

    // 推荐的方式:在一个循环里面根据偏移量发送数据
    bool SendData(const char* buf, int buf_length)
    {
    	// 已发送的字节数
    	int sent_bytes = 0;
    	int ret = 0;
    	while (true)
    	{
    		ret = send(m_hSocket, buf + sent_bytes, buf_length - sent_bytes, 0);
    		if (ret == -1)
    		{
    			if (errno == EWOULDBLOCK)
    			{
    				// 严谨做法是:如果发送不出去,应该缓存尚未发送出去的数据
    				break;
    			}
    			else if (errno == EINTR)
    				continue;
    			else
    				return false;
    		}
    		else if (ret == 0)
    		{
    			return false;
    		}
    
    		sent_bytes += ret;
    		if (sent_bytes == buf_length)
    			break;
    	}
    
    	return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    1. 返回值等于0。如果send或recv函数返回0,我们就认为对端关闭了连接,我们这段也关闭连接即可,这是实际开发时最常见的处理逻辑。
    2. 返回值小于0.对于send或recv函数返回值小于0的情况,此时并不代表函数一定调用出错。如下表:

    在这里插入图片描述

    4.阻塞与非阻塞socket的各自适用场景

    非阻塞模式一般用于需要支持高并发多QPS的场景(如服务器程序),但这种模式让程序的执行流和控制逻辑变得复杂;相反,阻塞模式逻辑简单,程序结构简单明了,常用于一些特殊场景中。

    举例两个阻塞模式的应用场景:
    场景一:某程序需要临时发送一个文件,文件分段发送,每发送一段,对端都会给予一个响应,该程序可以单独开一个任务线程,在这个任务线程函数里面,使用先send后recv再send再recv的模式,每次send和rec都是阻塞模式的。

    场景二:A端与B端之间的通信只有问答模式,即A端每发送给B端一个请求,B端必定会给A端一个响应,除此之外,B端不会向A端推送任何数据,此时A端就可以采用阻塞模式,在每次send完请求后,都可以直接使用阻塞式的recv函数接收一定要有的应答包。

  • 相关阅读:
    UE AI里的感知实现流程解析
    Netty编程面试题
    【ROS2】状态机 Smach 包的获取与使用 - Part1
    Redis数据类型-Hash-存储结构之ziplist
    我的世界村民为什么会消失?
    leetcode20-有效的括号
    UE4 C++ 笔记(二):基础知识
    JSP工作原理
    Curl 命令方式对elasticsearch备份和恢复
    多年以后「PageHelper」又深深的给我上了一课
  • 原文地址:https://blog.csdn.net/m0_56257585/article/details/126568161