大数跨境
0
0

08.connect函数在阻塞和非阻塞模式下的行为

08.connect函数在阻塞和非阻塞模式下的行为 CppGuide
2023-12-12
4

关注我,更多的优质内容第一时间阅读


当 socket 使用阻塞模式时, connect 函数会一直到有明确的结果才会返回(或连接成功或连接失败),如果服务器地址“较远”或者网络状况不好,连接速度比较慢,程序可能会在 connect 函数处阻塞好一会儿(如两三秒之久)。虽然这一般也不会对依赖于网络通信的程序造成什么影响,但在实际项目中,我们一般倾向使用所谓的异步 connect 技术,或者叫非阻塞 connect。这个流程一般有如下步骤:

1. 创建socket,并将 socket 设置成非阻塞模式;
2. 调用 connect 函数,此时无论 connect 函数是否连接成功会立即返回;如果返回 -1 并不一定表示连接出错,如果此时错误码是EINPROGRESS,则表示正在尝试连接;
3. 接着调用 select 函数,在指定的时间内判断该 socket 是否可写,如果可写说明连接成功,反之则认为连接失败。

按上述流程编写代码如下:

/**
 * 异步的connect写法,nonblocking_connect.cpp
 * zhangyl 2018.12.17
 */
#include <sys/types.h> 
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>

#define SERVER_ADDRESS  "127.0.0.1"
#define SERVER_PORT     3000
#define SEND_DATA       "helloworld"

int main(int argc, char* argv[])
{
    //1.创建一个socket
    int clientfd = socket(AF_INET, SOCK_STREAM, 0);
    if (clientfd == -1)
    {
        std::cout << "create client socket error." << std::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);
      std::cout << "set socket to nonblock error." << std::endl;
      return -1;
 }
 
 //2.连接服务器
 struct sockaddr_in serveraddr;
 serveraddr.sin_family = AF_INET;
 serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
 serveraddr.sin_port = htons(SERVER_PORT);
 for (;;)
 {
      int ret = connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
      if (ret == 0)
      {
          std::cout << "connect to server successfully." << std::endl;
          close(clientfd);
          return 0;
      } 
      else if (ret == -1) 
      {
          if (errno == EINTR)
          {
              //connect 动作被信号中断,重试connect
              std::cout << "connecting interruptted by signal, try again." << std::endl;
              continue;
          } 
          else if (errno == EINPROGRESS)
          {
              //连接正在尝试中
              break;
          } 
          else
          {
              //真的出错了,
              close(clientfd);
              return -1;
          }
      }
 }
 
 fd_set writeset;
 FD_ZERO(&writeset);
 FD_SET(clientfd, &writeset);
 struct timeval tv;
 tv.tv_sec = 3;  
 tv.tv_usec = 0;
 //3.调用select函数判断socket是否可写
 if (select(clientfd + 1, NULL, &writeset, NULL, &tv) == 1)
 {
      std::cout << "[select] connect to server successfully." << std::endl;
 } 
 else 
 {
      std::cout << "[select] connect to server error." << std::endl;
 }
 
 close(clientfd);
 
 return 0;
}

为了区别到底是在调用 connect 函数时判断连接成功还是通过 select 函数判断连接成功,我们在后者的输出内容中加上了“**[select]**”标签以示区别。

我们先用 nc 命令启动一个服务器程序:

nc -v -l 0.0.0.0 3000

然后编译客户端程序并执行:

[root@localhost testsocket]# g++ -g -o nonblocking_connect nonblocking_connect.cpp 
[root@localhost testsocket]# ./nonblocking_connect 
[select] connect to server successfully.

我们把服务器程序关掉,再重新启动一下客户端,这个时候应该会连接失败,程序输出结果如下:

[root@localhost testsocket]# ./nonblocking_connect 
[select] connect to server successfully.

