WindowsAPI查缺补漏-WinSock网络编程

碎碎念

Windows套接字即WinSock以UNIX的伯克利套接字规范为标准,包含同名接口函数,用法也一致。WinSock结课Windows消息机制又增加了许多扩展函数。

TCP/IP规定统一使用大端序传输数据,也称为网络字节顺序。本篇不讲计网,自己去学。

这节内容真是吔屎了...前面基础WinSock编程不支持Unicode还得转,看个乐子就行了,重点在后面I/O模型。

代码例子有ANSI和Unicode混用的情况,纯属懒得改了,用的时候小心点儿。

本篇只讲TCP和UDP连接,想学HTTP啥的可以关上了。

学习本篇需要智商在线,不在线的先去听《大香蕉》洗一下脑子。

地址表示

在sockaddr_in结构中同时指定IP地址和端口号。下文定义中出现sockaddr结构的均是为了兼容1.0旧版本,都可以当作sockaddr_in使。

1
2
3
4
5
6
struct sockaddr_in {
SHORT sin_family; //地址家族 AF_INET
USHORT sin_port; //端口号
struct in_addr sin_addr; //IP地址
CHAR sin_zero[8]; //0
};

对于sin_port要转为网络字节顺序,有以下函数可借助使用。

1
2
3
4
u_short htons(u_short hostshort); //把u_short型从主机字节顺序转换为TCP/IP网络字节顺序
u_long htonl(u_long hostlong); //把u_long型从主机字节顺序转换为TCP/IP网络字节顺序
u_short ntohs(u_short netshort); //把u_short型从TCP/IP网络字节顺序转换为主机字节顺序
u_long ntohl(u_long netlong); //把u_long型从TCP/IP网络字节顺序转换为主机字节顺序

in_addr结构用来表示IP地址:

1
2
3
4
5
6
7
8
9
10
11
typedef struct in_addr {
union {
struct {
u_char s_b1, s_b2, s_b3, s_b4; //4个u_char表示IP地址
}S_un_b;
struct {
u_short s_w1, s_w2; //2个u_short表示IP地址
}S_un_w;
u_long S_addr; //1个u_long表示IP地址
}S_un;
}IN_ADDR,*PIN_ADDR,FAR *LPIN_ADDR;

常用函数有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//经典版
ULONG inet_addr(
_In_ CONST PCHAR cp
); //把IP地址字符串转为网络字节序u_long型
PCHAR FAR inet_ntoa(
_In_ struct in_addr in
); //把一个in_addr转换为字符串

//新版
INT WSAAPI InetPton(
INT Family, //地址家族
PCTSTR pszAddrString, //IP地址字符串
PVOID pAddrBuf //返回网络字节顺序IP地址
);
PCTSTR WSAAPI InetNtop(
INT Family, //地址家族
CONST PVOID pAddr, //网络字节顺序IP地址
PTSTR pStringBuf, //返回IP地址字符串
SIZE_T StringBufSize //缓冲区长度 单位字符
);

常用的初始化方法为:

1
2
3
4
5
6
7
8
9
10
11
//经典版
sockaddr_in sockAddr;
sockAddr.sin_family = AF_INET;
sockAddr.sin_port = htons(12345);
sockAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");

//新版
sockaddr_in sockAddr;
sockAddr.sin_family = AF_INET;
sockAddr.sin_port = htons(12345);
InetPton(AF_INET, "127.0.0.1", &sockAddr.sin_addr.S_un.S_addr);

WinSock网络编程

WSAStartup

初始化WinSock库:

1
2
3
4
INT WSAStartup(
_In_ WORD wVersionRequested, //希望使用的socket版本
_Out_ LPWSADATA lpWSAData //返回动态链接库详细信息
); //成功返回0

wVersionRequested高字节为副版本号,低字节为主版本号。目前WinSock版本为2.2,需要包含WinSock2.h、用DLL为Ws2_32.dll,但Ws2_32.lib在VS中没有得自己搞。

lpWSAData有:

1
2
3
4
5
6
7
8
9
typedef struct WSAData {
WORD wVersion; //希望程序使用的WinSock版本
WORD wHighVersion; //实际可支持的最高WinSock版本
USHORT iMaxSockets; //已废弃
USHORT iMaxUdpDg; //已废弃
CHAR FAR* lpVendorInfo; //已废弃
CHAR szDescription[WSADESCRIPTION_LEN + 1]; //返回对WinSock实现的描述
CHAR szSystemStatus[WSASYS_STATUS_LEN + 1]; //返回相对状态或配置信息
} WSADATA;

例如常用方法:

1
2
3
4
5
6
7
8
9
10
11
#include <winsock2.h>
#pragma comment(lib,"Ws2_32")
INT main(INT argc, PTCHAR argv[]) {
WSADATA wsa = { 0 };
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
//WinSock初始化失败
return 0;
};
//...
return 0;
};

WSACleanup

释放WinSock资源:

1
INT WSACleanup(VOID);

socket

创建套接字:

1
2
3
4
5
SOCKET WSAAPI socket(
_In_ INT af, //地址家族
_In_ INT type, //套接字类型
_In_ INT protocol //协议类型
); //成功返回新建套接字句柄 错误INVALID_SOCKET 用WSAGetLastError获取错误代码

对于af有AF_INET或AF_INET6,分别为IPv4或IPv6。type常用的有:SOCK_STREAM流式套接字,只能读取TCP包;SOCK_DGRAM数据报套接字,只能读取UDP包;SOCK_RAW原始套接字,可读写内核未处理的IP包,能对网络底层传输机制进行控制。protocal需要配合type使用,有IPPROTO_TCP、IPPROTO_UDP等,为0表示t自动选择ype对应的默认协议。

Windows对原始套接字搞了一堆安全限制,这玩意儿容易被蠕虫用上。

例如常用方法:

1
2
3
4
5
6
SOCKET socketListen = socket(AF_INET, SOCK_STREAM, 0);
if (socketListen == INVALID_SOCKET) {
//创建套接字失败
WSACleanup();
return 0;
};

closesocket

关闭套接字资源:

1
2
3
INT closesocket(
_In_ SOCKET s
);

bind

将本地地址与监听套接字关联起来:

1
2
3
4
5
INT bind(
_In_ SOCKET s, //监听套接字句柄
_In_ CONST struct sockaddr FAR* name, //sockaddr_in结构地址
_In_ INT namelen //sockaddr_in结构长度
); //成功返回0 失败SOCKET_ERROR 用WSAGetLastError获取错误信息

例如常用方法如下,当sockAddr.sin_addr.S_un.S_addr为INADDR_ANY表示在本机所有IP地址上进行监听,多网卡时很好使,端口号设为0表示系统自动分配唯一端口号。

1
2
3
4
5
6
if (bind(socketListen, (sockaddr*)&sockAddr, sizeof(sockAddr)) == SOCKET_ERROR) {
//绑定失败
closesocket(socketListen);
WSACleanup();
return 0;
};

getsockname

获取分配给监听套接字的地址:

1
2
3
4
5
INT getsockname(
_In_ SOCKET s, //套接字句柄
_Out_ struct sockaddr* name, //返回地址信息
_Inout_ PINT namelen //结构体长 单位字节
); //成功返回0 失败SOCEKT_ERROR 用WSAGetLastError获取错误代码

listen

使主动连接套接字变为被动连接套接字,使一个进程可接收其他进程请求,变成一个服务器进程。

1
2
3
4
INT listen(
_In_ SOCKET s, //监听套接字句柄
_In_ INT backlog //连接队列最大长度
); //成功返回0 失败SOCKET_ERROR 用WSAGetLastError获取错误代码

backlog设为SOMAXCONN表示系统将其设为最大合理值,队列满后拒绝新连接请求并出现错误WSAECONNREFUSED。例如常用方法如下:

1
2
3
4
5
6
if (listen(socketListen, SOMAXCONN) == SOCKET_ERROR) {
//进入监听状态失败
closesocket(socketListen);
WSACleanup();
return 0;
};

accept

从套接字等待连接队列中抽取第一个连接并接受,成功则返回一个套接字句柄用于实际通信,原监听套接字仍保持监听状态。失败返回INVALID_SOCKET,用WSAGetLastError获取错误代码。

1
2
3
4
5
SOCKET accept(
_In_ SOCKET s, //监听套接字句柄
_Out_ struct sockaddr* addr, //返回客户端地址
_Inout_ PINT addrlen //结构体长
);

当队列中无等待连接时该函数阻塞调用进程直至新进程出现后才返回,所以一般新建进程负责通信。但这样也不合理,最好用I/O异步模型。

1
2
3
4
5
6
7
8
9
10
sockaddr_in sockAddrClient;
INT nAddrlen = sizeof(sockAddrClient);
while (TRUE) {
socketAccept = accept(socektListen, (sockaddr*)&sockAddrClient, &nAddrlen);
if (socketAccept == INVALID_SOCKET) {
//接受连接请求失败
continue;
};
//...
};

send/recv

在已连接的套接字上发送/接收数据:

1
2
3
4
5
6
7
8
9
10
11
12
INT send(
_In_ SOCKET s, //已连接的通信套接字句柄
_In_ CONST PCHAR buf, //要发送数据的缓冲区指针
_In_ INT len, //缓冲区长度 单位字节
_In_ INT flags //0
); //成功返回发送字节数 超过len则返回SOCKET_ERROR 用WSAGetLastError获取错误代码
INT recv( //都同上
_In_ SOCKET s,
_Out_ PCHAR buf,
_In_ INT len,
_In_ INT flags
);

connect

与服务器建立连接:

1
2
3
4
5
INT connect(
_In_ SOCKET s, //通信套接字句柄
_In_ CONST struct sockaddr* name, //服务器套接字
_In_ INT namelen //name结构长度
); //成功返回0 失败SOCKET_ERROR 用WSAGetLastError获取错误代码

sendto

向指定目的地发送数据,适用于UDP:

1
2
3
4
5
6
7
8
INT sendto(
_In_ SOCKET s, //套接字句柄
_In_ CONST PCHAR buf, //要发送数据缓冲区指针
_In_ INT len, //缓冲区长度 单位字节
_In_ INT flags, //0
_In_ CONST struct sockaddr* to, //目的地地址
_In_ INT tolen //结构体长
); //返回发送字节数 大于len返回SOCKET_ERROR 用WSAGetLastError获取错误代码

recvfrom

接收数据报,返回源地址:

1
2
3
4
5
6
7
8
INT recvfrom(
_In_ SOCKET s, //套接字句柄
_Out_ PCHAR buf, //接收数据缓冲区指针
_In_ INT len, //缓冲区长度 单位字节
_In_ INT flags, //0
_Out_ struct sockaddr* from, //返回源地址
_Inout_opt_ PINT fromlen //结构体长
); //返回接收到的字节数 连接已被关闭返回0 失败SOCKET_ERROR 用WSAGetLastError获取错误代码

例子:TCP

下面这个例子用的ANSI编码,看看就行了,重点在后面。

服务端源文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#include <winsock2.h>           // Winsock2头文件
#include <ws2tcpip.h> // inet_pton / inet_ntop需要使用这个头文件
#include "resource.h"
#pragma comment(lib, "Ws2_32") // Winsock2导入库
// 常量定义
const int BUF_SIZE = 1024;
// 全局变量
HWND g_hwnd; // 窗口句柄
HWND g_hListContent; // 聊天内容列表框窗口句柄
HWND g_hEditMsg; // 消息输入框窗口句柄
HWND g_hBtnSend; // 发送按钮窗口句柄
SOCKET g_socketListen = INVALID_SOCKET; // 监听套接字句柄
SOCKET g_socketAccept = INVALID_SOCKET; // 通信套接字句柄
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
// 对话框初始化
VOID OnInit(HWND hwndDlg);
// 按下启动服务按钮
VOID OnStart();
// 按下发送按钮
VOID OnSend();
// 服务器接收数据线程函数
DWORD WINAPI RecvProc(LPVOID lpParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_SERVER_DIALOG), NULL, DialogProc, NULL);
return 0;
};
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_INITDIALOG: {
OnInit(hwndDlg);
return TRUE;
};
case WM_COMMAND: {
switch (LOWORD(wParam)) {
case IDCANCEL: {
// 关闭套接字,释放WinSock库
if (g_socketAccept != INVALID_SOCKET)
closesocket(g_socketAccept);
if (g_socketListen != INVALID_SOCKET)
closesocket(g_socketListen);
WSACleanup();
EndDialog(hwndDlg, IDCANCEL);
break;
};
case IDC_BTN_START: {
OnStart();
break;
};
case IDC_BTN_SEND: {
OnSend();
break;
};
};
return TRUE;
};
};
return FALSE;
};
//////////////////////////////////////////////////////////////////////////
VOID OnInit(HWND hwndDlg) {
g_hwnd = hwndDlg;
g_hListContent = GetDlgItem(hwndDlg, IDC_LIST_CONTENT);
g_hEditMsg = GetDlgItem(hwndDlg, IDC_EDIT_MSG);
g_hBtnSend = GetDlgItem(hwndDlg, IDC_BTN_SEND);
EnableWindow(g_hBtnSend, FALSE);
return;
};
VOID OnStart() {
WSADATA wsa = { 0 };
// 1、初始化WinSock库
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
MessageBoxA(g_hwnd, "初始化WinSock库失败!", "WSAStartup Error", MB_OK);
return;
};
// 2、创建用于监听所有客户端请求的套接字
g_socketListen = socket(AF_INET, SOCK_STREAM, 0);
if (g_socketListen == INVALID_SOCKET) {
MessageBoxA(g_hwnd, "创建监听套接字失败!", "socket Error", MB_OK);
WSACleanup();
return;
};
// 3、将监听套接字与指定的IP地址、端口号捆绑
sockaddr_in sockAddr;
sockAddr.sin_family = AF_INET;
sockAddr.sin_port = htons(8000);
sockAddr.sin_addr.S_un.S_addr = INADDR_ANY;
if (bind(g_socketListen, (sockaddr*)&sockAddr, sizeof(sockAddr)) == SOCKET_ERROR) {
MessageBoxA(g_hwnd, "将监听套接字与指定的IP地址、端口号捆绑失败!", "bind Error", MB_OK);
closesocket(g_socketListen);
WSACleanup();
return;
};
// 4、使监听套节字进入监听(等待被连接)状态
if (listen(g_socketListen, 1) == SOCKET_ERROR) {
MessageBoxA(g_hwnd, "使监听套节字进入监听(等待被连接)状态失败!", "listen Error", MB_OK);
closesocket(g_socketListen);
WSACleanup();
return;
};
// 服务器监听中...
MessageBoxA(g_hwnd, "服务器监听中...", "服务启动成功", MB_OK);
EnableWindow(GetDlgItem(g_hwnd, IDC_BTN_START), FALSE);
// 5、等待连接请求,返回用于服务器客户端通信的套接字句柄
sockaddr_in sockAddrClient; // 调用accept返回客户端的IP地址、端口号
int nAddrlen = sizeof(sockaddr_in);
g_socketAccept = accept(g_socketListen, (sockaddr*)&sockAddrClient, &nAddrlen);
if (g_socketAccept == INVALID_SOCKET) {
MessageBoxA(g_hwnd, "接受连接请求失败!", "accept Error", MB_OK);
closesocket(g_socketListen);
WSACleanup();
return;
};
// 6、接受客户的连接请求成功,收发数据
CHAR szBuf[BUF_SIZE] = { 0 };
CHAR szIP[24] = { 0 };
inet_ntop(AF_INET, &sockAddrClient.sin_addr.S_un.S_addr, szIP, _countof(szIP));
wsprintfA(szBuf, "客户端[%s:%d]已连接!", szIP, ntohs(sockAddrClient.sin_port));
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szBuf);
EnableWindow(g_hBtnSend, TRUE);
// 创建线程,接收客户端数据
HANDLE hThread = NULL;
if ((hThread = CreateThread(NULL, 0, RecvProc, NULL, 0, NULL)) != NULL)
CloseHandle(hThread);
return;
};
VOID OnSend() {
CHAR szBuf[BUF_SIZE] = { 0 };
CHAR szShow[BUF_SIZE] = { 0 };
GetWindowTextA(g_hEditMsg, szBuf, BUF_SIZE);
wsprintfA(szShow, "服务器说:%s", szBuf);
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szShow);
send(g_socketAccept, szShow, strlen(szShow), 0);
SetWindowTextA(g_hEditMsg, "");
return;
};
DWORD WINAPI RecvProc(LPVOID lpParam) {
CHAR szBuf[BUF_SIZE] = { 0 };
int nRet = SOCKET_ERROR;
while (TRUE) {
ZeroMemory(szBuf, BUF_SIZE);
nRet = recv(g_socketAccept, szBuf, BUF_SIZE, 0);
if (nRet > 0)
// 收到客户端数据
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szBuf);
};
return 0;
};

客户端源文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
#include <winsock2.h>           // WinSock2头文件
#include <ws2tcpip.h> // inet_pton / inet_ntop需要使用这个头文件
#include "resource.h"
#pragma comment(lib, "Ws2_32") // WinSock2导入库
// 常量定义
const int BUF_SIZE = 1024;
// 全局变量
HWND g_hwnd; // 窗口句柄
HWND g_hListContent; // 聊天内容列表框窗口句柄
HWND g_hEditMsg; // 消息输入框窗口句柄
HWND g_hBtnSend; // 发送按钮窗口句柄
SOCKET g_socketClient = INVALID_SOCKET; // 通信套接字句柄
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
// 对话框初始化
VOID OnInit(HWND hwndDlg);
// 按下连接按钮
VOID OnConnect();
// 按下发送按钮
VOID OnSend();
// 客户端接收数据线程函数
DWORD WINAPI RecvProc(LPVOID lpParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_CLIENT_DIALOG), NULL, DialogProc, NULL);
return 0;
};
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_INITDIALOG: {
OnInit(hwndDlg);
return TRUE;
};
case WM_COMMAND: {
switch (LOWORD(wParam)) {
case IDCANCEL: {
// 关闭套接字,释放WinSock库
if (g_socketClient != INVALID_SOCKET)
closesocket(g_socketClient);
WSACleanup();
EndDialog(hwndDlg, IDCANCEL);
break;
};
case IDC_BTN_CONNECT: {
OnConnect();
break;
};
case IDC_BTN_SEND: {
OnSend();
break;
};
};
return TRUE;
};
};
return FALSE;
};
//////////////////////////////////////////////////////////////////////////
VOID OnInit(HWND hwndDlg) {
g_hwnd = hwndDlg;
g_hListContent = GetDlgItem(hwndDlg, IDC_LIST_CONTENT);
g_hEditMsg = GetDlgItem(hwndDlg, IDC_EDIT_MSG);
g_hBtnSend = GetDlgItem(hwndDlg, IDC_BTN_SEND);
EnableWindow(g_hBtnSend, FALSE);
return;
};
// 按下连接按钮
VOID OnConnect() {
WSADATA wsa = { 0 };
sockaddr_in sockAddrServer;
HANDLE hThread = NULL;
// 1、初始化WinSock库
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
MessageBoxA(g_hwnd, "初始化WinSock库失败!", "WSAStartup Error", MB_OK);
return;
};
// 2、创建与服务器的通信套接字
g_socketClient = socket(AF_INET, SOCK_STREAM, 0);
if (g_socketClient == INVALID_SOCKET) {
MessageBoxA(g_hwnd, "创建与服务器的通信套接字失败!", "socket Error", MB_OK);
WSACleanup();
return;
};
// 3、与服务器建立连接
sockAddrServer.sin_family = AF_INET;
sockAddrServer.sin_port = htons(8000);
inet_pton(AF_INET, "127.0.0.1", &sockAddrServer.sin_addr.S_un.S_addr);
if (connect(g_socketClient, (sockaddr*)&sockAddrServer, sizeof(sockAddrServer)) == SOCKET_ERROR) {
MessageBoxA(g_hwnd, "与服务器建立连接失败!", "connect Error", MB_OK);
closesocket(g_socketClient);
WSACleanup();
return;
};
// 4、建立连接成功,收发数据
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)"已连接到服务器!");
EnableWindow(GetDlgItem(g_hwnd, IDC_BTN_CONNECT), FALSE);
EnableWindow(g_hBtnSend, TRUE);
// 创建线程,接收服务器数据
if ((hThread = CreateThread(NULL, 0, RecvProc, NULL, 0, NULL)) != NULL)
CloseHandle(hThread);
return;
};
// 按下发送按钮
VOID OnSend() {
CHAR szBuf[BUF_SIZE] = { 0 };
CHAR szShow[BUF_SIZE] = { 0 };
GetWindowTextA(g_hEditMsg, szBuf, BUF_SIZE);
wsprintfA(szShow, "客户端说:%s", szBuf);
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szShow);
send(g_socketClient, szShow, strlen(szShow), 0);
SetWindowTextA(g_hEditMsg, "");
return;
};
DWORD WINAPI RecvProc(LPVOID lpParam) {
CHAR szBuf[BUF_SIZE] = { 0 };
int nRet = SOCKET_ERROR;
while (TRUE) {
ZeroMemory(szBuf, BUF_SIZE);
nRet = recv(g_socketClient, szBuf, BUF_SIZE, 0);
if (nRet > 0)
// 收到服务器数据
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szBuf);
};
return 0;
};

例子:UDP

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <winsock2.h>           // WinSock2头文件
#include <ws2tcpip.h> // inet_pton inet_ntop需要使用这个头文件
#include <stdio.h>
#pragma comment(lib, "Ws2_32") // WinSock2导入库
// 常量定义
const int BUF_SIZE = 1024;
int main() {
WSADATA wsa = { 0 };
SOCKET socketSendRecv = INVALID_SOCKET; // 服务器的收发数据套接字
sockaddr_in addrServer, addrClient; // 服务器、客户端地址
int nAddrLen = sizeof(sockaddr_in); // sockaddr_in结构体的长度
CHAR szBuf[BUF_SIZE] = { 0 }; // 接收数据缓冲区
CHAR szIP[24] = { 0 }; // 客户端IP地址
// 初始化WinSock库
WSAStartup(MAKEWORD(2, 2), &wsa);
// 创建收发数据套接字
socketSendRecv = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
// 将收发数据套接字绑定到任意IP地址和指定端口
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(8000);
addrServer.sin_addr.S_un.S_addr = INADDR_ANY;
bind(socketSendRecv, (SOCKADDR*)&addrServer, sizeof(addrServer));
// 从客户端接收数据,recvfrom函数会在参数addrClient中返回客户端的IP地址和端口号
recvfrom(socketSendRecv, szBuf, BUF_SIZE, 0, (SOCKADDR*)&addrClient, &nAddrLen);
inet_ntop(AF_INET, &addrClient.sin_addr.S_un.S_addr, szIP, _countof(szIP));
printf("从客户端[%s:%d]接收到数据:%s\n", szIP, ntohs(addrClient.sin_port), szBuf);
// 把收到的数据发送回去
sendto(socketSendRecv, szBuf, strlen(szBuf), 0, (SOCKADDR*)&addrClient, nAddrLen);
// 关闭收发数据套接字,释放WinSock库
closesocket(socketSendRecv);
WSACleanup();
return 0;
};

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <winsock2.h>           // WinSock2头文件
#include <ws2tcpip.h> // inet_pton inet_ntop需要使用这个头文件
#include <stdio.h>
#pragma comment(lib, "Ws2_32") // WinSock2导入库
// 常量定义
const int BUF_SIZE = 1024;
int main() {
WSADATA wsa = { 0 };
SOCKET socketSendRecv = INVALID_SOCKET; // 客户端的收发数据套接字
sockaddr_in addrServer; // 服务器地址
int nAddrLen = sizeof(sockaddr_in); // sockaddr_in结构体的长度
CHAR szBuf[BUF_SIZE] = "你好,老王!"; // 发送数据缓冲区
CHAR szIP[24] = { 0 }; // 服务器IP地址
// 初始化WinSock库
WSAStartup(MAKEWORD(2, 2), &wsa);
// 创建收发数据套接字
socketSendRecv = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
// 向服务器发送数据
addrServer.sin_family = AF_INET;
addrServer.sin_port = htons(8000);
inet_pton(AF_INET, "127.0.0.1", &addrServer.sin_addr.S_un.S_addr);
sendto(socketSendRecv, szBuf, strlen(szBuf), 0, (SOCKADDR*)&addrServer, nAddrLen);
// 从服务器接收数据
sockaddr_in addr; // 看一下recvfrom返回的服务器IP地址和端口号
ZeroMemory(szBuf, sizeof(szBuf));
recvfrom(socketSendRecv, szBuf, BUF_SIZE, 0, (sockaddr*)&addr, &nAddrLen);
inet_ntop(AF_INET, &addr.sin_addr.S_un.S_addr, szIP, _countof(szIP));
printf("从服务器[%s:%d]返回数据:%s\n", szIP, ntohs(addr.sin_port), szBuf);
// 关闭收发数据套接字,释放WinSock库
closesocket(socketSendRecv);
WSACleanup();
return 0;
};

异步I/O模型

非阻塞模式

ioctlsocket

使套接字工作在非阻塞模式下:

1
2
3
4
5
INT ioctlsocket(
_In_ SOCKET s, //套接字句柄
_In_ LONG cmd, //执行的命令
_Inout_ PULONG argp //指向cmd参数的指针
); //成功返回0 失败SOCKET_ERROR 用WSAGetLastError获取错误代码

cmd有FIONBIO、FIONREAD、SIOCATMARK等参数,自己去学。

例如设置为非阻塞模式:

1
2
ULONG ulArgp = 1;
ioctlsocket(socketListen, FIONBIO, &ulArgp); //argp为非0即可 恢复设置为0

此后每次发送、接收或管理连接时大多会因操作还未完成而失败,WSAGetLastError返回WSAEWOULDBLOCK,则需要多次调用直到返回成功。

例子

