前言

在阻塞和非阻塞模式下,常讨论的具有不同行为表现的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);

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

socket:

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

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

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

不仅如此,在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);

只要将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");
}

虽然返回值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. 返回值等于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函数接收一定要有的应答包。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