第3章WinSock编程初步 本章主要介绍使用WinSock编写网络通信程序应掌握的一些预备知识和方法。这些知识和方法包括Windows API函数的概念、如何将WinSock的开发组件和运行组件加载到程序中、WinSock中的网络地址如何表示、如何查询计算机和网络的配置信息等。 3.1WinSock API函数 WinSock是Windows操作系统的网络编程接口规范,其全称为Windows Sockets,它是微软公司以BSD UNIX的Berkeley Sockets规范为基础开发定义的,从1991年问世到现在出现过两个主版本——WinSock1和WinSock2。WinSock1只支持TCP/IP协议栈,最流行的版本是WinSock1.1; WinSock2完全兼容WinSock1,但支持多种协议,如NETBIOS、IPX等,最流行的版本是WinSock2.2。 WinSock规范给出了一套库函数的函数原型和函数功能说明,这套库函数就是所谓的Windows 套接字应用程序编程接口,常被简称为WinSock API函数或WinSock函数; 这套函数由底层网络软件供应商实现,并提供给高层网络应用程序开发者使用。 WinSock API函数可分为两大类: 一类是与Berkeley Sockets相兼容的基本函数,例如,用于创建套接字的socket()函数,用于发送数据的send()函数等,这一类函数不仅提供了与BSD Socket完全相同的功能,而且函数格式也兼容,因此使用这类函数编写的网络应用程序可以很容易地移植到Linux、UNIX等采用BSD Socket的系统中,保证了程序的可移植性; 另一类是Windows扩展函数,这类函数都以WSA为前缀,主要是为便于程序员充分利用Windows的消息驱动机制编程而提供的。 应用程序是调用WinSock API函数实现相互之间通信的,而WinSock API函数又利用了下层Windows操作系统中的网络通信协议和相关的系统调用来实现自身的通信功能。WinSock与网络应用程序和操作系统中的网络通信协议软件之间的关系如图3.1所示。 图3.1应用程序与WinSock以及网络协议软件的关系 熟练掌握常用WinSock API函数的功能和使用方法是使用WinSock进行网络编程的基础。对一个具体的WinSock API函数来说,学习掌握其使用方法的步骤与学习一般C/C++库函数的方法是类似的,首先是要记住该函数的名称与函数的主要功能,其次就是要清楚各参数的类型及作用以及函数返回值的类型及意义,最后还应掌握函数成功执行的必要条件并了解造成函数不能成功执行的常见原因。 3.2WinSock开发组件和运行组件 WinSock是以Windows动态链接库(Dynamic Link Library,DLL)的形式实现的。主要由两部分组成——开发组件和运行组件。 动态链接库是一个包含可供多个程序同时使用的函数和全局变量的库,DLL本身是不可执行的,应用程序在运行过程中可动态装入和连接DLL并直接使用其中的函数和全局变量。多个应用程序可同时使用内存中的单个DLL 副本,DLL是一种代码共享的方式。 从编程角度,动态链接库可以提高程序模块的灵活性,它本身是可以单独设计、编译和调试的。DLL 的编制与具体的编程语言及编译器无关,只要遵循约定的DLL接口规范和调用方式,不管是用何种语言编写的DLL,各种语言的程序都可使用。 Windows系统提供了大量的DLL,这些DLL包含允许Windows系统本身和应用程序使用的许多函数和资源,这些DLL一般被存放在C:\Windows\System32目录下。Windows的DLL多数情况下是带有DLL扩展名的文件,但也可能是exe或其他扩展名。使用Visual Studio的程序员可以创建自己的DLL。 要使用DLL中的函数必须要将DLL链接到应用程序。链接方式有两种: 一种称为显式方式,在这种方式中,首先要在程序中调用LoadLibrary调入DLL文件,再调用GetProcAddress获得DLL库中的函数指针,最后再通过指针调用DLL中的函数; 另一种称为隐式方式,这种方式要用到一种称为DLL导入库的文件(.lib文件),DLL导入库文件中包含DLL中定义的变量和函数等的地址符号表,使用这种方式,程序中只需要按照头文件中的函数声明来调用函数,在建立可执行程序时与DLL导入库文件链接就可以了。显然,使用隐式链接方式要方便得多。 WinSock开发组件是提供给程序员开发程序使用的,最主要的部分是包含程序开发所需的常量定义、宏定义、有关的数据结构定义以及函数调用接口的原型声明等的C/C++头文件,除此之外,还包括WinSock的实现文档以及WinSock动态链接库的导入库。运行组件则是指包含WinSock API函数的WinSock动态链接库。不同版本的WinSock所对应的开发组件的相关的文件名字是不同的。WinSock1对应的头文件是WinSock.h,导入库文件是Wsock32.lib,而WinSock2的头文件是WinSock2.h,导入库文件则是Ws2_32.lib。 由于WinSock2完全兼容WinSock1,因此,当系统中安装的是WinSock2时,程序中既可以使用WinSock1的头文件和导入库,也可以使用WinSock2的头文件和导入库,但如果要使用只有WinSock2才有的功能,就只能使用WinSock2的头文件和导入库了。 在Visual C++中使用WinSock,下面的步骤通常是必不可少的。 1. 包含WinSock头文件 包含WinSock头文件需要在程序文件首部使用编译预处理命令“#include”,比如程序要使用WinSock2,则程序前面需要使用如下预处理命令将WinSock2.h包含进来。 #include 需要说明一下,如果程序需要包含头文件Windows.h,但是程序已包含WinSock头文件,则程序就不用再包含Windows.h了。原因是WinSock的实现使用了Windows.h中的函数声明以及常量、变量等的定义,在WinSock的头文件中已将它包含进去了。 2. 链接WinSock导入库 链接WinSock导入库的方式有两种: 一种是通过在项目属性页中的“配置属性”→“链接器”→“输入”的“附加依赖项”中直接添加导入库名字; 另一种则是在程序中使用预处理命令“#pragma comment”。例如,程序要使用WinSock2时,可使用如下预处理命令。 #pragma comment (lib, "Ws2_32.lib") 该命令会添加一个注释到编译生成的OBJ文件,告诉链接器链接时要链接库文件Ws2_32.lib。 3. 加载WinSock动态链接库 应用程序运行时必须装入WinSock动态链接库才能调用WinSock函数实现网络通信功能。加载WinSock动态链接库要使用WSAStartup()函数,该函数原型如下。 int WSAStartup (WORD wVersionRequested, LPWSADATA lpWSAData ); 函数参数  wVersionRequested: 该参数是一个双字节类型数值,用于指明程序中要使用的WinSock库的版本号,其中,高位字节指定副版本,低位字节指定主版本。早期Windows平台一般使用版本号是1.1的WinSock1,现在常用的则是WinSock2的2.2版。指定该参数值时可使用十六进制方式给出,比如要加载WinSock2.2版,该值可设定为0x0202,但更常用的方法则是使用宏MAKEWORD(X,Y),参数X为副版本号,Y为主版本号。  lpWSAData: 该参数用于返回关于使用的WinSock版本的详细信息,它是一个指向WSADATA 结构体变量的指针。WSADATA 结构体的定义如下。 #define WSADESCRIPTION_LEN 256 #define WSASYSSTATUS_LEN 128 Typedef struct WSAData { WORD wVersion; //期望程序使用的 WinSock版本号 WORD wHighVersion; //加载的WinSock库所支持的最高WinSock版本号 char szDescription[WSADESCRIPTION_LEN+1]; //加载的WinSock库说明 char szSystemStatus[WSASYSSTATUS_LEN+1]; //状态和配置信息 unsigned short iMaxSockets; //能同时打开的socket的最大数(该字段被WinSock2及其后版本忽略) unsigned short iMaxUdpDg; //可发送UDP数据报的最大字节数(该字段被WinSock2及其后版本忽略) char FAR * lpVendorInfo; //厂商信息(该字段被WinSock2及其后版本忽略) } WSADATA; 函数返回值 WSAStartup()函数的返回值是一个整数,函数调用成功返回0,如果不成功则返回如下所列的值之一,这些常量在WinSock2.h中定义。  WSASYSNOTREADY: 对应数值为10091,表示此时WinSock不可用,用户应该检查是否存在适合的Windows Sockets DLL文件或底层网络子系统能否使用,另外,如果有多于一个的WinSock DLL在系统中,必须确保搜索路径中第一个WinSock DLL文件是当前加载的网络子系统所需要的,否则也会引发该错误。  WSAVERNOTSUPPORTED: 对应数值为10092,系统当前安装的WinSock实现不支持应用程序指定的WinSock版本,用户应检查是否有旧的Windows Socket DLL文件正在被访问。  WSAEINVAL: 对应数值为10022,表示提供了非法函数参数。  WSAEINPROGRESS: 对应数值为10036,一个阻塞操作正在执行。Windows Sockets只允许一个任务(或线程)在同一时间可以有一个未完成的阻塞操作,如果此时调用了任何函数(不管此函数是否引用了该套接字或任何其他套接字),此函数将以错误码WSAEINPROGRESS返回。  WSAEPROCLIM: 对应数值为10067,Windows Sockets的实现可能会限制同时使用它的应用程序的数量,如果系统中使用WinSock的应用程序数量已达到此限制,再调用WSAStartup()函数加载WinSock库则会失败并返回该错误码。  WSAEFAULT: 对应数值为10014,表示lpWSAData指向的地址非法。 假如一个程序要使用2.2版本的WinSock,那么程序中可采用如下代码加载WinSock动态链接库。 WSADATA wsaData; WORD wVersionRequested = MAKEWORD( 2, 2 ); if(WSAStartup( wVersionRequested, &wsaData )!=0) { //WinSock初始化错误处理代码 … } 如果程序在没有成功加载WinSock动态链接库的情况下调用WinSock API函数,则被调函数不能成功执行,并返回错误代码WSANOTINITIALISED,其对应值为10093。 4. 注销WinSock动态链接库 应用程序在完成对WinSock动态链接库的使用后,需要解除与WinSock库的绑定(注销),并且释放WinSock库所占用的系统资源。注销WinSock动态链接库需要使用WSACleanup()函数,该函数原型如下。 int WSACleanup (void); 该函数无参数,执行后将返回一个整数值,如果操作成功返回0,否则返回SOCKET_ERROR。常量SOCKET_ERROR在Winsok2.h中定义,其对应值为-1。 一个程序中的每一次WSAStartup()调用,都应该有一个WSACleanup()调用与之对应。 3.3网络字节顺序 前面已介绍过,计算机在存储多字节数据时存在大端字节顺序(Big Endian)和小端字节顺序(Little Endian)两种方式,对于字符编码,人们给出的编码标准中明确规定了采用的字节顺序,但对于整型数据则并不存在类似的规定,这是什么原因呢?整型数据是最基本的数据类型,也是计算机CPU指令能直接处理的数据类型,之所以存在大端和小端顺序两种字节顺序,就源于CPU内部表示整型数据的字节顺序不同,例如,常见的PC上基于X86架构的CPU是小端字节顺序的,而PowerPC系列的CPU大多采用的是大端字节顺序。为了提高处理速度,整数各字节无论是在外部存储器还是在内存中,其存放顺序都必须与CPU一致。 无论采用的是大端顺序还是小端顺序,在网络通信中,对一台计算机所采用的字节顺序都统称为主机字节顺序。当不同字节顺序的计算机在通过网络交换数据时,如果不做任何处理,将会出现严重问题。例如,一台使用PowerPC系列的CPU、运行UNIX的服务器发送一个16位数据0x1234 到一台采用Intel酷睿i5 系列 CPU运行Windows 7的PC时,这个16位数据将被Intel的CPU解释为0x3412,也就是将整数4660当成了13330。 为了解决这一问题,在编写网络程序时,规定发送端要发送的多字节数据必须先转换成与具体CPU无关的网络字节顺序再发送,接收端接收到数据后再将数据转换为主机字节数据。网络字节顺序采用的是大端存储方式。 在指定套接字的网络地址以及端口号时必须使用网络字节顺序,而由套接字函数返回的网络字节顺序的IP地址和端口号在本机处理时,则需要转换为主机字节顺序。在套接字编程接口中有专门的函数来完成网络字节顺序和主机字节顺序的转换。 1. htons()函数 该函数将一个16位的无符号短整型数据由主机字节顺序转换为网络字节顺序。 函数原型 u_shorthtons (u_shorthostshort); 函数参数 hostshort: 一个待转换的主机字节顺序的无符号短整型数据。 返回值 函数调用成功返回一个网络字节顺序的无符号短整型数。如果函数调用失败,则返回SOCKET_ERROR,进一步的出错信息可调用WSAGetLastError()获取。 2. ntohs()函数 该函数将一个16位的无符号短整型数据由网络字节顺序转换为主机字节数顺序返回。 函数原型 u_shortntohs (u_shortnetshort); 函数参数 netshort: 一个待转换的网络字节数顺序的无符号短整型数据。 返回值 函数调用成功返回一个主机字节顺序的无符号短整型数。如果函数调用失败,则返回SOCKET_ERROR,进一步的出错信息可调用WSAGetLastError()获取。 3. htonl()函数 该函数将一个32位的无符号长整型数据由主机字节顺序转换为网络字节顺序返回。 函数原型 u_long htonl (u_long hostlong); 函数参数 hostlong: 一个待转换的主机字节顺序的无符号长整型数据。 返回值 函数调用成功返回一个网络字节顺序的无符号长整型数。如果函数调用失败,则返回SOCKET_ERROR,进一步的出错信息可调用WSAGetLastError()获取。 4. ntohl()函数 该函数将一个32位的无符号长整型数据由网络字节顺序转换为主机字节数顺序返回。 函数原型 u_long ntohl (u_long netlong); 函数参数 netlong: 一个待转换的网络字节数顺序的无符号长整型数据。 返回值 函数调用成功返回一个主机字节顺序的无符号长整型数。如果函数调用失败,则返回SOCKET_ERROR,进一步的出错信息可调用WSAGetLastError()获取。 3.4WinSock的网络地址表示 在IP网络环境下,对于一个通信进程而言,必须明确三方面信息: 一是进程所在的主机IP地址,二是通信所采用的协议,三是所使用的协议端口号。IP地址可以区分网络中的不同主机,协议则指明通信所使用的传输层协议是TCP还是UDP,而端口号则可以用于区分同一主机中正在运行的采用同一传输层协议进行通信的不同进程。通过这三方面的信息可以唯一确定在网络中参与通信的一个进程,因此进程的网络地址可以使用三元组(协议,IP地址,端口号)来标识。 3.4.1地址结构 程序中可以通过套接字的不同类型来指明通信所使用的传输协议: 流式套接字采用TCP,数据报套接字采用UDP。IP地址和协议端口号又是如何表示的呢?针对不同的应用环境,WinSock定义了三种专门的地址结构来存储IP地址和端口号,这三种结构均继承于BSD Socket规范。 1. in_addr结构 用于存储一个IPv4地址,结构定义如下。 struct in_addr { union { struct{u_char s_b1,s_b2,s_b3,s_b4}S_un_b; struct {u_short s_w1,s_w2}S_un_w; U_long S_addr; } S_un ; } 该结构只有一个共用体(union)类型的字段S_un,专门用来存储32位的IP地址。仔细观察共用体的定义可以发现,共用体的三个成员分别如下。 (1) 由4个单字节字段组成的结构体变量S_un_b,使用该成员便于分别处理IP地址的4个字节; (2) 由2个双字字段组成的结构体变量S_un_w,使用该成员可将IP地址的高16位和低16位分别作为无符号短整数处理; (3) 无符号长整数变量S_addr,使用该成员可将IP地址作为一个无符号长整数处理。 由于人们习惯使用点分十进制表示的IP地址,因此当直接为该结构类型的变量赋值时通常使用S_un_b成员,具体方法可参见如下代码,这段代码将IP地址“192.168.1.1”赋值到in_addr型变量add中。 struct in_addr add; add.S_un.S_un_b.s_b1=192; add.S_un.S_un_b.s_b2=168; add.S_un.S_un_b.s_b3=1; add.S_un.S_un_b.s_b4=1; 2. sockaddr_in结构 用于存储IP地址、传输协议端口号等信息。该结构对IP地址和协议端口号进行了封装。结构定义如下。 struct sockaddr_in { short sin_family;//地址族,IP协议地址对应的值为AF_INET u_short sin_port; //16位端口号,需使用网络字节顺序 struct in_addr sin_addr; //32位IP地址,需使用网络字节顺序 char sin_zero[8]; //保留不用 } 该结构是专门针对TCP/IP协议地址结构的,字段sin_family用于存放代表不同协议族地址的代码,TCP/IP协议族的代码为AF_INET,该常量在Winsok2.h中定义; 字段sin_port用于存放程序通信使用的TCP或UDP端口号; sin_addr用于存放IP地址,它是一个in_addr结构的变量; 最后一个字段sin_zero是一个8字节大小的字符数组,它在TCP/IP协议族地址结构中没有意义,仅仅是为了与通用地址结构兼容而保留的。 3. sockaddr结构 sockaddr结构为通用套接字地址结构。当使用TCP/IP时,该结构内容与sockaddr_in完全相同。结构定义如下。 struct sockaddr { u_short sa_family; //协议地址族 char sa_data[14]; //协议地址 } sockaddr结构是为了兼容多个不同协议族的地址而设计的。事实上,不管是BSD Sockets规范还是WinSock规范,并不仅仅针对TCP/IP协议族,它们在设计时就考虑了要兼容现存的多个网络通信协议族,例如IPX、NETBIOS等,不同协议族的地址格式是完全不同的。当程序使用TCP/IP时,sockaddr结构的第一个域sa_family应填写AF_INET,表示IP协议族地址; 第二个域sa_data[14]的前两个字节是端口号,随后4个字节是IP地址,余下的8个字节不用。 不难看出,直接填写第二个字节还是比较复杂的,能不能找到一种简单方法为该地址结构类型的变量赋值?答案是肯定的。再回头观察一下sockaddr_in结构,可以发现这两个结构存储的内容完全一致,因此,在为sockaddr结构的变量赋值时,可先利用C/C++语言的强制类型转换将该结构变量的地址赋给一个sockaddr_in类型的指针,然后通过该指针将各项内容填入结构体变量中。具体方法参见如下代码。首先定义一个sockaddr_in结构类型的指针变量p,通过强制类型转换让p指向sockaddr结构类型的变量,然后通过指针p可按sockaddr_in类型的各字段完成赋值。 struct sockaddr a; struct sockaddr_in *p; p=(sockaddr_in *) &a; p->sin_family=AF_INET; p->sin_port=54321; p->sin_addr=inet_addr("192.168.1.1"); 其中,inet_addr()函数的功能是将参数指定的点分十进制表示的IP地址转换为无符号长整型(usigned long)。 当sockaddr结构指针作为函数形参时,可以直接将sockaddr_in结构变量的地址强制转换为struct sockaddr*类型作为实参。由于很多WinSock函数都是以sockaddr结构类型的指针作为参数的,这种方法在以后的例题中会经常使用。 3.4.2地址转换函数 点分十进制表示的IP地址是一种便于人们书写和记忆的格式,但它并不是计算机内部的IP地址表示方式,计算机内部的IP地址是以无符号长整数方式存储的。因此,在程序中,输入输出IP地址时通常是使用点分十进制表示的IP地址,而程序内部则使用无符号长整型的IP地址,这就导致程序中经常要进行IP地址不同表示形式间的转换。为了编程方便,WinSock提供了一组地址转换函数。 1. inet_addr()函数 函数原型 unsigned long inet_addr (const char * cp); 函数功能 将参数cp指向的点分十进制字符串表示的IP地址转换为32位的无符号长整型数。 函数参数 cp: 指向存放有一个点分十进制表示的IP地址的字符串,当字符串的形式为“a.b.c.d”时,a、b、c、d分别代表IP地址的4个字节; 当字符串形式为“a.b.c”时,a、b分别对应IP地址的高位两个字节,c对应于后两个字节; 当字符串形式为“a.b”时,a被解释成IP地址的最高一个字节,而b则解释为后面的三个字节24位; 当形式为“a”时,a将直接被作为网络字节顺序的二进制表示的IP地址返回。 返回值 函数调用成功后将返回一个无符号长整型数(unsigned long),它是以网络字节顺序表示的32位二进制IP地址,如果传入的字符串是一个非法的IP地址,返回值将是INADDR_NONE。 2. inet_aton()函数 函数原型 int inet_aton(const char *cp, struct in_addr *inp); 函数功能 将参数cp指向的点分十进制字符串表示的IP地址转换为32位的无符号长整型数,并存放于参数inp指向的in_addr结构变量中。 函数参数  cp: 与函数inet_addr的参数cp相同。  inp: 指向in_addr结构变量的指针,该结构变量用来保存转换后的IP地址。 返回值 如果这个函数成功,函数的返回值非零。如果输入地址不正确则会返回零。 3. inet_ntoa()函数 函数原型 char * inet_ntoa (struct in_addr in); 函数功能 将一个包含在in_addr结构变量中的长整型IP地址转换为点分十进制形式。 函数参数 in: 是一个保存有32位二进制IP地址的in_addr结构变量。 返回值 函数调用成功返回一个字符指针,该指针指向一个char型缓冲区,该缓冲区保存有由参数in的值转换而来的点分十进制表示的IP地址字符串。如果函数调用失败,则返回一个空指针NULL。 这三个函数来自于早期的BSD套接字规范,并不支持目前已逐渐普及的IPv6的地址转换,为了兼容IPv6的地址转换,新规范引入了inet_pton()与inet_ntop()两个函数来取代上面这三个函数。需要说明的是,这三个函数目前仍被广泛使用,因此读者也应掌握这三个函数的用法。 在Visual C++2017中使用这三个地址转换函数时,编译器将发出错误警告并停止编译,如果要关闭错误警告继续编译,需要在程序首部添加如下宏定义。 #define _WINSOCK_DEPRECATED_NO_WARNINGS 或者使用以下预处理命令: #pragma warning(disable : 4996) 或者在项目属性中将“配置属性/C/C++/常规”中的“SDL检查”设置为“否(sdl)”。 下面介绍已逐渐流行的新的地址转换函数inet_pton()和inet_ntop(),需要注意,使用这两个函数时需要包含头文件WS2tcpip.h。 4. inet_pton()函数 函数原型 int inet_pton(int af, const char *src, void *dst); 函数功能 参数af取值AF_INET时,该函数将参数src指向的点分十进制表示的IPv4地址转换为32位的网络字节顺序的无符号长整型数,并存放于参数dst指向的in_addr结构变量中; 参数af取值AF_INET6时,该函数将参数src指向的冒号十六进制表示的IPv6地址转换为二进制IPv6地址,并存放于参数dst指向的in6_addr结构变量中。由于篇幅所限,本书暂不考虑IPv6网络编程。 函数参数  af: 使用的地址族,目前有两个取值: AF_INET表示IPv4,AF_INET6表示IPv6。  src: 指向要转换的字符串形式表示的地址。  dst: 指向用于保存转换结果的地址结构变量,IPv4时该结构变量的类型为in_addr,IPv6则是in6_addr。 返回值 如果函数执行成功将返回1,出错将返回-1并将错误码设置为EAFNOSUPPORT,如果参数af指定的地址族或src格式不对,函数将返回0。 下面的代码是该函数的使用示例,功能是将IP地址“192.168.1.1”转换为无符号长整型数存放在in_addr型变量a中,然后按十六进制输出该地址。 in_addr a ; if (inet_pton(AF_INET, "192.168.1.1", &a) == 1) { cout << hex< #include "WinSock2.h"//包含WinSock库头文件 #include "ws2tcpip.h" #pragma comment(lib, "ws2_32.lib") //链接WinSock导入库 using namespace std; int main(int argc, char **argv) { /***加载WinSock DLL***/ WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { cout << "加载WinSock DLL失败!\n"; return 0; } char hostname[256]; //用于存放获取的本机名称或输入的远程主机域名 if (gethostname(hostname, sizeof(hostname)))//获取本机名字 { cout << "获取主机名字失败!\n" << endl; WSACleanup(); return 0; } cout << "本机名字为: " << hostname << endl; /*根据主机名字查询主机的IPv4地址*/ struct addrinfo hints, *p_addrinfo, *p; memset(&hints,0, sizeof(hints)); hints.ai_family = AF_INET; //只查询IPv4地址 unsigned int retval = getaddrinfo(hostname, NULL, &hints, &p_addrinfo); if (retval != 0) { printf("getaddrinfo failed with error: %d\n", retval); WSACleanup(); return 1; } /***输出IP地址***/ p = p_addrinfo; cout << "本机IP地址: " << endl; char ipaddr[20]; in_addr addr; while (p != NULL) { addr = ((sockaddr_in*)(p->ai_addr))->sin_addr; cout << inet_ntop(AF_INET, (void *)&addr, ipaddr, 20) << endl; p = p->ai_next; } WSACleanup(); return 0; } 如果不查询主机地址,而是直接从键盘输入因特网上某网站服务器的域名到字符数组hostname中,比如输入山东农业大学的Web服务器地址www.sdau.edu.cn,则可解析出该主机的IP地址。作为练习,请读者自己将该程序的功能改为输入主机域名输出相应服务器的IP地址。 3.6.2服务查询 服务器通常都能提供多种不同的服务,比如Web服务、FTP文件服务、Telnet虚拟终端等,每种服务都要使用TCP或是UDP在一个知名端口号上侦听客户发来的服务请求。WinSock提供了一组函数可以查询本机上所提供的服务、协议及端口号,程序中可以使用这些函数来获得服务对应的端口,或者是获得正在使用指定端口的服务。在这里,服务的名称通常是对应服务器程序的名称。 1. getservbyname() 该函数根据给定的服务名和所使用的协议名获取服务的端口号等信息。 函数原型 struct servent * getservbyname(const char * name, const char *proto); 函数参数  name: 一个指向服务名的指针。  proto: 指向协议名的指针(可选)。如果这个指针为空,getservbyname()返回第一个name与s_name或者某一个s_aliases匹配的服务条目。否则,getservbyname()对name和proto都进行匹配。 返回值 函数调用成功,函数将返回一个指向servent结构的指针,该结构由WinSock创建并管理,应用程序不能修改或者释放它的任何部分; 函数调用不成功,将返回一个空指针NULL,调用WSAGetLastError()可获得到引起函数失败的错误所对应的错误码。servent结构的声明如下: struct servent { char * s_name; //服务名 char ** s_aliases; // 一个以空指针结尾的服务别名队列 Short s_port; //连接该服务所需的端口号,以网络字节顺序排列 char * s_proto; //连接该服务所用的传输协议名 }; 该结构中s_proto所指向的传输协议名通常为TCP的协议名“TCP”或UDP的协议名称“UDP”。 2. getservbyport() 该函数根据给定的协议和端口号查询服务名称等信息。 函数原型 struct servent * getservbyport(int port, const char *proto); 函数参数  port: 给定的端口号,以网络字节顺序排列。  proto: 指向传输协议名的指针,可以为空指针。传输协议名通常为TCP或UDP。如果为空指针,函数返回第一个端口号与port匹配的服务条目。否则返回端口号与port以及传输协议名与proto都匹配的服务条目。 返回值 如果函数调用成功,将返回一个指向servent结构的指针,该结构由WinSock创建并管理,应用程序不能修改或者释放它的任何部分; 如果函数调用不成功,将返回一个空指针NULL,调用WSAGetLastError()可获得引起函数失败的错误所对应的错误码。 例3.2显示本机上所有的使用TCP的服务的名称及端口号。 #include "WinSock2.h" #include "iostream" #pragma comment(lib, "ws2_32.lib") using namespace std; int main(int argc, char **argv) { /***加载WinSock DLL***/ WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { cout << "加载WinSock DLL失败!\n"; return 1; } /***根据TCP端口号查询服务***/ struct servent * pServer; cout<<"本机运行的使用TCP的服务:"<s_name<<" 端口:"<s_port)<p_proto << endl; 2. getprotobynumber() 根据协议号查询协议的名字等信息。 函数原型 struct protoent * getprotobynumber(int number); 函数参数 number: 一个以主机字节顺序排列的协议号。 返回值 函数调用成功,将返回一个指向protoent结构的指针,该protoent结构由WinSock创建并管理,应用程序不能修改或者释放它的任何部分; 函数调用失败则返回一个空指针,应用程序可以通过WSAGetLastError()来获取错误代码。 例3.3显示本机运行的所有因特网协议的名称及相应的协议号。 #include "iostream" #include "WinSock2.h" #pragma comment(lib, "ws2_32.lib") using namespace std; int main(int argc, char **argv) { WSADATA wsaData; if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { cout << "加载WinSock DLL失败!\n"; return 1; } struct protoent *pProto; for (int i = 0; i <= 255; i++) { if ((pProto = getprotobynumber(i)) != NULL) cout << "协议名:" << pProto->p_name << " 编号:" << pProto->p_proto << endl; } WSACleanup(); return 0; } 3.6.4异步信息查询函数及其编程方法 前面所介绍的函数都与标准的Berkeley套接字函数兼容,当函数调用后,如果函数的功能没有执行完成,函数是不会返回的,紧跟在这些函数调用之后的语句在函数未返回前是无法执行的。如果一个函数要完成其功能需要花费较长时间,则应用程序也只能在这个函数上等待而不能执行其他操作(这种情况称为阻塞),一直到函数完成功能返回或者函数出错返回。套接字函数的这种工作模型被称为同步模型。 与同步模型所对应的是异步模型,这里所谓的“异步”,也就是指函数启动了规定的任务后立即返回调用方,并返回一个用来标识所启动任务的异步任务句柄。 为了适应Windows的消息驱动机制,同时也可以解决同步模型中那些耗时较多的套接字函数所引起的程序执行效率低的问题,WinSock2引入Windows的消息机制对近二十个套接字函数进行了扩展,就是所谓的异步扩展,扩展后的函数都以“WSA”开头。 在扩展后的“异步”模型中,套接字函数通常只是启动一个任务并向Windows系统注册一条消息后就立即返回,尽管所启动的任务还没有完成。函数返回后应用程序可以执行其他操作。当异步函数所启动的任务完成时,Windows系统就会向应用程序发送那条函数注册的消息,收到消息后,应用程序就可以根据任务完成的结果继续下一步的操作。 下面以gethostbyname()函数的异步扩展版本WSAAsyncGetHostByName()函数为例,介绍WinSock2的异步函数的格式以及编程方法。 与gethostbyname()函数一样,WSAAsyncGetHostByName()函数的功能是根据主机域名获取主机的IP地址等主机信息。函数被调用时,如果给定的参数均有效,该函数将初始化并启动查询操作后立即返回,返回值是异步任务句柄。在多数情况下,应用程序应该保存返回的句柄,它主要有两个用途: 一是区分不同的查询操作,主要用于应用程序发起了多个查询操作时,可以根据该句柄判断到底是哪一个查询操作完成了; 二是要取消该异步任务时使用,如果应用程序成功调用了该函数后,在规定的时间内一直没收到完成的消息,那么可通过该句柄调用WSACancelAsyncRequest()取消该操作。 WSAAsyncGetHostByName()函数的格式如下。 HANDLE WSAAsyncGetHostByName( HWND hWnd, unsigned int wMsg, const char FAR *name, const char FAR *buf, int buflen ); 函数参数  hWnd: 是一个窗口句柄,表示异步请求完成时应该收到本函数发出的消息的窗口。  wMsg: 当异步请求完成时,hWnd代表窗口将要收到的消息,一般是用户自定义的消息。  name: 指向主机的名字的字符指针(字符串),主机名字既可以是本机的名字,也可以是网络上任意一台主机的域名(DNS)。  buf: 接收hostent数据结构的数据区指针,该指针指向的数据区必须要比hostent结构所占用的存储空间大,这是因为该缓冲区不仅要容纳一个hostent结构,而且hostent结构所有成员所引用的所有数据也要保存在该存储区内。建议用户提供一个MAXGETHOSTSTRUCT字节大小的缓冲区。  buflen: buf所指缓冲区的大小。 返回值 函数返回值为函数所启动的异步查询任务句柄。 如果函数启动的异步查询任务执行成功,则将主机名和地址信息复制到buf缓冲区中,同时向句柄为hWnd的应用程序窗口发送消息wMsg。 消息的wParam参数包含WSAAsyncGetHostByName()函数在调用时返回的异步任务句柄。 消息的lParam参数的高16位包含着错误代码,如果成功该错误代码为0,若该错误代码为WSAENOBUFS,则说明buflen指出的缓冲区太小了,此时,lParma的低16位为实际所需缓冲区的大小。获取错误代码和lParma的低16位值可使用如下的宏。 WSAGETASYNCERROR(lParma); WSAGETASYNCBUFLEN(lParma); 函数的返回值指出异步操作是否成功启动,但并不能表明异步操作是否成功执行。若操作启动成功,返回该异步请求任务的句柄(一个HANDLE类型的非0值)。如果启动不成功则返回0,此时调用WSAGetLastError()可获取引起启动失败的错误代码。 需要特别说明,WSAAsyncGetHostByName()函数现在已不被提倡使用,在Visual C++2017中使用该函数时,编译器将发出错误警告并停止编译,如果要关闭错误警告继续编译,可以在头文件stdafx.h中的头部预处理命令#include "targetver.h"之下,添加如下宏定义。 #define _WINSOCK_DEPRECATED_NO_WARNINGS 或者在主对话框类的头文件中,在类定义之前添加: #pragma warning(disable : 4996) 通过上面函数的介绍,可以知道,在使用异步套接字函数编程时: (1) 程序必须有至少一个窗口,用于接收异步任务完成时发送给程序的消息; (2) 必须为应用程序添加一条自定义消息,用于函数调用时作为参数与函数所启动的异步任务相关联; (3) 需要编写该自定义消息的消息处理函数,用于处理任务完成后返回的结果。 下面的例题演示了使用WSAAsyncGetHostByName()函数编程的方法。 例3.4编写程序查询本机的主机名称及IP地址。要求使用WSAAsyncGetHostByName()函数。程序的运行界面如图3.2所示。 图3.2例3.4的程序界面 程序编写步骤如下。 (1) 使用MFC应用程序向导创建一个基于对话框的应用程序框架,取该项目名称为“Example3.4”; 创建项目过程中需要选中“Windows套接字”复选框,因为该项目中要调用WinSock函数。 调整对话框上原有的一个静态文本控件(ID_STATIC1)和两个命令按钮(IDOK、IDCANCLE)的位置,并添加一个静态文本框(ID_STATIC2)、一个编辑框(IDC_EDIT1)和一个列表框ListBox(IDC_LISTBOX1),调整它们的布局如图3.2所示,并按表3.1给出的值设置相应控件的Caption属性。 表3.1例3.4各控件的属性设置 控件的IDCaption属性Read Only属性 ID_STATIC1计算机名无 ID_STATIC2IP地址无 IDC_EDIT1无True IDOK显示主机名字与IP地址无 IDCANCLE退出无 (2) 编辑框(IDC_EDIT1)和列表框ID_LISTBOX1添加Control类别的控件变量。在主对话框的“对话框资源编辑器”中右击编辑框(IDC_EDIT1),在弹出菜单中单击“添加成员变量”按钮,在弹出的“添加成员变量窗口”中,输入成员变量名称为“m_sName”,类别设为默认的Control,变量类型保持默认值,单击“确定”按钮,成员变量添加完成。 按照同样方法为列表框ID_LISTBOX1添加Control类别的控件变量m_CtrListIP。 (3) 由于WSAAsyncGetHostByName()函数需要一个字符类型的缓冲区存放返回的hostent结构及hostent结构的成员引用的所有数据,因此调用该函数前需要事先定义该缓冲区。定义该缓冲区可直接在头文件Example3.4Dlg.h的类定义中添加如下代码。 char buf[MAXGETHOSTSTRUCT]; 其中,MAXGETHOSTSTRUCT是一个常量,表示存放hostent结构及相关数据最多所需要的存储空间大小。 (4) 如果在使用“应用程序向导”创建该程序框架时,没有在“高级功能”窗口中选择“Windows套接字”复选框,则需要手动添加WinSock动态链接库的加载代码。在Example3.4Dlg.cpp中的类成员函数BOOL CExample34Dlg::OnInitDialog()中添加如下代码。 WORD wVersionRequested; WSADATA wsaData; wVersionRequested=MAKEWORD(2,2); //生成版本号2.2 if(WSAStartup(wVersionRequested,&wsaData)!=0) return false ; 如果已选中“Windows套接字”复选框,则不需要该步骤。 (5) 添加命令按钮的消息处理函数。 打开主对话框资源编辑器,双击对话框资源编辑器中的“确定”按钮,在打开的代码资源编辑器中就会看到“确定”按钮的BN_CLICKED消息的处理函数已被添加进来了,按如下所示代码添加该函数所要完成的功能。 void CExample34Dlg::OnBnClickedOk() { // TODO: 在此添加控件通知处理程序代码 char hostname[32]; if( gethostname(hostname,sizeof(hostname))) //获取主机名 {//如果出错,在窗口中的静态文本框ID_STATIC2上显示出错信息 m_sName.SetWindowTextW(_T("gethostname calling error\n")); //本例使用的是Unicode编码,若使用ANSI编码需用SetWindowTextA()方法 } else { CString a(hostname); m_sName.SetWindowTextW(a); //在静态文本框ID_STATIC2上显示计算机名 WSAAsyncGetHostByName(this->GetSafeHwnd(), MY_MESSAGE, hostname, buf, sizeof(buf)); //启动获取主机信息的异步事件 } } 该函数首先调用gethostname()函数获取主机名,然后WSAAsyncGetHostByName()获取主机的IP地址信息并发送自定义消息MY_MESSAGE。 在上述代码中,WSAAsyncGetHostByName()函数的第二个参数MY_MESSAGE是用户自定义的消息,获取主机网络信息的异步任务执行完后将发送该消息,该消息将由第一个参数指定的窗口(在这里为本对话框)处理。在本程序中,希望主对话框即OnBnClickedOk()所属的对话框捕获并处理WSAAsyncGetHostByName()函数在获取主机网络信息的异步任务完成后所发送的通知消息MY_MESSAGE,因此WSAAsyncGetHostByName()的第一个参数应为OnBnClickedOk()函数所属的对话框的句柄。获取本对话框的窗口句柄可以用this>GetSafeHwnd()函数。 (6) 添加自定义消息MY_MESSAGE的处理程序。 首先在文件Example3.4Dlg.h的类定义前添加如下代码来定义一个消息。 #define MY_MESSAGEWM_USER +100 启动MFC类向导,选择类名为CExample34Dlg,选择“消息”选项卡,单击位于“消息”选项卡下部的“添加自定义消息”按钮,弹出“添加自定义消息”对话框,在“消息”文本框中输入“MY_MESSAGE”,消息处理程序名称采用默认的OnMyMessage。单击“确定”按钮返回。 单击MFC类向导中的“编辑代码”按钮,在打开的Example3.4Dlg.cpp中的成员函数afx_msg LRESULT CExample34Dlg::OnMyMessage()中添加相应代码,得到如下函数。 afx_msg CExample34Dlg ::OnMyMessage(WPARAM wParam, LPARAM lParam) { struct hostent *hptr; CString mtempstr;//用于临时保存点分十进制表示的IP地址的字符串变量 hptr=(struct hostent *)&buf; char **pptr; pptr=hptr->h_addr_list; int itemCount=m_CtrListIP.GetCount(); //获取列表框控件显示内容的条数 for(int i=0;iGetSafeHwnd(),WM_MESSAGE,hostname,buf,sizeof(buf)); //启动获取主机信息的异步事件 } (7) 编写自定义消息的处理函数,该函数将WSAAsyncGetHostByName()函数返回的hostent结构中的IP地址信息显示在列表框中。具体代码如下。 struct hostent *hptr; CString mtempstr;//用于临时保存点分十进制表示的IP地址的字符串变量 hptr=(struct hostent *)&buf; char **pptr; pptr=hptr->h_addr_list; int itemCount=m_List.GetCount();//获取列表框控件显示内容的条数 for(int i=0;i