实现多人即时聊天

编程爱好者

本篇介绍了什么是Winsock I/O模型。详细介绍了WSAAsyncSelect模型(异步选择模型)的用法和优缺点,并用WSAAsyncSelect模型开发了一个多人即时聊天程序。

了解Winsock I/O模型

Winsock I/O模型用于对套接字的I/O(输入/输出,如send或recv)进行管理和操作。共有六种类型的Winsock I/O模型,分别为:阻塞模型、select(选择模型)、WSAAsyncSelect(异步选择模型)、WSAEventSelect(事件选择模型)、Overlapped(重叠模型)、CompletionPort(完成端口模型)。每种模型都各有优缺点,程序员都根据程序的需要选择合适模型。

你对Winsock I/O模型应该不会感到陌生,其实在前两章我们的程序所使用的I/O模型就是阻塞模型。因为当程序运行到send或recv函数进行数据收发时,程序会进入等候状态,这也就是所谓的阻塞。也因为这样,所以服务器端通常要为连接进来的客户端开一些诸如接收数据的线程和发送数据的线程。阻塞模型的好处就是简单,当只处理少数连接者时可以选用它,但如果连接者的数量多的话,阻塞模型处理起来就显得吃力了。阻塞模型管理I/O的方法主要是开许多线程,连接者多的话,一来管理不方便,二来线程开多了对系统资源的消耗很大。

领悟WSAAsyncSelect模型

WSAAsyncSelect模型称为异步选择模型。除了Windows CE外,其它Windows操作系统都支持WSAAsyncSelect模型。

WSAAsyncSelect模型非常适用于Windows窗口程序开发,因为它处理网络通信的方式就像Windows窗口程序处理消息的方式一样。如Windows程序消息的WM_KEYDOWN表示按下了键盘的任意一个键,而WSAAsyncSelect的事件FD_READ表示有数据等待接收。

在典型的Windows程序中,消息都在一个称为窗口函数(函数名通常为“WndProc”)中处理。要使用WSAAsyncSelect模型,就要给出想进行网络通信的套接字,一个窗口句柄,一个自定义的消息(比WM_USER大的一个值),想要处理的网络事件(如收发数据)。

调用WSAAsyncSelect函数便可以使用WSAAsyncSelect模型,该函数定义如下:

int WSAAsyncSelect(SOCKET s,HWND hWnd,unsigned int wMsg,long lEvent);

●s——要进行网络通信的套接字;

●hWnd——窗口句柄,它标志的是当发生网络事件后,将要接收消息的那个窗口;

●wMsg——自定义的消息,当s套接字发生了网络事件,hWnd窗口的窗口函数(名字通常为“WndProc”)收到的就是这个消息;

●lEvent——网络事件,这些事件可以用运算符“|”按位或运算;

编写服务器端程序

现在我们可以动手实现QQ的多人即时聊天功能了,但我们并不打算那么快做QQ的界面,因为加入界面方面的程序会令整个程序看起来不清晰,这不利于学习,我们还是集中于网络通信方面的程序。

下面就是使用WSAAsyncSelect模型的服务器端代码,只给出重要的部分:

#define WM_SOCKET WM_USER + 100 //自定义的消息

#include <WinSock2.h> // WinSock2.h要放在windows.h前面

#include <windows.h>

#include <vector>

#include <algorithm>

using namespace std;

const int BUFFER_SIZE =10240; // 数据缓冲的大小

LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);

SOCKET ServerSocket;

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,PSTR szCmdLine, int iCmdShow)

