windows网络编程基础

套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中,并与网络中的其他应用程序进行通信。网络套接字是IP地址与端口的组合。

TCP服务端

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
// TCP 是什么? 传输层的协议,主要用于数据的传输。TCP 是
// 相对安全的,面向有连接的。确保数据的完整性。类似于打电
// 话,会有 响铃 + 接通 + 交流 的过程。

// UDP 是什么? 传输层的协议,主要用于数据的传输。UDP 是
// 相对不稳定的,面向无连接的。类似于 BB 机,只会将消息发
// 送出去,并不能保证消息一定会被接收到。

// 如何确保消息发送给当前局域网内的某一个机器? 当计算机连
// 接到网络中后,路由器会分配一个唯一的 ip 地址给具体的机,
// 器,当信息传递给路由器的时候,会根据相应的 ip 地址传递给
// 对应的设备。

// 如何确保消息发送给当前系统的某一个程序? 每一个独立套接字
// 都会拥有独一无二的端口,当系统接受到网络信息之后,会根据对
// 应的端口将信息发送给指定的程序。

// ip 和 端口 的组成是什么? ipv4 的地址实际上是一个四字节的
// 整数数据,组成通常 x.x.x.x 的形式,假设存在 1.1.1.127 这个
// ip 对应的四字节实际上是 0x0101017F。端口在计算机中通常有
// 65535 个,实际可以由一个 WORD 来保存。

// 数据在网络传输的过程中是如何存储的? 通常在个人计算机中,数据
// 都是以小端方式存储的,在网络数据的传输过程中,规定应该使用大端
// 的方式保存所有的数据。

#include <iostream>
#include <ws2tcpip.h>
// 0. 进行套接字编程必须用到的头文件和库
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")

// 根据传入的布尔值进行相应的输出
void check_result(bool b, LPCSTR msg)
{
// 参数一应该是一个表达式,如果表达式为 true 就输出
if (b)
{
printf("error: %s\n", msg);
ExitProcess(0);
}
}

// 创建一个互斥体,用于互斥输出
HANDLE Mutex = CreateMutex(NULL, FALSE, NULL);

// 专门用于接受指定套接字的数据
DWORD CALLBACK RecvThread(LPVOID param)
{
// 从参数获取到套接字
SOCKET client = (SOCKET)param;
INT number = { 0 };

// 循环接受套接字传入的数据,recv 的返回值应该是接受
// 到的数据长度,如果返回值 <= 0 就表示断开了连接
while (recv(client, (char*)&number, 4, 0) > 0)
{
WaitForSingleObject(Mutex, INFINITE);
printf("[%08X]: %d\n", GetCurrentThreadId(), number);
ReleaseMutex(Mutex);
}

return 0;
}

int main()
{
// 1. 初始化网络环境,搜索信号(3G? 4G?)
WSAData wsadata = { 0 };
int result = WSAStartup(0x0202, &wsadata);
check_result(result != 0 || wsadata.wVersion != 0x0202, "WSAStartup()");

// 2. 创建套接字[IP:PORT],选择服务商(联通? 电信? 移动)
// - AF_INET: 表示使用网络传输协议
// - SOCK_STREAM: 表示使用流式套接字(TCP)
// - IPPROTO_TCP 表示 TCP, IPPROTO_UDP 表示 UDP
SOCKET server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
check_result(server == INVALID_SOCKET, "socket()");

// 3. 绑定套接字到对应的 ip:port,类似于选择手机号
sockaddr_in serveraddr = { AF_INET };
serveraddr.sin_port = htons(0x1234);
// - 127.0.0.1 在大多数设备上指向的都是当前的主机
inet_pton(AF_INET, "127.0.0.1", &serveraddr.sin_addr);
result = bind(server, (sockaddr*)&serveraddr, sizeof(serveraddr));
check_result(result == SOCKET_ERROR, "bind()");

// 4. 开启监听模式,设置监听的最大数量,把卡装到手机开机
result = listen(server, SOMAXCONN);
check_result(result == SOCKET_ERROR, "listen()");

// 编写一个死循环,在循环内不断的接收客户端
while (true)
{
// 5. 等待客户端的连接,接电话
// - 等待哪一个套接字的连接、连接客户端的地址信息,结构体大小
sockaddr_in clientaddr = { 0 };
int length = sizeof(clientaddr);
SOCKET client = accept(server, (sockaddr*)&clientaddr, &length);
check_result(client == INVALID_SOCKET, "accept()");

// 6. 为[每一个客户端]创建[单独]的线程,进行消息的接收 recv
// - 如果没有创建线程,那么程序就会阻塞在 recv 函数,导致无法接受
CreateThread(NULL, 0, RecvThread, (LPVOID)client, 0, NULL);
}

// 7. 关闭套接字,挂断电话
closesocket(server);

// 8. 清理网络环境,销户
WSACleanup();

system("pause");
return 0;
}