奇怪?为什么连接不上也会得出一样的输出结果?难道程序有问题?这是因为:

  • 在 Windows 系统上,一个 socket 没有建立连接之前,我们使用 select 函数检测其是否可写,能得到正确的结果(不可写),连接成功后检测,会变为可写。所以,上述介绍的异步 connect 写法流程在 Windows 系统上是没有问题的。

  • 在 Linux 系统上一个 socket 没有建立连接之前,用 select 函数检测其是否可写,你也会得到可写的结果,所以上述流程并不适用于 Linux 系统。Linux 系统上正确的做法是,connect 之后,不仅要用 select 检测可写,还要调用 getsockopt 检测此时 socket 是否出错,通过错误码来检测确定是否连接上,错误码为 0 表示连接上,反之则表示未连接上。完整代码如下:

/**
 * Linux 下正确的异步的connect写法,linux_nonblocking_connect.cpp
 * zhangyl 2018.12.17
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#include <errno.h>

#define SERVER_ADDRESS "127.0.0.1"
#define SERVER_PORT     3000
#define SEND_DATA       "helloworld"

int main(int argc, char* argv[])
{
    //1.创建一个socket
    int clientfd = socket(AF_INET, SOCK_STREAM, 0);
    if (clientfd == -1)
    {
        std::cout << "create client socket error." << std::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);
        std::cout << "set socket to nonblock error." << std::endl;
        return -1;
    }

    //2.连接服务器
    struct sockaddr_in serveraddr;
    serveraddr.sin_family = AF_INET;
    serveraddr.sin_addr.s_addr = inet_addr(SERVER_ADDRESS);
    serveraddr.sin_port = htons(SERVER_PORT);
    for (;;)
    {
        int ret = connect(clientfd, (struct sockaddr *)&serveraddr, sizeof(serveraddr));
        if (ret == 0)
        {
            std::cout << "connect to server successfully." << std::endl;
            close(clientfd);
            return 0;
        }
        else if (ret == -1)
        {
            if (errno == EINTR)
            {
                //connect 动作被信号中断,重试connect
                std::cout << "connecting interruptted by signal, try again." << std::endl;
                continue;
            }
            else if (errno == EINPROGRESS)
            {
                //连接正在尝试中
                break;
            }
            else
            {
                //真的出错了,
                close(clientfd);
                return -1;
            }
        }
    }

    fd_set writeset;
    FD_ZERO(&writeset);
    FD_SET(clientfd, &writeset);
    struct timeval tv;
    tv.tv_sec = 3;
    tv.tv_usec = 0;
    //3.调用select函数判断socket是否可写
    if (select(clientfd + 1, NULL, &writeset, NULL, &tv) != 1)
    {
        std::cout << "[select] connect to server error." << std::endl;
        close(clientfd);
        return -1;
    }

    int err;
    socklen_t len = static_cast<socklen_t>(sizeof err);
    //4.调用getsockopt检测此时socket是否出错
    if (::getsockopt(clientfd, SOL_SOCKET, SO_ERROR, &err, &len) < 0)
    {
        close(clientfd);
        return -1;
    }

    if (err == 0)
        std::cout << "connect to server successfully." << std::endl;
    else
        std::cout << "connect to server error." << std::endl;
    
    close(clientfd);

    return 0;
}

Linux 操作系统上,步骤 3 中,可以使用 poll 函数代替 select 函数来判断 socket 是否可写;在 Windows 平台上可以使用 WSAEventSelectWSAAsyncSelect 函数判断连接是否成功,关于这三个函数我们将在后面的章节中详细讲解。

关注我,更多的优质内容第一时间阅读


更多的专题参见:cppguide.cn


技术交流群:加微信 cppxiaofang,备注加微信群。


付费课程

C/C++ 网络编程实战训练营 一期录像

C/C++ 网络编程二期录像 限时特惠


【声明】内容源于网络
0
0
CppGuide
专注于高质量高性能C++开发,站点:cppguide.cn
内容 1260
粉丝 0
CppGuide 专注于高质量高性能C++开发,站点:cppguide.cn
总阅读961
粉丝0
内容1.3k