基于WSAAsyncSelect模型的通信程序设计
一、问题描述
编写Win32程序模拟实现基于WSAAsyncSelect模型的两台计算机之间的通信,要求编程实现服务器端与客户端之间双向数据传递。客户端向服务器端发送“请输出从1到1000内所有的质数”,服务器回应客户端给出结果。
二、代码实现
①CInitSock.h具体代码:
1 #pragma once
2 #include <winsock2.h>
3 #pragma comment(lib,"WS2_32")
4 class CInitSock
5 {
6 public:
7 CInitSock(BYTE minorVer = 2, BYTE majorVer = 2)
8 {
9 //初始化W2_32.dll
10 WSADATA wsaData;
11 WORD sockVersion = MAKEWORD(minorVer, majorVer);
12 if (::WSAStartup(sockVersion, &wsaData) != 0)
13 {
14 exit(0);
15 }
16 }
17 ~CInitSock()
18 {
19 ::WSACleanup();
20 }
21 };
②服务器端代码:
1 #include "CInitSock.h" 2 #include<iostream> 3 #include<sstream> 4 using namespace std; 5 CInitSock initSock; 6 7 //自定义网络通知消息: 8 #define WM_SOCKET (WM_USER + 1) 9 10 //判断质数的函数1: 11 string calculatePrimeNumbers(int start, int end); 12 //判断质数的函数2: 13 string calculatePrimeNumbers_2(int start, int end); 14 15 LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); 16 int main() 17 { 18 char szClassName[] = "MainWClass"; 19 WNDCLASSEX wndclass; 20 21 //用描述主窗口的参数填充WNDCLASSEX结构 22 wndclass.cbSize = sizeof(wndclass); 23 wndclass.style = CS_HREDRAW | CS_VREDRAW; 24 wndclass.lpfnWndProc = WindowProc; 25 wndclass.cbClsExtra = 0; 26 wndclass.cbWndExtra = 0; 27 wndclass.hInstance = NULL; 28 wndclass.hIcon = ::LoadIcon(NULL, IDI_APPLICATION); 29 wndclass.hCursor = ::LoadCursor(NULL, IDC_ARROW); 30 wndclass.hbrBackground = (HBRUSH)::GetStockObject(WHITE_BRUSH); 31 wndclass.lpszMenuName = NULL; 32 wndclass.lpszClassName = szClassName; 33 wndclass.hIconSm = NULL; 34 ::RegisterClassEx(&wndclass); 35 36 37 //创建主窗口 38 HWND hWnd = ::CreateWindowEx( 39 0, 40 szClassName, 41 "", 42 WS_OVERLAPPEDWINDOW, 43 CW_USEDEFAULT, 44 CW_USEDEFAULT, 45 CW_USEDEFAULT, 46 CW_USEDEFAULT, 47 NULL, 48 NULL, 49 NULL, 50 NULL); 51 if (hWnd == NULL) 52 { 53 ::MessageBox(NULL, "创建窗口出错!", "error", MB_OK); 54 return -1; 55 } 56 USHORT nPort = 4567; 57 58 //创建监听套接字 59 SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); 60 sockaddr_in sin; 61 sin.sin_family = AF_INET; 62 sin.sin_port = htons(nPort); 63 sin.sin_addr.S_un.S_addr = INADDR_ANY; 64 65 //绑定套接字到本地机器 66 if (::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR) 67 { 68 printf("Failed bind()\n"); 69 return -1; 70 } 71 72 //将套接字设为窗口通知消息类型 73 ::WSAAsyncSelect(sListen, hWnd, WM_SOCKET, FD_ACCEPT | FD_CLOSE); 74 ::listen(sListen, 5); 75 76 //从消息队列中取出消息 77 MSG msg; 78 while (::GetMessage(&msg, NULL, 0, 0)) 79 { 80 ::TranslateMessage(&msg);//转化键盘消息 : 将键盘消息翻译成对应的字符消息 81 ::DispatchMessage(&msg);//将消息发送到相应的窗口函数进行处理 82 } 83 return msg.wParam; //当GetMessage返回0时的程序结束 84 85 } 86 87 LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) 88 { 89 switch (uMsg) 90 { 91 case WM_SOCKET: 92 { 93 SOCKET s = wParam;//取得有事件发生的套接字句柄 94 //查看是否出错 95 if (WSAGETSELECTERROR(lParam)) 96 { 97 ::closesocket(s); 98 return 0; 99 } 100 //处理发生的事件 101 switch (WSAGETSELECTEVENT(lParam)) 102 { 103 case FD_ACCEPT: //监听中的套接字检测到有连接进入 104 { 105 SOCKET client = ::accept(s, NULL, NULL); 106 ::WSAAsyncSelect(client, hWnd, WM_SOCKET, FD_READ | FD_WRITE | FD_CLOSE);//sListen client 107 } 108 break; 109 case FD_WRITE: 110 {} 111 break; 112 case FD_READ: 113 { 114 char szText[1024] = { 0 }; 115 if (::recv(s, szText, 1024, 0) == -1) 116 ::closesocket(s); 117 else 118 { 119 printf("接收到消息: %s", szText); 120 121 /*补充:向客户端发送数据*/ 122 int start, end; 123 char response[256]; 124 string calString; 125 if (sscanf(szText, "请输出%d到%d内的所有质数!", &start, &end) == 2) {//请输出从1到1000内所有的质数 126 //string calString = calculatePrimeNumbers(start, end); //基于嵌套循环的质数判断 127 string calString = calculatePrimeNumbers_2(start, end); //使用埃及筛法判断质数 128 ::send(wParam, calString.c_str(), calString.length(), 0); 129 } 130 else { 131 strcpy(response, "无效的指令,请重新发送指令"); 132 ::send(wParam, response, strlen(response), 0); 133 } 134 } 135 } 136 break; 137 case FD_CLOSE: 138 { 139 ::closesocket(s); 140 } 141 break; 142 } 143 } 144 return 0; 145 case WM_DESTROY: 146 ::PostQuitMessage(0); 147 return 0; 148 } 149 //我们不需要的消息,就交给系统默认处理 150 return ::DefWindowProc(hWnd, uMsg, wParam, lParam); 151 } 152 153 string calculatePrimeNumbers(int start, int end) 154 { 155 stringstream calResult; 156 for (int i = start; i <= end; i++) 157 { 158 if (i == 1) 159 continue; //1既不是质数也不是合数 160 bool isPrime = true; 161 for (int j = 2; j < i; j++) 162 { 163 if (i % j == 0) 164 { 165 isPrime = false; 166 break; 167 } 168 } 169 if (isPrime) 170 { 171 calResult << i << " "; 172 } 173 } 174 return calResult.str(); 175 } 176 177 string calculatePrimeNumbers_2(int start, int end) { //埃及筛法 178 stringstream calResult; 179 bool* sieve = new bool[end + 1]; 180 for (int i = 0; i <= end; i++) { 181 sieve[i] = true; 182 } 183 for (int p = 2; p <= sqrt(end); p++) { 184 if (sieve[p]) { 185 for (int i = p * p; i <= end; i += p) { 186 sieve[i] = false; 187 } 188 } 189 } 190 191 for (int num = start; num <= end; num++) { 192 if (num >= 2 && sieve[num] == true) { 193 calResult << num << " "; 194 } 195 } 196 return calResult.str(); 197 }
③客户端代码:
1 #include "CInitSock.h"
2 #include<stdio.h>
3 CInitSock initSock;
4 int main()
5 {
6 //创建套接字
7 SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
8 if (s == INVALID_SOCKET)
9 {
10 printf("Failed socket()\n");
11 return 0;
12 }
13 //填充sockaddr_in结构
14 sockaddr_in servAddr;
15 servAddr.sin_family = AF_INET;
16 servAddr.sin_port = htons(4567);
17
18 //
19 servAddr.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
20 //绑定这个套接字到一个本地地址
21 if (::connect(s, (LPSOCKADDR)&servAddr, sizeof(servAddr)) == -1)
22 {
23 printf("Falied connect()\n");
24 return 0;
25 }
26 //给服务器端发信息
27 char szText1[] = "请输出1到1000内的所有质数!";
28 ::send(s, szText1, strlen(szText1), 0);
29
30 //接收数据
31 char buff[4096];
32 int nRecv = ::recv(s, buff, 4096, 0);
33 if (nRecv > 0)
34 {
35 buff[nRecv] = '\0';
36 printf("接收到数据:%s", buff);
37 }
38
39 //关闭监听套接字
40 ::closesocket(s);
41 return 0;
42 }
三、运行结果
①服务器端接收到请求消息:
②客户端收到服务器端的回复:
四、关于质数求解的不同方法
①基本的嵌套循环判断每个数是否为质数
这是最常使用的方法,也是上述服务器中使用到的方法:
1 bool isPrime(int num) {
2 if (num < 2) {
3 return false;
4 }
5 for (int i = 2; i * i <= num; i++) {
6 if (num % i == 0) {
7 return false;
8 }
9 }
10 return true;
11 }
12
13 void findPrimes(int start, int end) {
14 printf("质数列表: ");
15 for (int num = start; num <= end; num++) {
16 if (isPrime(num)) {
17 printf("%d ", num);
18 }
19 }
20 printf("\n");
21 }
服务器上判断质数的代码:(和上述代码的思路相同)
1 string calculatePrimeNumbers(int start, int end)
2 {
3 stringstream calResult;
4 for (int i = start; i <= end; i++)
5 {
6 if (i == 1)
7 continue; //1既不是质数也不是合数
8 bool isPrime = true;
9 for (int j = 2; j < i; j++)
10 {
11 if (i % j == 0)
12 {
13 isPrime = false;
14 break;
15 }
16 }
17 if (isPrime)
18 {
19 calResult << i << " ";
20 }
21 }
22 return calResult.str();
23 }
②埃及筛法判断质数
思路:
一个素数的整数倍必定为合数
算法思想:
1.创建一个长度为n+1的数组sieve;
(长度为n+1是因为数组的下标是从0开始的,若n=10,你创建一个长度为n的数组,数组是从0--9的,并不包含10。)
2.初始化sieve数组的所有元素都为true,即默认所有的数都是质数;
3.进入for循环,进行判断;此时范围为for(int i=2;i<=n;i++);
(i从2开始,是因为数组中下标为0的数用不上,下标为1的数是用来表示数字1的,而1不是质数,所以要从数字2进行判断。)
4.在for循环中,如果prim[i]==1(i为质数),则将i的所有整数倍置为合数。
对i进行整数倍置为合数操作的时候,本应该 2i 3i 4i......都要置为合数的,但是有些数在之前的i-1已经操作过,所以此时不必在进行操作,我们只需要从i*i开始即可。
举个例子:
i=2,prim[i]==1---->if(i*i<=n)---->4 6 8 10 12 14 16 18 20 22......都置为合数
i=3,prim[i]==1---->if(i*i<=n)----->6 9 12 15 18 21 24 ......置为合数,此时6已经操作过了
i=5,prim[i]==1---->if(i*i<=n)----->10 15 20 25 30 35 ......置为合数,此时10、15、20已经置为合数过了,所以我们应从i*i开始操作
1 #include <stdio.h> 2 #include <stdbool.h> 3 #include <math.h> 4 5 void findPrimes(int start, int end) { 6 bool* sieve = new bool[end + 1]; 7 for (int i = 0; i <= end; i++) { 8 sieve[i] = true; 9 } 10 for (int p = 2; p <= sqrt(end); p++) { 11 if (sieve[p]) { 12 for (int i = p * p; i <= end; i += p) { 13 sieve[i] = false; 14 } 15 } 16 } 17 printf("使用优化的Sieve of Eratosthenes求出的质数列表: "); 18 for (int num = start; num <= end; num++) { 19 if (num >= 2 && sieve[num] == true) { 20 printf("%d ", num); 21 } 22 } 23 printf("\n"); 24 } 25 26 int main() { 27 int start = 1; 28 int end = 1000; 29 findPrimes(start, end); 30 return 0; 31 }
运行结果:
五、总结
①实验中遇到的问题及其解决办法:
错误①:
问题描述:
Unicode字符集下,不可将char*类型的值分配到“LPCWSTR”类型的实体。
原因:
“从Visual C2005开始,编译器不再进行从char到LPCWSTR的隐式转换了”
解决方法:
修改工程属性,项目属性->高级->字符集->使用Unicode字符集改为未设置。
修改完之后便可正常赋值:
错误②:
问题描述:
Visual Studio中找不到WM_SOCKET这个消息常量
解决办法:
在 Visual Studio 中,WM_SOCKET 不是一个内置的消息常量,这是因为 WM_SOCKET 是一个自定义的消息,它通常与网络编程和异步套接字(Socket)相关联。
需要补充自定义网络通知消息代码:
#define WM_SOCKET (WM_USER + 1)
理解:
在 Windows 系统中,当使用异步套接字编程模型时,可以通过 WSAAsyncSelect 函数为套接字关联一个窗口,并指定一个自定义的消息号,以便在套接字事件发生时通过消息机制通知该窗口。具体来说,WSAAsyncSelect 函数将 FD_SOCKET 事件与指定的套接字关联,并在事件发生时向关联的窗口发送一个消息。
②对于WSAAsyncSelect模型的了解:
1.WSAAsyncSelect模型的工作过程分析:
①创建套接字:
首先,需要使用socket函数创建一个套接字。套接字是网络通信的端点,可以在应用程序中发送和接收数据。
②注册事件通知:
使用WSAAsyncSelect函数将套接字与特定的窗口关联起来,并为套接字注册感兴趣的网络事件,例如连接请求、数据到达等。该函数还指定了在事件发生时应该通知的窗口消息。
③创建消息循环:
在接收事件通知之前,需要创建一个消息循环来处理系统发送的消息。消息循环可以使用GetMessage函数来获取消息,并将其分派给相应的窗口过程进行处理。
④接收事件通知:
当套接字接收到感兴趣的网络事件时,系统将发送一个消息给消息循环。应用程序可以使用TranslateMessage和DispatchMessage函数将该消息传递给关联的窗口过程进行处理。
⑤处理事件:
在窗口过程中,应用程序可以通过解析消息的参数来确定发生的事件类型。例如,如果收到FD_READ事件,表示有数据可以从套接字中读取。
⑥处理数据:
根据具体的应用需求,可以使用相应的函数(如recv函数或send函数)来接收或发送数据。数据的读取或发送过程可以在事件处理函数中完成。
⑦继续循环:
处理完事件后,窗口过程返回,并再次进入消息循环等待下一个事件的到来。循环将一直进行,直至应用程序退出或调用WSACleanup函数释放资源。