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