當今的網絡程序通用體系結構大多為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接收顯示信息。