網路程式設計(三)---- MFC 仿QQ聊天軟體
在應用程式開始的時候,我們先應該初始話winSock 庫,所以便會用到下面的一個函式。
BOOL AfxSocketInit( WSADATA* lpwsaData = NULL ); //用來初始化Socket,用WSAStartup();來初始化,在應用程式結束時他會自動呼叫WSACleanup()
我們在開始程式設計之前,應該呼叫這個函式,對Socket進行初始化。如果初始化成功返回非0 ,否則返回0.
可能人會問,這個函式載入的是那個版本的Socket庫呢?通過檢視底層程式碼,我們發現,他載入的是1.1版本的Socket
注意:這個函式只能在你自己應用程式的 CXXWinApp::InitInstance 中初始化.在初始化前還要記得加入標頭檔案Afxsock.h
我伺服器端程式 為 NetChatServer 所以我在的CNetChatServerApp::InitInstance()中加入
/////////////////////////////////////////////////////////////////////////////////////////////////////CNetChatServerApp::InitInstance()///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
if(!AfxSocketInit())
{
AfxMessageBox(_T("Socket 庫初始化出錯!"));
return false;
}
m_iSocket 是一個 CServerSocket*的 指標 ,CServerSocket類是一個我們自己的類我會在後面給出相應程式碼,他繼承於CSocket類。
m_iSocket = new CServerSocket(); // 1.動態建立一個伺服器Socket物件。
if(!m_iSocket)
{
AfxMessageBox(_T("動態建立伺服器套接字出錯!"));
return false;
}
接著建立套接字
if(!m_iSocket->Create(8989))
{
AfxMessageBox(_T("建立套接字錯誤!"));
m_iSocket->Close();
return false;
}
其中8989 是指定的埠號,但是要注意在儲存我們指定的8989埠前,這個埠是空閒的沒有被其他程序所佔用,那怎麼檢視埠是否被其他程序佔用呢?
首先開啟cmd 鍵入 netstat -aon
你會看到所有的TCP/UDP 資訊 ,但是由於太多了不好檢視,所以。我們再在最下面 tasklist|find “8989”
現在我們看到 我們沒有找到任何 和8989埠相關的東西,所以說明8989埠沒有被佔用。
建立了套接字以後按照win32的步驟我們就應該 對bind埠。
但是MFC 不這樣,應為MFC的Create內部已經呼叫了bind ,如下是MFC的底層程式碼
BOOL CAsyncSocket::Create(UINT nSocketPort, int nSocketType,long lEvent, LPCTSTR lpszSocketAddress)
{
if (Socket(nSocketType, lEvent))
{
if (Bind(nSocketPort,lpszSocketAddress))//呼叫了bind
return TRUE;
int nResult = GetLastError();
Close();
WSASetLastError(nResult);
}
return FALSE;
}
所以 我們不用在呼叫bind 了,直接對套接字進行監聽
if(!m_iSocket->Listen())
{
AfxMessageBox(_T("監聽失敗!"));
m_iSocket->Close();
return false;
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
然後過載ExitInstance,退出時對進行清理
int CNetChatServerApp::ExitInstance()
{
if(m_iSocket)
{
delete m_iSocket;
m_iSocket = NULL;
}
return CWinApp::ExitInstance();
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
下面 來看下CServerSocket的具體實現
#pragma once
#include "ClientSocket.h"
class CServerSocket : public CSocket
{
public:
CServerSocket();
virtual ~CServerSocket();
public :
CPtrList m_listSockets;//用來儲存伺服器與所有客戶端連線成功後的ClientSocket
public :
virtual void OnAccept(int nErrorCode);
};
#include "stdafx.h"
#include "NetChatServer.h"
#include "ServerSocket.h"
CServerSocket::CServerSocket()
{
}
CServerSocket::~CServerSocket()
{
}
void CServerSocket::OnAccept(int nErrorCode)
{
//接受到一個連線請求
CClientSocket* theClientSock(0);
theClientSock = new CClientSocket(&m_listSockets);
if(!theClientSock)
{
AfxMessageBox(_T("記憶體不足,客戶連線伺服器失敗!"));
return;
}
Accept(*theClientSock);
//加入list中便於管理
m_listSockets.AddTail(theClientSock);
CSocket::OnAccept(nErrorCode);
}
我們可以看到在CServerSocket中 又出現了一個CClientSocket的類,這個類和CServerSocket一樣,也是派生於CSocket類,但是專門用於客戶端的Socket。
在這裡必須過載OnAccept(int nErrorCode)函式,這樣CServerSocket才能接收到客戶端的請求,並且必須在OnAccept中呼叫Accept()函式對連線請求進行響應。
在OnAccept()我們用一個List 將ClientSocket指標儲存,以便以後呼叫訪問。
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
接著 我們再來看看CClientSocket類
#pragma once
#include "stdafx.h"
/////////////////////////////////////////////////
///說明,該類用於和客戶端建立通訊的Socket
/////////////////////////////////////////////////
class CClientSocket : public CSocket
{
public:
CClientSocket(CPtrList* pList);
virtual ~CClientSocket();
public:
CPtrList* m_pList;//儲存伺服器ClientSocket中List的東西,這個是中CServerSocket中傳過來的
CString m_strName; //連線名稱
public:
virtual void OnClose(int nErrorCode);
virtual void OnReceive(int nErrorCode);
void OnLogoIN(char* buff,int nlen);//處理登入訊息
void OnMSGTranslate(char* buff,int nlen);//轉發訊息給其他聊天群
CString UpdateServerLog();//伺服器端更新、記錄日誌
void UpdateAllUser(CString strUserInfo);//更新伺服器端的線上人員列表
private:
BOOL WChar2MByte(LPCWSTR srcBuff, LPSTR destBuff, int nlen);//多位元組的轉換
};
可以看到 我們過載了OnClose()、OnReceive()函式,這樣當套接字關閉、有資料到達時,就會自動呼叫這兩個函式,我們便可以在這兩個函式中響應、處理事件。
由於本人使用的是VS2010,並且採用的Unicode編碼,所以,經常要涉及Unicode轉多位元組的情況,於是就寫了WChar2MByte()進行轉換
#include "stdafx.h"
#include "NetChatServer.h"
#include "ClientSocket.h"
#include "Header.h"
#include "NetChatServerDlg.h"
CClientSocket::CClientSocket(CPtrList* pList)
:m_pList(pList),m_strName(_T(""))
{
}
CClientSocket::~CClientSocket()
{
}
/////////////////////////////////////////////////////////////////////
void CClientSocket::OnReceive(int nErrorCode)
{
//有訊息接收
//先得到資訊頭
HEADER head;
int nlen = sizeof HEADER;
char *pHead = NULL;
pHead = new char[nlen];
if(!pHead)
{
TRACE0("CClientSocket::OnReceive 記憶體不足!");
return;
}
memset(pHead,0, sizeof(char)*nlen );
Receive(pHead,nlen);
head.type = ((LPHEADER)pHead)->type;
head.nContentLen = ((LPHEADER)pHead)->nContentLen;
delete pHead;
pHead = NULL;
//再次接收,這次是資料類容
pHead = new char[head.nContentLen];
if(!pHead)
{
TRACE0("CClientSocket::OnRecive 記憶體不足!");
return;
}
if( Receive(pHead, head.nContentLen)!=head.nContentLen)
{
AfxMessageBox(_T("接收資料有誤!"));
delete pHead;
return;
}
////////////根據訊息型別,處理資料////////////////////
switch(head.type)
{
case MSG_LOGOIN:
OnLogoIN(pHead, head.nContentLen);
break;
case MSG_SEND:
OnMSGTranslate(pHead, head.nContentLen);
break;
default : break;
}
delete pHead;
CSocket::OnReceive(nErrorCode);
}
//關閉連線
void CClientSocket::OnClose(int nErrorCode)
{
CTime time;
time = CTime::GetCurrentTime();
CString strTime = time.Format("%Y-%m-%d %H:%M:%S ");
strTime = strTime + this->m_strName + _T(" 離開...\r\n");
((CNetChatServerDlg*)theApp.GetMainWnd())->DisplayLog(strTime);
m_pList->RemoveAt(m_pList->Find(this));
//更改伺服器線上名單
CString str1 = this->UpdateServerLog();
//通知客戶端重新整理線上名單
this->UpdateAllUser(str1);
this->Close();
//銷燬該套接字
delete this;
CSocket::OnClose(nErrorCode);
}
//登入
void CClientSocket::OnLogoIN(char* buff, int nlen)
{
//對得接收到的使用者資訊進行驗證
//... (為了簡化這步省略)
//登入成功
CTime time;
time = CTime::GetCurrentTime();
CString strTime = time.Format("%Y-%m-%d %H:%M:%S ");
CString strTemp(buff);
strTime = strTime + strTemp + _T(" 登入...\r\n");
//記錄日誌
((CNetChatServerDlg*)theApp.GetMainWnd())->DisplayLog(strTime);
m_strName = strTemp;
//更新服務列表
CString str1 = this->UpdateServerLog();
//更新線上所有客服端
this->UpdateAllUser(str1);
}
//轉發訊息
void CClientSocket::OnMSGTranslate(char* buff, int nlen)
{
HEADER head;
head.type = MSG_SEND;
head.nContentLen = nlen;
POSITION ps = m_pList->GetHeadPosition();
while(ps!=NULL)
{
CClientSocket* pTemp = (CClientSocket*)m_pList->GetNext(ps);
pTemp->Send(&head,sizeof(HEADER));
pTemp->Send(buff, nlen);
}
}
BOOL CClientSocket::WChar2MByte(LPCWSTR srcBuff, LPSTR destBuff, int nlen)
{
int n = 0;
n = WideCharToMultiByte(CP_OEMCP,0, srcBuff, -1, destBuff,0, 0, FALSE );
if(n<nlen)
return FALSE;
WideCharToMultiByte(CP_OEMCP, 0, srcBuff, -1, destBuff, nlen, 0, FALSE);
return TRUE;
}
//跟新所有線上使用者
void CClientSocket::UpdateAllUser(CString strUserInfo)
{
HEADER _head;
_head.type = MSG_UPDATE;
_head.nContentLen = strUserInfo.GetLength()+1;
char *pSend = new char[_head.nContentLen];
memset(pSend, 0, _head.nContentLen*sizeof(char));
if( !WChar2MByte(strUserInfo.GetBuffer(0), pSend, _head.nContentLen))
{
AfxMessageBox(_T("字元轉換失敗"));
delete pSend;
return;
}
POSITION ps = m_pList->GetHeadPosition();
while(ps!=NULL)
{
CClientSocket* pTemp = (CClientSocket*)m_pList->GetNext(ps);
//傳送協議頭
pTemp->Send((char*)&_head, sizeof(_head));
pTemp->Send(pSend,_head.nContentLen );
}
delete pSend;
}
//跟新伺服器線上名單
// 返回線上使用者列表的String
CString CClientSocket::UpdateServerLog()
{
CString strUserInfo = _T("");
POSITION ps = m_pList->GetHeadPosition();
while(ps!=NULL)
{
CClientSocket* pTemp = (CClientSocket*)m_pList->GetNext(ps);
strUserInfo += pTemp->m_strName + _T("#");
}
((CNetChatServerDlg*)theApp.GetMainWnd())->UpdateUserInfo(strUserInfo);
return strUserInfo;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
在上面的程式碼中 還涉及到一個HEADER struct 這是一個我們自定義的一個頭結構,相當於自定義的一個協議,不過這個很簡化。在這個協議裡我們要指定我們本次要傳送資料
的type,既是我們傳送的那種訊息的資料。還有資料的長度。為了不浪費空間,我們選擇2次傳送。每次給伺服器發資料時都 先發送一個協議頭,然後再發送資料本身。
其實也可以既不浪費空間,也只發送一次。但是那就在傳送之前,對資料進行序列化。在接收端接收到資料後又反序列化。但是C++中並沒有提供相應的方法,所以我們要麼自己寫,要麼用第三方的庫類。但是這種方法代價比,我們分兩次傳送代價高得多,所以為了方便我們就分2次傳送。
//////////////////////////////////////////////////////////////////////////////////////////////////////////////
////定義協議頭 因為直接要傳輸的類容中有不確定長的的類容
///為了避免浪費空間選擇分兩部分傳輸,故定義一個頭
////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma once
////////////自定義協議///////////////////
const int MSG_LOGOIN = 0x01; //登入
const int MSG_SEND = 0x11; //傳送訊息
const int MSG_CLOSE = 0x02; //退出
const int MSG_UPDATE = 0x21; //更新資訊
#pragma pack(push,1)
typedef struct tagHeader{
int type ;//協議型別
int nContentLen; //將要傳送內容的長度
}HEADER ,*LPHEADER;
#pragma pack(pop)
這裡面涉及了一個位元組對齊的知識,請檢視 http://blog.csdn.net/lh844386434/article/details/6680549
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
到這裡基本伺服器端基本有關傳送的框架全部搭建完畢,剩下的就是一些介面程式設計,比如什麼顯示之類的工作,在這裡我就不貼這些程式碼,但是呢我會在最後給出整個工程的下載地址,下面我們就簡單看看客戶端的程式碼
////////////////////////////////////////////////////////客戶端//////////////////////////////////////////////////////////////
客戶端相對來說要簡單的多,他只涉及一個CClientSocket,但是呢,這個類並不是和伺服器端那個一樣的,只是名字相同而已。
首先還是要初始化socket庫 不多說。位置和新增方法和客戶端一樣、接著建立客戶端的套接字、然後連線伺服器。
if(!AfxSocketInit())
{
AfxMessageBox(_T("初始化Socket庫失敗!"));
return false;
}
m_pSocket = new CClientSocket();
if(!m_pSocket)
{
AfxMessageBox(_T("記憶體不足!"));
return false;
}
if(!m_pSocket->Create())
{
AfxMessageBox(_T("建立套接字失敗!"));
return false;
}
CLogoInDlg* pLogoinDlg;//登入對話方塊
pLogoinDlg = new CLogoInDlg();
if(pLogoinDlg->DoModal()==IDOK)//這裡其實是點選了推出的按鈕,只是ID我用的是IDOK的,沒有修改
{
//不登入
delete pLogoinDlg;
m_pSocket->Close();
return false;
}
else
{
delete pLogoinDlg;
}
(上面還有一個CLogoInDlg類,那是一個登入對話方塊的類,在後面會給出他的部分程式碼。)
接著和伺服器端一樣,過載ExitInstance();
int CNetChatClientApp::ExitInstance()
{
if(m_pSocket)
{
delete m_pSocket;
m_pSocket = NULL;
}
return CWinApp::ExitInstance();
}
CClientSocket* CNetChatClientApp::GetMainSocket() const
{
return m_pSocket;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
然後 看看客戶端的CClientSocket的實現
#pragma once
class CClientSocket : public CSocket
{
public:
CClientSocket();
virtual ~CClientSocket();
public:
virtual void OnReceive(int nErrorCode);//客戶端接收訊息
BOOL SendMSG(LPSTR lpBuff, int nlen);//客戶端傳送訊息
BOOL LogoIn(LPSTR lpBuff, int nlen);//客戶端登入
CString m_strUserName;//使用者姓名
};
顯然我們必須過載OnReceive函式,來處理接收到的資料,其他函式是一些事件處理函式,和說明一樣
#include "stdafx.h"
#include "NetChatClient.h"
#include "ClientSocket.h"
#include "Header.h"
#include "NetChatClientDlg.h"
// CClientSocket
CClientSocket::CClientSocket()
:m_strUserName(_T(""))
{
}
CClientSocket::~CClientSocket()
{
}
void CClientSocket::OnReceive(int nErrorCode)
{
//首先接受head頭
HEADER head ;
char* pHead = NULL;
pHead = new char[sizeof(head)];
memset(pHead, 0, sizeof(head));
Receive(pHead, sizeof(head));
head.type =((LPHEADER)pHead)->type;
head.nContentLen = ((LPHEADER)pHead)->nContentLen;
delete pHead;
pHead = NULL;
char* pBuff = NULL;
pBuff = new char[head.nContentLen];
if(!pBuff)
{
AfxMessageBox(_T("記憶體不足!"));
return;
}
memset(pBuff, 0 , sizeof(char)*head.nContentLen);
if(head.nContentLen!=Receive(pBuff, head.nContentLen))
{
AfxMessageBox(_T("收到資料有誤!"));
delete pBuff;
return;
}
CString strText(pBuff);
switch(head.type)
{
case MSG_UPDATE:
{
CString strText(pBuff);
((CNetChatClientDlg*)(AfxGetApp()->GetMainWnd()))->UpdateUserInfo(strText);
}
break;
case MSG_SEND:
{
//顯示接收到的訊息
CString str(pBuff);
((CNetChatClientDlg*)(AfxGetApp()->GetMainWnd()))->UpdateText(str);
break;
}
default: break;
}
delete pBuff;
CSocket::OnReceive(nErrorCode);
}
BOOL CClientSocket::SendMSG(LPSTR lpBuff, int nlen)
{
//生成協議頭
HEADER head;
head.type = MSG_SEND;
head.nContentLen = nlen;
if(Send(&head, sizeof(HEADER))==SOCKET_ERROR)
{
AfxMessageBox(_T("傳送錯誤!"));
return FALSE;
};
if(Send(lpBuff, nlen)==SOCKET_ERROR)
{
AfxMessageBox(_T("傳送錯誤!"));
return FALSE;
};
return TRUE;
}
BOOL CClientSocket::LogoIn(LPSTR lpBuff, int nlen)
{
HEADER _head;
_head.type = MSG_LOGOIN;
_head.nContentLen = nlen;
int _nSnd= 0;
if((_nSnd = Send((char*)&_head, sizeof(_head)))==SOCKET_ERROR)
return false;
if((_nSnd = Send(lpBuff, nlen))==SOCKET_ERROR)
return false;
return TRUE;
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
最後來看看 登入、和傳送訊息的部分程式碼,也是和伺服器端一樣,分成兩步分發送,先發協議頭,再發內容
void CLogoInDlg::OnBnClickedBtnLogoin()
{
//登入
UpdateData();
if(m_strUser.IsEmpty())
{
AfxMessageBox(_T("使用者名稱不能為空!"));
return;
}
if(m_dwIP==0)
{
AfxMessageBox(_T("無效IP地址"));
return;
}
CClientSocket* pSock = theApp.GetMainSocket();
IN_ADDR addr ;
addr.S_un.S_addr = htonl(m_dwIP);
CString strIP(inet_ntoa(addr));
if(!pSock->Connect(strIP.GetBuffer(0),8989))
{
AfxMessageBox(_T("連線伺服器失敗!"));
return ;
}
//傳送
pSock->m_strUserName = m_strUser;
char* pBuff = new char[m_strUser.GetLength()+1];
memset(pBuff, 0, m_strUser.GetLength());
if(WChar2MByte(m_strUser.GetBuffer(0), pBuff, m_strUser.GetLength()+1))
pSock->LogoIn(pBuff, m_strUser.GetLength()+1);
delete pBuff;
CDialogEx::OnCancel();
}
void CLogoInDlg::OnBnClickedOk()
{
//退出
CClientSocket* pSock = theApp.GetMainSocket();
pSock->Close();
CDialogEx::OnOK();
}
///訊息傳送
void CNetChatClientDlg::OnBnClickedBtnSend()
{
//傳送訊息
UpdateData();
if(m_strSend.IsEmpty())
{
AfxMessageBox(_T("傳送類容不能為空!"));
return ;
}
CString temp ;
CTime time = CTime::GetCurrentTime();
temp = time.Format("%H:%M:%S");
//姓名 +_T("\n\t") 時間
m_strSend = theApp.GetMainSocket()->m_strUserName+_T(" ") + temp +_T("\r\n ") + m_strSend +_T("\r\n");
char* pBuff = new char[m_strSend.GetLength()*2];
memset(pBuff, 0, m_strSend.GetLength()*2);
//轉換為多位元組
WChar2MByte(m_strSend.GetBuffer(0), pBuff, m_strSend.GetLength()*2);
//
theApp.GetMainSocket()->SendMSG(pBuff, m_strSend.GetLength()*2);
delete pBuff;
m_strSend.Empty();
UpdateData(0);
}
void CNetChatClientDlg::UpdateUserInfo(CString strInfo)
{
CString strTmp;
CListBox* pBox = (CListBox*)GetDlgItem(IDC_LB_ONLINE);
pBox->ResetContent();
while(!strInfo.IsEmpty())
{
int n = strInfo.Find('#');
if(n==-1)
break;
strTmp = strInfo.Left(n);
pBox->AddString(strTmp);
strInfo = strInfo.Right(strInfo.GetLength()-n-1);
}
}
void CNetChatClientDlg::UpdateText(CString &strText)
{
((CEdit*)GetDlgItem(IDC_ET_TEXT))->ReplaceSel(strText);
}
/////////////////////////////////////////以上都是部分程式碼,我會在後面給出工程下載地址//////////////////////////////////////////
結束語: 我們簡單的過了一下windows的網路程式設計,由於本人水平有限,又是剛開始學著寫部落格,所以其中錯誤難免。請大家見諒。其實上面的程式碼只是實現了,群聊天室。
並沒有實現1對1的類似於QQ那種聊天,但是做到這一步,要實現QQ那種聊天那種應該很簡單了,加一點程式碼就可以了。還有 由於最近時間比較緊我沒有去寫介面。那樣的話又會新增更多的程式碼,但是我會在VC++重溫筆記中,重溫介面程式設計時,實現一個QQ2011 裡面那種介面效果,在這裡就不花時間了。
截圖
登入:
伺服器記錄日誌:
兩個使用者聊天:
說明:本程式在vs2010+win7 X64中通過
工程下載地址:http://download.csdn.net/source/3513082