{ WSADATA wsaData;

SOCKADDR_IN ServerAddr;

static TCHAR szAppName[] = TEXT("SysMets1");

HWND hwnd;

MSG msg;

WNDCLASS wndclass;

HWND hwnd; // 窗口句柄

//创建窗口,启动并创建套接字

WSAStartup( MAKEWORD( 2, 2 ), &wsaData );

ServerSocket = socket( AF_INET, SOCK_STREAM, IPPROTO_TCP );

ServerAddr.sin_family = AF_INET;

ServerAddr.sin_addr.s_addr = htonl( INADDR_ANY );

ServerAddr.sin_port = htons( 5555 );

bind( ServerSocket, ( SOCKADDR* )&ServerAddr, sizeof( ServerAddr ) );

// 服务器套接字会接收FD_ACCEPT、FD_CLOSE两个事件

WSAAsyncSelect( ServerSocket,hwnd,WM_SOCKET,FD_ACCEPT|FD_CLOSE);

listen( ServerSocket, 5 );

while(GetMessage(&msg,NULL,0,0))

{ TranslateMessage(&msg);

DispatchMessage(&msg); }

return msg.wParam;}

LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)

{ SOCKET Accept;

static vector<SOCKET> SockObjs; //用于保存客户端的套接字

static char buffer[BUFFER_SIZE]; //数据缓冲

switch(message)

{ case WM_CREATE:

return 0;

// 处理网络通信

case WM_SOCKET:

if( WSAGETSELECTERROR(lParam) )

{ vector<SOCKET>::iterator iter =

find( SockObjs.begin(), SockObjs.end(), (SOCKET)wParam );

if( iter != SockObjs.end() )

SockObjs.erase( iter );

closesocket( ( SOCKET )wParam );

break; }

switch(WSAGETSELECTEVENT(lParam))

{case FD_ACCEPT:

Accept = accept( ( SOCKET )wParam, NULL, NULL );

//客户端套接字对FD_READ, FD_WRITE, FD_CLOSE事件感兴趣

WSAAsyncSelect( Accept, hwnd, WM_SOCKET,

FD_READ | FD_WRITE | FD_CLOSE );

SockObjs.push_back(Accept);

break;

case FD_READ:

//处理接收数据操作

break;

case FD_WRITE:

//处理发送数据操作

break;

case FD_CLOSE: //关闭套接字

{ vector<SOCKET>::iterator iter =

find( SockObjs.begin(), SockObjs.end(), (SOCKET)wParam );

if( iter != SockObjs.end() )

SockObjs.erase( iter );

closesocket( ( SOCKET )wParam );}

break;}

return 0;

case WM_DESTROY:

closesocket( ServerSocket );

WSACleanup();

PostQuitMessage(0);

return 0;}

return DefWindowProc(hwnd, message, wParam, lParam);}

程序分析:程序使用向量(vector)储存了连接进来的客户端套接字,当客户端调用了closesocket函数关闭套接字,服务器端会收到一个FD_CLOSE网络事件,在这里可以把该套接字从向量(vector)里删除,并关闭该套接字。

对于一个send函数,在发送数据的时候不一定一次便能把所有数据都发出去。如数据缓冲区的大小为10240,在发送数据的时候极有可能真正发出去的数据少于这个数。所以,有必要修改前两章直接调用send函数发送数据的方式。

send函数返回的是已发出去的数据大小,所以可以把发送数据的程序改成以下形式,以保证所有字节都可以发送出去。

int nLeft = BUFFER_SIZE;

int idx = 0;

int ret;

while( nLeft> 0 )

{ ret = send( (SOCKET)wParam, &buffer[idx], nLeft, 0 );

if( ret == SOCKET_ERROR )

{ //发生错误 }

nLeft -= ret;

idx += ret;}

编写客户端程序

客户端处理网络通信的方式与服务器端一样,这里就不给出代码了。如图是客户端运行时的界面。程序做得比较简单,没有昵称,有兴趣的读者可以自己加上去。

21-g15-1-1.jpg

服务器端和客户端的代码下载地址:http://www.cpcw.com/xz/cx.rar

WSAAsyncSelect模型的优缺点

当你会用了WSAAsyncSelect模型时,你会很喜欢它,它特别适用于Windows窗口程序。而且它可以同时处理多个连接,管理起来也很方便,就像本篇的例子一样,可以有多个连接者一起聊天,而程序是单线程的。当然,它也有它的缺点,就是程序一定要有一个窗口,即使你的程序不需要窗口。