TCP客户端

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
#include <iostream>
#include <ws2tcpip.h>
// 0. 进行套接字编程必须用到的头文件和库
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")

#define CLIENT_COUNT 100

// 根据传入的布尔值进行相应的输出
void check_result(bool b, LPCSTR msg)
{
// 参数一应该是一个表达式,如果表达式为 true 就输出
if (b)
{
printf("error: %s\n", msg);
ExitProcess(0);
}
}

// 创建一个客户端进行连接
DWORD CALLBACK create_client(LPVOID param)
{
// 创建套接字[IP:PORT],选择服务商(联通? 电信? 移动)
// - AF_INET: 表示使用网络传输协议
// - SOCK_STREAM: 表示使用流式套接字(TCP)
// - IPPROTO_TCP 表示 TCP, IPPROTO_UDP 表示 UDP
SOCKET client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
check_result(client == INVALID_SOCKET, "socket()");

// 作为客户端来讲,每一个客户端套接字都会默认的分配端口

// 连接到服务器,打电话(需要知道打给谁)
sockaddr_in serveraddr = { AF_INET };
serveraddr.sin_port = htons(0x1234);
inet_pton(AF_INET, "127.0.0.1", &serveraddr.sin_addr);
int result = connect(client, (sockaddr*)&serveraddr, sizeof(serveraddr));
check_result(result == SOCKET_ERROR, "accept()");

// 收发消息 send\recv,打电话
while (true)
{
// 发送当前是第几个客户端到服务器
send(client, (char*)&param, 4, 0);
Sleep(500);
}
}

int main()
{
// 初始化网络环境,搜索信号(3G? 4G?)
WSAData wsadata = { 0 };
int result = WSAStartup(0x0202, &wsadata);
check_result(result != 0 || wsadata.wVersion != 0x0202, "WSAStartup()");

// 创建指定个数的 客户端
for (int i = 0; i < CLIENT_COUNT; ++i)
CreateThread(NULL, 0, create_client, (LPVOID)i, 0, NULL);

// WaitForMultipleObjects 能够等待的句柄最多 64 个

system("pause");

// 清理网络环境,销户
WSACleanup();

return 0;
}

iocp服务端

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
#include <iostream>
#include <ws2tcpip.h>
// 0. 进行套接字编程必须用到的头文件和库
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")

// 用于保存异步信息的结构体
struct MYOVERLAPPED
{
OVERLAPPED overlapped;
WSABUF wsabuf;
};

// 根据传入的布尔值进行相应的输出
void check_result(bool b, LPCSTR msg)
{
// 参数一应该是一个表达式,如果表达式为 true 就输出
if (b)
{
printf("error: %s\n", msg);
ExitProcess(0);
}
}

// 创建一个互斥体,用于互斥输出
HANDLE Mutex = CreateMutex(NULL, FALSE, NULL);

