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


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

| 返回值n | 返回值含义 |
|---|---|
| 大于0 | 成功发送(send)或接收(recv)n字节 |
| 0 | 对端关闭连接 |
| 小于0(-1) | 出错、信号被中断、对端TCP窗口太小导致数据发送不出去或当前网卡缓冲区已无数据可接收 |
逐一介绍三种情况:
示例:
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;
}

非阻塞模式一般用于需要支持高并发多QPS的场景(如服务器程序),但这种模式让程序的执行流和控制逻辑变得复杂;相反,阻塞模式逻辑简单,程序结构简单明了,常用于一些特殊场景中。
举例两个阻塞模式的应用场景:
场景一:某程序需要临时发送一个文件,文件分段发送,每发送一段,对端都会给予一个响应,该程序可以单独开一个任务线程,在这个任务线程函数里面,使用先send后recv再send再recv的模式,每次send和rec都是阻塞模式的。
场景二:A端与B端之间的通信只有问答模式,即A端每发送给B端一个请求,B端必定会给A端一个响应,除此之外,B端不会向A端推送任何数据,此时A端就可以采用阻塞模式,在每次send完请求后,都可以直接使用阻塞式的recv函数接收一定要有的应答包。