基于WSAAsyncSelect模型的通信程序设计

发布时间 2023-11-28 11:21:09作者: ref·雨

基于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 //判断质数的函数:
 11 boolean PNumJudgment(int num);
 12 char* AllThePNum(int start, int end);
 13 //判断质数的函数2:
 14 string calculatePrimeNumbers(int start, int end);
 15 
 16 LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
 17 int main()
 18 {
 19     char szClassName[] = "MainWClass";
 20     WNDCLASSEX wndclass;
 21     
 22     //用描述主窗口的参数填充WNDCLASSEX结构
 23     wndclass.cbSize = sizeof(wndclass);
 24     wndclass.style = CS_HREDRAW | CS_VREDRAW;
 25     wndclass.lpfnWndProc = WindowProc;
 26     wndclass.cbClsExtra = 0;
 27     wndclass.cbWndExtra = 0;
 28     wndclass.hInstance = NULL;
 29     wndclass.hIcon = ::LoadIcon(NULL, IDI_APPLICATION);
 30     wndclass.hCursor = ::LoadCursor(NULL, IDC_ARROW);
 31     wndclass.hbrBackground = (HBRUSH)::GetStockObject(WHITE_BRUSH);
 32     wndclass.lpszMenuName = NULL;
 33     wndclass.lpszClassName = szClassName;
 34     wndclass.hIconSm = NULL;
 35     ::RegisterClassEx(&wndclass);
 36 
 37 
 38     //创建主窗口
 39     HWND hWnd = ::CreateWindowEx(
 40         0,
 41         szClassName,
 42         "",
 43         WS_OVERLAPPEDWINDOW,
 44         CW_USEDEFAULT,
 45         CW_USEDEFAULT,
 46         CW_USEDEFAULT,
 47         CW_USEDEFAULT,
 48         NULL,
 49         NULL,
 50         NULL,
 51         NULL);
 52     if (hWnd == NULL)
 53     {
 54         ::MessageBox(NULL, "创建窗口出错!", "error", MB_OK);
 55         return -1;
 56     }
 57     USHORT nPort = 4567;
 58 
 59     //创建监听套接字
 60     SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
 61     sockaddr_in sin;
 62     sin.sin_family = AF_INET;
 63     sin.sin_port = htons(nPort);
 64     sin.sin_addr.S_un.S_addr = INADDR_ANY;
 65 
 66     //绑定套接字到本地机器
 67     if (::bind(sListen, (sockaddr*)&sin, sizeof(sin)) == SOCKET_ERROR)
 68     {
 69         printf("Failed bind()\n");
 70         return -1;
 71     }
 72 
 73     //将套接字设为窗口通知消息类型
 74     ::WSAAsyncSelect(sListen, hWnd, WM_SOCKET, FD_ACCEPT | FD_CLOSE);
 75     ::listen(sListen, 5);
 76 
 77     //从消息队列中取出消息
 78     MSG msg;
 79     while (::GetMessage(&msg, NULL, 0, 0))
 80     {
 81         ::TranslateMessage(&msg);//转化键盘消息 : 将键盘消息翻译成对应的字符消息
 82         ::DispatchMessage(&msg);//将消息发送到相应的窗口函数进行处理
 83     }
 84     return msg.wParam; //当GetMessage返回0时的程序结束
 85 
 86 }
 87 
 88 LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
 89 {
 90     switch (uMsg)
 91     {
 92     case WM_SOCKET:
 93     {
 94         SOCKET s = wParam;//取得有事件发生的套接字句柄
 95         //查看是否出错
 96         if (WSAGETSELECTERROR(lParam))
 97         {
 98             ::closesocket(s);
 99             return 0;
100         }
101         //处理发生的事件
102         switch (WSAGETSELECTEVENT(lParam))
103         {
104         case FD_ACCEPT: //监听中的套接字检测到有连接进入
105         {
106             SOCKET client = ::accept(s, NULL, NULL);
107             ::WSAAsyncSelect(client, hWnd, WM_SOCKET, FD_READ | FD_WRITE | FD_CLOSE);//sListen client
108         }
109         break;
110         case FD_WRITE:
111         {}
112         break;
113         case FD_READ:
114         {
115             char szText[1024] = { 0 };
116             if (::recv(s, szText, 1024, 0) == -1)
117                 ::closesocket(s);
118             else
119             {
120                 printf("接收到消息: %s", szText);
121 
122                 /*补充:向客户端发送数据*/
123                 int start, end, add_sum;
124                 char response[256];
125                 string calString;
126                 if (sscanf(szText, "请输出%d到%d内的所有质数!", &start, &end) == 2) {//请输出从1到1000内所有的质数
127                     string calString = calculatePrimeNumbers(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 boolean PNumJudgment(int num)
154 {
155     boolean flag = true;
156     for (int i = 2; i < (num / 2); i++) {
157         if (num % i == 0)
158             flag == false;
159     }
160     return flag;
161 }
162 
163 char* AllThePNum(int start, int end)
164 {
165     char PNumArr[4096];
166     for (int i = start; i <= end; i++)
167     {
168         if (PNumJudgment(i))
169         {
170             PNumArr[i] = i;
171 
172         }
173     }
174     return PNumArr;
175 }
176 
177 string calculatePrimeNumbers(int start, int end)
178 {
179     stringstream calResult;
180     for (int i = start; i <= end; i++)
181     {
182         if (i == 1)
183             continue; //1既不是质数也不是合数
184         bool isPrime = true;
185         for (int j = 2; j < i; j++)
186         {
187             if (i % j == 0)
188             {
189                 isPrime = false;
190                 break;
191             }
192         }
193         if (isPrime)
194         {
195             calResult << i << " ";
196         }
197     }
198     return calResult.str();
199 }

 

 

③客户端代码:

 

 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[1024];
 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函数释放资源。