// 专门用于接受指定套接字的数据
DWORD CALLBACK RecvThread(LPVOID param)
{
// 首先获取到参数传入的 iocp 对象
HANDLE iocp = (HANDLE)param;
DWORD read = 0;
ULONG_PTR completion_key = NULL;
MYOVERLAPPED* overlapped = nullptr;

// 不断的接受队列中传入的消息
while (true)
{
// 从完成端口队列获取信息
BOOL result = GetQueuedCompletionStatus(
iocp, // 从哪一个 iocp 获取
&read, // 实际接收的数量
&completion_key, // 在这里是产生消息的套接字

// 接收的 OVERLAPPED** ,但实际刚才传入的是 OVERLAPPED*
(LPOVERLAPPED*)&overlapped, // 重叠IO结构
INFINITE); // 等待时长

// 如果消息接收成功就输出
if (result == TRUE && read > 0)
{
WaitForSingleObject(Mutex, INFINITE);
printf("[%08X]: %d\n", GetCurrentThreadId(),
*(int*)overlapped->wsabuf.buf);
ReleaseMutex(Mutex);

// 需要再次投递一个请求
DWORD flags = 0;
WSARecv((SOCKET)completion_key, // 接受谁的信息
&overlapped->wsabuf, // 消息保存到哪里
1, // wsabuf 结构的数量
NULL, // 对于异步IO,可以填写0
&flags, // 对应 recv 的最后一个参数
(LPWSAOVERLAPPED)overlapped,// 重叠 IO 结构体
NULL); // 回调函数
}

}

return 0;
}

int main()
{
// [1]. IOCP 是用有限的线程处理多个异步操作,线程数量通常是CPU核心 * 2
SYSTEM_INFO system_info = { 0 };
GetSystemInfo(&system_info);

// [2]. 创建一个 IOCP 对象,维护所有的线程
HANDLE iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);

// [3]. 根据获取到的核心数量,创建 iocp 工作线程,传入 iocp 对象
for (DWORD i = 0; i < system_info.dwNumberOfProcessors * 2; ++i)
CreateThread(NULL, 0, RecvThread, (LPVOID)iocp, 0, NULL);

// 1. 初始化网络环境,搜索信号(3G? 4G?)
WSAData wsadata = { 0 };
int result = WSAStartup(0x0202, &wsadata);
check_result(result != 0 || wsadata.wVersion != 0x0202, "WSAStartup()");

// 2. 创建套接字[IP:PORT],选择服务商(联通? 电信? 移动)
// - AF_INET: 表示使用网络传输协议
// - SOCK_STREAM: 表示使用流式套接字(TCP)
// - IPPROTO_TCP 表示 TCP, IPPROTO_UDP 表示 UDP
SOCKET server = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
check_result(server == INVALID_SOCKET, "socket()");

// 3. 绑定套接字到对应的 ip:port,类似于选择手机号
sockaddr_in serveraddr = { AF_INET };
serveraddr.sin_port = htons(0x1234);
// - 127.0.0.1 在大多数设备上指向的都是当前的主机
inet_pton(AF_INET, "127.0.0.1", &serveraddr.sin_addr);
result = bind(server, (sockaddr*)&serveraddr, sizeof(serveraddr));
check_result(result == SOCKET_ERROR, "bind()");

// 4. 开启监听模式,设置监听的最大数量,把卡装到手机开机
result = listen(server, SOMAXCONN);
check_result(result == SOCKET_ERROR, "listen()");

// 编写一个死循环,在循环内不断的接收客户端
while (true)
{
// 5. 等待客户端的连接,接电话
// - 等待哪一个套接字的连接、连接客户端的地址信息,结构体大小
sockaddr_in clientaddr = { 0 };
int length = sizeof(clientaddr);
SOCKET client = accept(server, (sockaddr*)&clientaddr, &length);
check_result(client == INVALID_SOCKET, "accept()");

// [4]. 将每一个接收到的套接字都绑定到 iocp 上
// - 参数三:完成键,通常用于保存产生消息的句柄(客户端句柄)
CreateIoCompletionPort((HANDLE)client, iocp, client, 0);

// WSARecv 对需要使用单独的 MYOVERLAPPED 结构体。
MYOVERLAPPED* overlapped = new MYOVERLAPPED{ 0 };
overlapped->wsabuf.len = 0x10;
overlapped->wsabuf.buf = new CHAR[0x10]{ 0 };

// [5]. 投递一个接收客户端数据的请求,使用 WSARecv,对于每一个
DWORD flags = 0;
WSARecv(client, // 接受谁的信息
&overlapped->wsabuf, // 消息保存到哪里
1, // wsabuf 结构的数量
NULL, // 对于异步IO,可以填写0
&flags, // 对应 recv 的最后一个参数
(LPWSAOVERLAPPED)overlapped,// 重叠 IO 结构体
NULL); // 回调函数

// 将请求投递到 IOCP 的队列中,一旦请求执行完毕,GetQueuedCompletionStatus
// 就会从完成请求的队列中获取到这个投递的请求,在这个地方,主线程会被切换到工
// 作线程,执行 GetQueuedCompletionStatus,错误的真正地址是在这里。
}

