002之MFCSocket异步编程
当今的网络程序通用体系结构大多为C/S模式,服务器监听收到来自客户端的请求,然后响应并作出应答。
界面对话框如下,输入IP信息进行通信后再进行连接,连接成功即可开始通信。左侧为客户端,右侧为服务端。
1、创建基于对话框的MFC项目,包含Windows套接字。在工程中创建基于CasyncSocket的类用于通信。
客户端只需要一个进行通信,服务器端需要两个,一个用于监听,一个用于通信(头文件包含在h中与cpp中会有差别)。
1)客户端:新建基于CAsyncSocket的类CClientSocket用于客户端通信;并在CXXXDlg.h中声明创建实例对象m_ClientSocket;
2)服务端:新建基于CAsyncSocket的类CServerSocket用于服务,类CListenSocket用于监听;并在CXXXDlg.h中声明创建实例对象。m_ListenSocket/m_ServerSocket。注意互相在头文件中包含双方的头文件,便于后续使用。
2、在客户端/服务器端界面中编辑完善相应的控件,并增加相应的变量
Tips:部分控件设置Control变量主要是用于实现按钮在不同时期的可用性。
3、客户端实现连接/断开函数,服务器端实现监听与断开事件
1)客户端:发起连接,调用Create函数创建套接字并执行connect建立连接:断开时执行通信断开即可,并在对话框中显示通知。
1 #include "ClientSocket.h" 2 3 void Ccase003Dlg::OnBnClickedBnConnect() 4 { 5 // TODO: 在此添加控件通知处理程序代码 6 // TODO: 在此添加控件通知处理程序代码 7 BYTE nfield[4]; 8 CString strIP; 9 int sPort; 10 UpdateData(); //默认为true,将输入值传入控件,否则将控件变量值输出到文本框中 11 12 if(ServerIP.IsBlank()||m_str_port.IsEmpty())//判断输入变量是否合法 13 { 14 AfxMessageBox("IP地址与端口不能为空"); 15 return ; //如不执行return 则会继续执行赋IP操作 16 } 17 18 //将IP传给地址框 19 sPort=atoi(m_str_port); 20 ServerIP.GetAddress(nfield[0],nfield[1],nfield[2],nfield[3]); 21 strIP.Format("%d.%d.%d.%d",nfield[0],nfield[1],nfield[2],nfield[3]); 22 //初始化套接字创建socket句柄,默认有winsock自动选择端口号,默认为流式套接字 23 //第三个参数用来指定感兴趣的网络事件掩码位,默认全部包含,最后一个是指定套接字的网络地址 24 //成功返回非零,可调用GetLastError获取错误信息,客户端不接收其他客户连接,默认即可 25 m_clientsocket.Create(); 26 // m_clientsocket.Connect(strIP,atoi(m_str_port)); //用于建立连接;第二种结构为sockaddr结构指针,用于winapi 27 m_clientsocket.Connect(strIP,sPort); 28 } 29 30 void Ccase003Dlg::OnBnClickedBnDisconnect() 31 { 32 // TODO: 在此添加控件通知处理程序代码 33 34 m_clientsocket.Close(); //关闭套接字并释放描述符 35 m_listbox.AddString("已断开连接!"); 36 m_listbox.SetTopIndex(m_listbox.GetCount()-1); 37 //权限控制的内容过于重复, 暂时剔除
38 }
2)服务器端: 监听在获取IP地址后,调用Create创建套接字,并侦听连接请求。
1 void Ccase004Dlg::OnBnClickedListen() 2 { 3 // TODO: 在此添加控件通知处理程序代码 4 BYTE nfield[4]; 5 CString strIP; 6 int sPort; 7 UpdateData(); //默认为true,将输入值传入控件,否则将控件变量值输出到文本框中 8 9 if(ServerIP.IsBlank())//判断输入变量是否合法 10 { 11 AfxMessageBox("IP地址与端口不能为空"); 12 return ; //如不执行return 则会继续执行赋IP操作 13 } 14 15 sPort = atoi(m_str_port); 16 //将IP传给地址框 17 ServerIP.GetAddress(nfield[0],nfield[1],nfield[2],nfield[3]); 18 strIP.Format("%d.%d.%d.%d",nfield[0],nfield[1],nfield[2],nfield[3]); 19 //初始化套接字创建socket句柄,默认有winsock自动选择端口号,默认为流式套接字,1 20 //第三个参数用来指定感兴趣的网络事件掩码位,默认全部包含,最后一个是指定套接字的网络地址 21 //成功返回非零,可调用GetLastError获取错误信息,客户端不接收其他客户连接,默认即可 22 m_listensocket.Create(sPort,1,FD_ACCEPT,strIP); 23 24 m_listensocket.Listen(3); //参数用于指定愿意接受客户端数目,必须调用 25 m_listbox.AddString("开始监听"); 26 m_listbox.AddString("地址"+strIP+"端口"+m_str_port); 27 m_listbox.AddString("等待客户连接..."); 28 m_listbox.SetTopIndex(m_listbox.GetCount()-1); //设置为信息往下滚动 29 30 } 31 32 void Ccase004Dlg::OnBnClickedStopListen() 33 { 34 // TODO: 在此添加控件通知处理程序代码 35 m_listensocket.Close(); 36 m_listbox.AddString("停止监听"); 37 m_listbox.SetTopIndex(m_listbox.GetCount()-1); //设置为信息往下滚动 38 }
3)完成其他对应的功能模块:可以看出,除了监听/连接设置了不同的函数,发送信息、接收信息、断开连接、停止监听、清空列表、重新输入都是一样的代码。也就是说除了创建连接时有所差异,其余的代码都可以通用。
1 void Ccase003Dlg::OnBnClickedBnDisconnect() 2 { 3 // TODO: 在此添加控件通知处理程序代码 4 m_clientsocket.Close(); 5 m_listbox.AddString("已断开连接"); 6 m_listbox.SetTopIndex(m_listbox.GetCount()-1); 7 8 } 9 10 void Ccase003Dlg::OnBnClickedBnSend() 11 { 12 // TODO: 在此添加控件通知处理程序代码 13 UpdateData(); //此处可加一个判定,如果输入为空则提示并返回 14 m_clientsocket.Send(m_str_words,m_str_words.GetLength()); 15 m_listbox.AddString("发送: "+m_str_words); //在本端显示发送的内容 16 m_listbox.SetTopIndex(m_listbox.GetCount()-1); //设置为信息往下滚动 17 m_editbox.SetWindowText(""); //注意发送后清空输入内容 18 m_editbox.SetFocus(); //发送后焦点指定在编辑栏 19 } 20 21 void Ccase003Dlg::OnBnClickedBnRewrite() 22 { 23 // TODO: 在此添加控件通知处理程序代码 24 m_editbox.SetWindowText(""); 25 m_editbox.SetFocus(); 26 } 27 28 void Ccase003Dlg::OnBnClickedBnClear() 29 { 30 // TODO: 在此添加控件通知处理程序代码 31 m_listbox.ResetContent(); 32 }
4 、实现网络事件响应函数
在执行相应按钮操作后,系统会根据程序运行自动触发响应。在socket实例对象中重写相应的处理函数。客户端系统发起连接触发connect进行跟进,服务器端系统接收到connect请求触发accept响应,此时建立起连接,通过receive接收程序发送的数据,最后close关闭释放套接字。
1)客户端:客户端发起send在类中重写函数OnConnect
1 void CClientSocket::OnConnect(int nErrorCode) 2 { 3 // TODO: 在此添加专用代码和/或调用基类 4 Ccase003Dlg* plist=(Ccase003Dlg*)(AfxGetApp()->m_pMainWnd); 5 6 if(nErrorCode) 7 { 8 AfxMessageBox("不能连接,请重试"); 9 plist->m_clientsocket.Close(); //如果不关闭套接字,则因为前一次的套接字还未释放,故重按会失败 10 return ; 11 } 12 13 plist->m_listbox.AddString("连接成功!"); 14 plist->m_listbox.SetTopIndex(plist->m_listbox.GetCount()-1); 15 /* 16 plist->m_edit_ip.EnableWindow(FALSE); 17 plist->m_edit_port.EnableWindow(FALSE); 18 plist->m_bn_connect.EnableWindow(FALSE); 19 plist->m_bn_disconnect.EnableWindow(TRUE); 20 plist->m_bn_clear.EnableWindow(TRUE); 21 plist->m_bn_send.EnableWindow(TRUE); 22 plist->m_bn_rewrite.EnableWindow(TRUE); 23 plist->m_editbox.EnableWindow(TRUE); 24 */ 25 CAsyncSocket::OnConnect(nErrorCode); 26 }
2)服务器端:客户端发起在ListenSocket类中重写函数OnAccept。
1 void CListenSocket::OnAccept(int nErrorCode) 2 { 3 // TODO: 在此添加专用代码和/或调用基类 4 Ccase004Dlg* plist=(Ccase004Dlg*)(AfxGetApp()->m_pMainWnd); 5 6 Accept(plist->m_serversocket); //接收连接请求,并取出第一个连接创建套接字用于通信,原始套接字依然保持打开并监听 7 plist->m_serversocket.AsyncSelect(FD_READ|FD_WRITE|FD_CLOSE); //调用侦查 8 9 10 plist->m_listbox.AddString("连接成功!"); 11 plist->m_listbox.SetTopIndex(plist->m_listbox.GetCount()-1); 12 /* //如需设置权限,也可在此处获取对话框指针后进行设置,增强程序的鲁棒性 13 plist->m_edit_ip.EnableWindow(FALSE); 14 plist->m_edit_port.EnableWindow(FALSE); 15 plist->m_bn_listen.EnableWindow(FALSE); 16 plist->m_bn_stoplisten.EnableWindow(TRUE); 17 plist->m_bn_disconnect.EnableWindow(TRUE); 18 plist->m_bn_clear.EnableWindow(TRUE); 19 plist->m_bn_send.EnableWindow(TRUE); 20 plist->m_bn_rewrite.EnableWindow(TRUE); 21 plist->m_editbox.EnableWindow(TRUE); 22 */ 23 CAsyncSocket::OnAccept(nErrorCode); 24 }
3)完成OnReceive(接收)和OnClose(关闭)函数,客户端、监听端与服务器端功能一样。因需要接收信息,还需在ServerSocket类中重写OnConnect(不贴出)。
1 void CClientSocket::OnReceive(int nErrorCode) 2 { 3 // TODO: 在此添加专用代码和/或调用基类 4 char temp[200]; 5 int n=Receive(temp,200); 6 temp[n]='\0'; 7 CString message; 8 message.Format("收到: %s",temp); 9 Ccase003Dlg* plist=(Ccase003Dlg*)(AfxGetApp()->m_pMainWnd); 10 plist->m_listbox.AddString(message); 11 plist->m_listbox.SetTopIndex(plist->m_listbox.GetCount()-1); 12 13 CAsyncSocket::OnReceive(nErrorCode); 14 } 15 16 void CClientSocket::OnClose(int nErrorCode) 17 { 18 // TODO: 在此添加专用代码和/或调用基类 19 Ccase003Dlg* plist=(Ccase003Dlg*)(AfxGetApp()->m_pMainWnd); 20 plist->m_clientsocket.Close(); 21 plist->m_listbox.AddString("关闭连接"); 22 plist->m_listbox.SetTopIndex(plist->m_listbox.GetCount()-1); 23 24 /* plist->m_edit_ip.EnableWindow(TRUE); 25 plist->m_edit_port.EnableWindow(TRUE); 26 plist->m_bn_connect.EnableWindow(TRUE); 27 plist->m_bn_disconnect.EnableWindow(FALSE); 28 plist->m_bn_clear.EnableWindow(TRUE); 29 plist->m_bn_send.EnableWindow(FALSE); 30 plist->m_bn_rewrite.EnableWindow(FALSE); 31 plist->m_editbox.EnableWindow(FALSE); 32 */ 33 CAsyncSocket::OnClose(nErrorCode); 34 }
4、大功告成。可以补充完善相应的优化控制,比如点击连接之前不可以点击断开;连接之后断开应释放对应套接字,否则再点击会崩溃。
5、小结:会者不难,先了解原理,然后再码代码。
1)对于控件控制,初始化窗口时可设置初始状态(最好是全部初始化,这样在函数响应时可以按流程思路进行调整,也可同时设置对话框标题);
2)异步通信:服务器端打开监听(Listen),触发Accept接收请求,客户端发出连接,触发connect发送请求,至此可实现连接。使用发送Send,有信息触发Receive接收显示信息。
优质内容筛选与推荐>>