数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
#pragma once
// 套接字对象链表所用结构体
typedef struct _SOCKETOBJ{
SOCKET m_socket; // 通信套接字句柄
CHAR m_szIP[16]; // 客户端IP
USHORT m_usPort; // 客户端端口号
_SOCKETOBJ *m_pNext; // 下一个套接字对象结构体指针
}SOCKETOBJ, *PSOCKETOBJ;
PSOCKETOBJ g_pSocketObjHeader; // 套接字对象链表表头
int g_nTotalClient; // 客户端总数量
CRITICAL_SECTION g_cs; // 临界区对象,用于同步对套接字对象链表的操作
// 创建一个套接字对象
PSOCKETOBJ CreateSocketObj(SOCKET s) {
PSOCKETOBJ pSocketObj = new SOCKETOBJ;
if (pSocketObj == NULL)
return NULL;
EnterCriticalSection(&g_cs);
pSocketObj->m_socket = s;
// 添加第一个结点
if (g_pSocketObjHeader == NULL) {
g_pSocketObjHeader = pSocketObj;
g_pSocketObjHeader->m_pNext = NULL;
}
else {
pSocketObj->m_pNext = g_pSocketObjHeader;
g_pSocketObjHeader = pSocketObj;
};
g_nTotalClient++;
LeaveCriticalSection(&g_cs);
return pSocketObj;
};
// 释放一个套接字对象
VOID FreeSocketObj(PSOCKETOBJ pSocketObj) {
EnterCriticalSection(&g_cs);
PSOCKETOBJ p = g_pSocketObjHeader;
if (p == pSocketObj) // 移除的是头结点
g_pSocketObjHeader = g_pSocketObjHeader->m_pNext;
else
while (p != NULL) {
if (p->m_pNext == pSocketObj) {
p->m_pNext = pSocketObj->m_pNext;
break;
};
p = p->m_pNext;
};
if (pSocketObj->m_socket != INVALID_SOCKET)
closesocket(pSocketObj->m_socket);
delete pSocketObj;
g_nTotalClient--;
LeaveCriticalSection(&g_cs);
};
// 根据套接字查找套接字对象
PSOCKETOBJ FindSocketObj(SOCKET s) {
EnterCriticalSection(&g_cs);
PSOCKETOBJ pSocketObj = g_pSocketObjHeader;
while (pSocketObj != NULL) {
if (pSocketObj->m_socket == s) {
LeaveCriticalSection(&g_cs);
return pSocketObj;
};
pSocketObj = pSocketObj->m_pNext;
};
LeaveCriticalSection(&g_cs);
return NULL;
};
// 释放所有套接字对象
VOID DeleteAllSocketObj() {
SOCKETOBJ socketObj;
PSOCKETOBJ pSocketObj = g_pSocketObjHeader;
while (pSocketObj != NULL) {
socketObj = *pSocketObj;
if (pSocketObj->m_socket != INVALID_SOCKET)
closesocket(pSocketObj->m_socket);
delete pSocketObj;
pSocketObj = socketObj.m_pNext;
};
return;
};

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
#include <winsock2.h>           // Winsock2头文件
#include <ws2tcpip.h> // inet_pton / inet_ntop需要使用这个头文件
#include "resource.h"
#include "SOCKETOBJ.h"
#pragma comment(lib, "Ws2_32") // Winsock2导入库
// 常量定义
const int BUF_SIZE = 4096;
// 全局变量
HWND g_hwnd; // 窗口句柄
HWND g_hListContent; // 聊天内容列表框窗口句柄
HWND g_hEditMsg; // 消息输入框窗口句柄
HWND g_hBtnSend; // 发送按钮窗口句柄
SOCKET g_socketListen = INVALID_SOCKET; // 监听套接字句柄
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
// 对话框初始化
VOID OnInit(HWND hwndDlg);
// 按下启动服务按钮
VOID OnStart();
// 按下发送按钮
VOID OnSend();
// 服务器接收每一个客户端数据的线程函数
DWORD WINAPI RecvProc(LPVOID lpParam);
// 循环等待客户端连接请求的线程函数
DWORD WINAPI AcceptProc(LPVOID lpParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_SERVER_DIALOG), NULL, DialogProc, NULL);
return 0;
};
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_INITDIALOG: {
OnInit(hwndDlg);
return TRUE;
};
case WM_COMMAND: {
switch (LOWORD(wParam)) {
case IDCANCEL: {
// 关闭套接字,释放WinSock库
if (g_socketListen != INVALID_SOCKET)
closesocket(g_socketListen);
WSACleanup();
EndDialog(hwndDlg, IDCANCEL);
break;
};
case IDC_BTN_START: {
OnStart();
break;
};
case IDC_BTN_SEND: {
OnSend();
break;
};
};
return TRUE;
};
};
return FALSE;
};
//////////////////////////////////////////////////////////////////////////
VOID OnInit(HWND hwndDlg) {
WSADATA wsa = { 0 };
// 1、初始化WinSock库
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
MessageBoxA(g_hwnd, "初始化WinSock库失败!", "WSAStartup Error", MB_OK);
return;
};
// 初始化临界区对象,用于同步对套接字对象的访问
InitializeCriticalSection(&g_cs);
g_hwnd = hwndDlg;
g_hListContent = GetDlgItem(hwndDlg, IDC_LIST_CONTENT);
g_hEditMsg = GetDlgItem(hwndDlg, IDC_EDIT_MSG);
g_hBtnSend = GetDlgItem(hwndDlg, IDC_BTN_SEND);
EnableWindow(g_hBtnSend, FALSE);
return;
};
VOID OnStart() {
// 2、创建用于监听所有客户端请求的套接字
g_socketListen = socket(AF_INET, SOCK_STREAM, 0);
if (g_socketListen == INVALID_SOCKET) {
MessageBoxA(g_hwnd, "创建监听套接字失败!", "socket Error", MB_OK);
WSACleanup();
return;
};
// 3、将监听套接字与指定的IP地址、端口号捆绑
sockaddr_in sockAddr;
sockAddr.sin_family = AF_INET;
sockAddr.sin_port = htons(8000);
sockAddr.sin_addr.S_un.S_addr = INADDR_ANY;
if (bind(g_socketListen, (sockaddr*)&sockAddr, sizeof(sockAddr)) == SOCKET_ERROR) {
MessageBoxA(g_hwnd, "将监听套接字与指定的IP地址、端口号捆绑失败!","bind Error", MB_OK);
closesocket(g_socketListen);
WSACleanup();
return;
};
// 4、使监听套节字进入监听(等待被连接)状态
if (listen(g_socketListen, SOMAXCONN) == SOCKET_ERROR) {
MessageBoxA(g_hwnd, "使监听套节字进入监听(等待被连接)状态失败!", "listen Error", MB_OK);
closesocket(g_socketListen);
WSACleanup();
return;
};
// 服务器监听中...
MessageBoxA(g_hwnd, "服务器监听中...", "服务启动成功", MB_OK);
EnableWindow(GetDlgItem(g_hwnd, IDC_BTN_START), FALSE);
// 5、创建一个新线程循环等待连接请求
HANDLE hThread = NULL;
if ((hThread = CreateThread(NULL, 0, AcceptProc, NULL, 0, NULL)) != NULL)
CloseHandle(hThread);
return;
};
VOID OnSend() {
CHAR szBuf[BUF_SIZE] = { 0 };
CHAR szMsg[BUF_SIZE] = { 0 };
GetWindowTextA(g_hEditMsg, szBuf, BUF_SIZE);
wsprintfA(szMsg, "服务器说:%s", szBuf);
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szMsg);
SetWindowTextA(g_hEditMsg, "");
// 向每一个客户端发送数据
PSOCKETOBJ p = g_pSocketObjHeader;
while (p != NULL) {
send(p->m_socket, szMsg, strlen(szMsg), 0);
p = p->m_pNext;
};
return;
};
DWORD WINAPI RecvProc(LPVOID lpParam) {
SOCKET socketAccept = (SOCKET)lpParam;
PSOCKETOBJ pSocketObj = FindSocketObj(socketAccept);
CHAR szBuf[BUF_SIZE] = { 0 }; // 接收数据缓冲区
CHAR szMsg[BUF_SIZE] = { 0 };
int nRet = SOCKET_ERROR; // I/O操作返回值
PSOCKETOBJ p;
while (TRUE) {
ZeroMemory(szBuf, BUF_SIZE);
nRet = recv(socketAccept, szBuf, BUF_SIZE, 0);
if (nRet > 0) { // 接收到客户端数据
ZeroMemory(szMsg, BUF_SIZE); // 组合为 客户端[XXX.XXX.XXX.XXX:XXXX]说:......
wsprintfA(szMsg, "客户端[%s:%d]说:%s", pSocketObj->m_szIP, pSocketObj->m_usPort, szBuf);
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szMsg);
// 把收到的数据分发到每一个客户端
p = g_pSocketObjHeader;
while (p != NULL) {
if (p->m_socket != socketAccept)
send(p->m_socket, szMsg, strlen(szMsg), 0);
p = p->m_pNext;
};
}
else { // 连接已关闭
ZeroMemory(szMsg, BUF_SIZE); // 组合为 客户端[XXX.XXX.XXX.XXX:XXXX] 已退出!
wsprintfA(szMsg, "客户端[%s:%d] 已退出!", pSocketObj->m_szIP, pSocketObj->m_usPort);
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szMsg);
FreeSocketObj(pSocketObj);
// 如果没有客户端在线了,禁用发送按钮
if (g_nTotalClient == 0)
EnableWindow(g_hBtnSend, FALSE);
return 0;
};
};
return 0;
};
DWORD WINAPI AcceptProc(LPVOID lpParam) {
SOCKET socketAccept = INVALID_SOCKET; // 通信套接字句柄
sockaddr_in sockAddrClient;
int nAddrlen = sizeof(sockaddr_in);
CHAR szBuf[BUF_SIZE] = { 0 };
HANDLE hThread = NULL;
while (TRUE) {
socketAccept = accept(g_socketListen, (sockaddr*)&sockAddrClient, &nAddrlen);
if (socketAccept == INVALID_SOCKET) {
Sleep(100);
continue;
};
// 6、接受客户的连接请求成功
ZeroMemory(szBuf, BUF_SIZE);
// 创建一个套接字对象,保存客户端IP地址、端口号
PSOCKETOBJ pSocketObj = CreateSocketObj(socketAccept);
inet_ntop(AF_INET, &sockAddrClient.sin_addr.S_un.S_addr, pSocketObj->m_szIP, _countof(pSocketObj->m_szIP));
pSocketObj->m_usPort = ntohs(sockAddrClient.sin_port);
wsprintfA(szBuf, "客户端[%s:%d] 已连接!", pSocketObj->m_szIP, pSocketObj->m_usPort);
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szBuf);
EnableWindow(g_hBtnSend, TRUE);
// 创建线程,接收客户端数据
if ((hThread = CreateThread(NULL, 0, RecvProc, (LPVOID)socketAccept, 0, NULL)) != NULL)
CloseHandle(hThread);
};
return 0;
};

客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
#include <winsock2.h>           // Winsock2头文件
#include <ws2tcpip.h> // inet_pton / inet_ntop需要使用这个头文件
#include "resource.h"
#pragma comment(lib, "Ws2_32") // Winsock2导入库
// 常量定义
const int BUF_SIZE = 4096;
// 全局变量
HWND g_hwnd; // 窗口句柄
HWND g_hListContent; // 聊天内容列表框窗口句柄
HWND g_hEditMsg; // 消息输入框窗口句柄
HWND g_hBtnSend; // 发送按钮窗口句柄
HWND g_hBtnConnect; // 连接按钮窗口句柄
SOCKET g_socketClient = INVALID_SOCKET; // 通信套接字句柄
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
// 对话框初始化
VOID OnInit(HWND hwndDlg);
// 按下连接按钮
VOID OnConnect();
// 按下发送按钮
VOID OnSend();
// 客户端接收数据的线程函数
DWORD WINAPI RecvProc(LPVOID lpParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_CLIENT_DIALOG), NULL, DialogProc, NULL);
return 0;
};
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_INITDIALOG: {
OnInit(hwndDlg);
return TRUE;
};
case WM_COMMAND: {
switch (LOWORD(wParam)) {
case IDCANCEL: {
if (g_socketClient != INVALID_SOCKET)
closesocket(g_socketClient);
WSACleanup();
EndDialog(hwndDlg, IDCANCEL);
break;
};
case IDC_BTN_CONNECT: {
OnConnect();
break;
};
case IDC_BTN_SEND: {
OnSend();
break;
};
};
return TRUE;
};
};
return FALSE;
};
//////////////////////////////////////////////////////////////////////////
VOID OnInit(HWND hwndDlg) {
g_hwnd = hwndDlg;
g_hListContent = GetDlgItem(hwndDlg, IDC_LIST_CONTENT);
g_hEditMsg = GetDlgItem(hwndDlg, IDC_EDIT_MSG);
g_hBtnSend = GetDlgItem(hwndDlg, IDC_BTN_SEND);
g_hBtnConnect = GetDlgItem(hwndDlg, IDC_BTN_CONNECT);
EnableWindow(g_hBtnSend, FALSE);
return;
};
// 按下连接按钮
VOID OnConnect() {
WSADATA wsa = { 0 };
sockaddr_in sockAddrServer;
HANDLE hThread = NULL;
// 1、初始化WinSock库
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
MessageBoxA(g_hwnd, "初始化WinSock库失败!", "WSAStartup Error", MB_OK);
return;
};
// 2、创建与服务器的通信套接字
g_socketClient = socket(AF_INET, SOCK_STREAM, 0);
if (g_socketClient == INVALID_SOCKET) {
MessageBoxA(g_hwnd, "创建与服务器的通信套接字失败!", "socket Error", MB_OK);
WSACleanup();
return;
};
// 3、与服务器建立连接
sockAddrServer.sin_family = AF_INET;
sockAddrServer.sin_port = htons(8000);
inet_pton(AF_INET, "127.0.0.1", &sockAddrServer.sin_addr.S_un.S_addr);
if (connect(g_socketClient, (sockaddr*)&sockAddrServer, sizeof(sockAddrServer)) == SOCKET_ERROR) {
MessageBoxA(g_hwnd, "与服务器建立连接失败!", "connect Error", MB_OK);
closesocket(g_socketClient);
WSACleanup();
return;
};
// 4、建立连接成功,收发数据
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)"已连接到服务器!");
EnableWindow(g_hBtnConnect, FALSE);
EnableWindow(g_hBtnSend, TRUE);
// 创建线程,接收服务器数据
if ((hThread = CreateThread(NULL, 0, RecvProc, NULL, 0, NULL)) != NULL)
CloseHandle(hThread);
return;
};
// 按下发送按钮
VOID OnSend() {
CHAR szBuf[BUF_SIZE] = { 0 };
CHAR szMsg[BUF_SIZE] = { 0 };
GetWindowTextA(g_hEditMsg, szBuf, BUF_SIZE);
wsprintfA(szMsg, "我说:%s", szBuf);
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szMsg);
send(g_socketClient, szBuf, strlen(szBuf), 0);
SetWindowTextA(g_hEditMsg, "");
return;
};
DWORD WINAPI RecvProc(LPVOID lpParam) {
CHAR szBuf[BUF_SIZE] = { 0 };
int nRet = SOCKET_ERROR;
while (TRUE) {
ZeroMemory(szBuf, BUF_SIZE);
nRet = recv(g_socketClient, szBuf, BUF_SIZE, 0);
if (nRet > 0) // 收到服务器数据
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szBuf);
else { // 与服务器连接已关闭
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)"与服务器连接已关闭!");
EnableWindow(g_hBtnConnect, TRUE);
EnableWindow(g_hBtnSend, FALSE);
closesocket(g_socketClient);
WSACleanup();
return 0;
};
};
return 0;
};

select模型

同时管理多个套接字,管理多个连接,用集合表示管理的多个套接字。

select

检查若干个套接字状态:

1
2
3
4
5
6
7
INT select(
_In_ INT nIds, //忽略
_Inout_ fd_set* readfds, //指向一组套接字集合指针 用于检查可读性
_Inout_ fd_set* writefds, //指向一组套接字集合指针 用于检查可写性
_Inout_ fd_set* exceptfds, //指向一组套接字集合指针 用于检查错误
_In_ CONST struct timeval* timeout //函数等待最大时间 直到至少一个套接字满足指定条件 NULL无限等待 {0,0}立即返回
); //成功返回发生网络时间的套接字句柄数 超时返回0 错误返回SOCKET_ERROR 用WSAGetLastError获取错误代码

其中fd_set结构把多个套接字连接在一起,形成套接字集合:

1
2
3
4
typedef struct fd_set {
u_int fd_count; //套接字句柄数目 即fd_array大小
SOCKET fd_array[FD_SETSIZE]; //套接字句柄数组
} fd_set;

还有宏用于操作和检查套接字集合:

含义
FD_CLR(s,*set) 从集合中删除套接字s
FD_SET(s,*set) 把套接字s添加到集合中
FD_ISSET(s,*set) 当套接字s为集合成员则返回非0,否则0
FD_ZERO(*set) 初始化套接字集合为空集合,使用前总应清空

对于readfds标识要检查可读性套接字的集合。当套接字处于监听状态,一旦接收到连接请求,将被标记为可读的,从而保证在不阻塞情况下完成accept调用。对于其他套接字,可读性意味队列数据可读取,保证对recvWSARecvWSARecvFromrecvfrom调用不阻塞。对于面向连接的套接字,可读性指示已从对方接收到关闭套接字的请求。当套接字发生以下网络事件时将更新可读性套接字集合:

  • listen已被调用,accept调用正在挂起,接下来将完成accept调用。
  • 可以接收数据。
  • 连接已关闭/重置/终止。

对于writefds标识要检查可写性套接字的集合。当套接字正处理一个非阻塞connect调用,一旦连接建立成功完成,套接字是可写的。若套接字没处理connect调用,可写性意味可调用sendsendtoWSASendto进行发送。当套接字发生以下网络事件时将更新可写性套接字集合:

  • 如果处理非阻塞connect调用,connect已成功。
  • 可发送数据。

对于exceptfds检查OOB数据是否存在的或发生任何异常错误的套接字。当套接字发生以下网络事件时将更新异常套接字集合:

  • 如果处理非阻塞connect调用,connect调用失败。
  • OOB数据可用于读取。

select返回时会更新相关集合,把没有发生可读写网络事件的套接字从集合中移除,即会破坏原集合。3个参数中可任意2个为NULL,不是NULL的那个至少包含一个套接字句柄。集合中套接字数目最大为FD_SETSIZE,默认64,可在包含WinSock2.h前将该值定义为别的值,但最大不能超过WinSock下层协议限制的1024个。

对于timeval结构:

1
2
3
4
struct timeval { //时间间隔
LONG tv_sec; //单位秒
LONG tv_usec; //单位微秒
};

例子

修改上面那个例子一个函数即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
DWORD WINAPI AcceptProc(LPVOID lpParam) {
SOCKET socketAccept = INVALID_SOCKET; // 通信套接字句柄
sockaddr_in sockAddrClient;
int nAddrlen = sizeof(sockaddr_in);
CHAR szBuf[BUF_SIZE] = { 0 };
CHAR szMsg[BUF_SIZE] = { 0 };
fd_set fd;
PSOCKETOBJ pSocketObj, p;
int nRet = SOCKET_ERROR;
// (1)、初始化一个可读性套节字集合readfds,添加监听套节字句柄到这个集合
fd_set readfds;
FD_ZERO(&readfds);
FD_SET(g_socketListen, &readfds);
while (TRUE) {
// 复制一份套接字集合
fd = readfds;
// (2)、把timeout参数设置为NULL,select调用将无限期阻塞
nRet = select(0, &fd, NULL, NULL, NULL);
if (nRet <= 0)
continue;
// (3)、将原readfds集合与经过select函数处理过的fd集合比较
for (UINT i = 0; i < readfds.fd_count; i++) {
if (!FD_ISSET(readfds.fd_array[i], &fd))
continue;
if (readfds.fd_array[i] == g_socketListen) { // ①-是监听套接字,可读性表示需接受新连接
if (readfds.fd_count < FD_SETSIZE) {
socketAccept = accept(g_socketListen, (sockaddr*)&sockAddrClient, &nAddrlen);
if (socketAccept == INVALID_SOCKET)
continue;
// (4)、把通信套接字添加到原readfds集合
FD_SET(socketAccept, &readfds);
// 6、接受客户的连接请求成功
ZeroMemory(szBuf, BUF_SIZE);
// 创建一个套接字对象,保存客户端IP地址、端口号
pSocketObj = CreateSocketObj(socketAccept);
inet_ntop(AF_INET, &sockAddrClient.sin_addr.S_un.S_addr, pSocketObj->m_szIP, _countof(pSocketObj->m_szIP));
pSocketObj->m_usPort = ntohs(sockAddrClient.sin_port);
wsprintfA(szBuf, "客户端[%s:%d] 已连接!", pSocketObj->m_szIP, pSocketObj->m_usPort);
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szBuf);
EnableWindow(g_hBtnSend, TRUE);
}
else {
MessageBoxA(g_hwnd, "客户端连接数太多!", "accept Error", MB_OK);
continue;
};
}
else {
pSocketObj = FindSocketObj(readfds.fd_array[i]);
ZeroMemory(szBuf, BUF_SIZE);
nRet = SOCKET_ERROR;
nRet = recv(pSocketObj->m_socket, szBuf, BUF_SIZE, 0);
if (nRet > 0) { // ②-是通信套接字,接收到客户端数据
ZeroMemory(szMsg, BUF_SIZE);
wsprintfA(szMsg, "客户端[%s:%d]说:%s", pSocketObj->m_szIP, pSocketObj->m_usPort, szBuf);
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szMsg);
// 把收到的数据分发到每一个客户端
p = g_pSocketObjHeader;
while (p != NULL) {
if (p->m_socket != pSocketObj->m_socket)
send(p->m_socket, szMsg, strlen(szMsg), 0);
p = p->m_pNext;
};
}
else { // ③-是通信套接字,连接已关闭
ZeroMemory(szMsg, BUF_SIZE);
wsprintfA(szMsg, "客户端[%s:%d] 已退出!", pSocketObj->m_szIP, pSocketObj->m_usPort);
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szMsg);
FD_CLR(readfds.fd_array[i], &readfds);
FreeSocketObj(pSocketObj);
// 如果没有客户端在线了,禁用发送按钮
if (g_nTotalClient == 0)
EnableWindow(g_hBtnSend, FALSE);
};
};
}; // for循环
}; // while循环
};

WSAAsyncSelect模型

即异步选择模型,允许应用程序以Windows消息形式接收网络事件通知,为每个套接字绑定一个消息。当套接字上出现事先设置的事件时,系统给应用程序发送一个消息。优点是在系统开销不太大情况下同时处理多个客户端连接,缺点是不需要窗口也得搞个窗口用于处理套接字网络事件。

WSAAsyncSelect

通知指定套接字又网络事件发生:

1
2
3
4
5
6
INT WSAAsyncSelect(
_In_ SOCKET s, //需要消息通知的套接字句柄
_In_ HWND hWnd, //网络事件发生时 将接收消息的窗口
_In_ UINT wMsg, //网络事件发生时,接收到的消息类型
_In_ LONG lEvent //指定应用程序感兴趣的网络事件组合
); //成功返回0 失败SOCKET_ERROR 用WSAGetLastError获取错误代码

函数在检测到由lEvent指定的任何网络事件发生时向窗口hWnd发送wMsg消息,wParam标识发生网络事件的套接字句柄,lParam低位字指定已发生的网络事件,高位字包含错误代码。本函数自动将套接字设置为非阻塞模式,要手动用ioctlsocketWSAIoctl重设为阻塞模式前,应用本函数清除与套接字相关联的事件记录,即lEvent设为0。

lEvent有:

枚举值 希望接收的通知 事件发生时调用的函数
FD_READ 读就绪 recvrecvfromWSARecvWSARecvFrom
FD_WRITE 写就绪 sendsendtoWSASendWSASendTo
FD_ACCEPT 有连接接入 acceptWSAAccept
FD_CONNECT 连接完成
FD_CLOSE 套接字关闭
FD_OOB 带外数据到达 recvrecvfromWSARecvWSARecvFrom
FD_QOS 套接字服务质量QoS更改 WSAIoctl(SIO_GET_QOS)
FD_GROUP_QOS(废弃) 套接字组服务质量更改
FD_ROUTING_INTERFACE_CHANGE 指定目的地路由接口更改 WSAIoctl(SIO_ROUTING_INTERFACE_CHANGE)
FD_ADDRESS_LIST_CHANGE 套接字协议族本地地址列表更改 WSAIoctl(SIO_ADDRESS_LIST_CHANGE)

对于同一个套接字,只能在同一个消息中处理不同网络事件,而不能为不同网络事件指定不同消息。

可用宏WSAGETSELECTEVENT(lParam)获取错误代码,用WSAGETSELECTERROR(lParam)获取事件代码。

该函数已过时,被WSAEventSelect替代,使用时要用:

1
#define _WINSOCK_DEPRECATED_NO_WARNINGS

例子

例子同上,仅服务端发生变化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
#define _WINSOCK_DEPRECATED_NO_WARNINGS 1
#include <winsock2.h> // Winsock2头文件
#include <ws2tcpip.h> // inet_pton / inet_ntop需要使用这个头文件
#include "resource.h"
#include "SOCKETOBJ.h"
#pragma comment(lib, "Ws2_32") // Winsock2导入库
// 常量定义
const int BUF_SIZE = 4096;
// 自定义网络事件通知消息类型
const int WM_SOCKET = WM_APP + 1;
// 全局变量
HWND g_hwnd; // 窗口句柄
HWND g_hListContent; // 聊天内容列表框窗口句柄
HWND g_hEditMsg; // 消息输入框窗口句柄
HWND g_hBtnSend; // 发送按钮窗口句柄
SOCKET g_socketListen = INVALID_SOCKET; // 监听套接字句柄
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
// 对话框初始化
VOID OnInit(HWND hwndDlg);
// 按下启动服务按钮
VOID OnStart();
// 按下发送按钮
VOID OnSend();
// 服务器接收每一个客户端数据
VOID OnRecv(SOCKET s);
// 接受客户端连接请求
VOID OnAccept();
// 客户端关闭连接
VOID OnClose(SOCKET s);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_SERVER_DIALOG), NULL, DialogProc, NULL);
return 0;
};
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
SOCKET s;
switch (uMsg) {
case WM_INITDIALOG: {
OnInit(hwndDlg);
return TRUE;
};
case WM_COMMAND: {
switch (LOWORD(wParam)) {
case IDCANCEL: {
// 关闭套接字,释放WinSock库
if (g_socketListen != INVALID_SOCKET)
closesocket(g_socketListen);
WSACleanup();
EndDialog(hwndDlg, IDCANCEL);
break;
};
case IDC_BTN_START: {
OnStart();
break;
};
case IDC_BTN_SEND: {
OnSend();
break;
};
};
return TRUE;
};
case WM_SOCKET: {
// wParam参数标识了发生网络事件的套接字句柄
s = wParam;
switch (WSAGETSELECTEVENT(lParam)) {
case FD_ACCEPT: { // 接受客户端连接
OnAccept();
break;
};
case FD_READ: { // 接收客户端数据
OnRecv(s);
break;
};
case FD_WRITE: { // 发送数据,本例不需要处理,因为是按下发送按钮才发送
break;
};
case FD_CLOSE: { // 客户端连接关闭
OnClose(s);
break;
};
};
return TRUE;
};
};
return 0;
};
//////////////////////////////////////////////////////////////////////////
VOID OnInit(HWND hwndDlg) {
WSADATA wsa = { 0 };
// 1、初始化WinSock库
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
MessageBox(g_hwnd, TEXT("初始化WinSock库失败!"), TEXT("WSAStartup Error"), MB_OK);
return;
};
// 初始化临界区对象,用于同步对套接字对象的访问
InitializeCriticalSection(&g_cs);
g_hwnd = hwndDlg;
g_hListContent = GetDlgItem(hwndDlg, IDC_LIST_CONTENT);
g_hEditMsg = GetDlgItem(hwndDlg, IDC_EDIT_MSG);
g_hBtnSend = GetDlgItem(hwndDlg, IDC_BTN_SEND);
EnableWindow(g_hBtnSend, FALSE);
return;
};
VOID OnStart() {
// 2、创建用于监听所有客户端请求的套接字
g_socketListen = socket(AF_INET, SOCK_STREAM, 0);
if (g_socketListen == INVALID_SOCKET) {
MessageBox(g_hwnd, TEXT("创建监听套接字失败!"), TEXT("socket Error"), MB_OK);
WSACleanup();
return;
};
// 设置监听套接字为网络事件窗口消息通知
WSAAsyncSelect(g_socketListen, g_hwnd, WM_SOCKET, FD_ACCEPT/* | FD_CLOSE*/);
// 3、将监听套接字与指定的IP地址、端口号捆绑
sockaddr_in sockAddr;
sockAddr.sin_family = AF_INET;
sockAddr.sin_port = htons(8000);
sockAddr.sin_addr.S_un.S_addr = INADDR_ANY;
if (bind(g_socketListen, (sockaddr*)&sockAddr, sizeof(sockAddr)) == SOCKET_ERROR) {
MessageBox(g_hwnd, TEXT("将监听套接字与指定的IP地址、端口号捆绑失败!"), TEXT("bind Error"), MB_OK);
closesocket(g_socketListen);
WSACleanup();
return;
};
// 4、使监听套节字进入监听(等待被连接)状态
if (listen(g_socketListen, SOMAXCONN) == SOCKET_ERROR) {
MessageBox(g_hwnd, TEXT("使监听套节字进入监听(等待被连接)状态失败!"), TEXT("listen Error"), MB_OK);
closesocket(g_socketListen);
WSACleanup();
return;
};
// 服务器监听中...
MessageBox(g_hwnd, TEXT("服务器监听中..."), TEXT("服务启动成功"), MB_OK);
EnableWindow(GetDlgItem(g_hwnd, IDC_BTN_START), FALSE);
// 5、创建一个新线程循环等待连接请求
// CloseHandle(CreateThread(NULL, 0, AcceptProc, NULL, 0, NULL));
return;
};
VOID OnSend() {
CHAR szBuf[BUF_SIZE] = { 0 };
CHAR szMsg[BUF_SIZE] = { 0 };
GetWindowTextA(g_hEditMsg, szBuf, BUF_SIZE);
wsprintfA(szMsg, "服务器说:%s", szBuf);
SendMessage(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szMsg);
SetWindowTextA(g_hEditMsg, "");
// 向每一个客户端发送数据
PSOCKETOBJ p = g_pSocketObjHeader;
while (p != NULL) {
send(p->m_socket, szMsg, strlen(szMsg), 0);
p = p->m_pNext;
};
return;
};
VOID OnAccept() {
SOCKET socketAccept = INVALID_SOCKET; // 通信套接字句柄
sockaddr_in sockAddrClient;
int nAddrlen = sizeof(sockaddr_in);
socketAccept = accept(g_socketListen, (sockaddr*)&sockAddrClient, &nAddrlen);
if (socketAccept == INVALID_SOCKET)
return;
// 设置通信套接字为网络事件窗口消息通知类型
WSAAsyncSelect(socketAccept, g_hwnd, WM_SOCKET, FD_READ | FD_WRITE | FD_CLOSE);
// 6、接受客户的连接请求成功
CHAR szBuf[BUF_SIZE] = { 0 };
PSOCKETOBJ pSocketObj = CreateSocketObj(socketAccept);
inet_ntop(AF_INET, &sockAddrClient.sin_addr.S_un.S_addr, pSocketObj->m_szIP, _countof(pSocketObj->m_szIP));
pSocketObj->m_usPort = ntohs(sockAddrClient.sin_port);
wsprintfA(szBuf, "客户端[%s:%d] 已连接!", pSocketObj->m_szIP, pSocketObj->m_usPort);
SendMessage(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szBuf);
EnableWindow(g_hBtnSend, TRUE);
return;
};
VOID OnRecv(SOCKET s) {
PSOCKETOBJ pSocketObj = FindSocketObj(s);
CHAR szBuf[BUF_SIZE] = { 0 };
int nRet = SOCKET_ERROR;
nRet = recv(pSocketObj->m_socket, szBuf, BUF_SIZE, 0);
if (nRet > 0) {
CHAR szMsg[BUF_SIZE] = { 0 };
wsprintfA(szMsg, "客户端[%s:%d]说:%s", pSocketObj->m_szIP, pSocketObj->m_usPort, szBuf);
SendMessage(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szMsg);
// 把收到的数据分发到每一个客户端
PSOCKETOBJ p = g_pSocketObjHeader;
while (p != NULL) {
if (p->m_socket != pSocketObj->m_socket)
send(p->m_socket, szMsg, strlen(szMsg), 0);
p = p->m_pNext;
};
};
return;
};
VOID OnClose(SOCKET s) {
PSOCKETOBJ pSocketObj = FindSocketObj(s);
CHAR szBuf[BUF_SIZE] = { 0 };
wsprintfA(szBuf, "客户端[%s:%d] 已退出!", pSocketObj->m_szIP, pSocketObj->m_usPort);
SendMessage(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szBuf);
FreeSocketObj(pSocketObj);
// 如果没有客户端在线了,禁用发送按钮
if (g_nTotalClient == 0)
EnableWindow(g_hBtnSend, FALSE);
return;
};