// 7. 关闭套接字,挂断电话
closesocket(server);

// 8. 清理网络环境,销户
WSACleanup();

system("pause");
return 0;
}

// 使用 IOCP 的步骤: 主线程
// 1. 创建一个 iocp 对象
// 2. 根据 cpu 核心数量创建线程
// 3. 接收客户端,将接收的客户端绑定到 iocp,即添加到对象队列中
// 4. 投递一个 IO 请求,WSARecv 请求,即接收目标的信息
// 每一个请求都应该对应有不同的 OVERLAPPED 结构体,且应该在堆空间

// 使用 IOCP 的步骤: 工作线程
// 1. GetQueuedCompletionStatus 从完成队列中取出消息
// 2. 函数如果返回 FALSE 或者实际操作的字节 <=0 就失败
// 3. 可以从 OVERLAPPED 中获取到想要的信息并输出
// 4. 为了能够继续的获取下一次信息,需要再次投递请求

TCP文件发送

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
#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <ws2tcpip.h>
// 0. 进行套接字编程必须用到的头文件和库
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")

// windows 中检查了一些重复包含
#include <windows.h>
#include "function.h"
#define SECTION_SIZE 10240

// 发送文件的时候分成两个步骤:1 发送文件的请求头(基本信息)
typedef struct _FILE_HEADER
{
CHAR filename[MAX_PATH] = { 0 }; // 文件的名称
SIZE_T filesize = 0; // 文件的大小
DWORD section_count = 0; // 接收整个文件需要的次数
CHAR sig[128] = { 0 }; // 签名
} FILE_HEADER, *PFILE_HEADER;

// 发送文件的时候分成两个步骤:2 发送文件的请求体(n个,第几个区块,以及内容)
typedef struct _FILE_SECTION
{
int index = 0; // 文件的大小
DWORD size = 0; // 一次发送的大小
CHAR data[SECTION_SIZE] = { 0 }; // 发送的具体数据
} FILE_SECTION, * PFILE_SECTION;

// 根据传入的布尔值进行相应的输出
void check_result(bool b, LPCSTR msg)
{
// 参数一应是一个表达式,如果表达式为 true 就输出
if (b)
{
printf("error: %s\n", msg);
ExitProcess(0);
}
}


