- 1 效果展示
- 1.1 服务器和客户端界面展示
- 1.2 一对一之间通信的功能展示
- 1.3 一对多之间通信的功能展示
- 2 知识预备
- 2.1 WSAAsyncSelect模型介绍
- 2.2 WSAAsyncSelect模型的过程图
- 2.3 与SELECT模型比较
- 2.4 套接字WSAAsyncSelect模型实现
- 2.5 网络事件种类
- 2.6 WSAAsyncSelect模型的优势和不足
- 3 代码展示
- 3.1 服务端
- 3.1.1 定义变量
- 3.1.2 初始化界面
- 3.1.3 启动服务器
- 3.1.4 消息响应
- 3.1.5 发送消息
- 3.1.6 断开
- 3.2 客户端
- 3.2.1 连接服务器
- 3.2.2 消息响应
- 3.2.3 发送消息
- 4 总结与思考
- 5 源码下载
步骤:
- 启动服务器进行监听
- 客户端再连接服务器端
- 服务器与客户端互发消息
2 知识预备 2.1 WSAAsyncSelect模型介绍步骤与前面一样
WSAAsyncSelect模型是Windows Sockets的一个异步I/O模型。利用该模型应用程序可以在一个套接字上,接收以Windows消息为基础的网络事件。Windows Sockets应用程序在创建套接字后,调用WSAAsyncSelect()函数注册感兴趣的网络事件。当该事件发生时Windows窗口收到消息,然后应用程序就可以对接收到的网络事件进行处理。
2.2 WSAAsyncSelect模型的过程图 WSAAsyncSelect模型是非阻塞的。如图所示, Windows Sockets应用程序在调用recv)函数接收数据之前,调用WSAAsyncSelect()函数注册网络事件。WSAAsyncSelect()函数立即返回,线程继续运行。当系统中数据准备好时,向应用程序发送消息。应用程序接收到这个消息后,调用recv()函数接收数据。
SELECT模型介绍
WSAAsyncSelect模型与Select模型的相同点是,他们都可以对Windows套接字应用程序所使用的多个套接字进行有效的管理。
但WSAAsyncSelect模型与Select模型相比存在以下不同。
- WSAAsyncSelect模型是异步的。在应用程序中调用WSAAsyncSelect()函数,通知系统感兴趣的网络事件,该函数立即返回,应用程序继续运行。
- 发生网络事件时,应用程序得到通知的方式不同。select()函数返回时,说明某个或者某些套接字满足可读可写的条件,应用程序需要使用FD_ISSET宏,判断套接字是否存在于可读可写集合中。而对于WSAAsyncSelect模型来说,当网络事件发生时,系统向应用程序发送消息。
- WSAAsyncSelect模型应用在基于消息的Windows环境下,使用该模型时必须创建窗口.而Select模型广泛应用在Unix系统和Windows系统,使用该模型不需要创建窗口。
- 应用程序中调用WSAAsyncSelect()函数后,自动将套接字设置为非阻塞模式。而应用程序中调用select()函数后,并不能改变该套接字的工作方式。
WSAAsyncSelect()函数功能是请求当网络事件发生时为套接字发送消息。该函数声明如下:
int WSAAsyncSelect(
SOCKET s,
HWND hWnd,
unsigned int wMsg,
long lEvent
);
- s:需要事件通知的套接字。
- hWnd:当网络事件发生时接收消息的窗口句柄。
- wMsg:当网络事件发生时窗口收到的消息。
- lEvent:应用程序感兴趣的网络事件集合。
2.5 网络事件种类当应用程序中调用该函数后,自动将套接字设置为非阻塞模式。通常,应用程序声明的消息要比 Windows的WM_USER值大,以避免该消息与Windows预定义消息发生混淆。
种类 | 含义 |
---|---|
FD_READ | 欲接收可读的通知 |
FD_WRITE | 欲接收可写读的通知 |
FD_ACCEPT | 欲接收等待接受连接的通知 |
FD_CONNECT | 欲接收一次连接或者多点jion *** 作完成的通知 |
FD_OOB | 欲接收有带外(OOB)数据到达的通知 |
FD_CLOSE | 欲接收套接字关闭的通知 |
FD_QoS | 欲接收套接字服务质量发生变化的通知 |
FD_GROUP_QoS | 欲接收套接字组服务质量发生变化的通知 |
FD_ROUTING_INTERFACE_CHANGE | 欲在指定方向上,与路由接口发生变化的通知 |
FD_ADDRESS_LIST_CHANGE | 欲接收针对套接字的协议家族,本地地址列表发生变化的通知 |
调用WSAAsyncSelect()函数如下所示:
WSAAsyncSelect (s,hwnd,WM_SOCKET,FD_CONNECT | FD_READ|ED_CLOSE);
应用程序在一个套接字上成功调用了WSAAsyncSelect之后,会在与hWnd窗口句柄对应的窗口例程中,以Windows消息的形式,接收网络事件通知。
窗口例程通常定义如下:
LRESULT CALLBACK WindowProc(
HWND hwnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
);
- hWnd:窗口句柄。
- uMsg:消息。对Windows Sockets应用程序来说感兴趣的是在WSAAsyncSelect()函数中,由应用程序定义的消息。
- wParam:消息参数。在Windows Sockets应用程序中,该参数指明发生网络事件的套接字。
- lParam:消息参数。在Windows Sockets应用程序中,该参数低字节指明已经发生的网络事件。高字节包含可能出现的错误代码。
优势:
- 该模型的使用方便了在基于消息的Windows环境下开发套接字的应用程序。开发人员可以像处理其他消息一样对网络事件消息进行处理。
- 该模型确保接收所有数据提供了很好的机制。通过注册FD_CLOSE网络事件,从容关闭服务器与客户端的连接保证了数据全部接收。
不足:
- 该模型局限在,他基于Windows的消息机制,必须在应用程序中创建窗口。当然,在开发中可以根据具体情况是否显示该窗口。MFC的CSocketWnd类就是用来创建一个不显示的窗口,并在该类中声明接收网络事件消息处理函数。
- 由于调用WSAAsyncSelect()函数后,自动将套接字设置为非阻塞状态。当应用程序为接收到网络事件调用相应函数时,未必能够成功返回。这无疑增加了开发人员使用该模型的难度。对于这一点可以从MFC CSocket类的Accept()、Receive()和Send()函数的实现得到验证。
在ServerDlg.h里
#define WM_SOCKET WM_USER+1000 //套接字消息
...
public:
WSADATA wsd;//WSADATA变量
SOCKET listenSocket;//服务器监听套接字
SOCKET acceptSocket;//接受客户端连接请求的套接字
sockaddr_in addr;
sockaddr_in addr1;
int n = 0;//客户端数
SOCKET s[10];//套接字数组
int flag = 0;
vector<SOCKET> socket_arr[100];//存放acceptsocket
int count = 0;
CListCtrl m_Listctrl;
CEdit m_SendText;//发送消息内容
CButton m_Send;//发送按钮
CIPAddressCtrl m_Ip;//IP地址
CEdit m_Port;//端口
CButton m_Start;//启动
afx_msg void OnBnClickedButton2();
afx_msg void OnBnClickedButton1();
CButton m_Close;//断开
afx_msg void OnBnClickedButton3();
CEdit m_num;//选择发送的对象
void InitSocket();//初始化
...
在ServerDlg.cpp里
3.1.2 初始化界面//初始化
void CServerDlg::InitSocket()
{
//按钮初始化
m_Send.EnableWindow(false);
m_SendText.EnableWindow(false);
m_Close.EnableWindow(false);
//m_Listctrl.ShowScrollBar(SB_VERT, TRUE);
//服务器地址ip和端口初始化
char * strIP = "127.0.0.1";
DWORD dwAddress = ntohl(inet_addr(strIP));
m_Ip.SetAddress(dwAddress);
m_Port.SetWindowText("8080");
}
3.1.3 启动服务器
//启动服务器
void CServerDlg::OnBnClickedButton2()
{
CString str, str1;
BYTE a, b, c, d;
m_Ip.GetAddress(a, b, c, d);//获取服务器地址
str.Format(_T("%d.%d.%d.%d"), a, b, c, d);//CString->int
m_Port.GetWindowText(str1);//获取端口号
//设置地址
addr.sin_family = AF_INET;//地址族
addr.sin_port = htons(_ttoi(str1));//端口号
addr.sin_addr.S_un.S_addr = inet_addr(CT2CA(str));//IP地址
//创建套接字
listenSocket = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
//绑定套接字
::bind(listenSocket, (sockaddr*)&addr, sizeof(addr));
//监听套接字
::listen(listenSocket, 5);
//输出
CString str2, str3;
str2 = ::inet_ntoa(addr.sin_addr);//返回点分十进制的字符串
str3.Format("%d", ntohs(addr.sin_port)); //ntohs主要是将网络字节转为主机字节
m_Listctrl.InsertItem(count++, _T("服务器["+str2+":"+str3+"]已启动监听..."));
//按钮变化
m_Start.EnableWindow(false);
m_Close.EnableWindow(true);
//设置异步套接字
::WSAAsyncSelect(listenSocket, this->m_hWnd, WM_SOCKET, FD_ACCEPT | FD_READ | FD_CLOSE);
}
3.1.4 消息响应
//消息响应函数
LRESULT CServerDlg::OnSocket(WPARAM wPARAm, LPARAM lParam) {
CString str;
switch (lParam) {
case FD_ACCEPT:
{
//accept函数返回一个新的套接字,同时返回客户端的IP地址,初始化ClientAddr
int len = sizeof(addr1);
acceptSocket = ::accept(listenSocket, (sockaddr*)&addr1, &len);
//添加套接字
socket_arr->push_back(acceptSocket);
flag++;
n = n + 1;//客户端数加一
CString str1, str2,str3;
str1 = ::inet_ntoa(addr1.sin_addr);//返回点分十进制的字符串
str2.Format("%d", ntohs(addr1.sin_port)); //ntohs主要是将网络字节转为主机字节
str.Format("目前有%d个客户端\n", n);
str3.Format("%d", flag);
m_Listctrl.InsertItem(count++, _T("用户"+str3+"[" + str1 + ":" + str2 + "]已经连接成功"));
m_Listctrl.InsertItem(count++, _T(str));
//按钮变化
m_Send.EnableWindow(true);
m_SendText.EnableWindow(true);
}
break;
case FD_READ:
{
//接收消息
char buf[100] = { '}' ;int
= ret 0 ;int
= i 0 ;//迭代器遍历
for
( <vector::SOCKET>=iterator it - socket_arrbegin>();!= it - socket_arrend>();++ it)= {
ret::recv(*,it, buf100 ,0 );;
SOCKADDR_IN addClientint
= nLen sizeof ()addClient;//获取当前连接的客户端的IP地址和端口号,即初始化addClient
getpeername
(-socket_arrat>()i,( *sockaddr)&,addClient& )nLen;,
CString str, str1; str2.
strFormat("%d",+ i 1 );=
str1 :: inet_ntoa(.addClient)sin_addr;.
str2Format("%d",ntohs (.addClient)sin_port);if
( 0ret > )//说明有数据 {.
m_ListctrlInsertItem(++count,_T ("用户"+ + str "["++str1":"++str2"]说:" + ) buf);}
++
i;}
}
break
;case
: FD_CLOSE//欲接收客户端关闭情况
{
int
= ret 0 ;char
[ buf100]= '}' { ; int=
0 i ; for(
< ::vector=SOCKET>-iterator it begin socket_arr(>);!=- it end socket_arr(>);++) it=:: {
ret recv (*,,it100 buf, 0) ;if(
== 0ret)//客户端数量减一={
-
n 1 n ; ;.
CString strFormat
str("目前有%d个客户端\n",); n;int
SOCKADDR_IN addClient=
sizeof nLen ( );addClient//获取当前连接的客户端的IP地址和端口号,即初始化addClientgetpeername
(
*,(it* )sockaddr&,&addClient) ;nLen,,
CString str1; str2=str3::
str1 inet_ntoa (.)addClient;sin_addr.Format
str2("%d",ntohs( .)addClient)sin_port;.Format
str3("%d",+1 i);.InsertItem
m_Listctrl(++,count_T( "用户"++"["str3++ ":" str1 + + "]主动关闭" str2 ) );.InsertItem
m_Listctrl(++,count_T( ))str;//当没有客户端,按钮变化if
(
! (0)n > ).EnableWindow {
m_Send(false);.EnableWindow
m_SendText(false);}//删除对应accepsocket
=
-
it erase socket_arr(>);itif(
== -it end socket_arr(>))break; {
}}
}
}
break
;
}return
0
; }//发送消息
void
3.1.5 发送消息
::
OnBnClickedButton1 CServerDlg()// TODO: 在此添加控件通知处理程序代码=
{
""
CString str ; .GetWindowText
m_SendText();str//获取发送消息的内容if(
== ""str ) MessageBox( {
_T("发送消息不能为空"),_T( "提示"));}else
;
. {
CString str1GetWindowText
m_num();str1//选择客户端进行发送if(
== ""str1 ) MessageBox( {
_T("发送的人不能为空"),_T( "提示"));}else
int
= {
_ttoi num ( );str1if(
:: send(-atsocket_arr(>-1num),CT2CA( ),str.GetLength str(),0) !=) . SOCKET_ERRORInsertItem {
m_Listctrl(++,count_T( "服务器对用户"++ "说:" str1 + ) ) str;.SetWindowText
m_SendText("");//清空.SetWindowText
m_num("");}else
MessageBox
( {
_T("发送消息失败"),_T( "提示"));}}
}
}
//断开
void
3.1.6 断开
::
OnBnClickedButton3 CServerDlg()// TODO: 在此添加控件通知处理程序代码closesocket
{
(
);listenSocketclosesocket(
);acceptSocketWSACleanup(
);.EnableWindow
m_Send(false);.EnableWindow
m_SendText(false);.EnableWindow
m_Start(true);.EnableWindow
m_Close(false);.InsertItem
m_Listctrl(++,count_T( "服务器已关闭"));}仅展示连接和发送(依葫芦画瓢)
//客户端连接
3.2 客户端
void
3.2.1 连接服务器::
OnBnClickedButton1 CClientDlg()// TODO: 在此添加控件通知处理程序代码,
{
;
CString str//定义字符串变量 str1int;
//定义端口 port,,
BYTE a, b; c. dGetAddress
m_IpAddress(,,a, b) c; d//获取服务器地址.Format
str(_T("%d.%d.%d.%d"),,, a, b) c; d.GetWindowText
m_Port();str1//获取端口地址if(
== ""str||== "" str1)MessageBox( {
_T("服务器地址或端口不能为空"),_T( "提示"),); MB_ICONEXCLAMATION}else
//初始化套接字动态库
if {
(
WSAStartup (MAKEWORD(2,2) ,&) !=wsd0 ) MessageBox( {
_T("WSAStartup error:"+WSAGetLastError ( )),_T( "提示"),); MB_ICONEXCLAMATION}//创建TCP Socket,流式套接字
=
socket
s ( ,,AF_INET) SOCK_STREAM; IPPROTO_TCPif(
== )s MessageBox INVALID_SOCKET( {
_T("create socket error : "+WSAGetLastError ( )),_T( "提示"),); MB_ICONEXCLAMATION}//设置地址
=
_ttoi
port();str1//字符串转整型.=
addrntohssin_port ( );port//端口..
addr.sin_addr=S_uninet_addrS_addr ( CT2CA())str;//网络字节序.=
addr;sin_family //地址族 AF_INET//int i = 0;if
(
:: connect(,(s* )sockaddr&,sizeofaddr( ))addr!=)//设置异步套接字 SOCKET_ERROR:: {
WSAAsyncSelect
(,thiss- ,,>m_hWnd| WM_SOCKET) FD_READ ; FD_CLOSE.InsertItem
m_ListCtrl(0,_T( "连接服务器成功"));//设置按钮变化.
EnableWindow
m_Close(true);.EnableWindow
m_Connect(false);.EnableWindow
m_Port(false);.EnableWindow
m_IpAddress(false);.EnableWindow
m_Send(true);.EnableWindow
m_SendText(true);}else
.
InsertItem {
m_ListCtrl(0,_T( "连接服务器失败,请重试"));}}
}
//消息相应函数
::
3.2.2 消息响应
OnSocket
LRESULT CClientDlg(,)WPARAM wPARAmchar LPARAM lParam[ {
100 buf]='}' ; { switch ()
case :lParam//接收服务器消息 {
:: FD_READrecv
{
(
,,100s, buf0 ); ,;=
CString str1:: str2inet_ntoa
str1 ( .);addr.sin_addrFormat(
str2"%d",ntohs(. ))addr;sin_port.InsertItem(
m_ListCtrl0,_T("服务器[" ++":" + str1 + "]:" + str2 ) ) ; buf}break;
case
://欲接收服务器关闭消息
, FD_CLOSE;
{
=
CString str1:: str2inet_ntoa
str1 ( .);addr.sin_addrFormat(
str2"%d",ntohs(. ))addr;sin_port.InsertItem(
//if (::WSAGetLastError() != WSAECONNRESET) {
m_ListCtrl0,_T("服务器[" ++":" + str1 + "]已关闭" ) str2 ) ;.EnableWindow(
m_Connecttrue);.EnableWindow(
m_Sendfalse);.EnableWindow(
m_SendTextfalse);//}}break
;
}
return0
;
} //发送void
::
3.2.3 发送消息
OnBnClickedButton2
( CClientDlg)// TODO: 在此添加控件通知处理程序代码;.
{
GetWindowText
CString str1(
m_SendText);//获取发送消息内容str1if(==
"" )str1 MessageBox (_T {
("发送消息不能为空"),_T("提示" ));}else::
send
( {
,CT2CA(s) ,.str1GetLength( str1),0); .InsertItem(
m_ListCtrl0,_T("我说:" +)) ; str1.SetWindowText(
m_SendText"");//清空}}//accept函数返回一个新的套接字,同时返回客户端的IP地址,初始化ClientAddr
int
=
4 总结与思考
在设计中遇到的一个最主要问题就是如何在服务端显示不同客户端的信息以及接收它们不同的信息。查阅了很多资料,解决方法如下:
1、如何在服务端显示不同的客户端信息?
此代码来源于ServerDlg.cpp中的消息响应函数中
sizeof
( len ) ;=addr1::accept
acceptSocket ( ,(*listenSocket) &sockaddr,&)addr1; //添加套接字len-push_back
(
socket_arr)>;++acceptSocket;=
flag+1
n ; n //客户端数加一 ...
,,;
CString str1= str2::str3inet_ntoa
str1 ( .);addr1//返回点分十进制的字符串sin_addr.Format(
str2"%d",ntohs(. ))addr1;sin_port//ntohs主要是将网络字节转为主机字节case: //接收消息
1、每当接收到一个客户端连接,就把该套接字储存起来,这里我用的是vector容器,当然也可以用数组。
2、inet_ntoa函数主要是返回点分十进制的字符串,ntohs函数主要是是将网络字节转为主机字节
2、如何在服务端接收不同客户端发来的信息?
char FD_READ[
{
100
] buf='}'; int { = 0;
int ret = 0;
//迭代器遍历 i for (<
::
= -vectorbeginSOCKET>(iterator it ) socket_arr;>!=-end( it ) socket_arr;>++)=:: itrecv( {
ret*,,100,it0 buf) ;; int=sizeof
SOCKADDR_IN addClient(
) nLen ; //获取当前连接的客户端的IP地址和端口号,即初始化addClientgetpeernameaddClient(-
at
()socket_arr,>(*i)& ,sockaddr&);addClient, ,nLen;.
CString strFormat str1( str2"%d"
str,+1); i = ::inet_ntoa(
str1 . );.addClientFormatsin_addr("%d"
str2,ntohs(.) );addClientifsin_port(0)
//说明有数据 .ret > InsertItem( {++
m_Listctrl,_T(count"用户"+ +"["+ + str ":"++str1"]说:"+)str2) ; } buf++;}
}
ibreak;
;
.
GetWindowText(
这里有一个非常重要的函数是getpeername,它用于获取与某个套接字关联的外地协议地址。
再加上迭代器遍历acceptsocket,就可以循环接收不同客户端发来的消息
那还有一个疑问?反过来服务端给不同客户端发送消息怎么办?
解决办法,给每一个客户端标上flag,在界面添加一个文本框,每次服务端需要选择用户来发送消息,这样就可以根据标号来确定是哪一个acceptsocket了。
CString str1)
m_num;//选择客户端进行发送ifstr1(==""
) MessageBoxstr1 ( _T( {
"发送的人不能为空"),_T("提示") );}elseint=
_ttoi
( {
) num ; if(str1::send
( -at(-socket_arr1>),numCT2CA(), .GetLengthstr() str,0)!=) .InsertItem ( SOCKET_ERROR++ {
m_Listctrl,_T(count"服务器对用户"+ +"说:"+ ) str1 ) ; . strSetWindowText(""
m_SendText);//清空.SetWindowText(""
m_num);}elseMessageBox(
_T
( {
"发送消息失败"),_T("提示") );}}
5 源码下载
源码下载
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)