WSAEventSelect模型

即事件选择模型。在多个套接字上接收网络事件通知,将事件对象与网络事件集合绑定,当发生网络事件时,应用程序以事件形式接收网络事件通知。缺点是一个线程中最多只能等待64个事件,再多了就得创建多线程。

WSAEventSelect

将一个事件对象与网络事件集合关联在一起。

1
2
3
4
5
INT WSAEventSelect(
_In_ SOCKET s, //需要事件通知的套接字句柄
_In_ WSAEVENT hEventObject, //关联的事件对象
_In_ LONG lNetworkEvents //指定应用程序感兴趣的网络事件集合
); //成功返回0 失败SOCKET_ERROR 用WSAGetLastError获取错误代码

该函数自动将套接字设置为非阻塞模式,要用ioctlsocketWSAIoctl手动设置为阻塞模式前,用该函数清除与套接字相关联的事件记录,其中lNetworkEvents为0、hEventObject为NULL。这里lNetworkEvents与WSAAsyncSelect用法一样。对于同一个套接字,无法为不同网络事件指定不同事件对象,否则调用时将取消先前指定的事件对象。要取消指定套接字上网络事件的关联时,lNetworkEvents为0,hEventObject被忽略随便填。

WSACreateEvent

创建一个未命名的初始状态为未触发的手动重置事件对象,子进程不能继承返回的事件对象句柄。

1
WSAEVENT WSACreateEvent(VOID); //成功返回事件对象句柄 失败WSA_INVALID_EVENT 用WSAGetLastError获取错误代码

WSAResetEvent

将事件对象从已触发状态重置为未触发状态:

1
2
3
BOOL WSAResetEvent(
_In_ WSAEVENT hEvent //事件对象句柄
);

WSASetEvent

将指定事件对象设置为已触发状态:

1
2
3
BOOL WSASetEvent(
_In_ WSAEVENT hEvent //事件对象句柄
);

WSACloseEvent

关闭事件对象句柄,释放事件对象占用的资源:

1
2
3
BOOL WSACloseEvent(
_In_ WSAEVENT hEvent //事件对象句柄
);

WSAWaitForMultipleEvents

进入等待状态,直到指定的一个或全部事件对象已触发、超时或I/O完成例程已执行时返回。

1
2
3
4
5
6
7
DWORD WSAWaitForMultipleEvents(
_In_ DWORD cEvents, //lphEvents数组中事件对象句柄数
_In_ CONST WSAEVENT* lphEvents, //事件对象句柄数组
_In_ BOOL fWaitAll, //是否等待所有事件对象变为触发状态
_In_ DWORD dwTimeout, //超时事件 单位毫秒
_In_ BOOL fAlertable //当系统将一个I/O完成例程放入队列执行时 该函数是否返回
);

lphEvents数组最大只能64个。对于fWaitAll为TRUE则当lphEvents数组中所有事件对象都触发时函数才返回,返回值减WSA_WAIT_EVENT_0为事件对象在数组中最小的一个索引;为FALSE则任一事件对象触发就返回,返回值同理。dwTimeout为0时立即返回,为WSA_INFINITE时函数永远等待。fAlertable为TRUE则线程处于可通知等待状态,该函数可在系统执行I/O完成例程时返回WSA_WAIT_IO_COMPLETION,此时等待的事件对象未触发,需要再次调用该函数;为FALSE则线程处于不通知等待状态,不执行I/O完成例程。

函数成功则返回值可以是:

返回值 含义
WSA_WAIT_EVENT_0~WSA_WAIT_EVENT_0+cEvents-1
WSA_WAIT_IO_COMPLETION
WSA_WAIT_TIMEOUT 超时且fWaitAll指定条件不满足
WSA_WAIT_FAILED 失败

WSAEnumNetworkEvents

检查指定的套接字发生了哪些网络事件:

1
2
3
4
5
INT WSAEnumNetworkEvents(
_In_ SOCKET s, //套接字句柄
_In_opt_ WSAEVENT hEventObject, //事件对象句柄
_Out_ LPWSANETWORKEVENTS lpNetworkEvents
); //成功返回0 失败SOCKET_ERROR 用WSAGetLastError获取错误代码

hEventObject为NULL时表示不重置事件对象,否则函数执行后把该事件对象重置为未触发状态。WSANETWORKEVENTS结构有:

1
2
3
4
typedef struct _WSANETWORKEVENTS {
LONG lNetworkEvents; //发生哪些网络事件
INT iErrorCode[FD_MAX_EVENTS]; //相关错误代码数组
} WSANETWORKEVENTS, FAR* LPWSANETWORKEVENTS;

例子

还是上面那个例子,只需修改服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#include <winsock2.h>           // Winsock2头文件
#include <ws2tcpip.h> // inet_pton / inet_ntop需要使用这个头文件
#include "resource.h"
#include "SOCKETOBJ.h"
#pragma comment(lib, "Ws2_32") // Winsock2导入库
// 常量定义
const int BUF_SIZE = 4096;
// 全局变量
HWND g_hwnd; // 窗口句柄
HWND g_hListContent; // 聊天内容列表框窗口句柄
HWND g_hEditMsg; // 消息输入框窗口句柄
HWND g_hBtnSend; // 发送按钮窗口句柄
SOCKET g_socketListen = INVALID_SOCKET; // 监听套接字句柄
WSAEVENT g_eventArray[WSA_MAXIMUM_WAIT_EVENTS]; // 所有事件对象句柄数组
SOCKET g_socketArray[WSA_MAXIMUM_WAIT_EVENTS]; // 所有套接字句柄数组
int g_nTotalEvent; // 所有事件对象句柄总数
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
// 对话框初始化
VOID OnInit(HWND hwndDlg);
// 按下启动服务按钮
VOID OnStart();
// 按下发送按钮
VOID OnSend();
// 在所有事件对象上循环等待网络事件
DWORD WINAPI WaitProc(LPVOID lpParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_SERVER_DIALOG), NULL, DialogProc, NULL);
return 0;
};
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_INITDIALOG: {
OnInit(hwndDlg);
return TRUE;
};
case WM_COMMAND: {
switch (LOWORD(wParam)) {
case IDCANCEL: {
// 关闭套接字,释放WinSock库
if (g_socketListen != INVALID_SOCKET)
closesocket(g_socketListen);
WSACleanup();
EndDialog(hwndDlg, IDCANCEL);
break;
};
case IDC_BTN_START: {
OnStart();
break;
};
case IDC_BTN_SEND: {
OnSend();
break;
};
};
return TRUE;
};
};
return FALSE;
};
//////////////////////////////////////////////////////////////////////////
VOID OnInit(HWND hwndDlg) {
WSADATA wsa = { 0 };
// 1、初始化WinSock库
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
MessageBox(g_hwnd, TEXT("初始化WinSock库失败!"), TEXT("WSAStartup Error"), MB_OK);
return;
};
// 初始化临界区对象,用于同步对套接字对象的访问
InitializeCriticalSection(&g_cs);
g_hwnd = hwndDlg;
g_hListContent = GetDlgItem(hwndDlg, IDC_LIST_CONTENT);
g_hEditMsg = GetDlgItem(hwndDlg, IDC_EDIT_MSG);
g_hBtnSend = GetDlgItem(hwndDlg, IDC_BTN_SEND);
EnableWindow(g_hBtnSend, FALSE);
return;
};
VOID OnStart() {
// 2、创建用于监听所有客户端请求的套接字
g_socketListen = socket(AF_INET, SOCK_STREAM, 0);
if (g_socketListen == INVALID_SOCKET) {
MessageBox(g_hwnd, TEXT("创建监听套接字失败!"), TEXT("socket Error"), MB_OK);
WSACleanup();
return;
};
// 创建事件对象,为监听套接字把该事件对象与一些网络事件相关联
WSAEVENT hEvent = WSACreateEvent();
WSAEventSelect(g_socketListen, hEvent, FD_ACCEPT/* | FD_CLOSE*/);
// 把事件对象和监听套接字放入相关数组中
g_eventArray[g_nTotalEvent] = hEvent;
g_socketArray[g_nTotalEvent] = g_socketListen;
g_nTotalEvent++;
// 3、将监听套接字与指定的IP地址、端口号捆绑
sockaddr_in sockAddr;
sockAddr.sin_family = AF_INET;
sockAddr.sin_port = htons(8000);
sockAddr.sin_addr.S_un.S_addr = INADDR_ANY;
if (bind(g_socketListen, (sockaddr*)&sockAddr, sizeof(sockAddr)) == SOCKET_ERROR) {
MessageBox(g_hwnd, TEXT("将监听套接字与指定的IP地址、端口号捆绑失败!"), TEXT("bind Error"), MB_OK);
closesocket(g_socketListen);
WSACleanup();
return;
};
// 4、使监听套节字进入监听(等待被连接)状态
if (listen(g_socketListen, SOMAXCONN) == SOCKET_ERROR) {
MessageBox(g_hwnd, TEXT("使监听套节字进入监听(等待被连接)状态失败!"), TEXT("listen Error"), MB_OK);
closesocket(g_socketListen);
WSACleanup();
return;
};
// 服务器监听中...
MessageBox(g_hwnd, TEXT("服务器监听中..."), TEXT("服务启动成功"), MB_OK);
EnableWindow(GetDlgItem(g_hwnd, IDC_BTN_START), FALSE);
// 创建一个新线程在所有事件对象上循环等待网络事件
HANDLE hThread = NULL;
if ((hThread = CreateThread(NULL, 0, WaitProc, NULL, 0, NULL)) != NULL)
CloseHandle(hThread);
return;
};
VOID OnSend() {
CHAR szBuf[BUF_SIZE] = { 0 };
CHAR szMsg[BUF_SIZE] = { 0 };
GetWindowTextA(g_hEditMsg, szBuf, BUF_SIZE);
wsprintfA(szMsg, "服务器说:%s", szBuf);
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szMsg);
SetWindowTextA(g_hEditMsg, "");
// 向每一个客户端发送数据
PSOCKETOBJ p = g_pSocketObjHeader;
while (p != NULL) {
send(p->m_socket, szMsg, strlen(szMsg), 0);
p = p->m_pNext;
};
return;
};
DWORD WINAPI WaitProc(LPVOID lpParam) {
SOCKET socketAccept = INVALID_SOCKET; // 通信套接字句柄
sockaddr_in sockAddrClient;
int nAddrlen = sizeof(sockaddr_in);
int nIndex; // WSAWaitForMultipleEvents返回值
WSANETWORKEVENTS networkEvents; // WSAEnumNetworkEvents函数用的结构
WSAEVENT hEvent = NULL;
PSOCKETOBJ pSocketObj;
int nRet = SOCKET_ERROR; // I/O操作返回值
CHAR szBuf[BUF_SIZE] = { 0 };
CHAR szMsg[BUF_SIZE] = { 0 };
while (TRUE) {
// 在所有事件对象上等待,有任何一个事件对象触发,函数就返回
nIndex = WSAWaitForMultipleEvents(g_nTotalEvent, g_eventArray, FALSE, WSA_INFINITE, FALSE);
nIndex = nIndex - WSA_WAIT_EVENT_0;
// 查看触发的事件对象对应的套接字发生了哪些网络事件
WSAEnumNetworkEvents(g_socketArray[nIndex], g_eventArray[nIndex], &networkEvents);
// 接受客户端连接FD_ACCEPT网络事件
if (networkEvents.lNetworkEvents & FD_ACCEPT) {
if (g_nTotalEvent >= WSA_MAXIMUM_WAIT_EVENTS) {
MessageBox(g_hwnd, TEXT("客户端连接数太多!"), TEXT("accept Error"), MB_OK);
continue;
};
socketAccept = accept(g_socketListen, (sockaddr*)&sockAddrClient, &nAddrlen);
if (socketAccept == INVALID_SOCKET) {
Sleep(100);
continue;
};
// 6、接受客户的连接请求成功
// 创建事件对象,为通信套接字把该事件对象与一些网络事件相关联
hEvent = WSACreateEvent();
WSAEventSelect(socketAccept, hEvent, FD_READ | FD_WRITE | FD_CLOSE);
// 把事件对象和通信套接字放入相关数组中
g_eventArray[g_nTotalEvent] = hEvent;
g_socketArray[g_nTotalEvent] = socketAccept;
g_nTotalEvent++;
ZeroMemory(szBuf, BUF_SIZE);
// 创建一个套接字对象,保存客户端IP地址、端口号
PSOCKETOBJ pSocketObj = CreateSocketObj(socketAccept);
inet_ntop(AF_INET, &sockAddrClient.sin_addr.S_un.S_addr, pSocketObj->m_szIP, _countof(pSocketObj->m_szIP));
pSocketObj->m_usPort = ntohs(sockAddrClient.sin_port);
wsprintfA(szBuf, "客户端[%s:%d] 已连接!", pSocketObj->m_szIP, pSocketObj->m_usPort);
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szBuf);
EnableWindow(g_hBtnSend, TRUE);
}
// 接收客户端数据FD_READ网络事件
else if (networkEvents.lNetworkEvents & FD_READ) {
pSocketObj = FindSocketObj(g_socketArray[nIndex]);
ZeroMemory(szBuf, BUF_SIZE);
nRet = SOCKET_ERROR;
nRet = recv(g_socketArray[nIndex], szBuf, BUF_SIZE, 0);
if (nRet > 0) { // 接收到客户端数据
ZeroMemory(szMsg, BUF_SIZE);
wsprintfA(szMsg, "客户端[%s:%d]说:%s", pSocketObj->m_szIP, pSocketObj->m_usPort, szBuf);
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szMsg);
// 把收到的数据分发到每一个客户端
PSOCKETOBJ p = g_pSocketObjHeader;
while (p != NULL) {
if (p->m_socket != g_socketArray[nIndex])
send(p->m_socket, szMsg, strlen(szMsg), 0);
p = p->m_pNext;
};
};
}
// 发送数据FD_WRITE网络事件,本例不需要处理,因为是按下发送按钮才发送
else if (networkEvents.lNetworkEvents & FD_WRITE) {}
// 客户端连接关闭FD_CLOSE网络事件
else if (networkEvents.lNetworkEvents & FD_CLOSE) {
ZeroMemory(szMsg, BUF_SIZE);
pSocketObj = FindSocketObj(g_socketArray[nIndex]);
wsprintfA(szMsg, "客户端[%s:%d] 已退出!", pSocketObj->m_szIP, pSocketObj->m_usPort);
SendMessageA(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szMsg);
FreeSocketObj(pSocketObj);
// 更新事件对象、套接字数组
for (int j = nIndex; j < g_nTotalEvent - 1; j++) {
g_eventArray[j] = g_eventArray[j + 1];
g_socketArray[j] = g_socketArray[j + 1];
};
g_nTotalEvent--;
// 如果没有客户端在线了,禁用发送按钮
if (g_nTotalClient == 0)
EnableWindow(g_hBtnSend, FALSE);
};
};
};

Overlapped模型

即重叠模型,允许应用程序一次投递多个I/O请求。程序非阻塞调用I/O函数,当I/O操作完成时应用程序将接到通知。通知方式有事件通知和完成例程两种。

WSASocket

创建一个重叠套接字:

1
2
3
4
5
6
7
8
SOCKET WSASocket(
_In_ INT af, //地址家族
_In_ INT type, //套接字类型
_In_ INT protocol, //协议类型
_In_opt_ LPWSAPROTOCOL_INFO lpProtocolInfo, //特征
_In_ GROUP g, //保留 套接字组
_In_ DWORD dwFlags //属性
); //成功返回新套接字句柄 失败INVALID_SOCKET 用WSAGetLastError获取错误代码

此模型需要dwFlags为WSA_FLAG_OVERLAPPED。当dwFlags为NULL时等于socket

AcceptEx

接受一个客户端连接,返回本地地址和远程地址,并接收客户端发送的第一块数据。

1
2
3
4
5
6
7
8
9
10
BOOL AcceptEx(
_In_ SOCKET sListenSocket, //监听套接字句柄
_In_ SOCKET sAcceptSocket, //通信套接字句柄
_In_ PVOID lpOutputBuffer, //返回信息缓冲区
_In_ DWORD dwReceiveDataLength, //缓冲区第一块数据缓冲区字节数 为0不接收
_In_ DWORD dwLocalAddressLength, //缓冲区为本地地址信息保留的字节数 至少比传输协议sockaddr_in最大地址长度多16字节
_In_ DWORD dwRemoteAddressLength, //缓冲区为远程地址信息保留的字节数 长度同上
_Out_ LPDWORD lpdwBytesReceived, //同步模式下实际接收到的第一块数据的字节数 重叠模式无意义
_In_ LPOVERLAPPED lpOverlapped
); //成功返回TRUE 失败FALSE WSAGetLastError获取错误码

错误码为ERROR_IO_PENDING则操作已启动;为WSAECONNRESET则连接请求已传入,但随后远程客户端在接收函数调用前终止。

该函数在Mswsock.h中定义,需要Mswsock.lib,但函数指针可以通过WSAIoctl动态获得。

WSAIoctl

不知道用来干啥,但能用来获取某些函数指针。

1
2
3
4
5
6
7
8
9
10
11
INT WSAIoctl(
_In_ SOCKET s, //套接字句柄
_In_ DWORD dwIoControlCode, //操作控制代码
_In_ LPVOID lpvInBuffer, //输入缓冲区
_In_ DWORD cbInBuffer, //输入缓冲区大小 单位字节
_Out_ LPVOID lpvOutBuffer, //输出缓冲区
_In_ DWORD cbOutBuffer, //输出缓冲区 单位字节
_Out_opt_ LPDWORD lpcbBytesReturned, //指向实际输出字节数
_In_opt_ LPWSAOVERLAPPED lpOverlapped,
_In_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine //完成例程
); //成功返回0 失败SOCKET_ERROR 用WSAGetLastError获取错误码

获取AcceptEx一般方法:

1
2
3
4
5
#include <MSWSock.h>
LPFN_ACCEPTEX lpfnAcceptEx = NULL;
GUID GuidAcceptEx = WSAID_ACCEPTEX;
DWORD dwBytes;
WSAIoctl(socketListen, SIO_GET_EXTENSION_FUNCTION_POINTER, &GuidAcceptEx, sizeof(GuidAcceptEx), &lpfnAcceptEx, sizeof(lpfnAcceptEx), &dwBytes, NULL, NULL);

获取GetAcceptExSockaddrs修改为:

1
2
3
LPFN_GETACCEPTEXSOCKADDRS lpfnGetAcceptExSockaddrs = NULL;
GUID GuidGetAcceptExSockaddrs = WSAID_GETACCEPTEXSOCKADDRS;
//...

GetAcceptExSockaddrs

AcceptEx返回的信息缓冲区解析为3个不同的部分:第一块数据、本地套接字地址和远程套接字地址。当AcceptEx连接成功且用setsockopt在接受的套接字上设置了SO_UPDATE_ACCEPT_CONTEXT选项,则可用getsockname获取与接受套接字相关联的本地地址,用getpeername获取与所接受套接字相关联的远程地址。

1
2
3
4
5
6
7
8
9
10
VOID GetAcceptExSockaddrs(
_In_ PVOID lpOutputBuffer,
_In_ DWORD dwReceiveDataLength,
_In_ DWORD dwLocalAddressLength,
_In_ DWORD dwRemoteAddressLength,
_Out_ LPSOCKADDR* LocalSockaddr, //本地地址
_Out_ LPINT LocalSockaddrLength, //本地地址大小 单位字节
_Out_ LPSOCKADDR* RemoteSockaddr, //远程地址
_Out_ LPINT RemoteSockaddrLength //远程地址大小 单位字节
);

前4个参数与传递个AcceptEx的参数相等。

该函数在Mswsock.dll中导出,需要Mswsock.lib,但也可以用WSAIoctl动态获取。

WSASend/WSARecv

在指定套接字上发送/接收数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
INT WSASend(
_In_ SOCKET s, //套接字句柄
_In_ LPWSABUF lpBuffers,
_In_ DWORD dwBufferCount, //lpBuffers数组元素数量
_Out_ LPDWORD lpNumberOfBytesSent, //返回实际发送字节数指针
_In_ DWORD dwFlags, //调用行为标志 可0
_In_opt_ LPWSAOVERLAPPED lpOverlapped,
_In_opt_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine //完成例程
);
INT WSARecv(
_In_ SOCKET s,
_Inout_ LPWSABUF lpBuffers,
_In_ DWORD dwBufferCount,
_Out_ LPDWORD lpNumberOfBytesRecvd,
_Inout_opt_ LPDWORD lpFlags,
_In_ LPWSAOVERLAPPED lpOverlapped,
_In_ LPWSAOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
);

对于lpBuffers有:

1
2
3
4
typedef struct _WSABUF {
ULONG len; //缓冲区长度 单位字节
_Field_size_bytes_(len) CHAR FAR* buf; //缓冲区指针
} WSABUF, FAR* LPWSABUF;

当且仅当lpOverlapped不为NULL时,lpNumberOfBytesSent应为NULL。WSAOVERLAPPED为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct _OVERLAPPED {
ULONG_PTR Internal; //I/O请求状态代码
ULONG_PTR InternalHigh; //已传输字节数
union {
struct {
DWORD Offset;
DWORD OffsetHigh;
} DUMMYSTRUCTNAME; //非文件对象为0
PVOID Pointer; //保留
} DUMMYUNIONNAME;
HANDLE hEvent; //WSAEVENT事件对象
} OVERLAPPED, * LPOVERLAPPED;
#define WSAOVERLAPPED OVERLAPPED
typedef struct _OVERLAPPED* LPWSAOVERLAPPED

Internal返回I/O请求错误码,发出请求时该字段为STATUS_PENGING表示操作尚未开始。当lpCompletionRoutine为NULL则hEvent必须包含一个有效WSAEVENT事件对象句柄。

完成例程函数格式为:

1
2
3
4
5
6
VOID CALLBACK CompletionROUTINE(
_In_ DWORD dwError, //完成状态
_In_ DWORD cbTransferred, //发送字节数
_In_ LPWSAOVERLAPPED lpOverlapped,
_In_ DWORD dwFlags //0
);

当I/O操作立即完成时lpNumberOfBytesSent参数才返回数据,函数返回0,失败返回SOCKET_ERROR,用WSAGetLastError返回错误代码。错误代码为WSA_IO_PENDING为成功启动重叠操作并在稍后完成发送操作。任何其他错误代码表示未成功启动重叠操作,并不会出现完成通知。

为了传递更多信息,通常自定义一个OVERLAPPED结构,其中第一个字段为原OVERLAPPED结构,其后的部分称为I/O唯一数据或单I/O数据。每次用I/O操作函数都要创建一个该结构,一个I/O请求对应一个,I/O操作后立即释放。

1
2
3
4
5
6
7
8
typedef struct _PERIODATA {
OVERLAPPED m_overlapped; //重叠结构
SOCKET m_socket; //通信套接字句柄
WSABUF m_wsaBuf; //缓冲区结构
CHAR m_szBuffer[BUF_SIZE]; //缓冲区
IOOPERATION m_ioOperation; //操作类型
_PERIODATA* m_pNext;
}PERIODATA, * PPERIODATA;

举例用法:

1
2
3
PERIODATA perIoData;
WSARecv(socket, ..., (LPOVERLAPPED)&perIoData, NULL); //法1
WSARecv(socket, ..., &perIoData.m_overlapped, NULL); //法2

WSAGetOverlappedResult

判断I/O调用结果是否成功:

1
2
3
4
5
6
7
BOOL WSAAPI WSAGetOverlappedResult(
_In_ SOCKET s, //套接字句柄
_In_ LPWSAOVERLAPPED lpOverlapped,
_Out_ LPDWORD lpcbTransfer, //返回实际发送或接收的字节数
_In_ BOOL fWait, //是否等待重叠I/O操作完成
_Out_ LPDWORD lpdwFlags //返回调用行为标志
);

当fWait为TRUE则直到I/O操作完成后才返回,只有采用基于事件完成通知时才能为TRUE;为FALSE且I/O操作仍在进行时函数返回FALSE,WSAGetLastError返回错误码WSA_IO_INCOMPLETE。

成功则返回TRUE,并更新lpcbTransfer指向的值,表示重叠操作已完成;失败返回FALSE,lpcbTransfer无意义,表示重叠操作未完成、重叠操作已完成或若干参数存在错误。

例子