int main()
{
// 1. 初始化网络环境,搜索信号(3G? 4G?)
WSAData wsadata = { 0 };
int result = WSAStartup(0x0202, &wsadata);
check_result(result != 0 || wsadata.wVersion != 0x0202, "WSAStartup()");

// 2. 创建套接字[IP:PORT],选择服务商(联通? 电信? 移动)
// - AF_INET: 表示使用网络传输协议
// - SOCK_STREAM: 表示使用流式套接字(TCP)
// - IPPROTO_TCP 表示 TCP, IPPROTO_UDP 表示 UDP
SOCKET sender = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
check_result(sender == INVALID_SOCKET, "socket()");

// 3. 绑定套接字到对应的 ip:port,类似于选择手机号
sockaddr_in serveraddr = { AF_INET };
serveraddr.sin_port = htons(0x1234);
// - 127.0.0.1 在大多数设备上指向的都是当前的主机
inet_pton(AF_INET, "127.0.0.1", &serveraddr.sin_addr);
result = bind(sender, (sockaddr*)&serveraddr, sizeof(serveraddr));
check_result(result == SOCKET_ERROR, "bind()");

// 4. 开启监听模式,设置监听的最大数量,把卡装到手机开机
result = listen(sender, SOMAXCONN);
check_result(result == SOCKET_ERROR, "listen()");

// 5. 等待客户端的连接,接电话
// - 等待哪一个套接字的连接、连接客户端的地址信息,结构体大小
sockaddr_in clientaddr = { 0 };
int length = sizeof(clientaddr);
SOCKET reciver = accept(sender, (sockaddr*)&clientaddr, &length);
check_result(reciver == INVALID_SOCKET, "accept()");

// 6. 打开需要发送的文件,获取相关的信息填充到结构体中
HANDLE file = CreateFileA("demo.exe", GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
check_result(file == INVALID_HANDLE_VALUE, "CreateFileA()");
SIZE_T filesize = GetFileSize(file, NULL);
DWORD sectioncount = filesize % SECTION_SIZE == 0 ?
filesize / SECTION_SIZE : (filesize / SECTION_SIZE) + 1;
FILE_HEADER file_header = { "demo.exe", filesize, sectioncount };
calc_file_sig("demo.exe", file_header.sig);
send(reciver, (CHAR*)&file_header, sizeof(file_header), 0);

// 7. 根据计算出的区块数量,循环发送每一个区块
for (DWORD i = 0; i < sectioncount; ++i)
{
// 7.1 创建缓冲区用于保存需要发送的区块
FILE_SECTION section = { i };

// 7.2 修改文件指针指向每一个区块的首地址
SetFilePointer(file, i * SECTION_SIZE, 0, FILE_BEGIN);

// 7.3 从指定的位置读取数据保存并发送
ReadFile(file, section.data, SECTION_SIZE, &section.size, NULL);
//printf("%d:%d\n", i, section.size);
send(reciver, (CHAR*)&section, sizeof(section), 0);
}

// bug 产生的原因是过早的关闭了服务器(发送端)的套接字,如果已经关闭了
// 但是客户端还没有接收完数据,就会直接导致丢包

CloseHandle(file);

system("pause");
return 0;
}

TCP文件接收

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
#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <ws2tcpip.h>
// 0. 进行套接字编程必须用到的头文件和库
#include <winsock2.h>
#pragma comment(lib,"ws2_32.lib")

#include <windows.h>
#include "function.h"
#define SECTION_SIZE 10240

// 发送文件的时候分成两个步骤:1 发送文件的请求头(基本信息)
typedef struct _FILE_HEADER
{
CHAR filename[MAX_PATH] = { 0 }; // 文件的名称
SIZE_T filesize = 0; // 文件的大小
DWORD section_count = 0; // 接收整个文件需要的次数
CHAR sig[128] = { 0 }; // 签名
} FILE_HEADER, * PFILE_HEADER;

// 发送文件的时候分成两个步骤:2 发送文件的请求体(n个,第几个区块,以及内容)
typedef struct _FILE_SECTION
{
int index = 0; // 文件的大小
DWORD size = 0; // 一次发送的大小
CHAR data[SECTION_SIZE] = { 0 }; // 发送的具体数据
} FILE_SECTION, * PFILE_SECTION;

// 根据传入的布尔值进行相应的输出
void check_result(bool b, LPCSTR msg)
{
// 参数一应该是一个表达式,如果表达式为 true 就输出
if (b)
{
printf("error: %s\n", msg);
ExitProcess(0);
}
}


int main()
{
// 初始化网络环境,搜索信号(3G? 4G?)
WSAData wsadata = { 0 };
int result = WSAStartup(0x0202, &wsadata);
check_result(result != 0 || wsadata.wVersion != 0x0202, "WSAStartup()");

// 创建套接字[IP:PORT],选择服务商(联通? 电信? 移动)
// - AF_INET: 表示使用网络传输协议
// - SOCK_STREAM: 表示使用流式套接字(TCP)
// - IPPROTO_TCP 表示 TCP, IPPROTO_UDP 表示 UDP
SOCKET reciver = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
check_result(reciver == INVALID_SOCKET, "socket()");

// 作为客户端来讲,每一个客户端套接字都会默认的分配端口

// 连接到服务器,打电话(需要知道打给谁)
sockaddr_in serveraddr = { AF_INET };
serveraddr.sin_port = htons(0x1234);
inet_pton(AF_INET, "127.0.0.1", &serveraddr.sin_addr);
result = connect(reciver, (sockaddr*)&serveraddr, sizeof(serveraddr));
check_result(result == SOCKET_ERROR, "accept()");

// 直接接收服务器发送过来的文件头信息
FILE_HEADER file_header = { 0 };
recv(reciver, (CHAR*)&file_header, sizeof(file_header), 0);

// 使用接收到的名称创建文件
HANDLE file = CreateFileA("demo1.exe", GENERIC_WRITE, NULL,
NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
check_result(file == INVALID_HANDLE_VALUE, "CreateFileA()");

// 根据接收到的轮数,循环接收整个文件
for (DWORD i = 0; i < file_header.section_count; ++i)
{
// 7.1 创建缓冲区用于接收块的信息
FILE_SECTION section = { 0 };
int n = recv(reciver, (CHAR*)&section, sizeof(section), 0);

// 7.2 修改文件指针指向接收到的数据对应文职
SetFilePointer(file, section.index * SECTION_SIZE, 0, FILE_BEGIN);

// 7.3 将数据写到指定的位置
DWORD aaa;
if (section.size == 0)
break;
//printf("%d:%d\n", section.index, section.size);
WriteFile(file, section.data, section.size, &aaa, NULL);
}

CloseHandle(file);

if (verify_file_sig("demo1.exe", file_header.sig))
printf("签名校验失败\n");
else
printf("签名校验成功\n");

system("pause");

// 清理网络环境,销户
WSACleanup();

return 0;
}

function.h

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
#pragma once

#include <iostream>
#include <windows.h>
#include <openssl/md5.h>
#include <openssl/sha.h>
#include <openssl/bio.h>
#include <openssl/evp.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/rand.h>
#include <openssl/buffer.h>
#include <openssl/applink.c>
#pragma comment(lib, "libcrypto.lib")

#define PUB_KEY_FILE "pubkey.pem" // 公钥路径
#define PRI_KEY_FILE "prikey.pem" // 私钥路径

int md5_encrypt(const void* data, size_t len, unsigned char* md5)
{
// 初始化保存 md5 信息的结构体
MD5_CTX ctx = { 0 };
MD5_Init(&ctx);

// 将需要计算的数据传入到对应的结构中
MD5_Update(&ctx, data, len);

// 从结构中获取计算后的结果
MD5_Final(md5, &ctx);

return 0;
}

// 要求传入一个需要加密的串,以及串的长度,参数三是否需要换行,返回编码后的数据
char* Base64Encode(const char* input, int length, bool with_new_line)
{
// 创建一个 base64 对象,对象的特点就是使用 write 写入的
// 数据会被自动编码,使用 read 读取的数据会自动解码
BIO* b64 = BIO_new(BIO_f_base64());

// 默认编码之后存在换行符,通常不需要换行符
if (!with_new_line)
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);

// 再次创建了一个内存对象,对象的特点就是使用 write 写入的
// 数据会自动保存到某一个缓冲区。BIO_push 将两个对象进行关
// 联,也就是说传入的数据首先会进行 base64 编码,然后保存到
// 缓冲区。
b64 = BIO_push(b64, BIO_new(BIO_s_mem()));

// 将传入的数据进行编码,BIO_flush 将操作刷新到对象
BIO_write(b64, input, length);
BIO_flush(b64);

// 从 base64 对象中获取到相应的编码后的内容
BUF_MEM* bptr = NULL;
BIO_get_mem_ptr(b64, &bptr);

// 将编码后的数据拷贝到指定的位置
char* b64encode = new char[bptr->max]{};
memcpy(b64encode, bptr->data, bptr->max);

// 清理 BIO 对象,并返回结果
BIO_free_all(b64);
return b64encode;
}

char* Base64Decode(char* input, int length, bool with_new_line)
{
BIO* b64 = BIO_new(BIO_f_base64());

if (!with_new_line)
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL);

// 编码后的长度和原文大概比例是 4:3,使用编码后的长度
// 进行解码,空间是绝对足够的
char* buffer = (char*)malloc(length);
if (buffer) memset(buffer, 0, length);

// 创建一个内存对象,存入编码后的内容,并关联到 base64 对象
BIO* bmem = BIO_push(b64, BIO_new_mem_buf(input, length));

// 对 base64 read 就是解码数据
BIO_read(bmem, buffer, length);

// 清理并返回原文
BIO_free_all(bmem);
return buffer;
}

