5.7 I/O控制命令

套接字的I/O控制主要用于设置套接字的工作模式(阻塞模式还是非阻塞模式)。另外,也可以用来获取与套接字相关的I/O操作的参数信息。

Winsock提供了函数ioctlsocket和WSAIoctl来发送I/O控制命令,前者源自Winsock1版本,后者是前者的扩展版本,源自Winsock2版本。函数ioctlsocket声明如下:

    int ioctlsocket( SOCKET  s,  long  cmd, u_long*  argp);

其中,s为要设置I/O模式的套接字的描述符。cmd表示发给套接字的I/O控制命令,通常取值如下:

·FIONBIO:表示设置或清除阻塞模式的命令,当argp作为输入参数为0的时候,套接字将设置为阻塞模式;当argp作为输入参数为非0的时候,套接字将设置为非阻塞模式。有一种情况要注意:函数WSAAsynSelect会将套接字自动设置为非阻塞模式,而且如果对某个套接字调用了WSAAsynSelect函数,再想用ioctlsocket函数把套接字重新设置为阻塞模式,ioctlsocket会返回WSAEINVAL错误,此时如果想把套接字重新设置为阻塞模式,应该依旧调用WSAAsynSelect函数,并把其参数IEvent设置为0,这样套接字就又可变为阻塞模式了。大家今后在使用WSAAsynSelect函数的时候要做到心中有数,别想当然地以为通过ioctlsocket函数一定能把套接字设为阻塞模式。

·FIONREAD:用于确定套接字s自动读入数据量的命令,若s是流套接字(SOCET_STREAM)类型,则argp得到函数recv调用一次时可读入的数据量,通常和套接字中排队的数据总量相同;若s是数据报套接字(SOCK_DGRAM),则argp返回套接字排队的第一个数据报的大小。

·FIOASYNC:表示设置或清除异步I/O的命令。

argp为命令参数,是一个输入输出参数。如果函数成功就返回零,否则返回SOCKET_ERROR,此时可以用函数WSAGetLastError获取错误码。

比如下面的代码设置套接字为阻塞模式:

    u_long iMode = 0;
    ioctlsocket(m_socket, FIONBIO, &iMode);

如果参数iMode传入的是0,就设置阻塞,否则设置为非阻塞。

函数WSAIoctl是Winsock2中的I/O控制命令函数,功能更为强大,增加了一些输入参数,添加了一些新选项,并增加了一些输出函数以获得更多的信息,函数声明如下:

    int WSAIoctl(  SOCKET s,  DWORD dwIoControlCode,  LPVOID lpvInBuffer,
      DWORD cbInBuffer,  LPVOID lpvOutBuffer,  DWORD cbOutBuffer,
      LPDWORD lpcbBytesReturned,  LPWSAOVERLAPPED lpOverlapped,
      LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine);

·s:[in]套接字描述符(句柄)。

·dwIoControlCode:[in]存放用于操作的控制码,比如SIO_RCVALL(接收全部数据包的选项)。

·lpvInBuffer:[in]指向输入缓冲区地址。

·cbInBuffer:[in]输入缓冲区的字节大小。

·lpvOutBuffer:[out]指向输出缓冲区地址。

·cbOutBuffer:[in]输出缓冲区的字节大小。

·lpcbBytesReturned:[out]指向存放实际输出数据的字节大小的变量地址。

·lpOverlapped:[in]指向WSAOVERLAPPED结构体的地址(若是非重叠套接字则忽略该参数)。

·lpCompletionRoutine:[in]指向一个例程函数,该函数会在操作结束后调用(若是非重叠套接字则忽略该参数)。

如果函数成功就返回0,否则返回SOCKET_ERROR,可用WSAGetLastError获取错误码。

【例5.9】设置阻塞套接字为非阻塞套接字

(1)打开VC2017,新建一个控制台工程test。

