通信编程:WSAAsyncSelect 模型通信
Windows 程序工作原理
Windows 程序设计完全不同于 DOS 程序设计方法,采用的是基于事件驱动方式的程序设计模式。Windows 系统是通过事件驱动的,事件驱动也就是程序的事件是围绕着消息的产生与处理展开,一条消息是关于发生的事件的消息,事件驱动是靠消息循环机制来实现的。
在 Windows 编程中,所有的程序都是一个个窗口。在整个系统中,使用窗口句柄唯一标识一个窗口,每个窗口都有自己的窗口过程来处理消息。当 Windows 程序开始运行时,首先先初始化程序,然后初始化并创建一个窗口。窗口进入消息循环等待消息的到来,接收到消息的时候先看看是不是退出程序,如果是就终止程序。如果不是则判断是否是自己感兴趣的消息,如果是就进行相应的响应和处理。对于窗口不感兴趣的消息,则使用默认的消息处理过程。
Windows 为每一个应用程序维护相应的消息队列,应用程序的任务就是处理消息循环。16 位的操作系统中只有一个消息队列,所以系统必须等待当前任务处理消息后才可以发送下一消息到相应程序。32 位的系统中每一个运行的程序都会有一个消息队列,所以系统可以在多个消息队列中转换。Windows 应用程序的消息来源有输入消息、控制消息、系统消息、用户消息 4 种,这些消息会被窗口绑定的回调函数(Cal lback Function)处理。
Windows 程序样例
运行 Windows 应用程序在桌面显示 Windows 窗口,且窗口中居中显示“大家好,这是我的第一个 Windows API 程序!”同时播放背景音乐。一个简单的 Windows API 程序由 2 部分组成,分别是 WinMain 函数和 CALLBACK 函数,Windows 程序以 WinMain 函数作为进入程序的初始人口点。Windows 系统是通过事件驱动的,事件驱动围绕着消息的产生与处理展开,一条消息是关于发生的事件的消息。CALLBACK 函数就是当窗口接收到消息时,负责对对应的消息做出动作和响应。
WinMain 函数
WinMain 函数的原型为 int WINAPI WinMain (HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR IpCmdLine, int nCmdshow),一般情况下我们应该在 WinMain 函数中完成下面的操作:
首先先解释一下 Main 函数的各个参数的含义:
参数 | 说明 |
---|---|
hinstance | 应用程序当前实例的句柄 |
hPrevlnstance | 应用程序的先前实例的句柄 |
szCmdLine | 指向应用程序命令行的指针 |
iCmdShow | 指明窗口如何显示 |
实例化窗口类
接着初始化一些变量,例如窗口名、消息句柄和消息:
static TCHAR szAppName[] = TEXT("HelloWorld!");
HWND hwnd;
MSG msg;
接着需要实例化窗口类,并且设置窗口的一些基本属性,包括窗口的样式、图标、背景和鼠标样式等,还要绑定 callback 函数。
WNDCLASS wndclass; //WNDCLASS是一个由系统支持的结构,用来储存某一类窗口的信息
wndclass.style = CS_HREDRAW | CS_VREDRAW; // 窗口类的风格
wndclass.lpfnWndProc = WndProc; //窗口处理的回调函数
wndclass.cbClsExtra = 0; //窗口扩展
wndclass.cbWndExtra = 0; //窗口实例扩展
wndclass.hInstance = hInstance; //实例句柄
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); //窗口的图标
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); //窗口鼠标光标
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); //窗口背景色
wndclass.lpszMenuName = NULL; //窗口菜单
注册窗口
实例化窗口类后,我们需要将这个窗口对象注册,只有注册好的窗口对象才能够被显示。注册窗口需要使用 RegisterClass(&wndclass) 方法,注册成功返回 true,此时使用一个 if 判断一下是否注册成功,注册失败显示提示信息并返回。
wndclass.lpszClassName = szAppName; //窗口类名
if(!RegisterClass(&wndclass)) //注册窗口
{
//显示一个模态对话框
MessageBox(NULL,TEXT("This program RegisterClass is error!"), szAppName, MB_ICONERROR);
return 0;
}
显示并更新窗口
接下来需要在句柄中设置窗口显示的相关信息,并利用该句柄和 ShowWindow(hwnd, iCmdShow) 方法显示窗口,显示后用 UpdateWindow(hwnd) 更新窗口完成显示。
hwnd = CreateWindow(szAppName, //lpClassName:窗口类名
TEXT("The Hello Program"), //lpWindowName:窗口标题
WS_OVERLAPPEDWINDOW, //dwStyle:指定创建窗口的风格
CW_USEDEFAULT, //X:指定窗口的初始水平位置
CW_USEDEFAULT, //Y:指定窗口的初始垂直位置
CW_USEDEFAULT, //nWidth:以设备单元指明窗口的宽度
CW_USEDEFAULT, //nHeight:以设备单元指明窗口的高度
NULL, //hWndParent:指向被创建窗口的父窗口或所有者窗口的句柄
NULL, //hMenu:菜单句柄
hInstance, //hlnstance:与窗口相关联的模块实例的句柄
NULL); //lpParam:该值传递给窗口WM_CREATE消息
ShowWindow(hwnd, iCmdShow); //该函数设置指定窗口的显示状态
UpdateWindow(hwnd); //更新指定窗口的客户区
消息循环
窗口显示在桌面后,就需要循环侦听传来的消息,转换成相应的消息标识后做相应的处理即可。
while(GetMessage(&msg, NULL, 0, 0)) //从调用线程的消息队列里取得一个消息并将其放于指定的结构
{
TranslateMessage(&msg); //用于将虚拟键消息转换为字符消息
DispatchMessage(&msg); //函数分发一个消息给窗口程序
}
return msg.wParam;
CALLBACK 函数
回调函数并不由开发者直接调用执行,只是使用系统接口 API 函数作为起点,回调函数通常作为参数传递给系统 API,由该 API 来调用。回调函数可能被系统 API 调用一次,也可能被循环调用多次。此处就是当窗口接收到一个消息时,将调用 CALLBACK 函数对消息进行处理。代码如下:
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM IParam)
{
HDC hdc; //HDC:设备场景句柄
PAINTSTRUCT ps; //PAINTSTRUCT:绘图信息结构
RECT rect; //rect:存储成对出现的参数,比如一个矩形框的左上角坐标、宽度和高度
switch(message)
{
case WM_CREATE:
PlaySound(TEXT("hellowin.wav"), NULL, SND_FILENAME|SND_ASYNC); //播放音频
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps); //为指定窗口进行绘图工作的准备
GetClientRect(hwnd, &rect); //函数获取窗口客户区的大小
//在指定的矩形里写入格式化的正文
DrawText(hdc, TEXT("大家好,这是我的第一个Windows API 程序!"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY: //该函数向系统表明有个线程有终止请求
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, IParam); //调用缺省的窗口过程来为应用程序没有处理的任何窗口消息提供缺省的处理
}
运行效果
如果使用 DEV-C++ 来编译 Windows 程序,需要在编译时加入“-mwindows -lwinmm”命令,首先先打开工具中的“编译选项”。
选中“编译时加入以下命令”,并且写上“-mwindows -lwinmm”命令,保存并编译。
通过编译后运行的效果如下:
完整代码
#include <windows.h>
//LRESULT就是long,CALLBACK是回调函数,参数分别是窗口句柄,消息,消息参数,消息参数
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
/*
hinstance:应用程序当前实例的句柄
hPrevlnstance:应用程序的先前实例的句柄
szCmdLine:指向应用程序命令行的指针
iCmdShow:指明窗口如何显示
*/
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, PSTR szCmdLine, int iCmdShow)
{
static TCHAR szAppName[] = TEXT("HelloWorld!");
HWND hwnd;
MSG msg;
WNDCLASS wndclass; //WNDCLASS是一个由系统支持的结构,用来储存某一类窗口的信息
wndclass.style = CS_HREDRAW | CS_VREDRAW; // 窗口类的风格
wndclass.lpfnWndProc = WndProc; //窗口处理的回调函数
wndclass.cbClsExtra = 0; //窗口扩展
wndclass.cbWndExtra = 0; //窗口实例扩展
wndclass.hInstance = hInstance; //实例句柄
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); //窗口的图标
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); //窗口鼠标光标
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH); //窗口背景色
wndclass.lpszMenuName = NULL; //窗口菜单
wndclass.lpszClassName = szAppName; //窗口类名
if(!RegisterClass(&wndclass))
{
//显示一个模态对话框
MessageBox(NULL,TEXT("This program RegisterClass is error!"), szAppName, MB_ICONERROR);
return 0;
}
hwnd = CreateWindow(szAppName, //lpClassName:窗口类名
TEXT("The Hello Program"), //lpWindowName:窗口标题
//WS_POPUP | WS_BORDER | WS_THICKFRAME,
WS_OVERLAPPEDWINDOW, //dwStyle:指定创建窗口的风格
CW_USEDEFAULT, //X:指定窗口的初始水平位置
CW_USEDEFAULT, //Y:指定窗口的初始垂直位置
CW_USEDEFAULT, //nWidth:以设备单元指明窗口的宽度
CW_USEDEFAULT, //nHeight:以设备单元指明窗口的高度
NULL, //hWndParent:指向被创建窗口的父窗口或所有者窗口的句柄
NULL, //hMenu:菜单句柄
hInstance, //hlnstance:与窗口相关联的模块实例的句柄
NULL); //lpParam:该值传递给窗口WM_CREATE消息
ShowWindow(hwnd, iCmdShow); //该函数设置指定窗口的显示状态
UpdateWindow(hwnd); //更新指定窗口的客户区
while(GetMessage(&msg, NULL, 0, 0)) //从调用线程的消息队列里取得一个消息并将其放于指定的结构
{
TranslateMessage(&msg); //用于将虚拟键消息转换为字符消息
DispatchMessage(&msg); //函数分发一个消息给窗口程序
}
return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM IParam)
{
HDC hdc; //HDC:设备场景句柄
PAINTSTRUCT ps; //PAINTSTRUCT:绘图信息结构
RECT rect; //rect:存储成对出现的参数,比如一个矩形框的左上角坐标、宽度和高度
switch(message)
{
case WM_CREATE:
PlaySound(TEXT("hellowin.wav"), NULL, SND_FILENAME|SND_ASYNC);
return 0;
case WM_PAINT:
hdc = BeginPaint(hwnd, &ps); //为指定窗口进行绘图工作的准备
GetClientRect(hwnd, &rect); //函数获取窗口客户区的大小
//在指定的矩形里写入格式化的正文
DrawText(hdc, TEXT("大家好,这是我的第一个Windows API 程序!"), -1, &rect, DT_SINGLELINE | DT_CENTER | DT_VCENTER);
EndPaint(hwnd, &ps);
return 0;
case WM_DESTROY: //该函数向系统表明有个线程有终止请求
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hwnd, message, wParam, IParam); //调用缺省的窗口过程来为应用程序没有处理的任何窗口消息提供缺省的处理
}
WSAAsyncSelect 模型
WSAAsyncSelect 模型允许应用程序以 Windows 消息的形式接收网络事件通知,它为了适应 Windows 的消息驱动环境而设置的。WSAAsyncSelect 模型最突出的特点是与 Windows 的消息驱动机制融在了一起,这使得开发带 GUI 界面的网络程序变得很简单。但是如果连接增加,单个 Windows 函数处理上千个客户请求时,服务器性能势必会受到影响。
WSAAsyncSelect 模型使用 WSAAsyncSelect 函数自动把套接字设为非阻塞模式,并且为套接字绑定一个窗口句柄,当有网络事件发生时,便向这个窗口发送消息。
int
WSAAPI
WSAAsyncSelect(
_In_ SOCKET s,
_In_ HWND hWnd,
_In_ u_int wMsg,
_In_ long lEvent
);
参数 | 熟悉 |
---|---|
s | 需要设置的套接字句柄 |
hWnd | 指定一个窗口句柄,通知消息将被发送到与其对应的窗口过程中 |
wMsg | 网络事件到来时接收到的消息 ID,可以在 WM USER 以上的数值中任意选择一个 |
IEvent | 指定哪些通知码需要发送消息 |
参数 IEvent 指定了要发送的通知码,可以是如下取值的组合:
通知码 | 说明 |
---|---|
FD_READ | 套接字接收到对方发送过来的数据包,此时可以读取数据 |
FD_WRITE | 数据缓冲区满后再次变空时,通知应用程序表示可以继续发送数据了 |
FD_ACCEPT | 监听中的套接字检测到有连接进入 |
FD_CONNECT | 连接其他的主机,连接完成以后会接收到这个通知码 |
FD_CLOSE | 检测到套接字对应的连接被关闭 |
如果需要指定多个通知码就用 “|” 连接,例如下面这个调用表示当接收数据、可以发送数据、套接字关闭时发送消息。
::WSAAsyncSelect(client, hWnd, WM_SOCKET, FD_READ | FD_WRITE | FD_CLOSE);
成功调用 WSAAsyncSelect 之后,应用程序便开始以 Windows 消息的形式在 CALLBACK 函数接收网络事件通知。
WSAAsvncSelect 模型样例
注意无论是客户端还是服务器,都需要包含头文件 initsock.h 来载入 Winsock。
功能设计
模拟实现 TCP 协议通信过程,要求编程实现服务器端与客户端之间双向数据传递。也就是在一条 TCP 连接中,客户端和服务器相互发送一条数据即可。
initsock.h
#include <winsock2.h>
#pragma comment(lib, "WS2_32") // 链接到 WS2_32.lib
class CInitSock
{
public:
/*CInitSock 的构造器*/
CInitSock(BYTE minorVer = 2, BYTE majorVer = 2)
{
// 初始化WS2_32.dll
WSADATA wsaData;
WORD sockVersion = MAKEWORD(minorVer, majorVer);
if (::WSAStartup(sockVersion, &wsaData) != 0)
{
exit(0);
}
}
/*CInitSock 的析构器*/
~CInitSock()
{
::WSACleanup();
}
};
服务器
#include "initsock.h"
#include <iostream>
using namespace std;
#define WM_SOCKET WM_USER + 101 // 自定义消息
CInitSock theSock;
LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
int main()
{
static TCHAR szClassName[] = TEXT("MainWClass");
WNDCLASSEX wndclass;
// 用描述主窗口的参数填充WNDCLASSEX结构
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 = ::CreateWindowEx(
0,
szClassName,
TEXT(""),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
CW_USEDEFAULT,
NULL,
NULL,
NULL,
NULL);
if (hWnd == NULL)
{
::MessageBox(NULL, TEXT("创建窗口出错!"), TEXT("error"), MB_OK);
return -1;
}
// 创建监听套接字
SOCKET sListen = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
sockaddr_in sin;
USHORT nPort = 4567; // 此服务器监听的端口号
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)
{
cout << " Failed bind()" << endl;
return -1;
}
// 将套接字设为窗口通知消息类型。
::WSAAsyncSelect(sListen, hWnd, WM_SOCKET, FD_ACCEPT | FD_CLOSE);
// 进入监听模式
if (::listen(sListen, 5) == SOCKET_ERROR)
{
cout << " Failed listen()" << endl;
return 0;
}
cout << "服务器已启动监听,可以接收连接!" << endl;
// 从消息队列中取出消息
MSG msg;
while (::GetMessage(&msg, NULL, 0, 0))
{
// 转化键盘消息
::TranslateMessage(&msg);
// 将消息发送到相应的窗口函数
::DispatchMessage(&msg);
}
// 当GetMessage返回0时程序结束
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: // 监听中的套接字检测到有连接进入
{
sockaddr_in addrRemote;
int nAddrLen = sizeof(addrRemote);
SOCKET client = ::accept(s, (SOCKADDR*)&addrRemote, &nAddrLen);
::WSAAsyncSelect(client, hWnd, WM_SOCKET, FD_READ | FD_CLOSE);
cout << "\n与主机" << ::inet_ntoa(addrRemote.sin_addr) << "建立连接" << endl;
}
break;
case FD_READ:
{
char szText[1024] = { 0 };
char sendText[] = "你好,客户端!";
if (::recv(s, szText, 1024, 0) == -1)
{
::closesocket(s);
}
else
{
cout << " 接收到数据:" << szText << endl;
// 向客户端发送数据
if (::send(s, sendText, strlen(szText), 0) > 0)
{
cout << " 向客户端发送数据:" << sendText << endl;
}
}
}
break;
case FD_CLOSE:
{
::closesocket(s);
}
break;
}
}
return 0;
case WM_DESTROY:
{
::PostQuitMessage(0);
}
return 0;
}
// 将我们不处理的消息交给系统做默认处理
return ::DefWindowProc(hWnd, uMsg, wParam, lParam);
}
客户端
#include "InitSock.h"
#include <iostream>
using namespace std;
CInitSock initSock; // 初始化Winsock库
int main()
{
// 创建套接字
SOCKET s = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (s == INVALID_SOCKET)
{
cout << " Failed socket()" << endl;
return 0;
}
// 也可以在这里调用bind函数绑定一个本地地址
// 否则系统将会自动安排
char address[20] = "127.0.0.1";
// 填写远程地址信息
sockaddr_in servAddr;
servAddr.sin_family = AF_INET;
servAddr.sin_port = htons(4567);
// 注意,这里要填写服务器程序(TCPServer程序)所在机器的IP地址
// 如果你的计算机没有联网,直接使用127.0.0.1即可
servAddr.sin_addr.S_un.S_addr = inet_addr(address);
if (::connect(s, (sockaddr*)&servAddr, sizeof(servAddr)) == -1)
{
cout << " Failed connect() " << endl;
return 0;
}
else
{
cout << "与服务器 " << address << "建立连接" << endl;
}
char szText[] = "你好,服务器!";
if (::send(s, szText, strlen(szText), 0) > 0)
{
cout << " 发送数据:" << szText << endl;
}
// 接收数据
char buff[256];
int nRecv = ::recv(s, buff, strlen(buff), 0);
if (nRecv > 0)
{
buff[nRecv] = '\0';
cout << " 接收到数据:" << buff << endl;
}
// 关闭套接字
::closesocket(s);
return 0;
}
运行效果
参考资料
《Windows 网络与通信编程》,陈香凝 王烨阳 陈婷婷 张铮 编著,人民邮电出版社