实现步骤:
异步选择模型是基于Windows消息机制的,也就是消息队列,Windows操作系统会为每个窗口创建一个消息队列并且维护
1. 创建窗口:
??????? 创建窗口步骤:
??????????????? 第一步:创建窗口结构体:
typedef struct tagWNDCLASSEXW {
UINT cbSize;
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HINSTANCE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCWSTR lpszMenuName;
LPCWSTR lpszClassName;
HICON hIconSm;
} WNDCLASSEXW, *PWNDCLASSEXW, *NPWNDCLASSEXW, *LPWNDCLASSEXW;
??????????????? 第二步:注册窗口结构体:
ATOM RegisterClassA(
const WNDCLASSA *lpWndClass
);
??????????????? 第三步:创建窗口
HWND CreateWindowExA(
DWORD dwExStyle,
LPCSTR lpClassName,
LPCSTR lpWindowName,
DWORD dwStyle,
int X,
int Y,
int nWidth,
int nHeight,
HWND hWndParent,
HMENU hMenu,
HINSTANCE hInstance,
LPVOID lpParam
);
??????????????? 第四步:显示窗口
BOOL ShowWindow(
HWND hWnd,
int nCmdShow
);
??????????????? 第五步:更新窗口
BOOL UpdateWindow(
HWND hWnd
);
??????????????? 第六步:消息循环
BOOL GetMessage(
LPMSG lpMsg,
HWND hWnd,
UINT wMsgFilterMin,
UINT wMsgFilterMax
);
??????????????? 第七步:回调函数
LRESULT CALLBACK WindowProc(
_In_ HWND hwnd,
_In_ UINT uMsg,
_In_ WPARAM wParam,
_In_ LPARAM lParam
);
2. 异步选择模型核心:
??????? 第一步:将服务端socket与一个消息(自定义的消息)绑定在一起并投递给操作系统,投递完之后操作系统会帮我们监视着,如果有socket发来一些请求,操作系统会产生消息,并把消息放进消息队列里;
??????? 将socket与消息绑定在一起并投递给操作系统:
int WSAAsyncSelect( // 函数功能:将socket与消息绑定在一起,并投递给操作系统
SOCKET s, // 要绑定的socket
HWND hWnd, // 窗口句柄 绑定到那个窗口上
u_int wMsg, // 消息编号 填自定义消息 注意要比 WM_USER大
long lEvent // 消息类型 FD_READ FD_WRITE ...
);
??????? 第二步:循环从消息队列里往外取消息,之后进行分类处理;在窗口回调函数中,进行分类处理;
服务端代码:
/*
工程:win32桌面应用程序
项目:异步选择模型
项目已改多字符集
UDP/IP异步选择模型:
异步选择模型是基于Windows消息机制的,也就是消息队列
Windows操作系统会为每个窗口创建一个消息队列并且维护
创建窗口步骤:
第一步:创建窗口结构体
第二步:注册窗口结构体
第三步:创建窗口
第四步:显示窗口
第五步:更新窗口
第六步:消息循环
第七步:回调函数
异步选择模型核心:
第一步:将服务端socket,与一个消息绑定在一起并且投递给操作系统,
投递完之后操作系统会帮我们监管着,如果有socket发来一些请求,操作系统会产生消息,并把信息放在消息队列里;
第二步:循环从消息队列里往外取消息,进行分类处理,
*/
#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>
#include <string.h>
#pragma comment(lib, "ws2_32.lib")
// 自定义消息
#define UM_ASYNCSELECTMSG WM_USER + 1
// 声明回调函数
long CALLBACK fnWndProcBack(HWND hwnd, UINT msgID, WPARAM wParam, LPARAM lParam);
// 创建socket 并 绑定ip地址与端口号
SOCKET CreateSock_Bind();
// 窗口主函数
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hProInstance, LPSTR lpCmdLine, int nShowCmd)
{
// 1.创建窗口结构体
WNDCLASSEX wc;
// 给窗口结构体成员赋值
wc.cbSize = sizeof(wc);
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = NULL;
wc.hCursor = NULL;//loadcurser
wc.hIconSm = NULL; //任务栏图标
wc.lpszClassName = "UDP/IP服务端";
wc.lpszMenuName = NULL; //菜单栏
wc.hbrBackground = CreateSolidBrush(RGB(255, 255, 255));
wc.lpfnWndProc = fnWndProcBack;
wc.style = CS_HREDRAW | CS_VREDRAW;
//2.注册窗口结构体
RegisterClassEx(&wc);
//3.创建窗口
HWND hwnd = CreateWindowEx(WS_EX_OVERLAPPEDWINDOW, "UDP/IP服务端", "异步选择模型", WS_OVERLAPPEDWINDOW,
50, 50, 500, 900, NULL, NULL, hInstance, NULL);
//判断返回值
if (NULL == hwnd)
{
return 0;
}
//4.显示窗口
ShowWindow(hwnd, nShowCmd);
//5.更新窗口
UpdateWindow(hwnd);
// 创建socket 并 绑定ip地址与端口号
SOCKET socketServer = CreateSock_Bind();
/*int WSAAsyncSelect( // 函数功能:将socket与消息绑定在一起,并投递给操作系统
SOCKET s, // 要绑定的socket
HWND hWnd, // 窗口句柄 绑定到那个窗口上
u_int wMsg, // 消息编号 填自定义消息 注意要比 WM_USER大
long lEvent // 消息类型 FD_READ FD_WRITE ...
);*/
// 将成功创建的 socket绑定事件并投递给操作系统
int nAsyn = WSAAsyncSelect(socketServer, hwnd, UM_ASYNCSELECTMSG, FD_READ | FD_WRITE);
// 判断是否成功绑定并投递给操作系统
if (SOCKET_ERROR == nAsyn)
{
// 出错了
int a = WSAGetLastError();
// 获取可操作客户区
HDC hdc = GetDC(hwnd); // 用完之后要记得释放
// 定义字符串数组获取整型转成字符型的变量
char str[20] = { 0 };
// 打印错误码 itoa
TextOut(hdc, 0, 0, _itoa(a, str, 10), sizeof(str));
//释放socket
closesocket(socketServer);
//清理网络库
WSACleanup();
// 释放 HDC
ReleaseDC(hwnd, hdc);
return 0;
}
//6.消息循环
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
//翻译消息
TranslateMessage(&msg);
//分发消息
DispatchMessage(&msg);
}
//释放socket
closesocket(socketServer);
//清理网络库
WSACleanup();
return 0;
}
// 定义 TextOut函数中参数3 y轴所需的变量
int y = 0;
// 声明回调函数
long CALLBACK fnWndProcBack(HWND hwnd, UINT msgID, WPARAM wParam, LPARAM lParam)
{
// 通过参数2进行分类处理消息
switch (msgID)
{
// 自定义消息
case UM_ASYNCSELECTMSG:
{
// 获取可操作客户区
HDC hdc = GetDC(hwnd); // 用完之后要记得释放
// 通过回调函数参数3获取socket 服务端socket
SOCKET sock = (SOCKET)wParam;
// 将lParam 拆成两部分:HIWORD装高字节 LOWORD装低字节
// 高字节产生的错误码 低字节处理具体的消息种类
if (0 == HIWORD(lParam))
{
// 通过低字节分类处理消息
// 处理 FD_WRITE
if (LOWORD(lParam) == FD_WRITE)
{
// 打印数据 提示成功处理 FD_WRITE 窗口显示日志 调用TextOut函数
TextOut(hdc, 0, y, "FD_WRITE", strlen("FD_WRITE"));
// 记录y轴位置+20
y += 20;
}
// 处理 FD_READ
if (LOWORD(lParam) == FD_READ)
{
// 1.接收来之 客户端的发来的消息
// 定义recvfrom函数中参数2所需的 接收字节数的字符串数组
char strBuf[548] = { 0 };
// 声明recvfrom函数中参数5所需的 发送方所需的socket结构体
struct sockaddr sockClient;
// 获取客户端socket结构体的大小
int len = sizeof(sockClient);
int nRecv = recvfrom(sock, strBuf, 548, 0, &sockClient, &len);
// 判断是否成功接收消息
if (SOCKET_ERROR == nRecv)
{
// 出错了
int a = WSAGetLastError();
// 定义字符串数组获取整型转成字符型的变量
char str[20] = { 0 };
// 打印错误码 itoa
TextOut(hdc, 0, y, _itoa(a, str, 10), sizeof(str));
// 记录的输出日志位置y轴 +=20
y += 20;
}
// 打印成功接收的数据报
TextOut(hdc, 0, y, strBuf, strlen(strBuf));
// 记录的输出日志位置y轴 +=20
y += 20;
// 2.进行发送数据报操作
int nSend = sendto(sock, "send ok", sizeof("send ok"), 0, &sockClient, sizeof(sockClient));
// 判断是否成功发送消息
if (SOCKET_ERROR == nSend)
{
// 出错了
int a = WSAGetLastError();
// 定义字符串数组获取整型转成字符型的变量
char str[20] = { 0 };
// 打印错误码 itoa
TextOut(hdc, 0, y, _itoa(a, str, 10), sizeof(str));
// 记录的输出日志位置y轴 +=20
y += 20;
}
}
}
// 释放 HDC
ReleaseDC(hwnd, hdc);
}
break;
case WM_CREATE: // 只初始化一次
break;
case WM_DESTROY:
PostQuitMessage(0);
break;
}
return DefWindowProc(hwnd, msgID, wParam, lParam);
}
// 创建socket 并 绑定ip地址与端口号
SOCKET CreateSock_Bind()
{
//1.:打开网络库
WORD wVersionRequired = MAKEWORD(2, 2);
WSADATA wsadata;
int nStar = WSAStartup(wVersionRequired, &wsadata);
//判断
if (0 != nStar)
{
int a = WSAGetLastError();
return 0;
}
//2.校验版本
if (2 != HIBYTE(wsadata.wVersion) || 2 != LOBYTE(wsadata.wVersion))
{
int a = WSAGetLastError();
//清理网络库
WSACleanup();
return 0;
}
//3.:创建socket
SOCKET sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
//判断
if (INVALID_SOCKET == sock)
{
int a = WSAGetLastError();
//清理网络库
WSACleanup();
return 0;
}
//4.:绑定地址与端口
struct sockaddr_in si;
si.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
si.sin_port = htons(12345);
si.sin_family = AF_INET;
if (SOCKET_ERROR == bind(sock, (const struct sockaddr*)&si, sizeof(si)))
{
int a = WSAGetLastError();
//释放socket
closesocket(sock);
//清理网络库
WSACleanup();
return 0;
}
return sock;
}
客户端代码:
#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <stdlib.h>
#include <WinSock2.h>
#pragma comment(lib, "ws2_32.lib")
/*
UDP/IP 基本C/S模型 客户端:
与 UDP/IP 基本C/S模型 服务端代码的区别:
代码基本一致,只有bind 函数有区别
服务端需要进行绑定地址与端口号,因为服务端是一对多
客户端只需要创建服务端地址与端口结构体,与服务端发送数据报时,
直接传递的是创建的结构体就可以了,不用进行绑定,因为服务端的ip与端口号固定不变的
*/
// 1.打开网络库 2.校验版本号
void OpenAndCheckWSAStartup();
// 3.创建服务端socket
SOCKET Sock_Server();
// 全局声明要创建 创建客户端socket
SOCKET socketClient;
// 4.与客户端进行发收数据
void SendToAndRecvFrom(SOCKET sock);
int main()
{
// 1.打开网络库 2.校验版本号
OpenAndCheckWSAStartup();
// 3.创建服务端socket
SOCKET socketClient = Sock_Server();
// 4.与客户端进行发收数据
SendToAndRecvFrom(socketClient);
//释放socket
closesocket(socketClient);
//清理网络库
WSACleanup();
system("pause");
return 0;
}
// 1.打开网络库 2.校验版本号
void OpenAndCheckWSAStartup()
{
// 定义WSAStartup函数中参数1 所需的版本号
WORD wVersionRequestd = MAKEWORD(2, 2); // 设置2.2版本号
// 声明WSAStartup函数中参数1 所需的结构体
WSADATA WSAData;
// 打开网络库
int nRet = WSAStartup(wVersionRequestd, &WSAData);
if (0 != nRet)
{
//出错了
int a = WSAGetLastError();
printf("网络库打开失败 %d\n", a);
return;
}
// 检验版本号
if (2 != HIBYTE(WSAData.wVersion) || 2 != LOBYTE(WSAData.wVersion))
{
printf("校验版本号错误\n");
// 关闭网络库
WSACleanup();
return;
}
}
// 3.创建服务端socket
SOCKET Sock_Server()
{
// 创建服务端 socket
SOCKET sock_server = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
// 判断服务端socket是否成功创建
if (INVALID_SOCKET == sock_server)
{
//出错了
int a = WSAGetLastError();
printf("创建服务端socket失败,获取的错误码为 %d\n", a);
//关闭网络库
WSACleanup();
return 0;
}
return sock_server;
}
// 4.与客户端进行发收数据
void SendToAndRecvFrom(SOCKET sock)
{
/*int recvfrom(
SOCKET s,
char *buf,
int len,
int flags,
sockaddr *from,
int *fromlen
);
recvfrom函数:
作用:得到当前服务端接收到的消息
本质:复制 将协议缓存区的数据复制黏贴进自定义的buf
与recv函数一样是进行接收消息,不同的是recv是傻等,recvfrom是死等
参数1:客户端的socket
特点:
TCP是获取指定的,recv函数与客户端是1对1的关系 -- 傻等
UDP是无差别获取,recvfrom函数与客户端是1对多的关系 -- 死等
参数2:客户端消息的存储空间,也就是一个字符数组
广域网:各级路由器上的MTU最小值是576字节
tcp = 576 - 20(TCP包头) - 20(IP包头) = 536;
udp = 576 - 8(UDP包头) - 20(IP包头) = 548;
参数3:想要读取的字节个数, 参数2的字节数
参数4:数据的读取方式
0 从协议缓存区取一个数据报,然后就删除掉
MSG_PEEK 取出数据报,但是协议缓存内不删除,一直残留,导致影响后面的数据读取
MSG_OOB 带外数据
参数5:对方的IP地址端口号
此处结构体为相应的IP地址和端口号 struct sockaddr* sockClient; 填 &sockClient;
参数6:参数5结构体的大小 int len = sizeof(&sockClient); 填 &len;
*/
/*int sendto(
SOCKET s,
const char *buf,
int len,
int flags,
const sockaddr *to,
int tolen
);
sendto函数:
作用:向目标发送数据报
本质:send函数将我们的数据复制黏贴进行系统的协议发送缓冲区,计算机伺机发出去
参数1:当前客户端的socket(自己的socket)
参数2:给接收方发送的字节串 UDP统一字节数 548
参数3:参数2的大小(字节个数 548)
参数4:填0
参数5:对方IP地址与端口号结构体
参数6:参数5的大小
*/
// 声明sendto函数中参数5所需的 服务端地址与端口号结构体
struct sockaddr_in sockServer;
// 给服务端地址与端口号结构体成员赋值
sockServer.sin_family = AF_INET;
sockServer.sin_port = htons(12345);
sockServer.sin_addr.S_un.S_addr = inet_addr("127.0.0.1");
// 循环与客户端进行收发消息
while (1)
{
// 定义要发送的字节数组
char strBuf[548] = { 0 };
// 手动发送
scanf_s("%s", strBuf, 548);
// 设置输入0时,将关闭客户端
if ('0' == strBuf[0])
{
break;
}
// 1.发消息
int nSet = sendto(sock, strBuf, sizeof(strBuf), 0, (struct sockaddr*)&sockServer, sizeof(sockServer));
// 判断是否成功发送消息
if (SOCKET_ERROR == nSet)
{
// 出错了
int a = WSAGetLastError();
printf("发送消息失败,获取的错误码为:%d\n", a);
continue;
}
// 2.收消息
// 定义recvfrom 函数所需的接收字节数组
char buf[548] = { 0 };
// 声明recvfrom函数中参数5所需的 结构体相应的IP地址与端口号
struct sockaddr sockClient; // 对方的IP地址与端口号
// 定义recvfrom函数中参数6所需的 参数5结构体的大小
int len = sizeof(sockClient);
int nRet = recvfrom(sock, buf, 548, 0, &sockClient, &len);
// 判断是否成功接收消息
if (SOCKET_ERROR == nRet)
{
// 出错了
int a = WSAGetLastError();
printf("接收消息失败,获取的错误码为:%d\n", a);
continue;
}
// 进行接收消息
printf("Server say: %s\n", buf);
}
}
程序运行结果:
?
|