C++网络编程快速入门(三):阻塞与非阻塞式调用网络通信函数

阻塞与非阻塞定义

阻塞模式指的是当前某个函数执行效果未达预期,该函数会阻塞当前的执行线程,程序执行流在超时时间到达或者执行成功后恢复原有流程。非阻塞模式相反,即使某个函数执行结果未达预期,该函数也不会阻塞当前执行线程,而是立即返回。
网络socket编程中,常见的connectacceptsendrecv函数均具有阻塞与非阻塞两种调用方式。
阻塞与非阻塞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 或者 EAGAINTCP窗口太小,数据暂时发送不出去当前内核缓冲区中无可读数据
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() {
    // 创建一个socket
    int 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 被信号中断了,重试connect
                cout << "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

  • 1
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
网络编程,当然要用到Windows Socket(套接字)技术。Socket相关的操作由一系列API函数来完成,比如socket、bind、listen、connect、accept、send、sendto、recv、recvfrom等。调用这些API函数有一定的先后次序,有些函数的参数还比较复杂,对于开发者来说,不是很好用。于是,微软的MFC提供了两个类:CAsyncSocket和CSocket,极大地方便了Socket功能的使用。   CAsyncSocket类在较低层次上封装了Windows Socket API,并且通过内建一个(隐藏的)窗口,实现了适合Windows应用的异步机制(Windows Socket API默认情况下工作在阻塞,不方便直接在消息驱动的Windows程序上使用)。CSocket类从CAsyncSocket类派生,进一步简化了Socket功能的应用。不过很遗憾,正因为这两个类都内建了一个窗口,它们并不是线程安全的(thread-safe);如果要在多线程环境下应用Socket功能,建议自行封装Socket API函数。 基于TCP的socket编程的服务器端程序流程如下: 1、创建套接字 2、将套接字绑定到一个本地地址和端口号上(bind) 3、将套接字设为监听模,准备接受客户请求(listen) 4、等待客户请求,请求到来时接受请求,建立链接,并返回 一个新的基于此次通信的套接字(accept) 5、用返回的套接字和客户端进行通信(send、recv) 6、返回,等待另一客户请求 7、关闭套接字 基于TCP的socket编程的客户端程序流程如下: 1、创建套接字 2、向服务器端发送请求(connect) 3、和服务器端进行通信(send、recv) 4、关闭套接字 基于UDP的socket编程的服务器端程序流程如下: 1、创建套接字 2、将套接字绑定到本地地址和端口号上(bind) 3、等待接收数据(recvfrom) 4、关闭套接字 基于UDP的socket编程的客户端程序流程如下: 1、创建套接字 2、和服务器端进行通信(sendto) 3、关闭套接字 异步方指的是发送方不等接收方响应,便接着发下个数据包的通信方;而同步指发送方发出数据后,等收到接收方发回的响应,才发下一个数据包的通信方。   阻塞套接字是指执行此套接字的网络调用时,直到成功才返回,否则一直阻塞在此网络调用上,比如调用recv()函数读取网络缓冲区中的数据,如果没有数据到达,将一直挂在recv()这个函数调用上,直到读到一些数据,此函数调用才返回;而非阻塞套接字是指执行此套接字的网络调用时,不管是否执行成功,都立即返回。比如调用recv()函数读取网络缓冲区中数据,不管是否读到数据都立即返回,而不会一直挂在此函数调用上。在实际Windows网络通信软件开发中,异步非阻塞套接字是用的最多的。平常所说的C/S(客户端/服务器)结构的软件就是异步非阻塞的。   对于这些概念,初学者的理解也许只能似是而非,我将用一个最简单的例子说明异步非阻塞Socket的基本原理和工作机制。目的是让初学者不仅对Socket异步非阻塞的概念有个非常透彻的理解,而且也给他们提供一个用Socket开发网络通信应用程序的快速入门方法。操作系统是Windows 98(或NT4.0),开发工具是Visual C++6.0。   MFC提供了一个异步类CAsyncSocket,它封装了异步、非阻塞Socket的基本功能,用它做常用的网络通信软件很方便。但它屏蔽了Socket的异步、非阻塞等概念,开发人员无需了解异步、非阻塞Socket的原理和工作机制。因此,建议初学者学习编网络通信程序时,暂且不要用MFC提供的类,而先用Winsock2 API,这样有助于对异步、非阻塞Socket编程机制的理解。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

拾牙慧者

欢迎请作者喝奶茶

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值