Visual C++ 2017网络编程实战
上QQ阅读APP看书,第一时间看更新

5.8 套接字选项

5.8.1 基本概念

除了可以通过发送I/O控制命令来影响套接字的行为外,还可以设置套接字的选项来进一步对套接字进行控制,比如我们可以设置套接字的接收或发送缓冲区大小、指定是否允许套接字绑定到一个已经使用的地址、判断套接字是否支持广播、控制带外数据的处理、获取和设置超时参数等。当然除了设置选项外,还可以获取选项,选项的概念相当于属性的意思。所以套接字选项也可说是套接字属性,选项就是用来描述套接字本身属性特征的。

值得注意的是,有些选项(属性)只可获取,不可设置,而有些选项既可设置也可获取。

5.8.2 选项的级别

有一些选项是针对一种特定协议的,意思就是这些选项都是某种套接字特有的;又有一些选项适用于所有类型的套接字,因此就有了选项级别(level)概念,即选项的适用范围或适用对象,是适用所有类型套接字还是适用某种类型套接字。常用的级别有:

·SOL_SOCKET:该级别的选项与套接字使用的具体协议无关,只作用于套接字本身。

·SOL_LRLMP:该级别的选项作用于IrDA协议。

·IPPROTO_IP:该级别的选项作用于IPv4协议,因此与IPv4协议的属性密切相关,比如获取和设置IPv4头部的特定字段。

·IPPROTO_IPV6:该级别的选项作用于IPv6协议,有一些选项和IPPROTO_IP对应。

·IPPROTO_RM:该级别的选项作用于可靠的多播传输。

·IPPROTO_TCP:该级别的选项适用于流式套接字。

·IPPROTO_UDP:该级别的选项适用于数据报套接字。

这些都是宏定义,可以直接用在函数参数中。

通常,不同的级别选项值也不尽相同。下面我们来看一下级别为SOL_SOCKET的选项(见表5-3)。

表5-3 级别为SOL_SOCKET的选项

再来看一下级别IPPROTO_IP的常用选项(见表5-4)。

表5-4 级别IPPROTO_IP的常用选项

5.8.3 获取套接字选项

Winsock提供了API函数getsockopt来获取套接字的选项。函数getsockopt声明如下:

       int getsockopt(  SOCKET s,  int level,  int optname,  char* optval,  int*
optlen);

其中,参数s是套接字描述符;level表示选项的级别,比如可以取值SOL_SOCKET、IPPROTO_IP、IPPROTO_TCP、IPPROTO_UDP等;optname表示要获取的选项名称;optval[out]指向存放接收到的选项内容的缓冲区,char*表示传入的是optval的地址,optval具体类型要根据选项而定,具体可以参考5.8.2小节;optlen[in,out]指向optval所指缓冲区的大小。如果函数执行成功就返回0,否则返回SOCKET_ERROR,此时可用函数WSAGetLastError来获得错误码,常见的错误码如下:

·WSANOTINITIALISED:在调用getsockopt函数前没有成功调用WSAStartup函数。

·WSAENETDOWN:网络子系统出现故障。

·WSAEFAULT:参数optlen太小或optval所指缓冲区非法。

·WSAEINPROGRESS:一个阻塞的Windows Sockets 1.1调用正在进行,或者WindowsSockets在处理一个回调函数。

·WSAEINVAL:参数level未知或非法。

·WSAENOPROTOOPT:选项未知或不被指定的协议簇所支持。

·WSAENOTSOCK:描述符不是一个套接字描述符。

【例5.10】获取流和数据报套接字接收和发送的(内核)缓冲区大小

(1)新建一个控制台工程test。

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

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

       int _tmain(int argc, _TCHAR* argv[])
       {
        WORD wVersionRequested;
        WSADATA wsaData;
        int err;

        wVersionRequested = MAKEWORD(2, 2); //制作Winsock库的版本号
        err = WSAStartup(wVersionRequested, &wsaData); //初始化Winsock库
        if (err != 0) return 0;

        SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);//创建流套接字
        if (s == INVALID_SOCKET) {
             printf("Error at socket()\n");
             WSACleanup();
             return -1;
        }

        SOCKET su = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //创建数据报套接字
        if (s == INVALID_SOCKET) {
             printf("Error at socket()\n");
             WSACleanup();
             return -1;
        }

        DWORD optVal;
        int optLen = sizeof(optVal);
       //获取流套接字接收缓冲区大小
        if (getsockopt(s, SOL_SOCKET, SO_RCVBUF, (char*)&optVal, &optLen) ==
    SOCKET_ERROR)
             printf("getsockopt failed:%d", WSAGetLastError());
        else
             printf("流套接字接收缓冲区的大小: %ld bytes\n", optVal);

       //获取流套接字发送缓冲区大小
        if (getsockopt(s, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen) ==
    SOCKET_ERROR)
             printf("getsockopt failed:%d", WSAGetLastError());
        else
             printf("流套接字发送缓冲区的大小: %ld bytes\n", optVal);
             
        //获取数据报套接字接收缓冲区大小
        if (getsockopt(su, SOL_SOCKET, SO_RCVBUF, (char*)&optVal, &optLen) ==
    SOCKET_ERROR)
             printf("getsockopt failed:%d", WSAGetLastError());
        else
             printf("数据报套接字接收缓冲区的大小: %ld bytes\n", optVal);
        //获取数据报套接字发送缓冲区大小
        if (getsockopt(su, SOL_SOCKET, SO_SNDBUF, (char*)&optVal, &optLen) ==
    SOCKET_ERROR)
             printf("getsockopt failed:%d", WSAGetLastError());
        else
             printf("数据报套接字发送缓冲区的大小: %ld bytes\n", optVal);

        WSACleanup();
        system("pause");
        return 0;
        }