/*加密最大长度为加密长度-41*/
RSA* get_public_key()
{
// 打开公钥文件
FILE* public_file = nullptr;
if (fopen_s(&public_file, PUB_KEY_FILE, "r") == NULL)
{
// 从指定文件中读取公钥
RSA* rsa = PEM_read_RSAPublicKey(public_file, NULL, NULL, NULL);
if (public_file) fclose(public_file);
return rsa;
}
return nullptr;
}

RSA* get_private_key()
{
// 打开私钥文件
FILE* private_file = nullptr;
if (fopen_s(&private_file, PRI_KEY_FILE, "r") == NULL)
{
// 从指定文件中读取公钥
RSA* rsa = PEM_read_RSAPrivateKey(private_file, NULL, NULL, NULL);
if (private_file) fclose(private_file);
return rsa;
}
return nullptr;
}

BYTE* rsa_encrypt(BYTE* data, int data_len, RSA* rsa)
{
int rsa_len = RSA_size(rsa);
BYTE* encrypt = (BYTE*)malloc(rsa_len);

if (data_len > 117) return nullptr;

RSA_public_encrypt(data_len, data, encrypt, rsa, RSA_PKCS1_PADDING);

return encrypt;
}


// 解密数据,
BYTE* rsa_decrypt(BYTE* data, RSA* rsa)
{
int rsa_len = RSA_size(rsa);
BYTE* decrypt = (BYTE*)malloc(rsa_len);
RSA_private_decrypt(rsa_len, data, decrypt, rsa, RSA_PKCS1_PADDING);

return decrypt;
}