重写上面服务端。OVERLAPPED结构定义头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#pragma once
// 常量定义
const int BUF_SIZE = 4096;
// I/O操作类型,接受连接、接收数据、发送数据
enum IOOPERATION {
IO_UNKNOWN, IO_ACCEPT, IO_READ, IO_WRITE
};
// 自定义重叠结构,OVERLAPPED结构和I/O唯一数据
typedef struct _PERIODATA{
OVERLAPPED m_overlapped; // 重叠结构
SOCKET m_socket; // 通信套接字句柄
WSABUF m_wsaBuf; // 缓冲区结构
CHAR m_szBuffer[BUF_SIZE]; // 缓冲区
IOOPERATION m_ioOperation; // 操作类型
_PERIODATA *m_pNext;
}PERIODATA, *PPERIODATA;
PPERIODATA g_pPerIODataHeader; // 自定义重叠结构链表表头
// 创建一个自定义重叠结构
PPERIODATA CreatePerIOData(SOCKET s) {
PPERIODATA pPerIOData = new PERIODATA;
if (pPerIOData == NULL)
return NULL;
ZeroMemory(pPerIOData, sizeof(PERIODATA));
pPerIOData->m_socket = s;
pPerIOData->m_overlapped.hEvent = WSACreateEvent();
EnterCriticalSection(&g_cs);
// 添加第一个结点
if (g_pPerIODataHeader == NULL) {
g_pPerIODataHeader = pPerIOData;
g_pPerIODataHeader->m_pNext = NULL;
}
else {
pPerIOData->m_pNext = g_pPerIODataHeader;
g_pPerIODataHeader = pPerIOData;
};
LeaveCriticalSection(&g_cs);
return pPerIOData;
};
// 释放一个自定义重叠结构
VOID FreePerIOData(PPERIODATA pPerIOData) {
EnterCriticalSection(&g_cs);
PPERIODATA p = g_pPerIODataHeader;
if (p == pPerIOData) // 移除的是头结点
g_pPerIODataHeader = g_pPerIODataHeader->m_pNext;
else
while (p != NULL) {
if (p->m_pNext == pPerIOData) {
p->m_pNext = pPerIOData->m_pNext;
break;
};
p = p->m_pNext;
};
if (pPerIOData->m_overlapped.hEvent)
WSACloseEvent(pPerIOData->m_overlapped.hEvent);
delete pPerIOData;
LeaveCriticalSection(&g_cs);
};
// 根据事件对象查找自定义重叠结构
PPERIODATA FindPerIOData(HANDLE hEvent) {
EnterCriticalSection(&g_cs);
PPERIODATA pPerIOData = g_pPerIODataHeader;
while (pPerIOData != NULL) {
if (pPerIOData->m_overlapped.hEvent == hEvent) {
LeaveCriticalSection(&g_cs);
return pPerIOData;
};
pPerIOData = pPerIOData->m_pNext;
};
LeaveCriticalSection(&g_cs);
return NULL;
};
// 释放所有自定义重叠结构
VOID DeleteAllPerIOData() {
PERIODATA perIOData;
PPERIODATA pPerIOData = g_pPerIODataHeader;
while (pPerIOData != NULL) {
perIOData = *pPerIOData;
if (pPerIOData->m_overlapped.hEvent)
WSACloseEvent(pPerIOData->m_overlapped.hEvent);
delete pPerIOData;
pPerIOData = perIOData.m_pNext;
};
return;
};

服务端源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <winsock2.h> // Winsock2头文件
#include <ws2tcpip.h> // inet_pton / inet_ntop需要使用这个头文件
#include <Mswsock.h>
#include <strsafe.h>
#include "resource.h"
#include "SOCKETOBJ.h"
#include "PERIODATA.h"
#pragma comment(lib, "Ws2_32") // Winsock2导入库
#pragma comment(lib, "Mswsock") // Mswsock导入库
// 全局变量
HWND g_hwnd; // 窗口句柄
HWND g_hListContent; // 聊天内容列表框窗口句柄
HWND g_hEditMsg; // 消息输入框窗口句柄
HWND g_hBtnSend; // 发送按钮窗口句柄
SOCKET g_socketListen = INVALID_SOCKET; // 监听套接字句柄
WSAEVENT g_eventArray[WSA_MAXIMUM_WAIT_EVENTS]; // 所有事件对象句柄数组
int g_nTotalEvent; // 所有事件对象句柄总数
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
// 对话框初始化
VOID OnInit(HWND hwndDlg);
// 按下启动服务按钮
VOID OnStart();
// 在所有事件对象上循环等待网络事件
DWORD WINAPI WaitProc(LPVOID lpParam);
// 投递接受连接I/O请求
BOOL PostAccept();
// 投递发送数据I/O请求
BOOL PostSend(PSOCKETOBJ pSocketObj, LPTSTR pStr, int nLen);
// 投递接收数据I/O请求
BOOL PostRecv(PSOCKETOBJ pSocketObj);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_SERVER_DIALOG), NULL, DialogProc, NULL);
return 0;
};
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
CHAR szBuf[BUF_SIZE] = { 0 };
CHAR szMsg[BUF_SIZE] = { 0 };
PSOCKETOBJ p;
switch (uMsg) {
case WM_INITDIALOG: {
OnInit(hwndDlg);
return TRUE;
};
case WM_COMMAND: {
switch (LOWORD(wParam)) {
case IDCANCEL: {
// 关闭套接字,释放WinSock库
if (g_socketListen != INVALID_SOCKET)
closesocket(g_socketListen);
WSACleanup();
EndDialog(hwndDlg, IDCANCEL);
break;
};
case IDC_BTN_START: {
OnStart();
break;
};
case IDC_BTN_SEND: {
GetWindowText(g_hEditMsg, szBuf, BUF_SIZE);
wsprintf(szMsg, "服务器说:%s", szBuf);
SendMessage(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szMsg);
SetWindowText(g_hEditMsg, "");
// 向每一个客户端发送数据
p = g_pSocketObjHeader;
while (p != NULL) {
PostSend(p, szMsg, strlen(szMsg));
p = p->m_pNext;
};
break;
};
};
return TRUE;
};
};
return FALSE;
};
//////////////////////////////////////////////////////////////////////////
VOID OnInit(HWND hwndDlg) {
WSADATA wsa = { 0 };
// 1、初始化WinSock库
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
MessageBox(g_hwnd, TEXT("初始化WinSock库失败!"), TEXT("WSAStartup Error"), MB_OK);
return;
};
// 初始化临界区对象,用于同步对套接字对象的访问
InitializeCriticalSection(&g_cs);
g_hwnd = hwndDlg;
g_hListContent = GetDlgItem(hwndDlg, IDC_LIST_CONTENT);
g_hEditMsg = GetDlgItem(hwndDlg, IDC_EDIT_MSG);
g_hBtnSend = GetDlgItem(hwndDlg, IDC_BTN_SEND);
EnableWindow(g_hBtnSend, FALSE);
return;
};
VOID OnStart() {
// 2、创建用于监听所有客户端请求的套接字
g_socketListen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
if (g_socketListen == INVALID_SOCKET) {
MessageBox(g_hwnd, TEXT("创建监听套接字失败!"), TEXT("socket Error"), MB_OK);
WSACleanup();
return;
};
// 3、将监听套接字与指定的IP地址、端口号捆绑
sockaddr_in sockAddr;
sockAddr.sin_family = AF_INET;
sockAddr.sin_port = htons(8000);
sockAddr.sin_addr.S_un.S_addr = INADDR_ANY;
if (bind(g_socketListen, (sockaddr*)&sockAddr, sizeof(sockAddr)) == SOCKET_ERROR) {
MessageBox(g_hwnd, TEXT("将监听套接字与指定的IP地址、端口号捆绑失败!"), TEXT("bind Error"), MB_OK);
closesocket(g_socketListen);
WSACleanup();
return;
};
// 4、使监听套节字进入监听(等待被连接)状态
if (listen(g_socketListen, SOMAXCONN) == SOCKET_ERROR) {
MessageBox(g_hwnd, TEXT("使监听套节字进入监听(等待被连接)状态失败!"), TEXT("listen Error"), MB_OK);
closesocket(g_socketListen);
WSACleanup();
return;
};
// 服务器监听中...
MessageBox(g_hwnd, TEXT("服务器监听中..."), TEXT("服务启动成功"), MB_OK);
EnableWindow(GetDlgItem(g_hwnd, IDC_BTN_START), FALSE);
// 在所有事件对象上循环等待网络事件,本程序只用了一个线程
HANDLE hThread = NULL;
if ((hThread = CreateThread(NULL, 0, WaitProc, NULL, 0, NULL)) != NULL)
CloseHandle(hThread);
// 在此先投递几个接受连接I/O请求
for (int i = 0; i < 2; i++)
PostAccept();
return;
};
DWORD WINAPI WaitProc(LPVOID lpParam){
sockaddr_in* pRemoteSockaddr;
sockaddr_in* pLocalSockaddr;
int nAddrlen = sizeof(sockaddr_in);
int nIndex; // WSAWaitForMultipleEvents返回值
PPERIODATA pPerIOData = NULL; // 自定义重叠结构指针
PSOCKETOBJ pSocketObj = NULL; // 套接字对象结构指针
PSOCKETOBJ pSocketObjAccept = NULL; // 套接字对象结构指针,接受连接成功以后创建的
DWORD dwTransfer; // WSAGetOverlappedResult函数参数
DWORD dwFlags = 0; // WSAGetOverlappedResult函数参数
BOOL bRet = FALSE;
CHAR szBuf[BUF_SIZE] = { 0 };
while (TRUE){
// 在所有事件对象上等待,有任何一个事件对象触发,函数就返回
nIndex = WSAWaitForMultipleEvents(g_nTotalEvent, g_eventArray, FALSE, 1000, FALSE);
if (nIndex == WSA_WAIT_TIMEOUT || nIndex == WSA_WAIT_FAILED)
continue;
nIndex = nIndex - WSA_WAIT_EVENT_0;
WSAResetEvent(g_eventArray[nIndex]);
// 获取指定套接字上重叠I/O操作的结果
pPerIOData = FindPerIOData(g_eventArray[nIndex]);
pSocketObj = FindSocketObj(pPerIOData->m_socket);
bRet = FALSE;
bRet = WSAGetOverlappedResult(pPerIOData->m_socket, &pPerIOData->m_overlapped,&dwTransfer, TRUE, &dwFlags);
if (!bRet) {
if (pSocketObj != NULL) {
ZeroMemory(szBuf, BUF_SIZE);
wsprintf(szBuf, "客户端[%s:%d] 已退出!", pSocketObj->m_szIP, pSocketObj->m_usPort);
SendMessage(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szBuf);
FreeSocketObj(pSocketObj);
};
// 释放自定义重叠结构
FreePerIOData(pPerIOData);
// 更新事件对象数组
for (int j = nIndex; j < g_nTotalEvent - 1; j++)
g_eventArray[j] = g_eventArray[j + 1];
g_nTotalEvent--;
// 如果没有客户端在线了,禁用发送按钮
if (g_nTotalClient == 0)
EnableWindow(g_hBtnSend, FALSE);
continue;
};
// 处理已成功完成的I/O请求
switch (pPerIOData->m_ioOperation) {
case IO_ACCEPT: {
pSocketObjAccept = CreateSocketObj(pPerIOData->m_socket);
ZeroMemory(szBuf, BUF_SIZE);
GetAcceptExSockaddrs(pPerIOData->m_szBuffer, 0, sizeof(sockaddr_in) + 16, sizeof(sockaddr_in) + 16, (LPSOCKADDR*)&pLocalSockaddr, &nAddrlen, (LPSOCKADDR*)&pRemoteSockaddr, &nAddrlen);
inet_ntop(AF_INET, &pRemoteSockaddr->sin_addr.S_un.S_addr, pSocketObjAccept->m_szIP, _countof(pSocketObjAccept->m_szIP));
pSocketObjAccept->m_usPort = ntohs(pRemoteSockaddr->sin_port);
wsprintf(szBuf, "客户端[%s:%d] 已连接!", pSocketObjAccept->m_szIP, pSocketObjAccept->m_usPort);
SendMessage(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szBuf);
EnableWindow(g_hBtnSend, TRUE);
// 释放自定义重叠结构
FreePerIOData(pPerIOData);
// 更新事件对象数组
for (int j = nIndex; j < g_nTotalEvent - 1; j++)
g_eventArray[j] = g_eventArray[j + 1];
g_nTotalEvent--;
PostRecv(pSocketObjAccept);
PostAccept();
break;
};
case IO_READ: {
if (dwTransfer > 0) {
ZeroMemory(szBuf, BUF_SIZE);
wsprintf(szBuf, "客户端[%s:%d]说:%s", pSocketObj->m_szIP, pSocketObj->m_usPort, pPerIOData->m_szBuffer);
SendMessage(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szBuf);
// 把收到的数据分发到每一个客户端
PSOCKETOBJ p = g_pSocketObjHeader;
while (p != NULL) {
if (p->m_socket != pPerIOData->m_socket)
PostSend(p, szBuf, strlen(szBuf));
p = p->m_pNext;
};
PostRecv(pSocketObj);
}
else {
ZeroMemory(szBuf, BUF_SIZE);
wsprintf(szBuf, "客户端[%s:%d] 已退出!", pSocketObj->m_szIP, pSocketObj->m_usPort);
SendMessage(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szBuf);
FreeSocketObj(pSocketObj);
// 如果没有客户端在线了,禁用发送按钮
if (g_nTotalClient == 0)
EnableWindow(g_hBtnSend, FALSE);
};
// 释放自定义重叠结构
FreePerIOData(pPerIOData);
// 更新事件对象数组
for (int j = nIndex; j < g_nTotalEvent - 1; j++)
g_eventArray[j] = g_eventArray[j + 1];
g_nTotalEvent--;
break;
};
case IO_WRITE: {
if (dwTransfer <= 0) {
ZeroMemory(szBuf, BUF_SIZE);
wsprintf(szBuf, "客户端[%s:%d] 已退出!", pSocketObj->m_szIP, pSocketObj->m_usPort);
SendMessage(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szBuf);
FreeSocketObj(pSocketObj);
// 如果没有客户端在线了,禁用发送按钮
if (g_nTotalClient == 0)
EnableWindow(g_hBtnSend, FALSE);
};
// 释放自定义重叠结构
FreePerIOData(pPerIOData);
// 更新事件对象数组
for (int j = nIndex; j < g_nTotalEvent - 1; j++)
g_eventArray[j] = g_eventArray[j + 1];
g_nTotalEvent--;
break;
};
};
};
};
// 投递接受连接I/O请求
BOOL PostAccept() {
SOCKET socketAccept = INVALID_SOCKET; // 通信套接字句柄
BOOL bRet = FALSE;
socketAccept = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
if (socketAccept == INVALID_SOCKET)
return FALSE;
// 为接受连接创建一个自定义重叠结构
PPERIODATA pPerIOData = CreatePerIOData(socketAccept);
pPerIOData->m_ioOperation = IO_ACCEPT;
// 事件对象数组
g_eventArray[g_nTotalEvent] = pPerIOData->m_overlapped.hEvent;
g_nTotalEvent++;
bRet = AcceptEx(g_socketListen, socketAccept, pPerIOData->m_szBuffer, 0, sizeof(sockaddr_in) + 16, sizeof(sockaddr_in) + 16, NULL, (LPOVERLAPPED)pPerIOData);
if (!bRet)
if (WSAGetLastError() != WSA_IO_PENDING)
return FALSE;
return TRUE;
};
// 投递发送数据I/O请求
BOOL PostSend(PSOCKETOBJ pSocketObj, LPTSTR pStr, int nLen) {
DWORD dwFlags = 0;
// 为发送数据创建一个自定义重叠结构
PPERIODATA pPerIOData = CreatePerIOData(pSocketObj->m_socket);
ZeroMemory(pPerIOData->m_szBuffer, BUF_SIZE);
StringCchCopy(pPerIOData->m_szBuffer, BUF_SIZE, pStr);
pPerIOData->m_wsaBuf.buf = pPerIOData->m_szBuffer;
pPerIOData->m_wsaBuf.len = nLen;
pPerIOData->m_ioOperation = IO_WRITE;
// 事件对象数组
g_eventArray[g_nTotalEvent] = pPerIOData->m_overlapped.hEvent;
g_nTotalEvent++;
int nRet = WSASend(pSocketObj->m_socket, &pPerIOData->m_wsaBuf, 1, NULL, dwFlags, (LPOVERLAPPED)pPerIOData, NULL);
if (nRet == SOCKET_ERROR)
if (WSAGetLastError() != WSA_IO_PENDING)
return FALSE;
return TRUE;
};
// 投递接收数据I/O请求
BOOL PostRecv(PSOCKETOBJ pSocketObj) {
DWORD dwFlags = 0;
// 为接收数据创建一个自定义重叠结构
PPERIODATA pPerIOData = CreatePerIOData(pSocketObj->m_socket);
ZeroMemory(pPerIOData->m_szBuffer, BUF_SIZE);
pPerIOData->m_wsaBuf.buf = pPerIOData->m_szBuffer;
pPerIOData->m_wsaBuf.len = BUF_SIZE;
pPerIOData->m_ioOperation = IO_READ;
// 事件对象数组
g_eventArray[g_nTotalEvent] = pPerIOData->m_overlapped.hEvent;
g_nTotalEvent++;
int nRet = WSARecv(pSocketObj->m_socket, &pPerIOData->m_wsaBuf, 1, NULL, &dwFlags, (LPOVERLAPPED)pPerIOData, NULL);
if (nRet == SOCKET_ERROR)
if (WSAGetLastError() != WSA_IO_PENDING)
return FALSE;
return TRUE;
};

I/O完成端口IOCP模型

该模型使用线程池处理异步I/O请求,可管理成百上千个套接字,广泛用于各类高性能服务器,如Apache等。

完成端口是一个Windows I/O结构,可看作一个队列,它接收文件、套接字、邮件槽、管道等多种对象句柄,系统把重叠I/O操作完成事件通知放入队列。它是一个内核对象,单仅与创建它的进程关联,不能进程间共享。创建一个I/O完成端口时,系统维护设备列表、I/O完成队列、等待线程队列、以释放线程列表和已暂停线程列表,没有安全属性结构。

设备列表表示与完成端口关联的若干个设备,每一项都包含设备句柄和完成键,用CreateIoCompletionPort将I/O完成端口与指定句柄关联。

当一个异步I/O操作完成后,完成端口将该操作添加到I/O完成队列末尾,其中每一项都是一个完成包。每个完成包包含传输字节数、完成键、OVERLAPPED结构地址和状态代码。

当线程池中线程从I/O完成队列中获取I/O完成包时,调用线程ID会被添加到等待队列中,等待队列中每一项都包含一个线程ID。当I/O完成队列中出现一项时等待线程队列中一个线程将被唤醒,注意此过程以栈形式进行,即晚进入睡眠状态的线程先被唤醒。对于I/O请求少的用少量线程即可应付,剩下的一直休眠就行,系统也可将未被调度的线程内存资源交换到硬盘上并清除处理器中对应的高速缓存。

当完成端口唤醒一个线程时,该线程ID保存到已释放线程列表中。当一个以释放线程因某些情况进入等待状态时,完成端口将该线程ID从已释放线程列表移除并放进已暂停线程列表。

根据创建完成端口时指定的并发线程数,完成端口将尽可能多的线程保持在已释放线程列表中。当某已释放线程因某些原因进入等待状态时,该线程进入已暂停线程列表,同时已释放线程列表缩减一项,完成端口便可释放另一个正在等待线程。当该被等待线程再次被唤醒时,则离开已暂停线程列表重新进入已释放线程列表,意味着有时已释放线程列表中线程数量会大于允许的最大并发线程数量。但在正在运行线程数量降到允许最大并发线程数前,完成端口不会再继续唤醒其他线程。这就是为啥线程池中线程数量应大于完成端口中设置的并发线程数量。

CreateIoCompletionPort

创建一个I/O完成端口对象并将其指定句柄关联,或仅创建一个句柄关联的I/O完成端口,以后再关联。句柄与I/O完成端口关联后即可接收文件句柄I/O操作通知。

1
2
3
4
5
6
HANDLE WINAPI CreateIoCompletionPort(
_In_ HANDLE FileHandle, //已打开的句柄
_In_opt_ HANDLE ExistingCompletionPort, //已存在的I/O完成端口句柄
_In_ ULONG_PTR CompletionKey, //完成键 传递给处理函数的参数
_In_ DWORD NumberOfConcurrentThreads //同时处理I/O完成端口的I/O操作最大线程数
); //成功返回I/O完成端口句柄 失败NULL 用GetLastError获取错误代码

FileHandler设为INVALID_HANDLE_VALUE则仅创建一个I/O完成端口,不与句柄关联,此时ExistingCompletionPort必须为NULL,CompletionKey将被忽略。ExistingCompletionPort为NULL时,如果FileHandle有效,则创建新I/O完成端口并关联,无效则函数返回新I/O完成端口句柄且不关联。NumberOfConcurrentThreads为0则分配处理器数量相同的线程个数。

CloseHandle关闭I/O完成端口句柄。

GetQueuedCompletionStatus

向完成端口关联套接字句柄后即可在套接字上投递重叠I/O请求,在以后某时间用OVERLAPPED结构接收之前I/O请求结果结果。I/O操作完成后系统向完成端口对象发送一个完成包,I/O完成端口以队列方式管理完成包。

GetQueuedCompletionStatus从完成队列中取出完成包:

1
2
3
4
5
6
7
BOOL WINAPI GetQueuedCompletionStatus(
_In_ HANDLE CompletionPort, //完成端口句柄
_Out_ LPDWORD lpNumberOfBytes, //返回已完成I/O操作所发送或接收的字节数
_Out_ PULONG_PTR lpCompletionKey, //返回与文件句柄关联的完成键
_Out_ LPOVERLAPPED* lpOverlapped, //返回I/O操作函数指定的OVERLAPPED结构地址
_In_ DWORD dwMilliseconds //超时时间 等待完成包出现在完成队列的毫秒数
); //成功TRUE 失败FALSE 用GetLastError

等待时调用线程切换到睡眠模式超时则函数返回FALSE,lpOverlapped为NULL。dwMilliseconds为INFINITE则永不超时,为0且无I/O操作已完成并退出队列则立即返回。

CloseHandle关闭一个完成端口时,系统将所有等待该函数返回的线程唤醒并立即返回FALSE,GetLastError返回ERROR_ABANDONED_WAIT_0。

PostQueuedCompletionStatus

将一个自定义I/O完成包投递到I/O完成队列,也可以执行些自定义操作,还可以与线程池中线程通信。

1
2
3
4
5
6
BOOL WINAPI PostQueuedCompletionStatus(
_In_ HANDLE CompletionPort, //I/O完成端口句柄
_In_ DWORD dwNumberOfBytes,
_In_ ULONG_PTR dwCompletionKey,
_In_opt_ LPOVERLAPPED lpOverlapped
);

后仨参数为GetQueuedCompletionStatus对应返回参数。

GetQueuedCompletionStatusEx

当预计有大量I/O操作完成包时,用该函数从I/O完成队列中一次获取多个完成包,避免多个线程同时等待完成包,从而避免因线程切换带来的系统开销。

1
2
3
4
5
6
7
8
BOOL WINAPI GetQueuedCompletionStatusEx(
_In_ HANDLE CompletionPort, //完成端口句柄
_Out_ LPOVERLAPPED_ENTRY lpCompletionPortEntries,
_In_ ULONG ulCount, //lpCompletionPortEntries数组元素个数
_Out_ PULONG pulNumEntriesRemoved, //从I/O完成队列实际删除的完成包数
_In_ DWORD dwMilliseconds, //超时时间
_In_ BOOL fAlertable //调用线程是否处于可通知等待状态
);

其中OVERLAPPED_ENTRY结构有:

1
2
3
4
5
6
typedef struct _OVERLAPPED_ENTRY {
ULONG_PTR lpCompletionKey; //完成键
LPOVERLAPPED lpOverlapped;
ULONG_PTR Internal; //保留
DWORD dwNumberOfBytesTransferred; //已传输字节数
} OVERLAPPED_ENTRY, * LPOVERLAPPED_ENTRY;

当fAlertable为TRUE时,若I/O完成队列中无完成包,则调用线程进入可通知等待状态,系统将I/O完成例程或APC排队到线程并执行时函数返回;为FALSE则在获取到完成包或超时前不返回。

例子1

