通信编程:Winsock 接口载入
Winsock 编程接口
Winsock 是 Windows 下网络编程的规范,该规范是 Windows 下得到广泛应用的、开放的、支持多种协议的网络编程接口。从 1991 年的 1.0 版到 1995 年的 2.0.8 版,经过不断完善并在 Intel、Microsoft、Sun、SGI、Informix、Novell 等公司的全力支持下,已成为 Windows 网络编程的事实上的标准。——百度百科
通过 Winsock 编程接口就可以令多个应用程序通过网络来进行通信,Winsock 编程接口有 Winsock1 和 Winsock2 两个版本,目前主要使用 Winsock2 来进行开发。想要使用 Winsock2 库,就需要包含头文件来使用相关的 socket 函数和结构体,同时还要添加到 WS2_32.lib 的链接。
#include <winsock2.h>
#pragma comment(lib, "WS2_32") // 链接到 WS2_32.lib
Winsock 的载入和释放
载入与释放操作
每个基于 Winsock 开发的程序都需要载入对应版本的 Winsock DLL,这样才能使用 Winsock 提供的工具包。 想要载入 Winsock 库,需要使用 WSAStartup() 函数:
int
WSAAPI
WSAStartup(
_In_ WORD wVersionRequested,
_Out_ LPWSADATA lpWSAData
);
参数 | 类型 | 数据类型 | 说明 |
---|---|---|---|
wVersionRequested | 输入 | WORD | 指定要加载的 Winsock 版本 |
lpWSAData | 返回值 | LPWSADATA | 一个指向 WSADATA 结构的指针 |
其中 wVersionRequested 参数有 2 个字节,高字节指定次版本号,低字节指定主版本号,一般来说使用 Winsock2 时高字节和低字节都是 2。建立这个参数时,可以使用 MAKEWORD(a, b) 宏。函数的返回值时 LPWSADATA 结构,里面存储了加载的库的版本相关信息。
#define MAKEWORD(a, b) ((WORD)(((BYTE)(((DWORD_PTR)(a)) & 0xff)) | ((WORD)((BYTE)(((DWORD_PTR)(b)) & 0xff))) << 8))
想要释放 Winksock 库,可以使用 WSACleanup() 函数。
int
WSAAPI
WSACleanup(
void
);
CInitSock 类
由于每次使用 Winksock 程序都需要载入 Winksock 库,因为从封装性的角度来考虑,可以封装一个工具类来专门载入和释放 Winksock 库。首先先简单介绍一下 C++ 面向对象编程的构造器和析构器,注意和 Java 不同的是 Java 的类不需要写析构器。
函数 | 函数名 | 返回值 | 功能 |
---|---|---|---|
构造器 | 和类名相同 | 无 | 不需要用户显式调用,而是在创建对象时自动执行 |
析构器 | 在类名前面加一个 “~” 符号 | 无 | 不需要程序员显式调用,而是在销毁对象时自动执行 |
其实这个工具类只需要写构造器和析构器即可,其中构造器需要使用 MAKEWORD(a, b) 宏给一个 WORD 指定版本号,然后调用 WSAStartup() 函数载入 Winsock2 库。析构器则只需要调用 WSACleanup() 方法,目的就是在不需要使用 Winsock2 时自动把它释放掉。
#include <winsock2.h>
#pragma comment(lib, "WS2_32") // 链接到 WS2_32.lib
class CInitSock
{
public:
/*CInitSock 的构造器*/
CInitSock(BYTE minorVer = 2, BYTE majorVer = 2)
{
// 初始化WS2_32.dll
WSADATA wsaData;
WORD sockVersion = MAKEWORD(minorVer, majorVer);
if (::WSAStartup(sockVersion, &wsaData) != 0)
{
exit(0);
}
}
/*CInitSock 的析构器*/
~CInitSock()
{
::WSACleanup();
}
};
为了以后调用方便,这个工具类可以写在 initsock.h 头文件中。
Winsock 寻址方式
sockaddr_in 结构
Winsock 是 Windows 下网络编程的规范,是支持多种协议的网络编程接口,因此编址也需要顾及不同的协议栈。Winsock 的第一个版本使用 sockaddr 结构来编址,里面的 sa_family 成员制定了使用的编址方式。而对于 TCP/ IP 协议栈,可以直接使用 sockaddr_in 结构。
typedef struct sockaddr_in {
#if(_WIN32_WINNT < 0x0600)
short sin_family;
#else //(_WIN32_WINNT < 0x0600)
ADDRESS_FAMILY sin_family;
#endif //(_WIN32_WINNT < 0x0600)
USHORT sin_port;
IN_ADDR sin_addr;
CHAR sin_zero[8];
} SOCKADDR_IN, *PSOCKADDR_IN;
其中有几个重要的成员:
成员变量 | 说明 |
---|---|
sin_family | 地址家族 |
sin_port | 端口号 |
sin_addr | IPv4 地址 |
sin_zero[8] | 占位,用于和 sockaddr 结构大小对齐 |
其中对于 sin_family 变量必须使用 AF_INET 作为地址家族,表示使用 IP 编址。in_addr 结构用来存储 IP 地址,底层是使用一个共用体 union 来实现,可以用 4 个 uchar 或 2 个 ushort 或 1 个 ulong 来存储。
typedef struct in_addr {
union {
struct { UCHAR s_b1,s_b2,s_b3,s_b4; } S_un_b;
struct { USHORT s_w1,s_w2; } S_un_w;
ULONG S_addr;
} S_un;
sockaddr_in 结构初始化
因此对于 sockaddr_in 结构的初始化,实际上就是分别指定地址家族,绑定端口号和 IP 地址。
sockaddr_in sin;
sin.sin_family = AF_INET;
sin.sin_port = htons(4567);
sin.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
字节顺序
字节顺序是长度跨越多个字节的数据被存储的顺序,Intel x86 机器使用小尾顺序(little-endian),意思是最不重要的字节首先存储。大多数不使用小尾顺序的机器使用大尾顺序(big-endian),即最重要的字节首先存储。
因为协议数据要在这些机器间传输,所以就必须选定其中的一种方式做为标准,否则会引起混淆。TCP/IP 统一规定使用大尾方式传输数据,也称为网络字节顺序。sockaddr 和 sockaddr_in 结构中除了 sin_family 成员(它不是协议的一部分)外,其他所有值必须以网络字节顺序存储。
Winsock 提供了一些函数来处理本地机器的字节顺序和网络字节顺序的转换:
//将 u_short 类型变量从主机字节顺序转化到 TCP/IP 网络字节顺序
u_short htons(u_short hostshort);
//将 u_long 类型变量从主机字节顺序转化到 TCP/IP 网络字节顺序
u_long htonl(u_long hostlong);
//将 u_short 类型变量从 TCP/IP 网络字节顺序转化到主机字节顺序
u_short ntohs(u_short netshort);
//将 u_long 类型变量从 TCP/IP 网络字节顺序转化到主机字节顺序
u_long ntohl(u_long netlong);
参考资料
《Windows 网络与通信编程》,陈香凝 王烨阳 陈婷婷 张铮 编著,人民邮电出版社