// 函数方法生成密钥对
void generate_rsa_key()
{
// 生成 rsa 密钥对, 参数一密钥长度,参数二公钥指数 e,参数三四可以不指定
RSA* keypair = RSA_generate_key(1024, RSA_F4, NULL, NULL);

// 从生成的密钥对中读取私钥到内存对象
BIO* pri = BIO_new(BIO_s_mem());
PEM_write_bio_RSAPrivateKey(pri, keypair, NULL, NULL, 0, NULL, NULL);
// 获取密钥长度并且申请空间进行保存
size_t pri_len = BIO_pending(pri);
char* pri_key = (char*)calloc(pri_len + 1, sizeof(char));
BIO_read(pri, pri_key, pri_len);
// 将生成的私钥写入到指定的文件中
FILE* private_file = nullptr;
if (fopen_s(&private_file, PRI_KEY_FILE, "w") == NULL)
{
if (pri_key && private_file)
{
fputs(pri_key, private_file);
fclose(private_file);
}
}

BIO* pub = BIO_new(BIO_s_mem());
PEM_write_bio_RSAPublicKey(pub, keypair);
size_t pub_len = BIO_pending(pub);
char* pub_key = (char*)calloc(pub_len + 1, sizeof(char));
BIO_read(pub, pub_key, pub_len);
FILE* public_file = nullptr;
if (fopen_s(&public_file, PUB_KEY_FILE, "w") == NULL)
{
if (pub_key && public_file)
{
fputs(pub_key, public_file);
fclose(public_file);
}
}

// 释放对应的资源,防止泄露
RSA_free(keypair);
BIO_free_all(pub);
BIO_free_all(pri);

free(pri_key);
free(pub_key);
}

void calc_file_sig(LPCSTR filename, LPSTR out)
{
HANDLE file = CreateFileA(filename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

int size = GetFileSize(file, NULL);
DWORD read = 0;
BYTE* file_buffer = new BYTE[size]{ 0 };
ReadFile(file, file_buffer, size, &read, NULL);
UCHAR md5[16] = { 0 };
md5_encrypt(file_buffer, size, md5);
RSA* pub_key = get_public_key();
BYTE* sig = rsa_encrypt(md5, 16, pub_key);
memcpy(out, sig, 128);
}

bool verify_file_sig(LPCSTR filename, LPSTR sig)
{
HANDLE file = CreateFileA(filename, GENERIC_READ, NULL, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);

int size = GetFileSize(file, NULL);

DWORD read = 0;
BYTE* file_buffer = new BYTE[size]{ 0 };
ReadFile(file, file_buffer, size, &read, NULL);
UCHAR md5[17] = { 0 };
md5_encrypt(file_buffer, size, md5);

RSA* pri_key = get_private_key();
BYTE* sig_md5 = rsa_decrypt((BYTE*)sig, pri_key);

return !memcmp(sig_md5, md5, 128);
}