开头
- 用WIN API完成了socket客户端的编写
- cursor很适合用于写这种单文件的WIN API代码编写,写的很规范,它帮助我完成了API的调用,参数的选择和异常值处理,自己去写还挺费时间
- 但不得不吐槽下,我提的几个处理中文和处理多任务的需求,无论我换何种说法,它实现的都不太好,甚至还有错误
功能
客户端设计
- 主线程:建立连接后,创建一个能够发送消息的子线程,随后在死循环中不断读取网络消息,按照协议解析数据,分发到handleFile、handleCmd、handleMsg三个子线程中处理,主线程主要阻塞在接收消息的recv函数上
- 发送消息的子线程:从命令行中读取Unicode字符串,转化为字节数组发送到远端
- handleFile子线程:首先读取协议中的文件大小部分,然后按照每次1024字节的大小分块读取文件内容,在本地组装保存
- handleCmd子线程:读取命令内容,通过
CreateProcessA
创建命令执行,将执行结果写入到管道,等待执行完毕或超时后,从管道读出结果,并发送到远端
- handleMsg子线程:从远端读取字节数组,通过
MultiByteToWideChar
转化为Unicode字符串,打印到屏幕上
代码
#include <iostream>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <tchar.h>
#include <thread>
#include <io.h>
#include <fcntl.h>
#include<string>
#pragma comment(lib, "Ws2_32.lib")
#define DEFAULT_BUFLEN 512
#define DEFAULT_PORT "27015"
DWORD WINAPI sendToServer(LPVOID lpParam) {
SOCKET ConnectSocket = *(SOCKET*)lpParam;
char sendbuf[DEFAULT_BUFLEN];
int iResult;
while (true) {
printf("[发送] ");
fgets(sendbuf, sizeof(sendbuf), stdin);
iResult = send(ConnectSocket, sendbuf, strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
printf("[错误] 发送失败: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 1;
}
}
return 0;
}
VOID WINAPI handleFile(char* datalen, char* data_pre, int len_pre, SOCKET socket) {
int file_len = strtol(datalen, NULL, 10);
printf("[提示] 准备接收文件,大小为: %d\n", file_len);
char* fileData = new char[file_len];
int datalen_pre = len_pre - 6 - sizeof(int);
char buffer[1024];
int alreadyReceived = datalen_pre;
int iResult;
FILE* fp = fopen("hello", "wb");
//第一轮接收
fwrite(data_pre, 1, datalen_pre, fp);
//继续接收完剩下的文件
while (alreadyReceived < file_len)
{
int bytesToReceive = min((int)sizeof(buffer), file_len - alreadyReceived);
iResult = recv(socket, buffer, bytesToReceive, 0);
if (iResult == SOCKET_ERROR) {
printf("[错误] 接收文件失败: %d\n", WSAGetLastError());
closesocket(socket);
WSACleanup();
}
alreadyReceived += iResult;
fwrite(buffer, 1, iResult, fp);
}
fclose(fp);
printf("[提示] 文件成功保存到本地\n");
printf("[发送] ");
char sendbuf[DEFAULT_BUFLEN];
strcpy_s(sendbuf, "成功接收文件\n");
iResult = send(socket, sendbuf, (int)strlen(sendbuf), 0);
//在子线程中接受文件
/*std::thread t([&fileData]() {
for (int i = 0; i < strlen(fileData); i++) {
printf("\\x%02x ", fileData[i]);
}
});*/
//printf("[提示] 开启子线程 %d 来保存文件!\n", t.get_id());
}
VOID WINAPI handleMsg(char* content) {
_setmode(_fileno(stdout), _O_U8TEXT);
int length = MultiByteToWideChar(CP_UTF8, 0, content, -1, NULL, 0);
// 分配内存空间
wchar_t* wstr = new wchar_t[length];
// 转换为 Unicode 字符串
MultiByteToWideChar(CP_UTF8, 0, content, -1, wstr, length);
// 输出 Unicode 字符串
wprintf(L"[接收]: %s\n", wstr);
_setmode(_fileno(stdout), _O_TEXT);
// 释放内存空间
delete[] wstr;
printf("[发送]: ");
}
VOID WINAPI handleCmd(char* content, SOCKET socket) {
char* cmd = content;
SECURITY_ATTRIBUTES sa = { sizeof(SECURITY_ATTRIBUTES), NULL, TRUE };
HANDLE hRead, hWrite;
if (!CreatePipe(&hRead, &hWrite, &sa, 0)) {
printf("[错误] 创建管道失败!\n");
}
STARTUPINFOA si = { sizeof(STARTUPINFOA) };
si.hStdError = hWrite;
si.hStdOutput = hWrite;
si.dwFlags |= STARTF_USESTDHANDLES;
PROCESS_INFORMATION pi;
if (!CreateProcessA(NULL, cmd, NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi)) {
printf("[错误] 创建子进程失败\n");
}
CloseHandle(hWrite);
DWORD dwWaitResult = WaitForSingleObject(pi.hProcess, 1000);
if (dwWaitResult == WAIT_OBJECT_0) {
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
char buffer[4096];
DWORD bytesRead;
while (ReadFile(hRead, buffer, sizeof(buffer), &bytesRead, NULL)) {
if (bytesRead == 0) break;
printf("[提示] 本地命令执行结果为: %.*s\n", bytesRead, buffer);
printf("[发送]: ");
int iResult = send(socket, buffer, bytesRead, 0);
if (iResult == SOCKET_ERROR) {
printf("[错误] 发送命令执行结果失败: %d\n", WSAGetLastError());
closesocket(socket);
WSACleanup();
}
}
CloseHandle(hRead);
}
else if (dwWaitResult == WAIT_TIMEOUT) {
printf("[错误] 等待命令执行超时\n");
TerminateProcess(pi.hProcess, 1);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
CloseHandle(hRead);
}
else {
printf("[错误] 等待命令执行失败\n");
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
CloseHandle(hRead);
}
}
VOID WINAPI resolveData(char* data, int data_len, SOCKET socket) {
data[data_len] = '\0';
char* prefix = strtok(data, "|");
char *content = strtok(NULL, "|");
if (strcmp(prefix, "FILE") == 0) {
char* token = strtok(NULL, "|");
handleFile(content, token, data_len, socket);
}
if (strcmp(prefix, "MSG") == 0) {
handleMsg(content);
}
if (strcmp(prefix, "CMD") == 0) {
handleCmd(content, socket);
}
}
int __cdecl main(int argc, char** argv)
{
if (argc != 3) {
printf("[错误] Usage: %s server_ip server_port\n", argv[0]);
return 1;
}
WSADATA wsaData;
SOCKET ConnectSocket = INVALID_SOCKET;
struct addrinfo* result = NULL,
* ptr = NULL,
hints;
char sendbuf[DEFAULT_BUFLEN];
char recvbuf[DEFAULT_BUFLEN];
int iResult;
int recvbuflen = DEFAULT_BUFLEN;
// Initialize Winsock
iResult = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (iResult != 0) {
printf("[错误] Socket环境初始化失败: %d\n", iResult);
return 1;
}
ZeroMemory(&hints, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_socktype = SOCK_STREAM;
hints.ai_protocol = IPPROTO_TCP;
// Resolve the server address and port
iResult = getaddrinfo(argv[1], argv[2], &hints, &result);
if (iResult != 0) {
printf("[错误] 获取套接字地址结构失败: %d\n", iResult);
WSACleanup();
return 1;
}
// Attempt to connect to an address until one succeeds
for (ptr = result; ptr != NULL; ptr = ptr->ai_next) {
// Create a SOCKET for connecting to server
ConnectSocket = socket(ptr->ai_family, ptr->ai_socktype,
ptr->ai_protocol);
if (ConnectSocket == INVALID_SOCKET) {
printf("[错误] socket建立失败: %ld\n", WSAGetLastError());
WSACleanup();
return 1;
}
// Connect to server.
iResult = connect(ConnectSocket, ptr->ai_addr, (int)ptr->ai_addrlen);
if (iResult == SOCKET_ERROR) {
closesocket(ConnectSocket);
ConnectSocket = INVALID_SOCKET;
continue;
}
break;
}
freeaddrinfo(result);
if (ConnectSocket == INVALID_SOCKET) {
printf("[错误] 无法连接至服务器\n");
WSACleanup();
return 1;
}
/*获取服务器的IP地址*/
char ip[INET6_ADDRSTRLEN];
struct sockaddr_in* ipv4 = (struct sockaddr_in*)ptr->ai_addr;
struct sockaddr_in6* ipv6 = (struct sockaddr_in6*)ptr->ai_addr;
void* addr;
const char* ipver;
// get the pointer to the address itself,
// different fields in IPv4 and IPv6:
if (ipv4->sin_family == AF_INET) { // IPv4
addr = &(ipv4->sin_addr);
ipver = "IPv4";
}
else { // IPv6
addr = &(ipv6->sin6_addr);
ipver = "IPv6";
}
// convert the IP to a string and print it:
inet_ntop(ptr->ai_family, addr, ip, sizeof ip);
printf("[提示] 连接到服务器%s: %s\n", ipver, ip);
// Send an initial buffer
strcpy_s(sendbuf, "Hello, Server, I'm client\n");
iResult = send(ConnectSocket, sendbuf, (int)strlen(sendbuf), 0);
if (iResult == SOCKET_ERROR) {
printf("send failed with error: %d\n", WSAGetLastError());
closesocket(ConnectSocket);
WSACleanup();
return 1;
}
printf("[提示] 已给服务器发送初始问候\n");
HANDLE hThread;
DWORD dwThreadId;
hThread = CreateThread(NULL, 0, sendToServer, &ConnectSocket, 0, &dwThreadId);
if (hThread == NULL) {
printf("[错误] 创建对话子线程失败%d\n", GetLastError());
return 1;
}
printf("[提示] 在子线程 %d 中开始同服务端进行对话\n", dwThreadId);
// Receive until the peer closes the connection
do {
iResult = recv(ConnectSocket, recvbuf, recvbuflen, 0);
if (iResult > 0) {
resolveData(recvbuf, iResult, ConnectSocket);
memset(recvbuf, 0, DEFAULT_BUFLEN);
}
else if (iResult == 0)
printf("[提示] 关闭连接\n");
else
printf("[错误] 接收数据失败: %d\n", WSAGetLastError());
} while (iResult > 0);
// cleanup
closesocket(ConnectSocket);
WSACleanup();
return 0;
}
遇到的问题
- 编译时头文件包含的问题:
<winsock2.h>
头文件必须放在代码最前面
- 中文处理的问题:用
MultiByteToWideChar
将接收到的字节数组转Unicode字符串,并用wprintf
来打印中文字符,同时还需要用_setmode(_fileno(stdout), _O_U8TEXT)
临时设置stdout的中文显示格式,在wprintf
后需要将stdout显示格式改回来,否则printf
会报错
- socket粘包分包的问题:
- 重点说下这个,当时我在服务端明明是分两次发送的数据包,但是观察到在客户端被一次recv接收到了,必应查到是两次send的时机太近、前一次send的数据量太小,导致粘包,我还在两次send间加了sleep和函数调用,发现都没办法分开,内核就是判定这两次send要用同一个数据包
- 解决办法:在服务端发送数据时加入了包头,用来区分不同的数据包,同时在文件数据包中加入了文件大小的字段;这样在客户端读时,就能根据数据包头区分不同的数据内容,同时按照文件大小来分块读文件数据,避免读到其它的数据内容
- recv返回时机的问题:我这里用的是阻塞态的recv,只有读到数据后recv才会返回,还要注意虽然给recv指定了size参数,但这个size是指这次读取的最大容量,而非必须要读到这么多的容量,也就是说如果这次发送的数据包大小小于size,recv接收到这个数据包后也会立即返回,不会继续等待接受完所有size的数据
效果展示
- vs编译,大小只有17KB
- 运行截图