【服务器系统设计】socket的阻塞模式和非阻塞模式总结

1,180 阅读5分钟

前言

对socket在阻塞和非阻塞模式下各个socket函数的表现进行深入理解,是掌握网络编程的基本要求之一,也是重点和难点。 在阻塞和非阻塞模式下,我们常常讨论的具有不同行为表现的socket函数一般有connect,accept,send和recv。

定义

阻塞模式:指的是当某个函数执行成功的条件当前不满足时,该函数会阻塞当前执行线程,程序执行流在超时时间到达或执行成功的条件满足后恢复继续执行。 非阻塞模式:即使某个函数执行成功的条件不满足,该函数也不会阻塞当前执行线程,而是立即返回,继续执行程序流。

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

无论是在Windows还是Linux,默认创建的socket都是阻塞模式的。 在Linux上,可以通过使用fcntl函数或者ioctl函数给创建的socket增加O_NONBLOCK标志来将socket设置为非阻塞模式,示例代码如下:

int oldSocketFlag = fcntl(socketfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
fcntl(socketfd, F_SETFL, newSocketFlag);

当然,Linux上的socket函数也可以在创建时将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 accept(int sockfd, struct sockaddr* addr, socklen_t *addrlen, int flags);

只需要将accept4函数最后一个参数设置为SOCK_NONBLOCK即可。如下:

socklen_t addrlen = sizeof(clientaddr);
int clientfd = accept4(listenfd, &clientaddr, &addrlen, SOCK_NONBLOCK);

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

我们先来想一个问题,当程序调用了send函数,是不是就意味着将数据成功发送到网络上了? 其实,send和recv函数的命名其实有一种误导效果。

  • send函数从本质上来说并不是向网络发送数据,而是将应用层发送缓冲区的数据拷贝到内核缓冲区中,至于数据什么时候会从网卡缓冲区中真正地发送到网络中,要根据TCP/IP的协议栈的行为来确定。如果socket设置了TCP_NODELAY选项(即禁用nagel算法),存放到内核缓冲区的数据就会被立即发送出去;反之,一次放入内核缓冲区的数据包如果太小,则系统会在多个小的数据包凑成一个足够大的数据包后才会将数据发送出去。
  • recv函数在本质上并不是从网络收取数据,而是将内核缓冲区中的数据拷贝到应用程序的缓冲区中,当然,拷贝完成后会将内核缓冲区中的该部分的数据移除。

那么我们来想一种情况,假如说,有一个应用程序A跟应用程序B建立了TCP通信,而应用程序不断调用send函数,那么数据将会不断地拷贝到对应的内核缓冲区中,如果应用程序一直不调用recv函数,那么在应用程序B的内核缓冲区被填满后,应用程序A的内核缓冲区也会被填满,此时应用程序A继续调用send函数会是什么结果呢?具体的结果取决于socket是否是阻塞模式 (1)当socket是阻塞模式时,继续调用send/recv函数,程序会阻塞在send/recv调用处 (2)当socket是非阻塞模式时,继续调用send/recv函数,send/recv函数不会阻塞程序执行流,而是立即出错并返回,在Linux上会返回一个EWOULDBLOCK或者EAGAIN(两个错误码值相同)错误码。

非阻塞模式下如何正确地调用send/recv函数

由于非阻塞模式会让程序的执行流变得复杂,不像阻塞模式下那么简单明了。 以send函数为例,有人可能会写出这样的程序:

int n = send(socket, buf, buf_length, 0);
if(n == buf_length)
{
	std::cout << "send data succ!" << std::endl;
}

其实这种写法是不推荐的,正确地写法应该这样:

bool sendData(const char* buf, int buf_length)
{
	int send_bytes = 0; //已发送的字节数
	int ret = 0;
	while(true)
	{
		ret = send(socket_, buf + send_bytes, buf_length - send_bytes, 0);
		if(ret == -1)
		{
			if(errno == EWOULDBLOCK)
			{
				//缓存尚未发送出去的数据
				break;
			}
			else if(errno == EINTR)
			{
				continue;
			}
			else
			{
				return false;
			}
		}
		else if(ret == 0)
		{
			return false;
		}
		send_bytes += ret;
		if(send_bytes == buf_length)
		{
			break;	
		}
	}
	return true;
}

阻塞与非阻塞模式socket各自的应用场景

阻塞的socket函数在调用send、recv、connect、accept等函数时,如果条件不满足,就会阻塞其调用线程直至超时,非阻塞的socket则会立即返回。但这并不意味着非阻塞模式一定比阻塞模式好,两者各有优缺点。 非阻塞一般用于需要支持高并发的场景,但这种模式会让程序的执行流变得复杂,相反,阻塞模式的使用逻辑十分简单,程序结构简单明了,可以用在一些特殊场景中。 适合非阻塞模式的应用场景:

  • 需要支持高并发程序,如服务端程序

适合阻塞模式下的应用场景:

  • 某程序需要临时发送一个文件,文件分段发送,每发送一段,对端都会给予一个响应。
  • 程序A和程序B之间的通信只有问答模式,即A端每发送给B端一个请求,B端必定会给A端一个响应,除此之外,B端不会向A端推送任何数据,此时A端就可以采用阻塞模式。