重写服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <winsock2.h> // Winsock2头文件
#include <ws2tcpip.h> // inet_pton / inet_ntop需要使用这个头文件
#include <Mswsock.h>
#include <strsafe.h>
#include "resource.h"
#include "SOCKETOBJ.h"
#pragma comment(lib, "Ws2_32") // Winsock2导入库
#pragma comment(lib, "Mswsock") // Mswsock导入库
// 常量定义
const int BUF_SIZE = 4096;
// I/O操作类型
enum IOOPERATION{
IO_ACCEPT, IO_READ, IO_WRITE
};
// OVERLAPPED结构和I/O唯一数据
typedef struct _PERIODATA{
OVERLAPPED m_overlapped; // 重叠结构
SOCKET m_socket; // 通信套接字句柄
WSABUF m_wsaBuf;
CHAR m_szBuffer[BUF_SIZE];
IOOPERATION m_ioOperation; // 操作类型
}PERIODATA, * PPERIODATA;
// 全局变量
HWND g_hwnd; // 窗口句柄
HWND g_hListContent; // 聊天内容列表框窗口句柄
HWND g_hEditMsg; // 消息输入框窗口句柄
HWND g_hBtnSend; // 发送按钮窗口句柄
SOCKET g_socketListen; // 监听套接字句柄
HANDLE g_hCompletionPort; // 完成端口句柄
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
// 对话框初始化
VOID OnInit(HWND hwndDlg);
// 按下启动服务按钮
VOID OnStart();
// TrySubmitThreadpoolCallback的回调函数
VOID CALLBACK WorkerThreadProc(PTP_CALLBACK_INSTANCE Instance, PVOID Context);
// 投递接受连接I/O请求
BOOL PostAccept();
// 投递发送数据I/O请求
BOOL PostSend(SOCKET s, LPTSTR pStr, int nLen);
// 投递接收数据I/O请求
BOOL PostRecv(SOCKET s);
// 按下发送按钮
VOID OnSend();
// 获取逻辑处理器和物理处理器个数
ULARGE_INTEGER GetProcessorInformation();
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_SERVER_DIALOG), NULL, DialogProc, NULL);
return 0;
};
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_INITDIALOG: {
OnInit(hwndDlg);
return TRUE;
};
case WM_COMMAND: {
switch (LOWORD(wParam)) {
case IDCANCEL: {
// 关闭完成端口
if (g_hCompletionPort)
CloseHandle(g_hCompletionPort);
// 关闭套接字,释放WinSock库
if (g_socketListen != INVALID_SOCKET)
closesocket(g_socketListen);
WSACleanup();
EndDialog(hwndDlg, IDCANCEL);
break;
};
case IDC_BTN_START: {
OnStart();
break;
};
case IDC_BTN_SEND: {
OnSend();
break;
};
};
return TRUE;
};
};
return FALSE;
};
//////////////////////////////////////////////////////////////////////////
VOID OnInit(HWND hwndDlg) {
WSADATA wsa = { 0 };
// 1、初始化WinSock库
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
MessageBox(g_hwnd, TEXT("初始化WinSock库失败!"), TEXT("WSAStartup Error"), MB_OK);
return;
};
// 初始化临界区对象,用于同步对套接字对象的访问
InitializeCriticalSection(&g_cs);
g_hwnd = hwndDlg;
g_hListContent = GetDlgItem(hwndDlg, IDC_LIST_CONTENT);
g_hEditMsg = GetDlgItem(hwndDlg, IDC_EDIT_MSG);
g_hBtnSend = GetDlgItem(hwndDlg, IDC_BTN_SEND);
EnableWindow(g_hBtnSend, FALSE);
return;
};
VOID OnStart() {
ULARGE_INTEGER uli = { 0 };
// 2、创建用于监听所有客户端请求的套接字
g_socketListen = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
if (g_socketListen == INVALID_SOCKET) {
MessageBox(g_hwnd, TEXT("创建监听套接字失败!"), TEXT("socket Error"), MB_OK);
WSACleanup();
return;
};
// 3、将监听套接字与指定的IP地址、端口号捆绑
sockaddr_in sockAddr;
sockAddr.sin_family = AF_INET;
sockAddr.sin_port = htons(8000);
sockAddr.sin_addr.S_un.S_addr = INADDR_ANY;
if (bind(g_socketListen, (sockaddr*)&sockAddr, sizeof(sockAddr)) == SOCKET_ERROR) {
MessageBox(g_hwnd, TEXT("将监听套接字与指定的IP地址、端口号捆绑失败!"), TEXT("bind Error"), MB_OK);
closesocket(g_socketListen);
WSACleanup();
return;
};
// 4、使监听套节字进入监听(等待被连接)状态
if (listen(g_socketListen, SOMAXCONN) == SOCKET_ERROR) {
MessageBox(g_hwnd, TEXT("使监听套节字进入监听(等待被连接)状态失败!"), TEXT("listen Error"), MB_OK);
closesocket(g_socketListen);
WSACleanup();
return;
};
// 服务器监听中...
MessageBox(g_hwnd, TEXT("服务器监听中..."), TEXT("服务启动成功"), MB_OK);
EnableWindow(GetDlgItem(g_hwnd, IDC_BTN_START), FALSE);
// 5、
// 创建I/O完成端口,当GetQueuedCompletionStatus函数返回FALSE
// 并且错误代码为ERROR_ABANDONED_WAIT_0的时候可以确定完成端口已关闭
g_hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
// 设置线程池中工作线程的回调函数,线程池会决定如何管理工作线程
TrySubmitThreadpoolCallback(WorkerThreadProc, NULL, NULL);
// 添加监听套接字节点
PSOCKETOBJ pSocketObj = CreateSocketObj(g_socketListen);
// 将监听套接字与完成端口g_hCompletionPort相关联,pSocket作为句柄唯一数据
CreateIoCompletionPort((HANDLE)g_socketListen, g_hCompletionPort, (ULONG_PTR)pSocketObj, 0);
// 在此先投递物理处理器 * 2个接受连接I/O请求,GetProcessorInformation是自定义函数
uli = GetProcessorInformation();
for (DWORD i = 0; i < uli.HighPart * 2; i++)
PostAccept();
return;
};
// TrySubmitThreadpoolCallback的回调函数
VOID CALLBACK WorkerThreadProc(PTP_CALLBACK_INSTANCE Instance, PVOID Context) {
sockaddr_in* pRemoteSockaddr;
sockaddr_in* pLocalSockaddr;
int nAddrlen = sizeof(sockaddr_in);
PSOCKETOBJ pSocketObj = NULL; // 返回与套接字相关联的单句柄数据
PPERIODATA pPerIOData = NULL; // 返回I/O操作函数指定的OVERLAPPED结构的地址
DWORD dwTrans; // 返回已完成的I/O操作所发送或接收的字节数
PSOCKETOBJ pSocket; // 接受连接成功以后,添加一个套接字信息节点
BOOL bRet;
PSOCKETOBJ p;
CHAR szBuf[BUF_SIZE] = { 0 };
while (TRUE) {
bRet = GetQueuedCompletionStatus(g_hCompletionPort, &dwTrans, (PULONG_PTR)&pSocketObj, (LPOVERLAPPED*)&pPerIOData, INFINITE);
if (!bRet) {
if (GetLastError() == ERROR_ABANDONED_WAIT_0) // 完成端口已关闭
break;
else { // 客户端已关闭
ZeroMemory(szBuf, BUF_SIZE);
wsprintf(szBuf, "客户端[%s:%d] 已退出!", pSocketObj->m_szIP, pSocketObj->m_usPort);
SendMessage(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szBuf);
// 释放句柄唯一数据
FreeSocketObj(pSocketObj);
// 释放I/O唯一数据
delete pPerIOData;
// 如果没有客户端在线了,禁用发送按钮
if (g_nTotalClient == 1) // 监听套接字占用了一个结构,所以是1
EnableWindow(g_hBtnSend, FALSE);
continue;
};
};
// 对已完成的I/O操作进行处理,进行到这里,接受连接或数据收发工作已经完成了
switch (pPerIOData->m_ioOperation) {
case IO_ACCEPT: {
// 接受连接已成功,创建套接字信息结构,添加一个节点
pSocket = CreateSocketObj(pPerIOData->m_socket);
// 将通信套接字与完成端口g_hCompletionPort相关联,pSocket作为句柄唯一数据
CreateIoCompletionPort((HANDLE)pSocket->m_socket, g_hCompletionPort, (ULONG_PTR)pSocket, 0);
ZeroMemory(szBuf, BUF_SIZE);
GetAcceptExSockaddrs(pPerIOData->m_szBuffer, 0, sizeof(sockaddr_in) + 16, sizeof(sockaddr_in) + 16, (LPSOCKADDR*)&pLocalSockaddr, &nAddrlen, (LPSOCKADDR*)&pRemoteSockaddr, &nAddrlen);
inet_ntop(AF_INET, &pRemoteSockaddr->sin_addr.S_un.S_addr, pSocket->m_szIP, _countof(pSocket->m_szIP));
pSocket->m_usPort = ntohs(pRemoteSockaddr->sin_port);
wsprintf(szBuf, "客户端[%s:%d] 已连接!", pSocket->m_szIP, pSocket->m_usPort);
SendMessage(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szBuf);
EnableWindow(g_hBtnSend, TRUE);
// 释放I/O唯一数据
delete pPerIOData;
// 投递一个接收数据请求
PostRecv(pSocket->m_socket);
// 继续投递一个接受连接请求
PostAccept();
break;
};
case IO_READ: {
ZeroMemory(szBuf, BUF_SIZE);
wsprintf(szBuf, "客户端[%s:%d]说:%s", pSocketObj->m_szIP, pSocketObj->m_usPort, pPerIOData->m_szBuffer);
SendMessage(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szBuf);
// 释放I/O唯一数据
delete pPerIOData;
// 把收到的数据分发到每一个客户端
p = g_pSocketObjHeader;
while (p != NULL) {
if (p->m_socket != g_socketListen && p->m_socket != pSocketObj->m_socket)
PostSend(p->m_socket, szBuf, strlen(szBuf));
p = p->m_pNext;
};
// 继续投递接收数据请求
PostRecv(pSocketObj->m_socket);
break;
};
case IO_WRITE: {
// 释放I/O唯一数据
delete pPerIOData;
break;
};
};
};
return;
};
// 投递接受连接I/O请求
BOOL PostAccept() {
PPERIODATA pPerIOData = new PERIODATA;
ZeroMemory(&pPerIOData->m_overlapped, sizeof(OVERLAPPED));
pPerIOData->m_ioOperation = IO_ACCEPT;
SOCKET socketAccept = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
pPerIOData->m_socket = socketAccept;
BOOL bRet = AcceptEx(g_socketListen, socketAccept, pPerIOData->m_szBuffer, 0, sizeof(sockaddr_in) + 16, sizeof(sockaddr_in) + 16, NULL, (LPOVERLAPPED)pPerIOData);
if (!bRet)
if (WSAGetLastError() != WSA_IO_PENDING)
return FALSE;
return TRUE;
};
// 投递发送数据I/O请求
BOOL PostSend(SOCKET s, LPTSTR pStr, int nLen) {
DWORD dwFlags = 0;
PPERIODATA pPerIOData = new PERIODATA;
ZeroMemory(&pPerIOData->m_overlapped, sizeof(OVERLAPPED));
pPerIOData->m_ioOperation = IO_WRITE;
ZeroMemory(pPerIOData->m_szBuffer, BUF_SIZE);
StringCchCopy(pPerIOData->m_szBuffer, BUF_SIZE, pStr);
pPerIOData->m_wsaBuf.buf = pPerIOData->m_szBuffer;
pPerIOData->m_wsaBuf.len = BUF_SIZE;
int nRet = WSASend(s, &pPerIOData->m_wsaBuf, 1, NULL, dwFlags, (LPOVERLAPPED)pPerIOData, NULL);
if (nRet == SOCKET_ERROR)
if (WSAGetLastError() != WSA_IO_PENDING)
return FALSE;
return TRUE;
};
// 投递接收数据I/O请求
BOOL PostRecv(SOCKET s) {
DWORD dwFlags = 0;
PPERIODATA pPerIOData = new PERIODATA;
ZeroMemory(&pPerIOData->m_overlapped, sizeof(OVERLAPPED));
pPerIOData->m_ioOperation = IO_READ;
ZeroMemory(pPerIOData->m_szBuffer, BUF_SIZE);
pPerIOData->m_wsaBuf.buf = pPerIOData->m_szBuffer;
pPerIOData->m_wsaBuf.len = BUF_SIZE;
int nRet = WSARecv(s, &pPerIOData->m_wsaBuf, 1, NULL, &dwFlags, (LPOVERLAPPED)pPerIOData, NULL);
if (nRet == SOCKET_ERROR)
if (WSAGetLastError() != WSA_IO_PENDING)
return FALSE;
return TRUE;
};
// 按下发送按钮
VOID OnSend() {
CHAR szBuf[BUF_SIZE] = { 0 };
CHAR szMsg[BUF_SIZE] = { 0 };
GetWindowText(g_hEditMsg, szBuf, BUF_SIZE);
wsprintf(szMsg, "服务器说:%s", szBuf);
SendMessage(g_hListContent, LB_ADDSTRING, 0, (LPARAM)szMsg);
SetWindowText(g_hEditMsg, "");
// 向每一个客户端发送数据
PSOCKETOBJ p = g_pSocketObjHeader;
while (p != NULL) {
if (p->m_socket != g_socketListen)
PostSend(p->m_socket, szMsg, strlen(szMsg));
p = p->m_pNext;
};
return;
};
//////////////////////////////////////////////////////////////////////////
DWORD CountSetBits(ULONG_PTR bitMask) {
DWORD LSHIFT = sizeof(ULONG_PTR) * 8 - 1;
DWORD bitSetCount = 0;
ULONG_PTR bitTest = (ULONG_PTR)1 << LSHIFT;
for (DWORD i = 0; i <= LSHIFT; ++i) {
bitSetCount += ((bitMask & bitTest) ? 1 : 0);
bitTest /= 2;
};
return bitSetCount;
};
ULARGE_INTEGER GetProcessorInformation() {
PSYSTEM_LOGICAL_PROCESSOR_INFORMATION pBuf = NULL; // 缓冲区指针
PSYSTEM_LOGICAL_PROCESSOR_INFORMATION pTemp = NULL; // 临时指针
DWORD dwReturnedLength = 0; // GetLogicalProcessorInformation函数所用参数
DWORD dwLogicalProcessorCount = 0;// 逻辑处理器个数
DWORD dwProcessorCoreCount = 0; // 物理处理器个数
DWORD dwByteOffset = 0; //
ULARGE_INTEGER uli = { 0 }; // 返回值,低、高DWORD分别表示逻辑、物理处理器个数
// 第1次调用
GetLogicalProcessorInformation(pBuf, &dwReturnedLength);
pBuf = (PSYSTEM_LOGICAL_PROCESSOR_INFORMATION)new BYTE[dwReturnedLength];
// 第2次调用
GetLogicalProcessorInformation(pBuf, &dwReturnedLength);
pTemp = pBuf;
while (dwByteOffset + sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION) <= dwReturnedLength) {
if (pTemp->Relationship == RelationProcessorCore) {
// 物理处理器个数
dwProcessorCoreCount++;
// 逻辑处理器个数,一个超线程核心可以有多个逻辑处理器
dwLogicalProcessorCount += CountSetBits(pTemp->ProcessorMask);
};
dwByteOffset += sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION);
pTemp++;
};
delete[] pBuf;
uli.LowPart = dwLogicalProcessorCount;
uli.HighPart = dwProcessorCoreCount;
return uli;
};

例子2

文件复制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
#include <windows.h>
#include <tchar.h>
#include <strsafe.h>
#include "resource.h"
#pragma comment(linker,"\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
// I/O操作类型
enum IOOPERATION { IO_UNKNOWN, IO_READ, IO_WRITE };
// I/O唯一数据类,继承自OVERLAPPED结构
class PERIODATA : public OVERLAPPED {
public:
PERIODATA() {
Internal = InternalHigh = 0;
Offset = OffsetHigh = 0;
hEvent = NULL;
m_nBuffSize = 0;
m_pData = NULL;
};
~PERIODATA() {
if (m_pData != NULL)
VirtualFree(m_pData, 0, MEM_RELEASE);
};
BOOL AllocBuffer(SIZE_T nBuffSize) {
m_nBuffSize = nBuffSize;
m_pData = VirtualAlloc(NULL, m_nBuffSize, MEM_COMMIT, PAGE_READWRITE);
return m_pData != NULL;
};
BOOL Read(HANDLE hFile, PLARGE_INTEGER pliOffset = NULL) {
if (pliOffset != NULL) {
Offset = pliOffset->LowPart;
OffsetHigh = pliOffset->HighPart;
};
return ReadFile(hFile, m_pData, m_nBuffSize, NULL, this);
};
BOOL Write(HANDLE hFile, PLARGE_INTEGER pliOffset = NULL) {
if (pliOffset != NULL) {
Offset = pliOffset->LowPart;
OffsetHigh = pliOffset->HighPart;
};
return WriteFile(hFile, m_pData, m_nBuffSize, NULL, this);
};
private:
SIZE_T m_nBuffSize;
LPVOID m_pData;
};
// 常量定义
#define BUFSIZE (64 * 1024) // 缓冲区大小,内存分配粒度大小64K
#define MAX_PENDING_IO_REQS 4 // 最大I/O请求数
// 全局变量
HWND g_hwndDlg; // 程序窗口句柄
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
BOOL FileCopy(LPCTSTR pszFileSrc, LPCTSTR pszFileDest);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
return 0;
};
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
TCHAR szFileSrc[MAX_PATH] = { 0 }; // 返回用户选择的文件名的缓冲区
HANDLE hFileSrc = INVALID_HANDLE_VALUE; // 源文件句柄
LARGE_INTEGER liFileSizeSrc = { 0 }; // 源文件大小
TCHAR szFileSize[64] = { 0 }; // 源文件大小的字符串形式
TCHAR szFileDest[MAX_PATH] = { 0 }; // 目标文件名缓冲区
LPTSTR lpStr;
TCHAR szBuf[64] = { 0 };
OPENFILENAME ofn = { sizeof(OPENFILENAME) };
ofn.hwndOwner = hwndDlg;
ofn.lpstrFilter = TEXT("All(*.*)\0*.*\0");
ofn.lpstrFile = szFileSrc; // 返回用户选择的文件名的缓冲区
ofn.lpstrFile[0] = NULL; // 不需要初始化文件名编辑控件
ofn.nMaxFile = _countof(szFileSrc);
ofn.lpstrInitialDir = TEXT("C:\\"); // 初始目录
ofn.lpstrTitle = TEXT("请选择要打开的文件"); // 对话框标题栏中显示的字符串
ofn.Flags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_CREATEPROMPT;
switch (uMsg) {
case WM_INITDIALOG: {
g_hwndDlg = hwndDlg;
return TRUE;
};
case WM_COMMAND: {
switch (LOWORD(wParam)) {
case IDC_BTN_OPEN: {
if (GetOpenFileName(&ofn)) {
// 显示文件路径
SetDlgItemText(hwndDlg, IDC_EDIT_FILEPATH, szFileSrc);
// 获取、显示文件大小
hFileSrc = CreateFile(szFileSrc, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFileSrc == INVALID_HANDLE_VALUE) {
MessageBox(hwndDlg, TEXT("打开文件失败"), TEXT("提示"), MB_OK);
return TRUE;
};
GetFileSizeEx(hFileSrc, &liFileSizeSrc);
_i64tot_s(liFileSizeSrc.QuadPart, szFileSize, _countof(szFileSize), 10);
SetDlgItemText(hwndDlg, IDC_EDIT_FILESIZE, szFileSize);
CloseHandle(hFileSrc);
};
break;
};
case IDC_BTN_COPY: {
// 获取源文件路径
GetDlgItemText(hwndDlg, IDC_EDIT_FILEPATH, szFileSrc, _countof(szFileSrc));
// 设置目标文件路径,拼接目标文件名为“源文件名_复制.xxx”
StringCchCopy(szFileDest, _countof(szFileDest), szFileSrc);
lpStr = _tcsrchr(szFileDest, TEXT('.')); // ".xxx"
StringCchCopy(szBuf, _countof(szBuf), lpStr); // 临时保存".xxx"到szBuf
StringCchCopy(lpStr, _countof(szFileDest), TEXT("_复制")); // "源文件名_复制"
StringCchCat(szFileDest, _countof(szFileDest), szBuf); // "源文件名_复制.xxx"
// 开始复制工作
FileCopy(szFileSrc, szFileDest);
MessageBox(hwndDlg, TEXT("复制工作完成"), TEXT("提示"), MB_OK);
break;
};
};
return TRUE;
};
case WM_CLOSE: {
EndDialog(hwndDlg, 0);
return TRUE;
};
};
return FALSE;
};
BOOL FileCopy(LPCTSTR pszFileSrc, LPCTSTR pszFileDest) {
HANDLE hFileSrc = INVALID_HANDLE_VALUE; // 源文件句柄
HANDLE hFileDest = INVALID_HANDLE_VALUE; // 目标文件句柄
LARGE_INTEGER liFileSizeSrc = { 0 }; // 源文件大小
LARGE_INTEGER liFileSizeDest = { 0 }; // 目标文件大小
HANDLE hCompletionPort; // 完成端口句柄
PERIODATA arrPerIOData[MAX_PENDING_IO_REQS]; // I/O唯一数据对象数组
LARGE_INTEGER liNextReadOffset = { 0 }; // 读取源文件使用的文件偏移
INT nReadsInProgress = 0; // 正在进行中的读取请求的个数
INT nWritesInProgress = 0; // 正在进行中的写入请求的个数
// 打开源文件,不使用系统缓存,异步I/O
hFileSrc = CreateFile(pszFileSrc, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED, NULL);
if (hFileSrc == INVALID_HANDLE_VALUE) {
MessageBox(g_hwndDlg, TEXT("打开文件失败"), TEXT("提示"), MB_OK);
return FALSE;
};
// 创建目标文件,最后一个参数设置为hFileSrc表示目标文件使用和源文件相同的属性
hFileDest = CreateFile(pszFileDest, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_NO_BUFFERING | FILE_FLAG_OVERLAPPED, hFileSrc);
if (hFileDest == INVALID_HANDLE_VALUE) {
MessageBox(g_hwndDlg, TEXT("创建文件失败"), TEXT("提示"), MB_OK);
return FALSE;
};
// 获取源文件大小
GetFileSizeEx(hFileSrc, &liFileSizeSrc);
// 目标文件大小设置为内存分配粒度的整数倍
liFileSizeDest.QuadPart = ((liFileSizeSrc.QuadPart / BUFSIZE) * BUFSIZE) + (((liFileSizeSrc.QuadPart % BUFSIZE) > 0) ? BUFSIZE : 0);
// 设置目标文件大小,扩展到内存分配粒度的整数倍,这是为了以内存分配粒度为单位进行I/O操作
SetFilePointerEx(hFileDest, liFileSizeDest, NULL, FILE_BEGIN);
SetEndOfFile(hFileDest);
// 创建I/O完成端口,并将其与源文件和目标文件的文件句柄相关联,注意使用了不同的完成键
hCompletionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);
if (hCompletionPort != NULL) {
CreateIoCompletionPort(hFileSrc, hCompletionPort, IO_READ, 0);
CreateIoCompletionPort(hFileDest, hCompletionPort, IO_WRITE, 0);
}
else
return FALSE;
// 在此先投递MAX_PENDING_IO_REQS(这里是4)个写入操作完成数据包,从而开始读取源文件工作
for (int i = 0; i < MAX_PENDING_IO_REQS; i++) {
arrPerIOData[i].AllocBuffer(BUFSIZE);
PostQueuedCompletionStatus(hCompletionPort, 0, IO_WRITE, &arrPerIOData[i]);
nWritesInProgress++;
};
// 循环直至所有I/O操作完成
while ((nReadsInProgress > 0) || (nWritesInProgress > 0)) {
ULONG_PTR CompletionKey;
DWORD dwNumberOfBytesTransferred;
PERIODATA* pPerIOData;
GetQueuedCompletionStatus(hCompletionPort, &dwNumberOfBytesTransferred, &CompletionKey, (LPOVERLAPPED*)&pPerIOData, INFINITE);
switch (CompletionKey) {
case IO_READ: { // 读取源文件的一部分操作完成,开始写入目标文件
nReadsInProgress--;
pPerIOData->Write(hFileDest);
nWritesInProgress++;
break;
};
case IO_WRITE: { // 写入目标文件的一部分操作完成,开始读取源文件
nWritesInProgress--;
if (liNextReadOffset.QuadPart < liFileSizeDest.QuadPart) {
pPerIOData->Read(hFileSrc, &liNextReadOffset);
nReadsInProgress++;
liNextReadOffset.QuadPart += BUFSIZE;
};
break;
};
};
};
// 复制操作已经完成,清理工作
CloseHandle(hFileSrc);
CloseHandle(hFileDest);
if (hCompletionPort != NULL)
CloseHandle(hCompletionPort);
// 设置目标文件为实际大小,这次不使用FILE_FLAG_NO_BUFFERING,文件操作不受扇区大小对齐这个限制
hFileDest = CreateFile(pszFileDest, GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFileDest == INVALID_HANDLE_VALUE) {
MessageBox(g_hwndDlg, TEXT("设置目标文件大小失败"), TEXT("提示"), MB_OK);
return FALSE;
}
else {
SetFilePointerEx(hFileDest, liFileSizeSrc, NULL, FILE_BEGIN);
SetEndOfFile(hFileDest);
CloseHandle(hFileDest);
};
return TRUE;
};

线程池

碎碎念

线程池是代表应用程序高效执行异步回调的工作线程的集合,用于减少应用程序线程数量并提供对工作线程的管理。主要应用于:

  • 高度并行的应用程序,可异步调度大量小型工作项,如分布式索引搜索、网络I/O。
  • 需要频繁创建和销毁进程且每个进程运行时间短的应用程序,线程池可降低线程管理复杂度和线程创建和销毁的系统开销。
  • 在后台并行处理独立工作项的应用程序,如加载多个选项卡。
  • 在内核对象上执行独占等待或在一个对象上等待某事件从而阻塞的应用程序。
  • 创建一个线程以等待某个事件的应用程序。

工作对象

这小节好好学,下面几小节有些重复的东西就懒得写了。

TrySubmitThreadpoolCallback

设置线程池工作线程调用的回调函数。线程池技术这里不讲。

1
2
3
4
5
BOOL WINAPI TrySubmitThreadpoolCallback(
_In_ PTP_SIMPLE_CALLBACK pfns, //回调函数
_Inout_opt_ PVOID pv, //传递给回调函数的自定义数据
_In_opt_ PTP_CALLBACK_ENVIRON pcbe //回调环境
);

其中回调函数格式为:

1
2
3
4
VOID CALLBACK SimpleCallback(
_Inout_ PTP_CALLBACK_INSTANCE Instance, //回调实例
_Inout_opt_ PVOID pv //传递来的参数
);

CreateThreadpoolWork

创建一个线程池工作对象:

1
2
3
4
5
PTP_WORK WINAPI CreateThreadpoolWork(
_In_ PTP_WORK_CALLBACK pfnwk, //回调函数
_Inout_opt_ PVOID pv, //传递给回调函数的自定义数据
_In_opt_ PTP_CALLBACK_ENVIRON pcbe //回调环境
); //成功返回指向线程池工作对象定义的指针 失败NULL 用GetLastError

每次将工作对象提交到线程池中时,线程池中工作线程都会调用该回调函数。pcbe一般用InitializeThreadpoolEnvironment,也可为NULL表示回调函数在默认回调环境中执行,即默认线程池。

回调函数定义格式为:

1
2
3
4
5
VOID CALLBACK WorkCallback(
_Inout_ PTP_CALLBACK_INSTANCE Instance,
_Inout_opt_ PVOID Context, //传递来的参数
_Inout_ PTP_WORK Work
);

SubmitThreadpoolWork

将一个工作对象提交到线程池:

1
2
3
VOID WINAPI SubmitThreadpoolWork(
_Inout_ PTP_WORK pwk
);

有时TrySubmitThreadpoolCallback会因内存不足或限额而失败,这时可用CreateThreadpoolWork显式创建一个工作对象,再用该函数将工作对象提交到线程池中。该函数无返回值,因为它不会失败。

WaitForThreadpoolWorkCallbacks

等待工作对象回调函数执行完成,还可选择是否取消未开始执行的工作对象中的回调函数。

1
2
3
4
VOID WINAPI WaitForThreadpoolWorkCallbacks(
_Inout_ PTP_WORK pwk, //工作对象定义
_In_ BOOL fCancelPendingCallbacks //是否取消未开始执行的工作对象中的回调函数
);

当工作对象尚未提交时,函数立即返回。

CloseThreadpoolWork

关闭并释放指定工作对象:

1
2
3
VOID WINAPI CloseThreadpoolWork(
_Inout_ PTP_WORK pwk
);

当工作对象中没有未完成的回调函数,则立即释放工作对象;若有尚未完成的回调函数,则再回调函数执行完后异步释放工作对象。

若存在与工作对象关联的清理组则无需用改函数,用CloseThreadpoolCleanupGroupMembers释放与清理组关联的工作对象、计时器对象和等待对象。

计时器对象

CreateThreadpoolTimer

创建一个线程池计时器对象:

1
2
3
4
5
PTP_TIMER WINAPI CreateThreadpoolTimer(
_In_ PTP_TIMER_CALLBACK pfnti, //时间已到时调用的回调函数
_Inout_opt_ PVOID pv, //传递给回调函数的自定义参数
_In_opt_ PTP_CALLBACK_ENVIRON pcbe
); //成功返回指向线程池计时器对象定义的指针 失败NULL 用GetLastError

回调函数格式为:

1
2
3
4
5
VOID CALLBACK TimerCallback(
_Inout_ PTP_CALLBACK_INSTANCE Instance,
_Inout_opt_ PVOID Context,
_Inout_ PTP_TIMER Timer
);

SetThreadpoolTimer

设置线程池计时器对象:

1
2
3
4
5
6
VOID WINAPI SetThreadpoolTimer(
_Inout_ PTP_TIMER pti,
_In_opt_ PFILETIME pftDueTime, //计时器对象触发UTC时间
_In_ DWORD msPeriod, //多久触发一次 单位毫秒
_In_opt_ DWORD msWindowLength //执行计时器对象回调前可延迟的最长时间 单位毫秒
);

pftDueTime为正数时表示一个UTC绝对时间,程序可取的SYSTEMTIME时间,用SystemTimeToFileTime转为本地文件时间,再用LocalFileTimeToFileTime转为UTC文件时间。pftDueTime为NULL时停用计时器对象回调函数,正在执行的不受影响。pftDueTime指向全0的FILETIME结构时立即触发。pftDueTime为负数表示相对时间,单位100纳秒。

msPeriod为正数,直到CloseThreadpoolTimer关闭计时器对象或用SetThreadpoolTimer重新设置计时器对象。msPeriod为0表示一次性的。

回调函数会在从指定的触发时间到指定的触发时间加msWindowLength毫秒这个范围内触发,系统尽量安排若干个计时器对象同时触发,减少线程环境切换、线程唤醒及睡眠等电源和系统开销。

重新设置可再次调用该函数直接改。

WaitForThreadpoolTimerCallbacks

等待计时器对象回调函数执行完成,还可选择是否取消未开始执行的计时器对象中的回调函数。

1
2
3
4
VOID WINAPI WaitForThreadpoolTimerCallbacks(
_Inout_ PTP_TIMER pti,
_In_ BOOL fCancelPendingCallbacks
);

CloseThreadpoolTimer

关闭并释放指定计时器对象:

1
2
3
VOID WINAPI CloseThreadpoolTimer(
_Inout_ PTP_TIMER pti
);

当计时器对象中没有未完成的回调函数,则立即释放计时器对象;若有尚未完成的回调函数,则再回调函数执行完后异步释放计时器对象。

若存在与计时器对象关联的清理组则无需用改函数,用CloseThreadpoolCleanupGroupMembers释放与清理组关联的工作对象、计时器对象和等待对象。

但在该函数后仍可能会有计时器对象的回调函数执行,一般释放时需要:用SetThreadpoolTimer(Ex)且pftDueTime为NULL,msPeriod和msWindowLength为0,再用WaitForThreadpoolTimerCallbacks且fCancelPendingCallbacks为TRUE,最后用该函数。

IsThreadpoolTimerSet

判断指定计时器对象是否被SetThreadpoolTimer(Ex)设置,且pftDueTime为非NULL。

1
2
3
BOOL WINAPI IsThreadpoolTimerSet(
_Inout_ PTP_TIMER pti
);

