文章目錄
- 一、實驗背景與目的
- 二、實驗設計與實現思路
- 1. 設計思想
- 2. 核心代碼實現
- 總結
一、實驗背景與目的
這次實驗主要是為了讓大家了解基于 WSAAsyncSelect 模型通信程序的編寫、編譯和執行過程。通過實踐操作,深入掌握這種模型在實現計算機之間通信時的應用。
任務是編寫一個 Win32 程序,模擬兩臺計算機之間的雙向通信。具體來說,客戶端要向服務器端發送 “請輸出從 1 到 1000 內所有的質數” 這條指令,然后服務器端要能準確回應結果,把 1 到 1000 內的質數全部找出來并發回給客戶端。
二、實驗設計與實現思路
1. 設計思想
在設計思想上,采用了WSAAsyncSelect 模型。這個模型基于消息通知機制,服務器端通過窗口過程函數來接收網絡事件消息,比如有客戶端連接請求(FD_ACCEPT)、可讀數據(FD_READ)、連接關閉(FD_CLOSE)等,然后根據不同事件做出相應處理,實現高效的異步通信。
2. 核心代碼實現
(1)客戶端
#include <stdio.h>
#include <winsock2.h>
#pragma comment(lib, "WS2_32") // 鏈接到WS2_32.lib
class CInitSock
{public:CInitSock(BYTE minorVer = 2, BYTE majorVer = 2){// 初始化WS2_32.dllWSADATA wsaData;WORD sockVersion = MAKEWORD(minorVer, majorVer);if(::WSAStartup(sockVersion, &wsaData) != 0)return;}~CInitSock(){ ::WSACleanup(); }
};
CInitSock theSock; //加載套接字庫int main()
{// 創建套節字SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);if(s == INVALID_SOCKET){printf(" Failed socket() \n");return 0;}// 也可以在這里調用bind函數綁定一個本地地址,無則系統將會自動安排// 填寫遠程地址信息sockaddr_in servAddr; servAddr.sin_family = AF_INET;servAddr.sin_port = htons(4567);//要連接的服務器地址servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");//沒有聯網,直接使用127.0.0.1即可if(::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1){printf(" Failed connect() \n");return 0;}//發送數據char buf[] = "請輸出從1到1000內所有的質數";printf("發送數據:%s\n",buf);Sleep(6);// 接收數據char buff[3000];int nRecv = ::recv(s, buff, 3000, 0);// 關閉套節字::closesocket(s);return 0;
}
(2)服務器端
#include <stdio.h>
#include <winsock2.h>
#include <math.h>#pragma comment(lib, "WS2_32.lib")#define WM_SOCKET WM_USER + 101 // 自定義消息class CInitSock {
public:CInitSock(BYTE minorVer = 2, BYTE majorVer = 2) {// 初始化WS2_32.dllWSADATA wsaData;WORD sockVersion = MAKEWORD(minorVer, majorVer);if (::WSAStartup(sockVersion, &wsaData) != 0)return;}~CInitSock() {::WSACleanup();}
};CInitSock theSock; // 加載套接字庫bool isprime(int p) {if (p < 2) return false;int sq = (int)sqrt(p);for (int i = 2; i <= sq; i++) {if (p % i == 0)return false;}return true;
}char* getallprime(int n) {static char szprime[4096];strcpy(szprime, "質數:");for (int i = 2; i <= n; i++) {if (isprime(i)) {char sznum[10];itoa(i, sznum, 10);strcat(szprime, sznum);strcat(szprime, ","); // 附加分隔符}}size_t len = strlen(szprime);if (len > 0) {szprime[len - 1] = '\0'; // 替換最后一個逗號為字符串結束符}return szprime;
}LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);int main() {wchar_t szClassName[] = L"MainWClass"; // 使用寬字符WNDCLASSEX wndclass;wndclass.cbSize = sizeof(wndclass);wndclass.style = CS_HREDRAW | CS_VREDRAW;wndclass.lpfnWndProc = WindowProc;wndclass.cbClsExtra = 0;wndclass.cbWndExtra = 0;wndclass.hInstance = NULL;wndclass.hIcon = ::LoadIcon(NULL, IDI_APPLICATION);wndclass.hCursor = ::LoadCursor(NULL, IDC_ARROW);wndclass.hbrBackground = (HBRUSH)::GetStockObject(WHITE_BRUSH);wndclass.lpszMenuName = NULL;wndclass.lpszClassName = szClassName;wndclass.hIconSm = NULL;::RegisterClassEx(&wndclass);HWND hWnd = ::CreateWindowExW(0,szClassName,L"", // 用 L 表示寬字符WS_OVERLAPPEDWINDOW,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,CW_USEDEFAULT,NULL,NULL,NULL,NULL);if (hWnd == NULL) {::MessageBoxW(NULL, L"創建窗口出錯!", L"error", MB_OK);return -1;}USHORT nPort = 4567; // 此服務器監聽的端口號SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);sockaddr_in sin;sin.sin_family = AF_INET;sin.sin_port = htons(nPort);sin.sin_addr.S_un.S_addr = INADDR_ANY;if (::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR) {printf("Failed bind() \n");return -1;}::WSAAsyncSelect(sListen, hWnd, WM_SOCKET, FD_ACCEPT | FD_CLOSE);::listen(sListen, 5);MSG msg;while (::GetMessage(&msg, NULL, 0, 0)) {::TranslateMessage(&msg);::DispatchMessage(&msg);}return msg.wParam;
}LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {switch (uMsg) {case WM_SOCKET: {SOCKET s = wParam;if (WSAGETSELECTERROR(lParam)) {::closesocket(s);return 0;}switch (WSAGETSELECTEVENT(lParam)) {case FD_ACCEPT: {SOCKET client = ::accept(s, NULL, NULL);::WSAAsyncSelect(client, hWnd, WM_SOCKET, FD_READ | FD_CLOSE);break;}case FD_READ: {char szText[1024] = { 0 };if (::recv(s, szText, sizeof(szText) - 1, 0) == SOCKET_ERROR) {::closesocket(s);}else {printf("接收數據:%s\n", szText);char* szReply = getallprime(1000);::send(s, szReply, strlen(szReply), 0);}break;}case FD_CLOSE: {::closesocket(s);break;}}}return 0;case WM_DESTROY:::PostQuitMessage(0);return 0;}return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
}
總結
在調試過程中,剛開始還遇到了一些小問題,比如數據接收不完整或者格式不對,但通過仔細檢查代碼,調整緩沖區大小、發送接收的順序等,最終都順利解決了。現在放一張關鍵截圖,內容是服務器端成功接收客戶端消息并回復以及客戶端成功接收服務器返回質數結果的截圖。
- 遇到的問題
1.端口號沖突問題
一開始,我在設置服務器端口號的時候,隨意選了一個號碼,結果程序運行不起來。后來我琢磨著可能是端口號出了問題,于是換了一個沒被占用的端口號,比如 4567 號,這才解決了問題。這讓我認識到網絡編程中端口號的選擇很關鍵,要是選了已經被其他程序占用了的端口號,那通信肯定就建立不起來了。
2.客戶端連接失敗問題
客戶端連接服務器的時候,也出現了連接不上的情況。我先是反復檢查網絡連接,確定沒問題后,懷疑是服務器端的設置有誤。后來仔細檢查代碼,發現是服務器端沒有正確監聽對應的端口號,把服務器端的監聽端口號和客戶端要連接的端口號設置成一致后,客戶端就能成功連接了。
3.數據接收不完整問題
當服務器端向客戶端發送數據時,有時候客戶端接收的數據不完整。一開始我還以為是數據發送那邊出了問題,后來仔細排查,發現是因為接收緩沖區的大小設置不夠。于是我把客戶端接收緩沖區的大小調大,這樣一來,客戶端就能完整地接收服務器端發送過來的數據了,這讓我明白要根據實際的數據量大小合理設置緩沖區,不然很容易出現數據丟失或者接收不全的情況。
4.網絡通信中的超時問題
這次實驗讓我深深體會到網絡編程的魅力和挑戰。通過實際編寫代碼,我對 WSAAsyncSelect 模型有了更深入的理解,也掌握了如何利用消息循環處理異步網絡事件。自己搭建起一個能正常通信的客戶端 - 服務器系統,那種成就感簡直爆棚。而且在這個過程中,我學會了更好地調試程序,關注錯誤處理,這對我今后學習更復雜的網絡編程技術打下了堅實基礎。在這個信息時代,網絡通信是如此的重要,通過這次實驗,我仿佛看到了網絡背后那復雜而又精妙的運行機制,也更加堅定了我在網絡安全領域深入學習的決心。我期待以后能參與更多有意思的項目,不斷提升自己的編程能力和專業知識水平。