通信编程: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 网络与通信编程》,陈香凝 王烨阳 陈婷婷 张铮 编著,人民邮电出版社

posted @ 2021-10-09 16:40  乌漆WhiteMoon  阅读(511)  评论(0编辑  收藏  举报