(2)在test.cpp中输入如下代码:

    #include "stdafx.h"
    #define _WINSOCK_DEPRECATED_NO_WARNINGS // 为了使用inet_ntoa时不出现警告
    #include <Winsock2.h>
    #pragma comment(lib, "ws2_32.lib") //Winsock库的引入库

    #include <assert.h>
    #include <stdio.h>

    int main(int argc, char* argv[])
    {
    u_long argp;
    int res;
    char ip[] = "120.4.6.99"; //120.4.6.99是和本机同一网段的地址 ,但并不存在
    int port = 13334;
    struct sockaddr_in server_address;

    // Initialize Winsock
    WSADATA wsaData;
    int iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
    if (iResult != NO_ERROR)
         printf("Error at WSAStartup()\n");

    memset(&server_address, 0, sizeof(server_address));
    server_address.sin_family = AF_INET;
    DWORD dwIP = inet_addr(ip);
    server_address.sin_addr.s_addr = dwIP;
    server_address.sin_port = htons(port);

    SOCKET sock = socket(PF_INET, SOCK_STREAM, 0);
    assert(sock >= 0);

    long t1 = GetTickCount();

    int ret = connect(sock, (struct sockaddr*)&server_address,
sizeof(server_address));
    printf("connect ret code is: %d\n", ret);
    if (ret == -1)
    {
             long t2 = GetTickCount();

             printf("time used:%dms\n", t2 - t1);

             printf("connect failed...\n");
             if (errno == EINPROGRESS)
             {
                  printf("unblock mode ret code...\n");
             }
     }
     else
     {
             printf("ret code is: %d\n", ret);
     }

     argp = 1;
       res = ioctlsocket(sock, FIONBIO, (u_long FAR*)&argp);
     if (SOCKET_ERROR == res)
     {
             printf("Error at ioctlsocket(): %ld\n", WSAGetLastError());
             WSACleanup();
             return -1;
     }
     
     puts("设置非阻塞模式后:\n");

     memset(&server_address, 0, sizeof(server_address));
     server_address.sin_family = AF_INET;
     dwIP = inet_addr(ip);
     server_address.sin_addr.s_addr = dwIP;
     server_address.sin_port = htons(port);

     t1 = GetTickCount();
     ret = connect(sock, (struct sockaddr*)&server_address,
sizeof(server_address));
     printf("connect ret code is: %d\n", ret);
     if (ret == -1)
     {
             long t2 = GetTickCount();
             
             printf("time used:%dms\n", t2 - t1);
             
             printf("connect failed...\n");
             if (errno == EINPROGRESS)
                {
                             printf("unblock mode ret code...\n");
                }
         }
         else
         {
             printf("ret code is: %d\n", ret);
        }

         closesocket(sock);
        WSACleanup(); //释放套接字库
         return 0;
        }

在代码中,我们首先创建了一个套接字sock,刚开始默认是阻塞的,然后用connect函数去连接一个和本机IP同一子网的不真实存在的IP,会发现用了20多秒。接着我们用ioctlsocket函数把套接字sock设置为非阻塞,再同样用connect函数去连接一个和本机IP同一子网的不真实存在的IP,会发现connect立即返回了,这就说明我们设置套接字为非阻塞成功了。

(3)保存工程并运行,运行结果如图5-17所示。

图5-17

可以看到,大概等了20多秒后才提示connect失败。

把套接字设为非阻塞模式后,很多winsock api函数就会立即返回,但并不意味着操作已经完成。我们可以通过下例感受这一点。

函数WSAIoctl主要用于控制套接字的工作模式,声明如下:

        int WSAIoctl(  SOCKET s,  DWORD dwIoControlCode,  LPVOID lpvInBuffer,DWORD
    cbInBuffer,  LPVOID lpvOutBuffer,  DWORD cbOutBuffer,LPDWORD lpcbBytesReturned,
    LPWSAOVERLAPPED lpOverlapped,LPWSAOVERLAPPED_COMPLETION_ROUTINE
    lpCompletionRoutine);

其中,参数s表示套接字描述符;dwIoControlCode表示要执行操作的控制码;lpvInBuffer指向输入缓冲区;cbInBuffer表示输入缓冲区的字节大小;lpvOutBuffer[out]指向输出缓冲区;cbOutBuffer表示输出缓冲区的字节大小;lpcbBytesReturned[out]指向实际输出数据的字节大小;lpOverlapped指向WSAOVERLAPPED结构,该参数用于重叠套接字,非重叠套接字则忽略该参数;lpCompletionRoutine指向操作结束后调用例程,非重叠套接字则忽略该参数。如果函数执行成功就返回0,否则返回SOCKET_ERROR,此时可以用WSAGetLastError获取错误码。

需要注意的是,当套接字处于阻塞模式时,该函数可能阻塞线程;若套接字处于非阻塞模式且指定的操作不能及时完成时,WSAGetLastError将返回WSAEWOULDBLOCK错误码,此时程序可以将套接字改为阻塞模式后再次发送请求。