内核对象

CreateThreadpoolWait

创建一个线程池等待对象:

1
2
3
4
5
PTP_WAIT WINAPI CreateThreadpoolWait(
_In_ PTP_WAIT_CALLBACK pfnwa, //等待的内核对象触发或超时时被调用的回调函数
_Inout_opt_ PVOID pv, //传递给回调函数的自定义数据
_In_opt_ PTP_CALLBACK_ENVIRON pcbe //回调环境
); //成功返回指向线程池等待对象定义 失败NULL 用GetLastError

回调函数定义格式:

1
2
3
4
5
6
VOID CALLBACK WaitCallback(
_Inout_ PTP_CALLBACK_INSTANCE Instance, //回调实例定义
_Inout_opt_ PVOID Context, //传递来的参数
_Inout_ PTP_WAIT Wait, //生成回调等待对象定义
_In_ TP_WAIT_RESULT WaitResult //等待结果
);

WaitResult可以是WAIT_OBJECT_0和WAIT_TIMEOUT。

SetThreadpoolWait

设置等待对象:

1
2
3
4
5
VOID WINAPI SetThreadpoolWait(
_Inout_ PTP_WAIT pwa, //等待对象定义
_In_opt_ HANDLE h, //内核对象句柄
_In_opt_ PFILETIME pftTimeout //超时时间 UTC
);

h为NULL表示停用等待对象回调函数,正在执行的回调函数不受影响。pftDueTime为正数时表示一个UTC绝对时间,程序可取的SYSTEMTIME时间,用SystemTimeToFileTime转为本地文件时间,再用LocalFileTimeToFileTime转为UTC文件时间。pftDueTime为NULL时停用等待对象回调函数,正在执行的不受影响。pftDueTime指向全0的FILETIME结构时立即触发。pftDueTime为负数表示相对时间,单位100纳秒。

一个等待对象只能等待一个内核对象句柄,重复调用本函数设置等待对象将替换之前的句柄。内核对象触发后变为不活跃状态,还使用则必须用该函数设置。

WaitForThreadpoolWaitCallbacks

等待等待对象回调函数执行完,还可选择是否取消尚未开始执行的等待对象中的回调函数。

1
2
3
4
VOID WINAPI WaitForThreadpoolWaitCallbacks(
_Inout_ PTP_WAIT pwa,
_In_ BOOL fCancelPendingCallbacks
);

CloseThreadpoolWait

关闭并释放指定等待对象:

1
2
3
VOID WINAPI CloseThreadpoolWait(
_Inout_ PTP_WAIT pwa
);

但在该函数后仍可能会有等待对象的回调函数执行,一般释放时需要:用SetThreadpoolTimer(Ex)且h为NULL,再用WaitForThreadpoolTimerCallbacks且fCancelPendingCallbacks为TRUE,最后用该函数。

线程池I/O完成对象

CreateThreadpoolIo

创建一个I/O完成对象:

1
2
3
4
5
6
PTP_IO WINAPI CreateThreadpoolIo(
_In_ HANDLE fl, //绑定到I/O完成对象的文件句柄
_In_ PTP_WIN32_IO_CALLBACK pfnio, //回调函数
_Inout_opt_ PVOID pv, //传递给回调函数的自定义数据
_In_opt_ PTP_CALLBACK_ENVIRON pcbe //回调环境
); //成功返回指向线程池I/O完成对象定义的指针 失败NULL 用GetLastError

回调函数定义:

1
2
3
4
5
6
7
8
VOID CALLBACK IoCompletionCallback(
_Inout_ PTP_CALLBACK_INSTANCE Instance,
_Inout_opt_ PVOID Context, //传递来的参数
_Inout_opt_ PVOID Overlapped,
_In_ ULONG IoResult, //I/O操作结果 成功NO_ERROR 否则失败
_In_ ULONG_PTR NumberOfBytes, //已传输字节数
_Inout_ PTP_IO Io
);

StartThreadpoolIo

创建一个I/O完成对象。

1
2
3
VOID WINAPI StartThreadpoolIo(
_Inout_ PTP_IO pio
);

启动每个异步I/O操作前都要用一次该函数,否则线程池将忽略异步I/O操作完成回调,并导致内存破坏。当异步I/O操作失败,即操作结果不是NO_ERROR时,必须用CancelThreadpoolIo取消本次完成通知。当绑定到I/O完成对象的文件句柄有FILE_SKIP_COMPLETION_PORT_ON_SUCCESS通知模式,且异步I/O操作成功返回,则不会调用I/O完成对象回调函数,这时也必须用CancelThreadpoolIo取消本次完成通知。

CancelThreadpoolIo

取消通过StartThreadpoolIo得到的完成通知:

1
2
3
VOID WINAPI CancelThreadpoolIo(
_Inout_ PTP_IO pio
);

WaitForThreadpoolIoCallbacks

等待I/O完成对象回调函数执行完,还可选择是否取消尚未开始执行的I/O完成对象中的回调函数。

1
2
3
4
VOID WINAPI WaitForThreadpoolIoCallbacks(
_Inout_ PTP_IO pio,
_In_ BOOL fCancelPendingCallbacks
);

CloseThreadpoolIo

关闭并释放指定I/O完成对象:

1
2
3
VOID WINAPI CloseThreadpoolIo(
_Inout_ PTP_IO pio
);

若I/O完成对象中存在尚未完成的回调函数时,执行完后异步释放I/O完成对象。用该函数前,应先关闭关联的文件句柄并等待所有未完成异步I/O操作完成,用后不可再向I/O完成对象发起任何I/O请求。

I/O请求可在线程池任何进程上执行,调用取消函数和处理I/O函数可能不是同一个线程,取消线程池线程上I/O请求需要同步,否则导致未知I/O错误。方法是对异步I/O用CancelIoEx时指定所需取消I/O请求的OVERLAPPED结构地址,或自己搞个同步机制确保目标线程用CancelIoEx后没有再启动其他I/O请求。

回调函数返回时

*WhenCallbackReturns

线程池释放指定关键段对象、互斥量对象、信号量对象的所有权、将指定事件对象设为有信号状态、卸载指定DLL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
VOID WINAPI LeaveCriticalSectionWhenCallbackReturns(
_Inout_ PTP_CALLBACK_INSTANCE pci,
_Inout_ PCRITICAL_SECITION pCS
);
VOID WINAPI ReleaseMutexWhenCallbackReturns(
_Inout_ PTP_CALLBACK_INSTANCE pci,
_In_ HANDLE hMutex
);
VOID WINAPI ReleaseSemaphoreWhenCallbackReturns(
_Inout_ PTP_CALLBACK_INSTANCE pci,
_In_ HANDLE hSemaphore,
_In_ DWORD dwReleaseCount
);
VOID WINAPI SetEventWhenCallbackReturns(
_Inout_ PTP_CALLBACK_INSTANCE pci,
_In_ HANDLE hEvent
);
VOID WINAPI FreeLibraryWhenCallbackReturns( //回调函数在DLL时 所在回调函数完成后卸载DLL
_Inout_ PTP_CALLBACK_INSTANCE pci,
_In_ HMODULE hModule
);

上述函数会自动调用LeaveCriticalSectionReleaseMutexReleaseSemaphoreSetEventFreeLibrary,且上述函数只能用其中一个,否则后一个调用覆盖前一个调用。

CallbackMayRunLong

表示回调函数操作需要较长时间:

1
2
3
BOOL WINAPI CallbackMayRunLong(
_Inout_ PTP_CALLBACK_INSTANCE pci
);

该函数返回TRUE表示线程池中有另外的线程或能创建新线程处理其他工作项,这时当前回调函数可放心使用当前线程;返回FALSE时线程池只能在延迟一段时间后尝试创建新线程,这样将影响线程池执行效率,所以这种情况下建议将当前回调函数分块处理。

DisassociateCurrentThreadFromCallback

解除当前正在执行的回调函数与发起回调的对象之间的关联,当前线程将不再为该对象执行回调函数。若当前线程为最后一个代表该对象执行回调的线程,则任何等待对象回调完成的线程都将被释放。说白了就是宣告线程池工作完成,任何因用WaitForThreadpool*Callbacks函数而阻塞的线程可尽快返回。

1
2
3
VOID WINAPI DisassociateCurrentThreadFromCallback(
_Inout_ PTP_CALLBACK_INSTANCE pci
);

新线程池对象

CreateThreadpool

不适用进程默认线程池,创建一个新的线程池以执行回调。

1
2
3
PTP_POOL WINAPI CreateThreadpool(
_Reserved_ PVOID reserved //NULL
); //成功返回指向新线程池定义的指针 失败NULL 用GetLastError

SetThreadpoolThreadMinimum/SetThreadpoolThreadMaximum

设置线程池最小/最大并发数,默认最小并发数为1,最大为500。

1
2
3
4
5
6
7
8
BOOL WINAPI SetThreadpoolThreadMinimum(
_Inout_ PTP_POOL ptpp,
_In_ DWORD dwMinThreadCount
);
VOID WINAPI SetThreadpoolThreadMaximum(
_Inout_ PTP_POOL ptpp,
_In_ DWORD dwMaxThreadCount
);

最大和最小并发线程数为相同值时,线程池创建一组在线程池生命周期内永不被销毁的线程。

InitializzeThreadpoolEnvironment

初始化一个回调环境定义TP_CALLBACK_ENVIRON结构。

1
2
3
VOID InitializeThreadpoolEnvironment(
_Out_ PTP_CALLBACK_ENVIRON pcbe
);

SetThreadpoolCallbackPool

设置指定线程池回调环境:

1
2
3
4
VOID SetThreadpoolCallbackPool(
_Inout_ PTP_CALLBACK_ENVIRON pcbe,
_In_ PTP_POOL
);

DestroyThreadpoolEnvironment

销毁回调环境:

1
2
3
VOID DestroyThreadpoolEnvironment(
_Inout_ PTP_CALLBACK_ENVIRON pcbe
);

CloseThreadpool

关闭并释放线程池:

1
2
3
VOID WINAPI CloseThreadpool(
_Inout_ PTP_POOL ptpp
);

SetThreadpoolCallbackRunsLong

表示与回调环境相关联的回调函数需要较长时间完成:

1
2
3
VOID SetThreadpoolCallbackRunsLong(
_Inout_ PTP_CALLBACK_ENVIRON pcbe
);

SetThreadpoolCallbackLibrary

通知线程池只要有未完成的回调,就要确保指定DLL保持加载状态。若回调函数可能获取加载程序锁,则用该函数防止DllMain中一个线程正等待回调函数结束而另一个正执行回调函数的线程尝试获取加载程序锁时发生死锁。若包含回调函数的DLL可能被卸载,则DllMain清理代码必须在释放对象前取消未完成的回调。

1
2
3
4
VOID SetThreadpoolCallbackLibrary(
_Inout_ PTP_CALLBACK_ENVIRON pcbe,
_In_ PVOID pModule
);

线程池清理组

通过回调环境将工作对象、计时器对象、等待对象或I/O完成对象与清理组关联,用释放清理组函数时自动清理上述对象。

CreateThreadpoolCleanupGroup

创建一个线程池清理组:

1
PTP_CLEANUP_GROUP WINAPI CreateThreadpoolCleanupGroup(VOID); //成功返回指向新线程池清理组定义 失败NULL 用GetLastError

SetThreadpoolCallbackCleanupGroup

将清理组与指定回调环境相关联:

1
2
3
4
5
VOID SetThreadpoolCallbackCleanupGroup(
_Inout_ PTP_CALLBACK_ENVIRON pcbe,
_In_ PTP_CLEANUP_GROUP ptpcg,
_In_opt_ PTP_CLEANUP_GROUP_CANCEL_CALLBACK pfng //清理回调函数
);

释放线程池关联对象前取消清理组,或用CloseThreadpoolCleanupGroupMembers时,调用pfng回调函数。

回调函数定义:

1
2
3
4
VOID NTAPI CleanupGroupCancelCallback(
_Inout_opt_ PVOID ObjectContext, //创建对象时指定的自定义数据
_Inout_opt_ PVOID CleanupContext //释放清理组成员时传递来的参数
);

CreateThreadpool*导致从清理组中隐式添加一个成员,用CloseThreadpool*隐式删除一个成员。

CloseThreadpoolCleanupGroupMembers

释放指定清理组中所有成员,等待所有进行中的回调函数执行完,还可选择是否取消任何未完成的回调函数。

1
2
3
4
5
VOID WINAPI CloseThreadpoolCleanupGroupMembers(
_Inout_ PTP_CLEANUP_GROUP ptpcg,
_In_ BOOL fCancelPendingCallbacks, //是否取消尚未开始执行对象中的回调函数
_Inout_opt_ PVOID pvCleanupContext //传递给清理回调函数的自定义数据
);

该函数会阻塞,直到所有正在执行的任何回调函数完成。函数返回后清理组中任何对象都被释放,不能通过CloseThreadpoolWork等单独释放任何对象。关闭清理组不影响关联回调环境,回调环境直到DestroyThreadpoolEnvironment前会一直存在。该函数也不关闭清理组本身。

CloseThreadpoolCleanupGroup

关闭清理组:

1
2
3
VOID WINAPI CloseThreadpoolCleanupGroup(
_Inout_ PTP_CLEANUP_GROUP ptpcg
);

例子1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <windows.h>
#include <strsafe.h>
#include "resource.h"
#pragma comment(linker,"\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
// 全局变量
HWND g_hwndDlg;
PTP_WORK g_pTpWork;
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
// 工作对象回调函数
VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
return 0;
};
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_INITDIALOG: {
g_hwndDlg = hwndDlg;
// 创建一个工作对象
g_pTpWork = CreateThreadpoolWork(WorkCallback, NULL, NULL);
if (g_pTpWork == NULL)
ExitProcess(0);
return TRUE;
};
case WM_COMMAND: {
switch (LOWORD(wParam)) {
case IDC_BTN_SUBMIT: {
SendMessage(GetDlgItem(g_hwndDlg, IDC_LIST_INFO), LB_RESETCONTENT, 0, 0);
// 提交工作对象
SubmitThreadpoolWork(g_pTpWork);
SubmitThreadpoolWork(g_pTpWork);
SubmitThreadpoolWork(g_pTpWork);
SubmitThreadpoolWork(g_pTpWork);
break;
};
};
return TRUE;
};
case WM_CLOSE: {
// 关闭工作对象
if (g_pTpWork != NULL)
CloseThreadpoolWork(g_pTpWork);
EndDialog(hwndDlg, 0);
return TRUE;
};
};
return FALSE;
};
VOID CALLBACK WorkCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_WORK Work) {
DWORD dwThreadId = GetCurrentThreadId();
TCHAR szBuf[64] = { 0 };
StringCchPrintf(szBuf, _countof(szBuf), TEXT("线程ID[%d]\t开始工作"), dwThreadId);
SendMessage(GetDlgItem(g_hwndDlg, IDC_LIST_INFO), LB_ADDSTRING, 0, (LPARAM)szBuf);
Sleep(1000);
StringCchPrintf(szBuf, _countof(szBuf), TEXT("线程ID[%d]\t工作完成"), dwThreadId);
SendMessage(GetDlgItem(g_hwndDlg, IDC_LIST_INFO), LB_ADDSTRING, 0, (LPARAM)szBuf);
return;
};

例子2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include <windows.h>
#include <Commctrl.h>
#include "resource.h"
#pragma comment(linker,"\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
// 1秒,10000000个100纳秒
const int nSecond = 10000000;
// 全局变量
PTP_TIMER g_pTpTimer;
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
// 计时器对象回调函数
VOID CALLBACK TimerCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_TIMER Timer);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
return 0;
};
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
SYSTEMTIME st = { 0 };
FILETIME ftLocal = { 0 }, ftUTC = { 0 };
//ULARGE_INTEGER uli = { 0 };
switch (uMsg) {
case WM_INITDIALOG: {
// 创建一个计时器对象
g_pTpTimer = CreateThreadpoolTimer(TimerCallback, NULL, NULL);
return TRUE;
};
case WM_COMMAND: {
switch (LOWORD(wParam)) {
case IDC_BTN_SET: {};
case IDC_BTN_RESET: {
//// 自定义一个时间
//st.wYear = 2021;
//st.wMonth = 8;
//st.wDay = 7;
//st.wHour = 18;
//st.wMinute = 28;
//st.wSecond = 0;
//st.wMilliseconds = 0;
// 获取日期时间控件的时间
SendDlgItemMessage(hwndDlg, IDC_DATETIMEPICKER, DTM_GETSYSTEMTIME, 0, (LPARAM)&st);
// 系统时间转换成FILETIME时间
SystemTimeToFileTime(&st, &ftLocal);
// 本地FILETIME时间转换成UTC的FILETIME时间
LocalFileTimeToFileTime(&ftLocal, &ftUTC);
// 设置计时器对象
SetThreadpoolTimer(g_pTpTimer, &ftUTC, 5000, 10);
//// 相对时间的计时器对象
//uli.QuadPart = (ULONGLONG)-(10 * nSecond);
//ftUTC.dwHighDateTime = uli.HighPart;
//ftUTC.dwLowDateTime = uli.LowPart;
//SetThreadpoolTimer(g_pTpTimer, &ftUTC, 5000, 10);
break;
};
case IDC_BTN_STOP: {
// 停止计时器对象
SetThreadpoolTimer(g_pTpTimer, NULL, 0, 0);
break;
};
case IDCANCEL: {
// 关闭并释放计时器对象
SetThreadpoolTimer(g_pTpTimer, NULL, 0, 0);
WaitForThreadpoolTimerCallbacks(g_pTpTimer, TRUE);
CloseThreadpoolTimer(g_pTpTimer);
EndDialog(hwndDlg, 0);
break;
};
};
return TRUE;
};
};
return FALSE;
};
VOID CALLBACK TimerCallback(PTP_CALLBACK_INSTANCE Instance, PVOID Context, PTP_TIMER Timer) {
ShellExecute(NULL, TEXT("open"), TEXT("Calc.exe"), NULL, NULL, SW_SHOW);
return;
};

IPHelper

由IPHLPAPI.dll提供,头文件IPHlpApi.h,导入库为IPHlpApi.lib。

获取本地计算机网络适配器信息

GetAdaptersInfo

获取本地计算机网络适配器信息,已废弃用GetAdaptersAddress

1
2
3
4
DWORD GetAdaptersInfo(
_Out_ PIP_ADAPTER_INFO pAdapterInfo,
_Inout_ PULONG pOutBufLen //缓冲区大小
);

若pOutBufLen不够则该参数返回所需大小,返回错误码ERROR_BUFFER_OVERFLOW,执行成功返回ERROR_SUCCESS。IP_ADAPTER_INFO结构有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
typedef struct _IP_ADAPTER_INFO {
struct _IP_ADAPTER_INFO* Next; //列表中下一个适配器指针
DWORD ComboIndex; //保留
char AdapterName[MAX_ADAPTER_NAME_LENGTH + 4]; //适配器名称
char Description[MAX_ADAPTER_DESCRIPTION_LENGTH + 4]; //适配器描述
UINT AddressLength; //适配器MAC地址长度
BYTE Address[MAX_ADAPTER_ADDRESS_LENGTH]; //适配器MAC地址 字节数组
DWORD Index; //适配器索引
UINT Type; //适配器类型
UINT DhcpEnabled; //是否为此适配器启用DHCP
PIP_ADDR_STRING CurrentIpAddress; //保留
IP_ADDR_STRING IpAddressList; //与此适配器关联的IPv4地址列表
IP_ADDR_STRING GatewayList; //适配器定义的IP地址默认网关
IP_ADDR_STRING DhcpServer; //适配器定义的DHCP服务器IP地址
BOOL HaveWins; //此适配器是否使用WINS
IP_ADDR_STRING PrimaryWinsServer; //主WINS服务器IPv4地址
IP_ADDR_STRING SecondaryWinsServer; //辅助WINS服务器IPv4地址
time_t LeaseObtained; //当前DHCP租约时间
time_t LeaseExpires; //当前DHCP租约期满时间
} IP_ADAPTER_INFO, * PIP_ADAPTER_INFO;

GetAdaptersAddress

获取与本地计算机上的网络适配器相关联的IPv4和IPv6地址信息:

1
2
3
4
5
6
7
ULONG WINAPI GetAdaptersAddress(
_In_ ULONG Family, //地址簇
_In_ ULONG Flags, //地址类型
_In_ PVOID Reserved, //保留
_Inout_ PIP_ADAPTER_ADDRESS AdapterAddresses,
_Inout_ PULONG SizePointer //缓冲区大小
); //成功返回ERROR_SUCCESS

Family可以是:

枚举值 返回内容
AF_UNSPEC 返回与IPv4或IPv6相关适配器的IPv4和IPv6地址
AF_INET 只返回与IPv4相关的适配器的IPv4地址
AF_INET6 只返回与IPv6相关的适配器的IPv6地址

IP_ADAPTER_ADDRESS结构太复杂了自己去学。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <winsock2.h>               // WinSock2头文件
#include <IPHlpApi.h>
#include <stdio.h>
#pragma comment(lib, "Ws2_32") // WinSock2导入库
#pragma comment(lib, "IPHlpApi") // IPHlpApi导入库
int main() {
PIP_ADAPTER_INFO pAdapterInfo = NULL; // IP_ADAPTER_INFO结构体链表缓冲区的指针
PIP_ADAPTER_INFO pAdapter = NULL;
ULONG ulOutBufLen = 0; // 缓冲区的大小
// 第一次调用返回所需缓冲区大小,然后分配缓冲区
GetAdaptersInfo(pAdapterInfo, &ulOutBufLen);
pAdapterInfo = (PIP_ADAPTER_INFO)new CHAR[ulOutBufLen];
// 第二次调用返回所需的适配器信息
if (GetAdaptersInfo(pAdapterInfo, &ulOutBufLen) == ERROR_SUCCESS) {
pAdapter = pAdapterInfo;
while (pAdapter) {
// 适配器的名称
printf("适配器的名称:\t%s\n", pAdapter->AdapterName);
// 适配器的描述
printf("适配器的描述:\t%s\n", pAdapter->Description);
// 适配器的Mac地址
printf("适配器Mac地址:\t");
for (UINT i = 0; i < pAdapter->AddressLength; i++)
if (i == (pAdapter->AddressLength - 1))
printf("%02X\n", (int)pAdapter->Address[i]);
else
printf("%02X-", (int)pAdapter->Address[i]);
// IP地址
printf("IP地址:\t%s\n", pAdapter->IpAddressList.IpAddress.String);
// 子网掩码
printf("子网掩码:\t%s\n", pAdapter->IpAddressList.IpMask.String);
// 默认网关
printf("默认网关:\t%s\n", pAdapter->GatewayList.IpAddress.String);
// 是否为此适配器启用动态主机配置协议(DHCP)
if (pAdapter->DhcpEnabled) {
printf("启用DHCP:\t是\n");
printf("DHCP服务器:\t%s\n", pAdapter->DhcpServer.IpAddress.String);
}
else
printf("启用DHCP:\t否\n");
printf("**********************************************************\n");
pAdapter = pAdapter->Next;
};
}
else
printf("GetAdaptersInfo函数调用失败!\n");
delete[] pAdapterInfo;
return 0;
};

杂项

ConnectEx

建立到指定面向连接的套接字的连接,并可在建立连接口发送一块数据:

1
2
3
4
5
6
7
8
9
10
BOOL PASCAL ConnectEx(
_In_ SOCKET s, //套接字句柄
_In_ CONST struct sockaddr* name, //指定服务器IP地址与端口号
_In_ INT namelen, //name结构长度 单位字节
_In_opt_ PVOID lpSendBuffer, //建立连接后要发送数据的缓冲区
_In_opt_ DWORD dwSendDataLength, //缓冲区大小 单位字节
_Out_opt_ LPDWORD lpdwBytesSend, //返回建立连接后实际发送的字节数
_In_ LPOVERLAPPED lpOverlapped
); //成功返回TRUE 失败FALSE 用WSAGetLastError
typedef VOID(*LPFN_CONNECTEX)();

错误代码为ERROR_IO_PENDING表示连接操作已成功启动但仍在进行。该函数可用WSAIoctl动态获得,GUID为WSAID_CONNECTEX。

gethostname

获取本地计算机标准主机名:

1
2
3
4
INT gethostname(
_Out_ PCHAR name, //接收缓冲区
_In_ INT namelen //缓冲区长度 单位字节
); //成功返回0 失败SOCKET_ERROR 用WSAGetLastError

gethostbyname

返回指定主机名的主机名称和地址信息,已废弃用getaddrinfo

1
2
3
struct hostent* FAR gethostbyname(
_In_ CONST PCHAR name //主机名称
); //成功返回hostent结构指针 失败NULL 用WSAGetLastError

其中hostent结构:

1
2
3
4
5
6
7
8
struct  hostent {
CHAR FAR* h_name; //主机名称
CHAR FAR* FAR* h_aliases; //主机名称别名
SHORT h_addrtype; //地址类型 通常AF_INET
SHORT h_length; //地址长度 单位字节
CHAR FAR* FAR* h_addr_list; //网络字节顺序的主机地址列表
#define h_addr h_addr_list[0]
};

例如:

1
2
3
4
5
CHAR szBuf[64], szIP[16] = { 0 };
gethostname(szBuf, _countof(szBuf));
hostent* pHost = gethostbyname(szBuf);
inet_ntop(AF_INET, pHost->h_addr_list[0], szIP, _countof(szIP));
printf("%s\n", szIP);

TransmitFile

在一个已连接的套接字上传输文件数据。该函数用系统缓存管理器获取文件数据,在套接字上提供高性能文件数据传输。

1
2
3
4
5
6
7
8
9
10
BOOL PASCAL TransmitFile(
SOCKET hSocket, //面向连接的套接字句柄
HANDLE hFile, //已打开的要传输的文件句柄
DWORD nNumberOfBytesToWrite, //要传输的文件的字节数 0为整个文件
DWORD nNumberOfBytesPerRead, //每次发送数据块大小 0为默认大小
LPOVERLAPPED lpOverlapped,
LPTRANSMIT_FILE_BUFFERS lpTransmitBuffers, //传输文件缓冲区
DWORD dwFlags //调用行为
); //成功返回TRUE 失败FALSE 用WSAGetLastError
typedef VOID(*LPFN_TRANSMITFILE)();

通过设置lpOverlapped结构的Offset和OffsetHigh字段,指定文件开始数据传输的偏移量。lpOverlapped为NULL时数据传输从文件当前字节偏移量开始;不为NULL时可能该函数返回前重叠I/O请求不会完成,那么该函数返回FALSE,错误码为ERROR_IO_PENDING或WSA_IO_PENDING,文件传输完成后系统将重叠结构hEvent字段或hSocket指定的套接字指定事件设置为已触发状态。lpTransmitBuffers涉及发送文件数据前后要发送的数据,只想传输文件数据则设置为NULL。dwFlags可以是:

枚举值 含义
TF_DISCONNECT 该函数操作进入等待队列后,发起一个传输层断开动作
TF_REUSE_SOCKET 为套接字句柄重用做准备,即该函数完成后仍可被用作AcceptEx的客户机套接字,必须同时指定TF_DISCONNECT
TF_USE_DEFAULT_WORKER 文件传输使用系统默认线程,用于发送大型文件
TF_USE_SYSTEM_THREAD 该操作用系统默认线程执行
TF_USE_KERNEL_APC 用内核异步过程调用处理该请求,而不用工作线程
TF_WRITE_BEHIND 该请求应立即返回,即使远端未确认已收到数据,不能与TF_DISCONNECT或TF_REUSE_SOCKET同时使用

该函数可用WSAIoctl动态获得,GUID为WSAID_TRANSMITFILE。

URLDownloadToFile

从网络上下载一个文件:

1
2
3
4
5
6
7
HRESULT URLDownloadToFile(
_In_opt_ LPUNKNOWN pCaller, //调用应用程序不是ActiveX组件时为NULL
_In_ LPCTSTR szURL, //要下载的URL字符串 可HTTP或HTTPS
_In_opt_ LPCTSTR szFileName, //要下载的文件本地保存路径
_Reserved_ DWORD dwReserved, //0
_In_opt_ LPBINDSTATUSCALLBACK lpfnCB
);

