目录
- 阻塞与非阻塞定义
- send与recv
- connect
- 一些问题
- 为什么要将监听socket设置为非阻塞
阻塞与非阻塞定义
阻塞模式指的是当前某个函数执行效果未达预期,该函数会阻塞当前的执行线程,程序执行流在超时时间到达或者执行成功后恢复原有流程。非阻塞模式相反,即使某个函数执行结果未达预期,该函数也不会阻塞当前执行线程,而是立即返回。
网络socket编程中,常见的connect、accept、send、recv函数均具有阻塞与非阻塞两种调用方式。
阻塞与非阻塞socket具有各自适用的场景
非阻塞模式一般用于需要支持高并发QPS的场景,但是该模式会让程序执行流和控制逻辑变复杂。
阻塞模式逻辑简单,结构简单。
send与recv
send函数本质不是向网络发送数据,而是将应用层发送缓冲区的数据拷贝到内核缓冲区,至于数据什么时候从网卡缓冲区中真正的发到网络中,要根据TCP/IP协议栈的行为来确定。
如果禁用nagel算法,存放到内核缓冲区的数据就会被立即发送出去。
否则如果一次放入缓冲区的数据包太小,系统会在多个小的数据包凑成一个足够大的数据包后再发送。
反之,recv函数的本质则是将内核缓冲区的数据拷贝到应用缓冲区
而两个程序进行网络通信时,发送的一方会将内核缓冲区的数据通过网络传输给接收方的内核缓冲区。这里的内核缓冲区也可以被称为TCP窗口
如果一端一直发送数据,对端应用一直不收取数据的话,则两端的内核缓冲区很快会被填满,导致调用send函数被阻塞(如果是阻塞模式下的话),从而影响当前线程的流程。如果是阻塞模式下德华,对端和本端的TCP窗口已满,数据发送不出去,send函数会立即返回-1,并且得到EWOULDBLOCK的错误码。
下面是非阻塞模式下send和recv函数的返回值总结
返回值 | 返回值含义 |
---|---|
大于0 | 成功发送或者接受n个字节 |
0 | 对端关闭连接 |
小于0 | 出错、信号被中断、对端窗口太小导致数据发送不出去、当前网卡缓冲区无数据可接收 |
此时需要判断返回值是否是我们期望的发送or接收的字节数。
如果对端的TCP窗口可能因为接收了部分数据就满了,此时n的值就是(0,buf_length]了。
所以一般在循环中调用send函数,如果数据一次性发送不出去,则记录偏移量,下一次从偏移量处接着发送,直到全部发送完为止:
bool sendData(int socketfd, const char* buf, int bufLength)
{// 已经发送的字节数int sentBytes = 0;int ret = 0;while (true) {ret = send(socketfd, buf + sentBytes, bufLength - sentBytes, 0);if (ret == -1) {if (errno == EWOULDBLOCK) {// 缓存尚未发送出去的数据,这里不具体写// ... 缓存未发送出去的数据break;} else if (errno == EINTR) {continue;} else {return false;}} else if (ret == 0) {return false;}// 否则发送成功sentBytes += ret;if (sentBytes == bufLength)break;}return true;
}
当返回值为-1的时候我们需要根据不同的错误码来进行对应处理:
错误码 | send函数 | recv函数 |
---|---|---|
EWOULDBLOCK 或者 EAGAIN | TCP窗口太小,数据暂时发送不出去 | 当前内核缓冲区中无可读数据 |
EINTR | 被信号中断,需要重试 | 被信号中断,需要重试 |
不是以上两种 | 出错 | 出错 |
connect
使用非阻塞的connect的步骤如下:
1、创建socket,将socket设置为非阻塞模式
2、调用connect函数,无论connect函数是否连接成功都立即返回;
3、调用select函数,在指定时间内判断该socket是否可写,若可写,则说明连接成功,反之认为连接失败。不过在linux系统上有些特殊:
connect之后,不仅要调用select检测是否可写,还要调用getsockpt
检测此时socket是否出错,通过错误码来检测是否连接上,错误码为0表示连接上。
在上一讲中我们在服务端使用了select函数来监听三种事件的发生,在客户端也是可以用的。在这个问答中:select()可以用于客户端,而不仅仅是服务器吗?有这样一个回答:
在客户端套接字上使用select()
的另一个好理由是跟踪传出的TCP连接进度。例如,这允许设置连接超时。 将客户端套接字设置为非阻塞。 调用connect()
。可能它会返回EINPROGRESS错误集(连接正在进行中,因为套接字是非阻塞的,所以不会被阻止)。 现在select()
配置FD_SET
以跟踪客户端套接字为’write-ready’。你也可以设置超时。 分析select()
结果。 分析上次客户端套接字操作是否失败或成功。 最有用的是你可以在不同状态的几个套接字上使用它。因此,您可以真正无阻塞地处理多个套接字(客户端,服务器,传出,侦听,接受…)。所有这一切只有一个线程。
代码如下:
#include <iostream>
#include <sys/types.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT 3000
#define SEND_DATA "helloworld"using namespace std;int main() {// 创建一个socketint clientfd = socket(AF_INET, SOCK_STREAM, 0);if (clientfd == -1) {cout << " create client socket error " << endl;return -1;}// 将clientfd设置为非阻塞模式int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);int newSocketFlag = oldSocketFlag | O_NONBLOCK;if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1) {close(clientfd);cout << "set socket to noblock error" << endl;return -1;}// 连接服务器struct sockaddr_in serveraddr;serveraddr.sin_family = AF_INET;serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);serveraddr.sin_port = htons(SERVER_PORT);// 此处与之前的阻塞式connect就不一样了,需要用for循环,来轮询状态while (true) {int ret = connect(clientfd, (struct sockaddr *)& serveraddr, sizeof(serveraddr));if (ret == 0) {cout << "connect to server sucessfully" << endl;close(clientfd);return 0;} else if (ret == -1) {if (errno == EINTR) {// connect 被信号中断了,重试connectcout << "connect interruptted by signal, try again" << endl;continue;} else if (errno == EINPROGRESS) {// 连接尝试中break;} else {// 真的出错了close(clientfd);return -1;}}}fd_set writeset;FD_ZERO(&writeset);FD_SET(clientfd, &writeset);struct timeval time;time.tv_sec = 3;time.tv_usec = 0;// 调用select判断socket是否可写if (select(clientfd + 1, NULL, &writeset, NULL, &time) != 1) {cout << "select connect to server error" << endl;close(clientfd);return -1;}int err;socklen_t len = static_cast<socklen_t>(sizeof err);// 调用getsockopt检测此时socket是否出错if (::getsockopt(clientfd, SOL_SOCKET, SO_ERROR, &err, &len) < 0) {close(clientfd);return -1;}if (err == 0) {cout << "connect to server successfully" <<endl;} else {cout << "connect to server error" << endl; }close(clientfd);return 0;
}
一些问题
为什么要将监听socket设置为非阻塞
在第二讲中我们谈到select模型,常见的网络通信模型都会使用IO多路复用技术如select、poll、epoll等。当有新的连接请求到来时,监听套接字变为可读,然后调用accept()接收新连接、返回一个连接套接字。
如果监听套接字是阻塞的,问题可能出在什么地方?
根据TCP三次握手的示意图:
从图中可知,connect()会先于accep()函数返回。
当一个连接到来的时候,监听套接字可读,此时,我们稍微等一段时间之后再调用accept()。就在这段时间内,客户端设置linger选项(l_onoff = 1, l_linger = 0),然后调用了close(),那么客户端将不经过四次挥手过程,通过发送RST报文断开连接。服务端接收到RST报文,系统会将排队的这个未完成连接直接删除,此时就相当于没有任何的连接请求到来, 而接着调用的accept()将会被阻塞,直到另外的新连接到来时才会返回。这是与IO多路复用的思想相违背的(系统不阻塞在某个具体的IO操作上,而是阻塞在select、poll、epoll这些IO复用上的)。
上述这种情况下,如果监听套接字为非阻塞的,accept()不会阻塞住,立即返回-1,同时errno = EWOULDBLOCK