在上述代码中,首先创建了一个流套接字和数据报套接字,然后通过getsockopt函数来获取它们接收和发送缓冲区的大小,最后输出。注意,缓冲区大小的选项级别是SOL_SOCKET,不要写错了。而且,获取缓冲区大小的时候,optVal的类型要定义为DWORD,然后把其指针传给getsockopt。

(3)保存工程并运行,运行结果如下:

    流套接字接收缓冲区的大小: 8192 bytes
    流套接字发送缓冲区的大小: 8192 bytes
    数据报套接字接收缓冲区的大小: 8192 bytes
    数据报套接字发送缓冲区的大小: 8192 bytes

【例5.11】获取当前套接字类型

(1)新建一个控制台工程test。

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

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

    int _tmain(int argc, _TCHAR* argv[])
    {
    WORD wVersionRequested;
    WSADATA wsaData;
    int err;

    wVersionRequested = MAKEWORD(2, 2); //制作Winsock库的版本号
    err = WSAStartup(wVersionRequested, &wsaData); //初始化Winsock库
    if (err != 0) return 0;

    SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建流套接字
    if (s == INVALID_SOCKET) {
             printf("Error at socket()\n");
             WSACleanup();
             return -1;
    }

    SOCKET su = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); //创建数据报套接字
    if (s == INVALID_SOCKET) {
             printf("Error at socket()\n");
             WSACleanup();
             return -1;
    }

    DWORD optVal;
    int optLen = sizeof(optVal);
    //获取套接字s的类型
    if (getsockopt(s, SOL_SOCKET, SO_TYPE, (char*)&optVal, &optLen) ==
SOCKET_ERROR)
             printf("getsockopt failed:%d", WSAGetLastError());
    else
    {
             if(SOCK_STREAM== optVal) // SOCK_STREAM宏定义值为1
                  printf("当前套接字是流套接字\n");
             else if(SOCK_DGRAM == optVal) // SOCK_ DGRAM宏定义值为2
                  printf("当前套接字是数据报套接字\n");
    }
    //获取套接字su的类型
    if (getsockopt(su, SOL_SOCKET, SO_TYPE, (char*)&optVal, &optLen) ==
SOCKET_ERROR)
             printf("getsockopt failed:%d", WSAGetLastError());
    else
    {
             if (SOCK_STREAM == optVal)  // SOCK_STREAM宏定义值为1
                  printf("当前套接字是流套接字\n");
             else if (SOCK_DGRAM == optVal) // SOCK_ DGRAM宏定义值为2
                  printf("当前套接字是数据报套接字\n");
     }
     WSACleanup();
     system("pause");
     return 0;
     }

在上述代码中,先创建了一个流套接字s和数据报套接字su,然后用getsockopt来获取套接字类型并输出。获取套接字类型的选项是SO_TYPE,因此我们把SO_TYPE传入getsockopt函数中。

(3)保存工程并运行,运行结果如下:

    当前套接字是流套接字
    当前套接字是数据报套接字

【例5.12】判断套接字是否处于监听状态

