1. 程式人生 > >網路程式設計(三)---- MFC 仿QQ聊天軟體

網路程式設計(三)---- MFC 仿QQ聊天軟體

今天來八一八,MFC的SOCKET 程式設計,利用CSocket實現一個基於TCP實現一個QQ聊天程式。你會發現,MFC要比WIN32 簡單的多。但是如果你不理解具體API socket基礎知識,你可能會覺得有一點費解。 所以在開始之前 我還是請大家先看看http://blog.csdn.net/lh844386434/article/details/6664025    

在應用程式開始的時候,我們先應該初始話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