例如:

1
2
TCHAR szURL[] = TEXT("https://xxx/xxx.iso"), szFileName[] = TEXT("D:\\xxx\\xxx.iso");
URLDownloadToFile(NULL, szURL, szFileName, 0, NULL);

时间校对

有三种时间服务协议,自己去找国内可用的时间服务器。

时间日期协议RFC-867定义使用ASCⅡ字符串准确表示日期和时间;时间协议提供32位数组表示从UTC至今秒数;网络时间协议较复杂。这里举例用时间协议。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <WinSock2.h>
#include <WS2tcpip.h>
#include <stdio.h>
#pragma comment(lib, "ws2_32")
// 根据时间协议Time Protocol返回的时间更新系统时间
VOID SetTimeFromTP(ULONG ulTime);
int main() {
WSADATA wsa = { 0 };
SOCKET socketClient = INVALID_SOCKET;
sockaddr_in addrServer; // 时间服务器的地址
int nRet;
// 1、初始化WinSock库
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0)
return 0;
// 2、创建与服务器进行通信的套接字
if ((socketClient = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET)
return 0;
// 3、使用connect函数来请求与服务器连接
addrServer.sin_family = AF_INET;
inet_pton(AF_INET, "132.163.97.1", (LPVOID)(&addrServer.sin_addr.S_un.S_addr));
addrServer.sin_port = htons(37);
if (connect(socketClient, (sockaddr*)&addrServer, sizeof(addrServer)) == SOCKET_ERROR)
return 0;
// 4、接收时间协议返回的时间,自1900年1月1日0点0分0秒0毫秒逝去的毫秒数
ULONG ulTime = 0;
nRet = recv(socketClient, (PCHAR)&ulTime, sizeof(ulTime), 0);
if (nRet > 0) {
// 网络字节序到本机字节序
ulTime = ntohl(ulTime);
SetTimeFromTP(ulTime);
printf("成功与时间服务器的时间同步!\n");
}
else
printf("时间服务器未能返回时间!\n");
closesocket(socketClient);
WSACleanup();
return 0;
};
// 根据时间协议Time Protocol返回的时间更新系统时间
VOID SetTimeFromTP(ULONG ulTime) {
FILETIME ft;
SYSTEMTIME st;
ULARGE_INTEGER uli;
st.wYear = 1900;
st.wMonth = 1;
st.wDay = 1;
st.wHour = 0;
st.wMinute = 0;
st.wSecond = 0;
st.wMilliseconds = 0;
// 系统时间转换为文件时间才可以加上已经逝去的时间ulTime
SystemTimeToFileTime(&st, &ft);
// 文件时间单位是1/1000 0000秒,即1000万分之1秒(100-nanosecond)
// 不要将指向FILETIME结构的指针强制转换为ULARGE_INTEGER *或__int64 *值,
// 因为这可能导致64位Windows上的对齐错误
uli.HighPart = ft.dwHighDateTime;
uli.LowPart = ft.dwLowDateTime;
uli.QuadPart += (ULONGLONG)10000000 * ulTime;
ft.dwHighDateTime = uli.HighPart;
ft.dwLowDateTime = uli.LowPart;
// 再将文件时间转换为系统时间,更新系统时间
FileTimeToSystemTime(&ft, &st);
SetSystemTime(&st);
return;
};

系统网络连接

断开网络就是把所有网卡都禁用了,恢复连接就是启用所有网卡。设备的启用、禁用就是对该设备的卸载和重安装。

UuidFromString

把一个UUID字符串转换为UUID类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
RPC_STATUS RPC_ENTRY UuidFromStringW(
_In_opt_ RPC_WSTR StringUuid, //UUID字符串
_Out_ UUID* Uuid
); //成功返回RPC_S_OK 失败RPC_S_INVALID_STRING_UUID
RPC_STATUS RPC_ENTRY UuidFromStringA(
_In_opt_ RPC_CSTR StringUuid, //UUID字符串
_Out_ UUID* Uuid
);
typedef _Null_terminated_ USHORT __RPC_FAR* RPC_WSTR;
typedef _Null_terminated_ UCHAR __RPC_FAR* RPC_CSTR;
typedef GUID UUID;
typedef struct _GUID {
ULONG Data1;
USHORT Data2;
USHORT Data3;
UCHAR Data4[ 8 ];
} GUID;

例如:

1
2
GUID guid;
UuidFromString((RPC_WSTR)TEXT("4D36E972-E325-11CE-BFC1-08002BE10318"), &guid);

SetupDiGetClassDevs

返回一个包含本机所有被请求设备的设备信息集句柄:

1
2
3
4
5
6
HDEVINFO SetupDiGetClassDevs(
_In_opt_ CONST GUID* ClassGuid, //设备安装/接口类的GUID
_In_opt_ PCTSTR Enumerator, //PnP枚举器GUID 或符号名称 或PnP设备实例ID
_In_opt_ HWND hwndParent, //与设备信息集中安装设备实例关联的用户界面窗口句柄
_In_ DWORD Flags //设备安装/接口类标志 过滤指定设备信息集中设备
); //成功返回设备信息集句柄 失败INVALID_HANDLE_VALUE 用GetLastError

Flags可以是:

枚举值 含义
DIGCF_ALLCLASSES 返回所有设备安装/接口类已安装设备列表,ClassGuid为NULL
DIGCF_PRESENT 仅返回系统当前存在的设备
DIGCF_PROFILE 仅返回属于当前硬件配置文件的设备

SetupDiEnumDeviceInfo

枚举设备信息集中的设备,返回一个设备的信息。

1
2
3
4
5
BOOL SetupDiEnumDeviceInfo(
_In_ HDEVINFO DeviceInfoSet, //设备信息集句柄
_In_ DWORD MemberIndex, //设备索引 从0开始
_Out_ PSP_DEVINFO_DATA DeviceInfoData //返回指定设备信息
); //成功TRUE 失败FALSE 用GetLastError

MemberIndex开始为0,依次递增,直到枚举完毕返回FALSE,错误码ERROR_NO_MORE_ITEMS。其中SP_DEVINFO_DATA结构为:

1
2
3
4
5
6
typedef struct _SP_DEVINFO_DATA {
DWORD cbSize; //该结构大小
GUID ClassGuid; //设备安装类GUID
DWORD DevInst; //设备实例句柄
ULONG_PTR Reserved;
} SP_DEVINFO_DATA, * PSP_DEVINFO_DATA;

SetupDiSetClassInstallParams

设置或清除设备信息集或特定设备的类安装参数:

1
2
3
4
5
6
BOOL SetupDiSetClassInstallParams(
_In_ HDEVINFO DeviceInfoSet, //设备信息集句柄
_In_opt_ PSP_DEVINFO_DATA DeviceInfoData,
_In_opt_ PSP_CLASSINSTALL_HEADER ClassInstallParams, //设置或清除安装参数
_In_ DWORD ClassInstallParamsSize //ClassInstallParams结构大小
);

这里ClassInstallParams改用SP_PROPCHANGE_PARAMS结构:

1
2
3
4
5
6
7
8
9
10
typedef struct _SP_PROPCHANGE_PARAMS {
SP_CLASSINSTALL_HEADER ClassInstallHeader;
DWORD StateChange;
DWORD Scope;
DWORD HwProfile;
} SP_PROPCHANGE_PARAMS, * PSP_PROPCHANGE_PARAMS;
typedef struct _SP_CLASSINSTALL_HEADER {
DWORD cbSize;
DI_FUNCTION InstallFunction; //设备安装请求代码
} SP_CLASSINSTALL_HEADER, * PSP_CLASSINSTALL_HEADER;

其中InstallFunction需要为DIF_PROPERTYCHANGE表示要更改设备安装属性,StateChange为DICS_ENABLE或DICS_DISABLE表示启用或禁用,Scope为DICS_FLAG_GLOBAL表示全局作用域。

SetupDiCallClassInstaller

执行设备安装:

1
2
3
4
5
BOOL SetupDiCallClassInstaller(
_In_ DI_FUNCTION InstallFunction,
_In_ HDEVINFO DeviceInfoSet, //设备信息集句柄
_In_opt_ PSP_DEVINFO_DATA DeviceInfoData
);

其中InstallFunction设置为DIF_PROPERTYCHANGE表示要更改设备安装属性。

SetupDiDestroyDeviceInfoList

删除设备信息集并释放相关内存:

1
2
3
BOOL SetupDiDestroyDeviceInfoList(
_In_ HDEVINFO DeviceInfoSet
);

例子

COM组件法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include <windows.h>
#include <NetCon.h>
#include "resource.h"
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
BOOL ConnectNetwork(BOOL bConnect);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
return 0;
};
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_COMMAND: {
switch (LOWORD(wParam)) {
case IDC_BTN_CONNECT: {
ConnectNetwork(TRUE);
break;
};
case IDC_BTN_DISCONNECT: {
ConnectNetwork(FALSE);
break;
};
case IDCANCEL: {
EndDialog(hwndDlg, 0);
break;
};
};
return TRUE;
};
};
return FALSE;
};
BOOL ConnectNetwork(BOOL bConnect) {
HRESULT hr;
INetConnectionManager* pNetConnManager;
INetConnection* pNetConn;
IEnumNetConnection* pEnumNetConn;
ULONG uCeltFetched;
CoInitializeEx(NULL, 0);
hr = CoCreateInstance(CLSID_ConnectionManager, NULL, CLSCTX_SERVER, IID_INetConnectionManager, (LPVOID*)&pNetConnManager);
if (FAILED(hr))
return FALSE;
pNetConnManager->EnumConnections(NCME_DEFAULT, &pEnumNetConn);
pNetConnManager->Release();
if (pEnumNetConn == NULL)
return FALSE;
while (pEnumNetConn->Next(1, &pNetConn, &uCeltFetched) == S_OK)
if (bConnect)
pNetConn->Connect(); //启用连接
else
pNetConn->Disconnect(); //禁用连接
CoUninitialize();
return TRUE;
};

SetupAPI法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <windows.h>
#include <SetupAPI.h>
#include "resource.h"
#pragma comment(lib, "Rpcrt4")
#pragma comment(lib, "SetupAPI")
// 全局变量
HWND g_hwndDlg;
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
BOOL ConnectNetwork(BOOL bConnect);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
return 0;
};
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
switch (uMsg) {
case WM_INITDIALOG: {
g_hwndDlg = hwndDlg;
return TRUE;
};
case WM_COMMAND: {
switch (LOWORD(wParam)) {
case IDC_BTN_CONNECT: {
ConnectNetwork(TRUE);
break;
};
case IDC_BTN_DISCONNECT: {
ConnectNetwork(FALSE);
break;
};
case IDCANCEL: {
EndDialog(hwndDlg, 0);
break;
};
};
return TRUE;
};
};
return FALSE;
};
BOOL ConnectNetwork(BOOL bConnect) {
GUID guid;
DWORD dwNewState;
HDEVINFO hDevInfoSet;
SP_DEVINFO_DATA spDevInfoData;
int nDeviceIndex = 0;
SP_PROPCHANGE_PARAMS spPropChangeParams;
if (bConnect)
dwNewState = DICS_ENABLE; //启用
else
dwNewState = DICS_DISABLE; //禁用
// 网卡安装类GUID
UuidFromString((RPC_WSTR)TEXT("4D36E972-E325-11CE-BFC1-08002BE10318"), &guid);
// 获取设备信息集句柄
hDevInfoSet = SetupDiGetClassDevs(&guid, NULL, NULL, DIGCF_PRESENT);
if (hDevInfoSet == INVALID_HANDLE_VALUE) {
MessageBox(g_hwndDlg, TEXT("获取设备信息集句柄出错!"), TEXT("错误提示"), MB_OK);
return FALSE;
};
// 枚举设备
ZeroMemory(&spDevInfoData, sizeof(SP_DEVINFO_DATA));
spDevInfoData.cbSize = sizeof(SP_DEVINFO_DATA);
ZeroMemory(&spPropChangeParams, sizeof(SP_PROPCHANGE_PARAMS));
spPropChangeParams.ClassInstallHeader.cbSize = sizeof(SP_CLASSINSTALL_HEADER);
spPropChangeParams.ClassInstallHeader.InstallFunction = DIF_PROPERTYCHANGE;
spPropChangeParams.StateChange = dwNewState; // 启用或禁用
spPropChangeParams.Scope = DICS_FLAG_GLOBAL;
while (TRUE) {
if (!SetupDiEnumDeviceInfo(hDevInfoSet, nDeviceIndex, &spDevInfoData))
if (GetLastError() == ERROR_NO_MORE_ITEMS)
break;
nDeviceIndex++;
// 安装该设备
SetupDiSetClassInstallParams(hDevInfoSet, &spDevInfoData, (PSP_CLASSINSTALL_HEADER)&spPropChangeParams, sizeof(spPropChangeParams));
SetupDiCallClassInstaller(DIF_PROPERTYCHANGE, hDevInfoSet, &spDevInfoData);
};
// 销毁设备信息集句柄
SetupDiDestroyDeviceInfoList(hDevInfoSet);
return TRUE;
};

FTP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
#include <windows.h>
#include <WinInet.h>
#include <tchar.h>
#include <strsafe.h>
#include "resource.h"
#pragma comment(lib, "Wininet.lib")
#pragma comment(linker,"\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
return 0;
};
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
static TCHAR szFile[MAX_PATH] = { 0 }; // 返回用户选择的本地文件名的缓冲区
static TCHAR szFileTitle[MAX_PATH] = { 0 }; // 返回用户所选本地文件的文件名和扩展名的缓冲区
OPENFILENAME ofn = { sizeof(OPENFILENAME) };
ofn.hwndOwner = hwndDlg;
ofn.lpstrFilter = TEXT("All(*.*)\0*.*\0");
ofn.lpstrFile = szFile;
//ofn.lpstrFile[0] = NULL;
ofn.nMaxFile = _countof(szFile);
ofn.lpstrFileTitle = szFileTitle;
ofn.nMaxFileTitle = _countof(szFileTitle);
ofn.lpstrInitialDir = TEXT("C:\\");
ofn.lpstrTitle = TEXT("请选择要打开的文件");
ofn.Flags = OFN_EXPLORER | OFN_PATHMUSTEXIST | OFN_FILEMUSTEXIST | OFN_CREATEPROMPT;
static HINTERNET hInternet = NULL; // Internet初始化句柄
static HINTERNET hConnect = NULL; // FTP会话句柄
TCHAR szCurrentDirectory[MAX_PATH] = { 0 };
DWORD dwCurrentDirectory = _countof(szCurrentDirectory);
switch (uMsg) {
case WM_INITDIALOG: {
// 打开并初始化WinINet库
hInternet = InternetOpen(TEXT("Mozilla/5.0"), INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
// 连接到指定站点的FTP、HTTP(或HTTPS)服务
hConnect = InternetConnect(hInternet, TEXT("127.0.0.1"), INTERNET_DEFAULT_FTP_PORT, TEXT("admin"), TEXT("123456"), INTERNET_SERVICE_FTP, 0, 0);
return TRUE;
};
case WM_COMMAND: {
switch (LOWORD(wParam)) {
case IDC_BTN_BROWSE: {
if (GetOpenFileName(&ofn))
SetDlgItemText(hwndDlg, IDC_EDIT_FILE, szFile);
break;
};
case IDC_BTN_PUTFILE: {
// 获取本地文件路径
GetDlgItemText(hwndDlg, IDC_EDIT_FILE, szFile, _countof(szFile));
// 上传本地文件到FTP服务器上
FtpPutFile(hConnect, szFile, szFileTitle, FTP_TRANSFER_TYPE_BINARY, 0);
break;
};
case IDC_BTN_GETFILE: {
// 从FTP服务器下载一个文件到程序当前目录
GetModuleFileName(NULL, szFile, _countof(szFile));
StringCchCopy(_tcsrchr(szFile, TEXT('\\')) + 1, _tcslen(szFileTitle) + 1, szFileTitle);
FtpGetFile(hConnect, szFileTitle, szFile, FALSE, FILE_ATTRIBUTE_NORMAL, FTP_TRANSFER_TYPE_BINARY | INTERNET_FLAG_RESYNCHRONIZE, 0);
break;
};
case IDC_BTN_CREATEDIRECTORY: {
// 在FTP服务器上创建一个新的目录
FtpCreateDirectory(hConnect, TEXT("新建文件夹"));
break;
};
case IDC_BTN_REMOVEDIRECTORY: {
// 删除FTP服务器上的一个目录,注意,只能删除非空目录,无法删除当前目录
FtpSetCurrentDirectory(hConnect, TEXT("/"));
FtpRemoveDirectory(hConnect, TEXT("新建文件夹"));
break;
};
case IDC_BTN_SETDIRECTORY: {
// 设置FTP会话的当前目录
FtpSetCurrentDirectory(hConnect, TEXT("新建文件夹"));
FtpGetCurrentDirectory(hConnect, szCurrentDirectory, &dwCurrentDirectory);
SetDlgItemText(hwndDlg, IDC_EDIT_DIRECTORY, szCurrentDirectory);
break;
};
case IDC_BTN_GETDIRECTORY: {
// 获取FTP会话的当前目录
FtpGetCurrentDirectory(hConnect, szCurrentDirectory, &dwCurrentDirectory);
SetDlgItemText(hwndDlg, IDC_EDIT_DIRECTORY, szCurrentDirectory);
break;
};
};
return TRUE;
};
case WM_CLOSE: {
// 关闭相关句柄
InternetCloseHandle(hConnect);
InternetCloseHandle(hInternet);
EndDialog(hwndDlg, 0);
return TRUE;
};
};
return FALSE;
};

HTTP

HttpQueryInfo

用于获取与HTTP请求关联的标头信息:

1
2
3
4
5
6
7
BOOLAPI HttpQueryInfo(
_In_ HINTERNET hRequest, // HttpOpenRequest或InternetOpenUrl函数返回的Internet句柄
_In_ DWORD dwInfoLevel, // 获取标志
_Out_ LPVOID lpBuffer, // 接收所请求信息的缓冲区的指针
_Inout_ LPDWORD lpdwBufferLength,// 缓冲区的长度,以字节为单位
_Inout_opt_ LPDWORD lpdwIndex // 从0开始的标头索引,用于枚举具有相同名称的多个标头时
);

hRequest参数指定为HttpOpenRequest或InternetOpenUrl函数返回的Internet句柄。dwInfoLevel参数用于指定获取标志,可以是以下值的组合:

常量 含义
HTTP_QUERY_CACHE_CONTROL 获取Cache-Control,缓存控制
HTTP_QUERY_CONNECTION 获取Connection,为特定连接指定的任何选项,控制不再转发给代理的标头
HTTP_QUERY_DATE 获取Date,创建报文的日期时间
HTTP_QUERY_TRANSFER_ENCODING 获取Transfer-Encoding,报文主体的传输编码方式
HTTP_QUERY_UPGRADE 获取Upgrade,服务器支持的其他通信协议
HTTP_QUERY_VIA 获取Via,代理服务器的相关信息
HTTP_QUERY_WARNING 获取Warning,警告信息
HTTP_QUERY_ACCEPT 获取Accept,文档类型
HTTP_QUERY_ACCEPT_CHARSET 获取Accept-Charset,字符集
HTTP_QUERY_ACCEPT_ENCODING 获取Accept-Encoding,内容编码
HTTP_QUERY_ACCEPT_LANGUAGE 获取Accept-Language,语言(自然语言)
HTTP_QUERY_AUTHORIZATION 获取Authorization,认证信息
HTTP_QUERY_EXPECT 获取Expect,客户端是否应期待100系列响应
HTTP_QUERY_FROM 获取From,电子邮件地址
HTTP_QUERY_HOST 获取Host,主机名和端口号
HTTP_QUERY_IF_MATCH 获取If-Match,服务器会比较If-Match和资源的ETag值,仅当两者一致时,才会执行请求
HTTP_QUERY_IF_NONE_MATCH 获取If-None-Match,服务器会比较If-Match和资源的ETag值,仅当两者不一致时,才会执行请求
HTTP_QUERY_IF_MODIFIED_SINCE 获取If-Modified-Since,比较资源的更新时间,在指定的日期时间之后如果资源发生了更新, 服务器会接受请求
HTTP_QUERY_IF_UNMODIFIED_SINCE 获取If-Unmodified-Since,比较资源的更新时间,在指定的日期时间之后没有发生更新的情况下, 服务器才会接受请求
HTTP_QUERY_IF_RANGE 获取If-Range,指定的If-Range值(ETag值或时间)和请求资源的ETag值或时间相一致时作为范围请求处理;反之,则返回全体资源
HTTP_QUERY_MAX_FORWARDS 获取Max-Forwards,可以将请求转发到下一个入站服务器的代理或网关的数量
HTTP_QUERY_PROXY_AUTHORIZATION 获取Proxy-Authorization,代理服务器要求客户端的认证信息
HTTP_QUERY_RANGE 获取Range,实体的字节范围请求
HTTP_QUERY_REFERER 获取Referer,请求来源
HTTP_QUERY_USER_AGENT 获取User-Agent,HTTP客户端程序的信息
HTTP_QUERY_ACCEPT_RANGES 获取响应头的Accept-Ranges,是否接受字节范围请求
HTTP_QUERY_AGE 获取响应头的Age,资源创建经过的时间
HTTP_QUERY_ETAG 获取响应头的ETag,资源的匹配信息
HTTP_QUERY_LOCATION 获取响应头的Location,令客户端重定向至指定URL
HTTP_QUERY_PROXY_AUTHENTICATE 获取响应头的Proxy-Authenticate,把代理服务器所要求的认证信息发送给客户端
HTTP_QUERY_RETRY_AFTER 获取响应头的Retry-After,多久之后可以再次发送请求
HTTP_QUERY_SERVER 获取响应头的Server,HTTP服务器的安装信息
HTTP_QUERY_VARY 获取响应头的Vary,代理服务器缓存的管理信息
HTTP_QUERY_WWW_AUTHENTICATE 获取响应头的WWW-Authenticate,访问请求指定资源的认证方案
HTTP_QUERY_ALLOW 获取Allow,服务器支持的HTTP请求方法
HTTP_QUERY_CONTENT_ENCODING 获取Content-Encoding,实体主体的内容编码方式
HTTP_QUERY_CONTENT_LANGUAGE 获取Content-Language,实体主体的语言(自然语言)
HTTP_QUERY_CONTENT_LENGTH 获取Content-Length,实体主体的大小(以字节为单位)
HTTP_QUERY_CONTENT_LOCATION 获取Content-Location,和Location不同, Content-Location表示的是实体主体返回的资源对应的URL
HTTP_QUERY_CONTENT_MD5 获取Content-MD5,为实体主体提供端到端消息完整性检查(MIC)
HTTP_QUERY_CONTENT_RANGE 获取Content-Range,完整实体主体中应插入部分实体主体的位置以及完整实体主体的总大小
HTTP_QUERY_CONTENT_TYPE 获取Content-Type,实体主体的文档类型
HTTP_QUERY_EXPIRES 获取Expires,实体主体过期的日期时间
HTTP_QUERY_LAST_MODIFIED 获取Last-Modified,资源的最后修改日期时间
HTTP_QUERY_COOKIE 获取Cookie,与请求关联的cookie
HTTP_QUERY_SET_COOKIE 获取Set-Cookie,为请求设置的cookie的值
HTTP_QUERY_RAW_HEADERS 获取服务器返回的所有标头,每个标头都以’\0’结尾,所有标头末尾有一个附加的’\0’
HTTP_QUERY_RAW_HEADERS_CRLF 获取服务器返回的所有标头,每个标头都以回车换行分隔
HTTP_QUERY_REQUEST_METHOD 获取请求方法,通常是GET或POST
HTTP_QUERY_STATUS_CODE 获取服务器返回的状态码
HTTP_QUERY_STATUS_TEXT 获取服务器在响应行上返回的任何附加文本
HTTP_QUERY_CONTENT_BASE 获取用于解析实体内的相对URL的基本URL
HTTP_QUERY_MIME_VERSION 获取用于构造消息的MIME协议的版本
HTTP_QUERY_PROXY_CONNECTION 获取代理连接标头
HTTP_QUERY_PUBLIC 获取服务器上可用的方法
HTTP_QUERY_UNLESS_MODIFIED_SINCE 获取Unless-Modified-Since标头
HTTP_QUERY_VERSION 获取服务器返回的最后一个响应代码
HTTP_QUERY_X_CONTENT_TYPE_OPTIONS 获取X-Content-Type-Options标头值
HTTP_QUERY_P3P 获取P3P标头值
HTTP_QUERY_X_P2P_PEERDIST 获取X-P2P-PeerDist标头值
HTTP_QUERY_TRANSLATE 获取translate标头值
HTTP_QUERY_X_UA_COMPATIBLE 获取X-UA-Compatible标头值
HTTP_QUERY_DEFAULT_STYLE 获取Default-Style标头值
HTTP_QUERY_X_FRAME_OPTIONS 获取X-Frame-Options标头值
HTTP_QUERY_X_XSS_PROTECTION 获取X-XSS-Protection标头值
HTTP_QUERY_FLAG_NUMBER 对于值为数字(例如状态代码)的标头,将数据作为32位数字返回
HTTP_QUERY_FLAG_REQUEST_HEADERS 仅查询请求头
HTTP_QUERY_FLAG_SYSTEMTIME 将标头值作为SYSTEMTIME结构返回,用于值为日期时间字符串的标头
HTTP_QUERY_CONTENT_ID 获取内容ID
HTTP_QUERY_CONTENT_TRANSFER_ENCODING 获取资源的附加内容编码
HTTP_QUERY_CUSTOM 获取lpBuffer指定的标头名称并将标头数据存储在lpBuffer中
HTTP_QUERY_PRAGMA 获取特定于实现的指令,这些指令可能应用于请求/响应链上的任何接收者
HTTP_QUERY_URI 获取部分或全部统一资源标识符(URI),通过该标识符可以识别Request-URI资源

lpBuffer参数指定为接收所请求信息的缓冲区的指针。lpdwBufferLength参数用于指定缓冲区的长度,以字节为单位。lpdwIndex参数指定为从0开始的标头索引,用于枚举具有相同名称的多个标头时。要枚举具有相同名称的多个标头,第1次调用HttpQueryInfo函数的时候可以将lpdwIndex参数指向的DWORD型变量的值设置为0,函数返回时,该参数指向的DWORD型变量的值会被设置为下一个标头的索引,程序可以通过lpdwIndex参数的返回值循环调用HttpQueryInfo函数,每次调用HttpQueryInfo函数都应该通过调用GetLastError函数获取错误代码,直到返回错误代码ERROR_HTTP_HEADER_NOT_FOUND。

InternetSetOption

设置Internet选项:

1
2
3
4
5
6
BOOLAPI InternetSetOption(
_In_opt_ HINTERNET hInternet, // Internet句柄
_In_ DWORD dwOption, // 要设置的Internet选项
_In_opt_ LPVOID lpBuffer, // 包含Internet选项设置的缓冲区的指针
_In_ DWORD dwBufferLength // 缓冲区的大小
);

hInternet参数指定为要设置的Internet句柄。dwOption参数指定为要设置的Internet选项,可以是以下值之一(可以用于InternetSetOption和InternetQueryOption函数):