(1)新建一个控制台工程test。

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

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

    int _tmain(int argc, _TCHAR* argv[])
    {
    WORD wVersionRequested;
    WSADATA wsaData;
    int err;
    sockaddr_in service;
    char ip[] = "120.4.6.200";//本机IP

    wVersionRequested = MAKEWORD(2, 2); //制作Winsock库的版本号
    err = WSAStartup(wVersionRequested, &wsaData); //初始化Winsock库
    if (err != 0) return 0;

    SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建一个流套接字
    if (s == INVALID_SOCKET) {
         printf("Error at socket()\n");
         WSACleanup();
         return -1;
    }

    service.sin_family = AF_INET;
    service.sin_addr.s_addr = inet_addr(ip);
    service.sin_port = htons(9900);
    if (bind(s, (SOCKADDR*)&service, sizeof(service))==SOCKET_ERROR)//绑定套接字
    {
         printf("bind failed\n");
         WSACleanup();
         return -1;
    }

    DWORD optVal;
    int optLen = sizeof(optVal);
    //获取选项SO_ACCEPTCONN的值
    if (getsockopt(s, SOL_SOCKET, SO_ACCEPTCONN, (char*)&optVal, &optLen) ==
SOCKET_ERROR)
             printf("getsockopt failed:%d", WSAGetLastError());
    else printf("监听前,选项SO_ACCEPTCONN的值=%ld, 套接字未处于监听状态\n", optVal);

    // 开始侦听
    if (listen(s, 100) == SOCKET_ERROR)
    {
             printf("listen failed:%d\n", WSAGetLastError());
             WSACleanup();
             return -1;
     }
     //获取选项SO_ACCEPTCONN的值
     if (getsockopt(s, SOL_SOCKET, SO_ACCEPTCONN, (char*)&optVal, &optLen) ==
SOCKET_ERROR)
     {
             printf("getsockopt failed:%d", WSAGetLastError());
             WSACleanup();
             return -1;
     }
     else printf("监听后,选项SO_ACCEPTCONN的值=%ld, 套接字处于监听状态\n", optVal);

     WSACleanup();
     system("pause");
     return 0;
     }

在上述代码中,分别在调用监听函数listen前后分别获取了选项SO_ACCEPTCONN的值,可以发现监听前该选项值为0,监听后选项值为1了,符合预期。

(3)保存工程并运行,运行结果如下:

    监听前,选项SO_ACCEPTCONN的值=0,套接字未处于监听状态
    监听后,选项SO_ACCEPTCONN的值=1,套接字处于监听状态

5.8.4 设置套接字选项

Winsock提供了API函数setsockopt来获取套接字的选项。函数getsockopt声明如下:

        int setsockopt(  SOCKET s,  int level,  int optname,  const char* optval, int
optlen);

其中,参数s是套接字描述符;level表示选项的级别,比如可以取值SOL_SOCKET、IPPROTO_IP、IPPROTO_TCP、IPPROTO_UDP等;optname表示要获取的选项名称;optval指向存放要设置的选项值的缓冲区,char*表示传入的是optval的地址,optval具体类型要根据选项而定,具体可以参考5.8.2小节的内容;optlen指向optval所指缓冲区的大小。如果函数执行成功就返回0,否则返回SOCKET_ERROR,此时可用函数WSAGetLastError来获得错误码。错误码和getsockopt出错时类似,这里不再赘述。

【例5.13】启用套接字的保活机制

(1)新建一个控制台工程test。

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

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

       int _tmain(int argc, _TCHAR* argv[])
       {
        WORD wVersionRequested;
        WSADATA wsaData;
        int err;
        sockaddr_in service;
        char ip[] = "120.4.6.200";//本机IP

        wVersionRequested = MAKEWORD(2, 2); //制作Winsock库的版本号
        err = WSAStartup(wVersionRequested, &wsaData); //初始化Winsock库
        if (err != 0) return 0;

        SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); //创建一个流套接字
        if (s == INVALID_SOCKET) {
             printf("Error at socket()\n");
             WSACleanup();
             return -1;
        }

        service.sin_family = AF_INET;
        service.sin_addr.s_addr = inet_addr(ip);
        service.sin_port = htons(9900);
        if (bind(s, (SOCKADDR*)&service, sizeof(service))==SOCKET_ERROR)//绑定套接字
        {
             printf("bind failed\n");
             WSACleanup();
             return -1;
        }

        BOOL  optVal=TRUE;//一定要初始化
        int optLen = sizeof(BOOL);

        //获取选项SO_KEEPALIVE的值
        if (getsockopt(s, SOL_SOCKET, SO_KEEPALIVE, (char*)&optVal, &optLen) ==
    SOCKET_ERROR)
        {
             printf("getsockopt failed:%d", WSAGetLastError());
             WSACleanup();
             return -1;
        }
        else printf("监听后,选项SO_ACCEPTCONN的值=%ld\n", optVal);

        optVal = TRUE;
        if (setsockopt(s, SOL_SOCKET, SO_KEEPALIVE, (char*)&optVal, optLen) !=
    SOCKET_ERROR)
        {
             printf("启用保活机制成功\n");
        }
        if (getsockopt(s, SOL_SOCKET, SO_KEEPALIVE, (char*)&optVal, &optLen) ==
    SOCKET_ERROR)
        {
             printf("getsockopt failed:%d", WSAGetLastError());
             WSACleanup();
             return -1;
        }
        else printf("设置后,选项SO_KEEPALIVE的值=%d\n", optVal);

        WSACleanup();
        system("pause");
        return 0;
        }

值得注意的是,存放选项SO_KEEPALIVE值的变量类型是BOOL,并且要初始化。

(3)保存工程并运行,运行结果如下:

    设置前,选项SO_ACCEPTCONN的值=0
    启用保活机制成功
    设置后,选项SO_KEEPALIVE的值=1