常量 含义
INTERNET_OPTION_BYPASS_EDITED_ENTRY 设置或获取系统是否应该检查网络以获取较新的内容,并在发现较新的版本时覆盖已编辑的缓存条目
INTERNET_OPTION_CACHE_TIMESTAMPS 从Internet缓存中获取INTERNET_CACHE_TIMESTAMPS结构,该结构包含LastModified和Expires时间
INTERNET_OPTION_CALLBACK 设置或获取为Internet句柄定义的回调函数的地址
INTERNET_OPTION_CLIENT_CERT_CONTEXT lpBuffer参数必须是指向CERT_CONTEXT结构的指针,而不是指向CERT_CONTEXT指针的指针。如果应用程序收到ERROR_INTERNET_CLIENT_AUTH_CERT_NEEDED,它必须在重试请求之前调用InternetErrorDlg或使用InternetSetOption提供证书,然后调用CertDuplicateCertificateContext,这样传递的证书上下文可以由应用程序独立释放,仅用于InternetSetOption函数
INTERNET_OPTION_CODEPAGE 默认情况下,Unicode URL的主机或授权部分根据IDN规范进行编码。当IDN被禁用时,在请求或连接句柄上设置该选项可以为URL的主机部分指定代码页编码方案,调用InternetSetOption时的lpBuffer参数包含所需的DBCS代码页;如果在lpBuffer参数中没有指定代码页,WinINet使用默认的系统代码页(CP_ACP)。注意:如果未禁用IDN,则忽略该选项
INTERNET_OPTION_CODEPAGE_PATH 默认情况下,URL的路径部分采用UTF8编码。WinINet对高位字符执行转义(%)编码,在请求或连接句柄上设置该选项会禁用UTF8编码并设置为指定的代码页。调用InternetSetOption时的lpBuffer参数指定为URL路径所需的DBCS代码页;如果在lpBuffer参数中没有指定代码页,WinINet使用默认的CP_UTF8
INTERNET_OPTION_CODEPAGE_EXTRA 默认情况下,URL的路径部分采用默认系统代码页(CP_ACP),不会对额外部分执行转义(%)转换,在请求或连接句柄上设置该选项会禁用CP_ACP编码。调用InternetSetOption时的lpBuffer参数指定为URL额外部分所需的DBCS代码页;如果在lpBuffer参数中没有指定代码页,WinINet使用默认的系统代码页(CP_ACP)
INTERNET_OPTION_COMPRESSED_CONTENT_LENGTH 对于WinINet解压缩服务器提供的Content-Encoding请求,将服务器报告的响应正文的Content-Length作为ULONGLONG型,适用于Windows 10 1507及更高版本
INTERNET_OPTION_CONNECT_RETRIES 设置或获取WinINet尝试解析和连接到主机的次数,默认值为5
INTERNET_OPTION_CONNECT_TIMEOUT 设置或获取Internet连接请求的超时值(以毫秒为单位),设置为0xFFFFFFFF表示禁用计时器
INTERNET_OPTION_CONNECTED_STATE 设置或获取连接状态
INTERNET_OPTION_CONTEXT_VALUE 设置或获取与Internet句柄关联的上下文值地址的DWORD_PTR
INTERNET_OPTION_DATA_RECEIVE_TIMEOUT 设置或获取对FTP事务数据通道请求的响应的超时值(以毫秒为单位),仅用于FTP
INTERNET_OPTION_DATA_SEND_TIMEOUT 设置或获取对FTP事务数据通道请求的超时值(以毫秒为单位),仅用于FTP
INTERNET_OPTION_DATAFILE_NAME 获取下载实体的文件名称
INTERNET_OPTION_DATAFILE_EXT 设置下载实体的文件扩展名
INTERNET_OPTION_DIAGNOSTIC_SOCKET_INFO 获取HTTP请求的数据的INTERNET_DIAGNOSTIC_SOCKET_INFO结构,Windows 7不再支持该选项
INTERNET_OPTION_DIGEST_AUTH_UNLOAD 注销摘要式身份验证SSPI包,清除为该进程创建的所有凭据,仅用于InternetSetOption函数
INTERNET_OPTION_ENABLE_HTTP_PROTOCOL 设置HTTP版本,可用的值仅有HTTP_PROTOCOL_FLAG_HTTP2(0x2),仅用于Windows10 1507及更高版本
INTERNET_OPTION_ENABLE_REDIRECT_CACHE_READ 设置是否从WinINet缓存中为给定请求返回重定向,默认值为FALSE,仅用于Windows 8及更高版本
INTERNET_OPTION_ENCODE_EXTRA 设置或获取查询字符串中的非ASCII字符是否应进行%编码,默认值为FALSE,仅用于Windows 8.1及更高版本
INTERNET_OPTION_END_BROWSER_SESSION 从硬盘驱动器上的密码缓存中刷新未使用的条目,还重置同步模式为每个会话一次时使用的缓存时间,仅用于InternetSetOption函数
INTERNET_OPTION_ERROR_MASK 设置可以由客户端应用程序处理的错误掩码,可以是以下值的组合:INTERNET_ERROR_MASK_COMBINED_SEC_CERT表示所有证书错误都将使用相同的错误代码ERROR_INTERNET_SEC_CERT_ERRORS;INTERNET_ERROR_MASK_INSERT_CDROM表示客户端应用程序可以处理错误代码ERROR_INTERNET_INSERT_CDROM;INTERNET_ERROR_MASK_LOGIN_FAILURE_DISPLAY_ENTITY_BODY表示客户端应用程序可以处理错误代码ERROR_INTERNET_LOGIN_FAILURE_DISPLAY_ENTITY_BODY
INTERNET_OPTION_ENTERPRISE_CONTEXT 设置用于请求的企业ID,仅用于Windows 10 1507及更高版本
INTERNET_OPTION_EXTENDED_ERROR 获取Winsock错误代码,映射到线程上下文中最后返回的ERROR_INTERNET_*错误消息
INTERNET_OPTION_FROM_CACHE_TIMEOUT 设置或获取系统在检查缓存中是否有资源副本之前等待网络请求响应的时间
INTERNET_OPTION_HANDLE_TYPE 获取Internet句柄类型,例如INTERNET_HANDLE_TYPE_CONNECT_FTP、INTERNET_HANDLE_TYPE_CONNECT_HTTP等
INTERNET_OPTION_HSTS 设置或获取WinINet是否应遵循来自服务器的HTTP严格传输安全(HSTS)指令,如果启用,对具有WinINet缓存的HSTS策略的域的https://方案请求将被重定向到匹配的https://URL。默认值为FALSE。仅用于Windows 8.1及更高版本
INTERNET_OPTION_HTTP_DECODING 使WinINet能够对gzip和deflate编码方案执行解码
INTERNET_OPTION_HTTP_PROTOCOL_USED 获取HTTP版本,可用的值仅有HTTP_PROTOCOL_FLAG_HTTP2(0x2),仅用于Windows 10 1507及更高版本
INTERNET_OPTION_HTTP_VERSION 设置或获取HTTP版本的HTTP_VERSION_INFO结构
INTERNET_OPTION_IDN 设置请求或连接句柄以启用或禁用IDN
INTERNET_OPTION_IGNORE_OFFLINE 设置或获取是否应忽略请求句柄的全局脱机标志
INTERNET_OPTION_MAX_CONNS_PER_1_0_SERVER 设置或获取HTTP/1.0服务器允许的最大连接数
INTERNET_OPTION_MAX_CONNS_PER_PROXY 设置或获取每个CERN代理允许的最大连接数
INTERNET_OPTION_MAX_CONNS_PER_SERVER 设置或获取每个服务器允许的最大连接数
INTERNET_OPTION_OPT_IN_WEAK_SIGNATURE 将弱签名(例如SHA-1)视为不安全签名,这将指示WinINet使用CERT_CHAIN_OPT_IN_WEAK_SIGNATURE参数调用CertGetCertificateChain
INTERNET_OPTION_PARENT_HANDLE 获取指定Internet句柄的父句柄
INTERNET_OPTION_PASSWORD 设置或获取与InternetConnect函数返回的Internet句柄关联的密码
INTERNET_OPTION_PER_CONNECTION_OPTION 设置或获取INTERNET_PER_CONN_OPTION_LIST结构,该结构指定特定连接的选项列表
INTERNET_OPTION_PROXY 设置或获取INTERNET_PROXY_INFO结构,该结构包含现有InternetOpen句柄的代理数据
INTERNET_OPTION_PROXY_PASSWORD 设置或获取用于访问代理的密码
INTERNET_OPTION_PROXY_SETTINGS_CHANGED 通知当前WinINet实例代理设置已更改,并且必须使用新设置进行更新。要通知所有可用的WinINet实例,需要将InternetSetOption的lpBuffer参数设置为NULL并将dwBufferLength设置为0
INTERNET_OPTION_PROXY_USERNAME 设置或获取用于访问代理的用户名
INTERNET_OPTION_READ_BUFFER_SIZE 设置或获取读缓冲区的大小,仅用于FTP
INTERNET_OPTION_CONTROL_RECEIVE_TIMEOUT或 INTERNET_OPTION_RECEIVE_TIMEOUT 设置或获取对请求响应的超时值(以毫秒为单位)
INTERNET_OPTION_REFRESH 从注册表中重新读取代理数据
INTERNET_OPTION_REQUEST_FLAGS 获取正在进行下载的状态的特殊状态标志,可以是以下值之一:INTERNET_REQFLAG_CACHE_WRITE_DISABLED表示无法缓存Internet请求(例如,HTTPS请求);INTERNET_REQFLAG_FROM_CACHE表示响应来自缓存;INTERNET_REQFLAG_NET_TIMEOUT表示请求超时;INTERNET_REQFLAG_NO_HEADERS表示原始响应不包含标头;INTERNET_REQFLAG_VIA_PROXY表示请求是通过代理发出的
INTERNET_OPTION_REQUEST_PRIORITY 设置或获取竞争连接的请求的优先级
INTERNET_OPTION_RESET_URLCACHE_SESSION 为进程启动一个新的缓存会话
INTERNET_OPTION_SECONDARY_CACHE_KEY 设置或获取二级缓存键的字符串值,仅供内部使用
INTERNET_OPTION_SECURITY_CERTIFICATE 获取格式化字符串形式的SSL/PCT服务器证书
INTERNET_OPTION_SECURITY_CERTIFICATE_STRUCT 获取INTERNET_CERTIFICATE_INFO结构形式的SSL/PCT服务器证书
INTERNET_OPTION_SECURITY_FLAGS 设置或获取Internet句柄的安全标志,可以是以下值的组合:SECURITY_FLAG_FORTEZZA表示Fortezza已用于为指定的连接提供保密、身份验证和/或完整性;SECURITY_FLAG_IGNORE_CERT_CN_INVALID表示忽略ERROR_INTERNET_SEC_CERT_CN_INVALID错误消息;SECURITY_FLAG_IGNORE_CERT_DATE_INVALID表示忽略ERROR_INTERNET_SEC_CERT_DATE_INVALID错误消息;SECURITY_FLAG_IGNORE_REDIRECT_TO_HTTP表示忽略ERROR_INTERNET_HTTPS_TO_HTTP_ON_REDIR错误消息;SECURITY_FLAG_IGNORE_REDIRECT_TO_HTTPS表示忽略ERROR_INTERNET_HTTP_TO_HTTPS_ON_REDIR错误消息;SECURITY_FLAG_IGNORE_REVOCATION表示忽略证书吊销问题;SECURITY_FLAG_IGNORE_UNKNOWN_CA表示忽略未知的证书颁发机构问题;SECURITY_FLAG_IGNORE_WEAK_SIGNATURE表示忽略弱证书签名问题;SECURITY_FLAG_IGNORE_WRONG_USAGE表示忽略错误的使用问题;SECURITY_FLAG_SECURE表示使用安全传输;SECURITY_FLAG_STRENGTH_MEDIUM表示使用中等(56位)加密;SECURITY_FLAG_STRENGTH_STRONG表示使用强(128位)加密;SECURITY_FLAG_NORMALBITNESS或SECURITY_FLAG_STRENGTH_WEAK表示使用弱(40位)加密;SECURITY_FLAG_UNKNOWNBIT表示用于加密的位大小是未知的
INTERNET_OPTION_SECURITY_KEY_BITNESS 获取加密密钥的位大小,数值越大,使用的加密强度越大
INTERNET_OPTION_CONTROL_SEND_TIMEOUT或 INTERNET_OPTION_SEND_TIMEOUT 设置或获取发送请求的超时值
INTERNET_OPTION_SERVER_CERT_CHAIN_CONTEXT 以复制的PCCERT_CHAIN_CONTEXT的形式获取服务器的证书链上下文,可以将此复制的上下文传递给任何采用PCCERT_CHAIN_CONTEXT的加密API函数
INTERNET_OPTION_SETTINGS_CHANGED 通知系统注册表设置已更改
INTERNET_OPTION_SUPPRESS_SERVER_AUTH 设置HTTP请求对象,使其不会登录到源服务器,但会自动登录到HTTP代理服务器,该选项不同于请求标志INTERNET_FLAG_NO_AUTH,后者阻止对代理服务器和源服务器进行身份验证。设置该模式将禁止在与源服务器通信时使用任何凭据材料(先前提供的用户名/密码或客户端SSL证书);但是,如果请求必须通过身份验证代理传输,WinINet仍将根据用户的Intranet区域设置对HTTP代理执行自动身份验证,默认的Intranet区域设置是允许使用用户的默认凭据自动登录。为了确保抑制所有识别信息,调用者应该将INTERNET_OPTION_SUPPRESS_SERVER_AUTHINTERNET_FLAG_NO_COOKIES请求标志结合起来,该选项只能在请求对象被发送之前设置,在请求发送后尝试设置该选项将返回ERROR_INTERNET_INCORRECT_HANDLE_STATE
INTERNET_OPTION_SUPPRESS_BEHAVIOR 设置在进程范围内抑制行为的通用选项,可以是以下值的组合:INTERNET_SUPPRESS_RESET_ALL表示禁用所有抑制,重新启用默认和配置的行为,该选项相当于分别设置INTERNET_SUPPRESS_COOKIE_POLICY_RESET和INTERNET_SUPPRESS_COOKIE_PERSIST_RESET;INTERNET_SUPPRESS_COOKIE_POLICY表示忽略任何已配置的cookie策略并允许设置cookie;INTERNET_SUPPRESS_COOKIE_POLICY_RESET表示禁用INTERNET_SUPPRESS_COOKIE_POLICY抑制,允许根据配置的cookie策略评估cookie;INTERNET_SUPPRESS_COOKIE_PERSIST表示抑制cookie的持久性,即使服务器已将它们指定为持久性;INTERNET_SUPPRESS_COOKIE_PERSIST_RESET表示禁用INTERNET_SUPPRESS_COOKIE_PERSIST抑制,重新启用cookie的持久性,但任何先前被抑制的cookie都不会持久化
INTERNET_OPTION_URL 获取下载资源的完整URL字符串
INTERNET_OPTION_USER_AGENT 设置或获取由InternetOpen函数提供并在后续HttpSendRequest函数中使用的Internet句柄上的用户代理字符串
INTERNET_OPTION_USERNAME 设置或获取与InternetConnect函数返回的Internet句柄关联的用户名字符串
INTERNET_OPTION_VERSION 获取包含Wininet.dll版本号的INTERNET_VERSION_INFO结构
INTERNET_OPTION_WRITE_BUFFER_SIZE 设置或获取写缓冲区大小(以字节为单位),仅用于FTP

lpBuffer参数指定为包含Internet选项设置的缓冲区的指针。dwBufferLength参数用于指定缓冲区的大小。如果lpBuffer参数指定的是一个字符串,则大小以字符为单位;如果lpBuffer参数指定的是字符串以外的任何数据,则大小以字节为单位。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
#include <windows.h>
#include <WinInet.h>
#include <tchar.h>
#include <strsafe.h>
#include "resource.h"
#pragma comment(lib, "Wininet.lib")
#pragma comment(linker,"\"/manifestdependency:type='win32' name='Microsoft.Windows.Common-Controls' version='6.0.0.0' processorArchitecture='*' publicKeyToken='6595b64144ccf1df' language='*'\"")
// 函数声明
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam);
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) {
DialogBoxParam(hInstance, MAKEINTRESOURCE(IDD_MAIN), NULL, DialogProc, NULL);
return 0;
};
INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT uMsg, WPARAM wParam, LPARAM lParam) {
static HINTERNET hInternet = NULL; // Internet初始化句柄
HINTERNET hConnect = NULL; // HTTP会话句柄
HINTERNET hRequest = NULL; // HTTP请求句柄
TCHAR szServerName[MAX_PATH] = { 0 }; // 主机名或IP地址
LPCTSTR arrszAcceptTypes[] = { TEXT("text/*"), TEXT("*/*"), NULL }; // 文档类型
DWORD dwNumberOfBytesAvailable = 0; // InternetQueryDataAvailable函数参数
LPVOID lpBuf = NULL; // 缓冲区指针
DWORD dwNumberOfBytesRead = 0; // 实际读取到的字节数
BOOL bRet = FALSE; // InternetReadFile函数返回值
TCHAR szFileLocal[MAX_PATH] = { 0 }; // 本地文件名缓冲区
HANDLE hFileLocal = INVALID_HANDLE_VALUE; // 本地文件文件句柄
LPVOID lpBuffer = NULL; // HttpQueryInfo函数所用的缓冲区指针
DWORD dwBufferLength = 0; // 缓冲区的长度
LPCSTR lpStrOptional = "username=Admin&password=123456&remember=yes&login=%E7%99%BB%E5%BD%95"; // 请求主体
HINTERNET hFile = NULL; // InternetOpenUrl函数返回的Internet文件句柄
switch (uMsg) {
case WM_INITDIALOG: {
SetDlgItemText(hwndDlg, IDC_EDIT_HOST, TEXT("www.httptest.com"));
// 打开并初始化WinINet库
hInternet = InternetOpen(TEXT("Mozilla/5.0"), INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
return TRUE;
};
case WM_COMMAND: {
switch (LOWORD(wParam)) {
case IDC_BTN_GET: {
// 连接到指定站点的HTTP服务,返回一个HTTP会话句柄
if (GetDlgItemText(hwndDlg, IDC_EDIT_HOST, szServerName, _countof(szServerName)) > 0)
hConnect = InternetConnect(hInternet, szServerName, INTERNET_DEFAULT_HTTP_PORT, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
else {
MessageBox(hwndDlg, TEXT("请输入主机名或IP地址"), TEXT("错误提示"), MB_OK);
return TRUE;
};
// 创建一个HTTP请求句柄
hRequest = HttpOpenRequest(hConnect, TEXT("GET"), TEXT("index.html"), NULL, TEXT("http://www.httptest.com/"), arrszAcceptTypes, INTERNET_FLAG_KEEP_CONNECTION, 0);
// 将一个或多个HTTP请求标头添加到HTTP请求句柄
HttpAddRequestHeaders(hRequest, TEXT("Accept-Encoding:gzip, deflate\r\nAccept-Language:zh-CN\r\n\r\n"), -1, HTTP_ADDREQ_FLAG_ADD | HTTP_ADDREQ_FLAG_REPLACE | HTTP_ADDREQ_FLAG_COALESCE);
// 将指定的请求发送到HTTP服务器
HttpSendRequest(hRequest, NULL, 0, NULL, 0);
// 创建本地文件
GetModuleFileName(NULL, szFileLocal, _countof(szFileLocal));
StringCchCopy(_tcsrchr(szFileLocal, TEXT('\\')) + 1, _tcslen(TEXT("index.html")) + 1, TEXT("index.html"));
hFileLocal = CreateFile(szFileLocal, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
// 循环读取数据
do {
dwNumberOfBytesRead = 0;
bRet = FALSE;
// 查询服务器上指定文件的可用数据量
InternetQueryDataAvailable(hRequest, &dwNumberOfBytesAvailable, 0, 0);
if (dwNumberOfBytesAvailable > 0) {
lpBuf = new BYTE[dwNumberOfBytesAvailable];
// 从打开的Internet文件中读取数据
bRet = InternetReadFile(hRequest, lpBuf, dwNumberOfBytesAvailable, &dwNumberOfBytesRead);
// 写入本地文件
WriteFile(hFileLocal, lpBuf, dwNumberOfBytesRead, NULL, NULL);
delete[]lpBuf;
};
} while (bRet && dwNumberOfBytesRead > 0);
MessageBox(hwndDlg, TEXT("下载index.html成功"), TEXT("操作成功"), MB_OK);
// 关闭本地文件句柄,关闭HTTP请求句柄,关闭HTTP会话句柄
CloseHandle(hFileLocal);
InternetCloseHandle(hRequest);
InternetCloseHandle(hConnect);
break;
};
case IDC_BTN_POST: {
// 连接到指定站点的HTTP服务,返回一个HTTP会话句柄
if (GetDlgItemText(hwndDlg, IDC_EDIT_HOST, szServerName, _countof(szServerName)) > 0)
hConnect = InternetConnect(hInternet, szServerName, INTERNET_DEFAULT_HTTP_PORT, NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
else {
MessageBox(hwndDlg, TEXT("请输入主机名或IP地址"), TEXT("错误提示"), MB_OK);
return TRUE;
};
// 创建一个HTTP请求句柄
hRequest = HttpOpenRequest(hConnect, TEXT("POST"), TEXT("login.php"), NULL, TEXT("http://www.httptest.com/login.html"), arrszAcceptTypes, INTERNET_FLAG_KEEP_CONNECTION, 0);
// 将一个或多个HTTP请求标头添加到HTTP请求句柄,POST请求主体长度为68个字节
HttpAddRequestHeaders(hRequest, TEXT("Accept-Encoding:gzip, deflate\r\nAccept-Language:zh-CN\r\nContent-Type:application/x-www-form-urlencoded\r\nContent-Length:68\r\n\r\n"), -1, HTTP_ADDREQ_FLAG_ADD | HTTP_ADDREQ_FLAG_REPLACE | HTTP_ADDREQ_FLAG_COALESCE);
// 将指定的请求发送到HTTP服务器,附带POST请求主体
HttpSendRequest(hRequest, NULL, 0, (LPVOID)lpStrOptional, strlen(lpStrOptional));
// 获取所有请求标头,每个标头都以回车换行分隔
lpBuffer = NULL;
dwBufferLength = 0;
HttpQueryInfo(hRequest, HTTP_QUERY_RAW_HEADERS_CRLF | HTTP_QUERY_FLAG_REQUEST_HEADERS, NULL, &dwBufferLength, NULL);
lpBuffer = new BYTE[dwBufferLength + 2];
ZeroMemory(lpBuffer, dwBufferLength + 2);
HttpQueryInfo(hRequest, HTTP_QUERY_RAW_HEADERS_CRLF | HTTP_QUERY_FLAG_REQUEST_HEADERS, lpBuffer, &dwBufferLength, NULL);
MessageBox(hwndDlg, (LPCTSTR)lpBuffer, TEXT("请求标头"), MB_OK);
delete[]lpBuffer;
// 获取服务器返回的所有标头,每个标头都以回车换行分隔
lpBuffer = NULL;
dwBufferLength = 0;
HttpQueryInfo(hRequest, HTTP_QUERY_RAW_HEADERS_CRLF, NULL, &dwBufferLength, NULL);
lpBuffer = new BYTE[dwBufferLength + 2];
ZeroMemory(lpBuffer, dwBufferLength + 2);
HttpQueryInfo(hRequest, HTTP_QUERY_RAW_HEADERS_CRLF, lpBuffer, &dwBufferLength, NULL);
MessageBox(hwndDlg, (LPCTSTR)lpBuffer, TEXT("响应标头"), MB_OK);
delete[]lpBuffer;
// 关闭HTTP请求句柄,关闭HTTP会话句柄
InternetCloseHandle(hRequest);
InternetCloseHandle(hConnect);
//////////////////////////////////////////////////////////////////////////
//// 连接到指定站点的HTTP服务,返回一个HTTP会话句柄
//if (GetDlgItemText(hwndDlg, IDC_EDIT_HOST, szServerName, _countof(szServerName)) > 0)
//{
// hConnect = InternetConnect(hInternet, szServerName, INTERNET_DEFAULT_HTTP_PORT,
// NULL, NULL, INTERNET_SERVICE_HTTP, 0, 0);
//}
//else
//{
// MessageBox(hwndDlg, TEXT("请输入主机名或IP地址"), TEXT("错误提示"), MB_OK);
// return TRUE;
//}
//// 创建一个HTTP请求句柄
//hRequest = HttpOpenRequest(hConnect, TEXT("GET"), TEXT("index.php"), NULL,
// TEXT("http://www.httptest.com/login.php"), arrszAcceptTypes, INTERNET_FLAG_KEEP_CONNECTION, 0);
//// 将一个或多个HTTP请求标头添加到HTTP请求句柄
//HttpAddRequestHeaders(hRequest,
// TEXT("Accept-Encoding:gzip, deflate\r\nAccept-Language:zh-CN\r\n\r\n"), -1,
// HTTP_ADDREQ_FLAG_ADD | HTTP_ADDREQ_FLAG_REPLACE | HTTP_ADDREQ_FLAG_COALESCE);
//// 将指定的请求发送到HTTP服务器
//HttpSendRequest(hRequest, NULL, 0, NULL, 0);
//// 创建本地文件
//GetModuleFileName(NULL, szFileLocal, _countof(szFileLocal));
//StringCchCopy(_tcsrchr(szFileLocal, TEXT('\\')) + 1,
// _tcslen(TEXT("index.php")) + 1, TEXT("index.php"));
//hFileLocal = CreateFile(szFileLocal, GENERIC_READ | GENERIC_WRITE,
// FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
//// 循环读取数据
//do
//{
// dwNumberOfBytesRead = 0;
// bRet = FALSE;
// // 查询服务器上指定文件的可用数据量
// InternetQueryDataAvailable(hRequest, &dwNumberOfBytesAvailable, 0, 0);
// if (dwNumberOfBytesAvailable > 0)
// {
// lpBuf = new BYTE[dwNumberOfBytesAvailable];
// // 从打开的Internet文件中读取数据
// bRet = InternetReadFile(hRequest, lpBuf,
// dwNumberOfBytesAvailable, &dwNumberOfBytesRead);
// // 写入本地文件
// WriteFile(hFileLocal, lpBuf, dwNumberOfBytesRead, NULL, NULL);
// delete[]lpBuf;
// }
//} while (bRet && dwNumberOfBytesRead > 0);
//MessageBox(hwndDlg, TEXT("下载index.php成功"), TEXT("操作成功"), MB_OK);
//// 关闭本地文件句柄,关闭HTTP请求句柄,关闭HTTP会话句柄
//CloseHandle(hFileLocal);
//InternetCloseHandle(hRequest);
//InternetCloseHandle(hConnect);
//////////////////////////////////////////////////////////////////////////
// 打开指定URL的HTTP资源
hFile = InternetOpenUrl(hInternet, TEXT("http://www.httptest.com/index.php"), TEXT("Accept-Encoding:gzip, deflate\r\nAccept-Language:zh-CN\r\n\r\n"), _tcslen(TEXT("Accept-Encoding:gzip, deflate\r\nAccept-Language:zh-CN\r\n\r\n")), INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTP | INTERNET_FLAG_IGNORE_REDIRECT_TO_HTTPS | INTERNET_FLAG_KEEP_CONNECTION | INTERNET_FLAG_NO_AUTH | INTERNET_FLAG_RESYNCHRONIZE, 0);
// 创建本地文件
GetModuleFileName(NULL, szFileLocal, _countof(szFileLocal));
StringCchCopy(_tcsrchr(szFileLocal, TEXT('\\')) + 1, _tcslen(TEXT("index.php")) + 1, TEXT("index.php"));
hFileLocal = CreateFile(szFileLocal, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
// 循环读取数据
do {
dwNumberOfBytesRead = 0;
bRet = FALSE;
// 查询服务器上指定文件的可用数据量
InternetQueryDataAvailable(hFile, &dwNumberOfBytesAvailable, 0, 0);
if (dwNumberOfBytesAvailable > 0) {
lpBuf = new BYTE[dwNumberOfBytesAvailable];
// 从打开的Internet文件中读取数据
bRet = InternetReadFile(hFile, lpBuf, dwNumberOfBytesAvailable, &dwNumberOfBytesRead);
// 写入本地文件
WriteFile(hFileLocal, lpBuf, dwNumberOfBytesRead, NULL, NULL);
delete[]lpBuf;
};
} while (bRet && dwNumberOfBytesRead > 0);
MessageBox(hwndDlg, TEXT("下载index.php成功"), TEXT("操作成功"), MB_OK);
// 关闭本地文件句柄,关闭Internet文件句柄
CloseHandle(hFileLocal);
InternetCloseHandle(hFile);
break;
};
};
return TRUE;
};
case WM_CLOSE: {
InternetCloseHandle(hInternet);
EndDialog(hwndDlg, 0);
return TRUE;
};
};
return FALSE;
};