掌握基於SOCKET接口的各種網絡API函數的功能與調用方法,掌握基於TCP協議的網絡程序設計的方法,掌握網絡字節數據與主機字節數據之間的轉換。掌握Windows系統下字符的轉換處理,實現一個文本聊天程序,了解語音聊天的實現技術。
本程序采用win32對話框作為主窗口的界面設計,采用面向鏈接的Csocket套接字作為局域網內的數據傳輸的載體。語音聊天部分使用動態鏈接庫sound.dll來實現本地語音的錄制與播放。從而完成一個具有局域網內多人在線文字與語音連天功能的聊天室程序。
1.程序界面設計
為使設計界面時方便的使用控件,我用對話框窗口作為本程序的主窗口和設置窗口,這樣可以非常方便的拖用控件。
VC6 控件工具欄
界面設計如下:
(1)程序主窗口:
程序主界面
(2)程序設置窗口:
程序設置界面
(3)程序圖標
程序圖標
2.程序基本功能的實現
(1)連接初始化
void CExample2_ChatRoomDlg::OnTextchat()
{
extern bool IsClient;
extern CString ip;
extern CString Set_nicname;
extern int port;
extern bool IsSetted;
CString portStr;
if(IsSetted==false){
AfxMessageBox("請先進行相關參數設置!");
GlobalSetting SettingDlg;
SettingDlg.DoModal();
}
else
{
portStr.Format("%d",port);
// TODO: Add your control notification handler code here
if(IsClient==true)
{
if(!m_bInit)
{
//BYTE f0,f1,f2,f3;
//CString name;
//((CIPAddressCtrl *)(GetDlgItem(IDC_IPADDRESS)))->GetAddress(f0,f1,f2,f3);
//ip.Format("%d.%d.%d.%d",f0,f1,f2,f3);
m_bClient=true;
m_clientsocket.Create();
if(m_clientsocket.Connect(ip,port))
{
m_clientsocket.Init(this);
SetWindowText("局域網聊天程序-客戶端:"+Set_nicname);
SetDlgItemText(IDC_SHOWTEXT,"客戶端連接成功!");
SetDlgItemText(IDC_STATUS,"客戶端| 當前連接的服務器:"+ip+ "|端口:" + portStr +"\r\n"+"昵稱:"+Set_nicname);
m_bInit=true;
}
else
{
m_clientsocket.Close();
AfxMessageBox("客戶端連接失敗!");
m_bInit=false;
}
}
}
else
{
if(!m_bInit)
{
m_bClient=false;
m_bInit=true;
SetWindowText("局域網聊天程序-服務器:"+Set_nicname);
SetDlgItemText(IDC_STATUS,"服務器|當前IP:"+ ip +"|端口:" + portStr +"\r\n" + "昵稱:"+Set_nicname);
//GetDlgItem(IDC_TEXTCHAT)->ModifyStyle(0,WS_DISABLED);
//GetDlgItem(IDC_SETTING)->ModifyStyle(0,WS_DISABLED);
if(m_pListenSocket.Init(port,this)==FALSE)
{
m_bInit=false;
return;
}
}
}
GetDlgItem(IDC_TEXTCHAT)->EnableWindow(FALSE);
GetDlgItem(IDC_SETTING)->EnableWindow(FALSE);
}
}
(2)待發送文字准備
獲取文本框中文字做相應的處理后賦值給msg.m_strText
int m_iLineCurrentPos=((CEdit *)(GetDlgItem(IDC_SHOWTEXT)))->GetLineCount();
((CEdit *)(GetDlgItem(IDC_SHOWTEXT)))->LineScroll(m_iLineCurrentPos);
msg.m_strText=nicname+" "+nowtime+"\r\n"+in+" \r\n";
將文本數據傳送給套接字子程序,由套接字發送
if(!m_bClient)
{
POSITION pos;
for(pos=m_connectionList.GetHeadPosition();pos!=NULL;)
{
CClientSocket * t= (CClientSocket *)m_connectionList.GetNext(pos);
t->SendMessage(&msg);
}
}
else
{
m_clientsocket.SendMessage(&msg);
}
(2)語音部分
初始化套接字
void CExample2_ChatRoomDlg::OnSound()
{
// TODO: Add your control notification handler code here
if(m_bInit==false)
{
AfxMessageBox("檢查網絡連接!");
return;
}
static BOOL issend=TRUE;
CString ip;
BYTE f0,f1,f2,f3;
((CIPAddressCtrl *)(GetDlgItem(IDC_IPADDRESS)))->GetAddress(f0,f1,f2,f3);
ip.Format("%d.%d.%d.%d",f0,f1,f2,f3);
int port=GetDlgItemInt(IDC_PORT);
typedef long _stdcall SETIP(char *);
typedef void _stdcall SETPORT(int);
typedef void _stdcall STARTSOUND();
typedef void _stdcall STOPSOUND();
static HINSTANCE sound=LoadLibrary("../Sound/Sound.dll");
if(issend)
{
if(sound!=NULL)
{
SETIP *setip=(SETIP*)GetProcAddress(sound,"setIpAddr");
SETPORT *setport=(SETPORT*)GetProcAddress(sound,"setPort");
STARTSOUND *start=(STARTSOUND*)GetProcAddress(sound,"SoundStart");
setport(port);
if(setip(ip.GetBuffer(0)))
{
start();
SetDlgItemText(IDC_SOUND,"停止語音");
issend=FALSE;
ip.ReleaseBuffer();
}
else
{
AfxMessageBox("不能連接到服務器,檢查網絡環境與設置!");
FreeLibrary(sound);
return;
}
}
else
{
AfxMessageBox("sound.dll組件加載失敗");
}
}
else
{
if(sound!=NULL)
{
STOPSOUND *stop= (STOPSOUND*)GetProcAddress(sound,"SoundStop");
stop();
FreeLibrary(sound);
}
SetDlgItemText(IDC_SOUND,"語音聊天");
issend=TRUE;
}
}
錄制聲音
void CExample2_ChatRoomDlg::OnNewsend()
{
// TODO: Add your control notification handler code here
if(m_willchating==TRUE)
{
m_sound.Init(this);
m_sound.Record();
SetDlgItemText(IDC_NEWSEND,"停止語音");
m_willchating=FALSE;
}
else
{
CSingleLock lock(&m_mutex,TRUE);
m_sound.StopRecord();
SetDlgItemText(IDC_NEWSEND,"語音聊天");
m_willchating=TRUE;
lock.Unlock();
}
}
播放聲音
void CExample2_ChatRoomDlg::WriteBufferFull(LPARAM lp,WPARAM wp)
{
m_sound.Play();//發出本地聲音
CSingleLock lock(&m_mutex,TRUE);
CMessg msg;
msg.m_strText="";
msg.m_tag=1;
memcpy(msg.m_buffer,m_sound.m_cBufferIn,MAX_BUFFER_SIZE);
if(!m_bClient)
{
POSITION pos;
for(pos=m_connectionList.GetHeadPosition();pos!=NULL;)
{
CClientSocket * t= (CClientSocket *)m_connectionList.GetNext(pos);
t->SendMessage(&msg);
}
}
else
{
m_clientsocket.SendMessage(&msg);
}
m_sound.FreeRecordBuffer();
m_sound.FreePlayBuffer();
lock.Unlock();
}
3.網絡傳輸功能的實現
(1)服務器
ServerSocket.cpp主要代碼如下:
BOOL CServerSocket::Init(UINT port, CExample2_ChatRoomDlg* dlg)
{
m_uPort=port;
m_dlg=dlg;
if(Create(m_uPort)==FALSE)
{
AfxMessageBox("服務器套接字創建失敗……");
return FALSE;
}
if(this->Listen()==FALSE)
{
AfxMessageBox("服務器監聽錯誤");
return FALSE;
}
portStr.Format("%d",port);
m_dlg->SetDlgItemText(IDC_SHOWTEXT,"服務器建立成功!\r\n服務器IP地址:"+ip+" 端口:"+portStr+"\r\n請在客戶端中填入該服務器的ip和端口,然后就開始盡情聊天吧!");
return TRUE;
}
void CServerSocket::OnAccept(int nErrorCode)
{
// TODO: Add your specialized code here and/or call the base class
m_dlg->ProcessPendingAccept();
CSocket::OnAccept(nErrorCode);
}
(2)客戶端
ClientSocket.cpp主要代碼如下:
CClientSocket::CClientSocket()
{
m_aSessionIn=NULL;
m_aSessionOut=NULL;
m_sfSocketFile=NULL;
m_bInit=false;
m_bClose=false;
}
CClientSocket::~CClientSocket()
{
if(m_aSessionIn)
delete m_aSessionIn;
if(m_aSessionOut)
delete m_aSessionOut;
if(m_sfSocketFile)
delete m_sfSocketFile;
}
// Do not edit the following lines, which are needed by ClassWizard.
#if 0
BEGIN_MESSAGE_MAP(CClientSocket, CSocket)
//{{AFX_MSG_MAP(CClientSocket)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
#endif // 0
/////////////////////////////////////////////////////////////////////////////
// CClientSocket member functions
void CClientSocket::OnReceive(int nErrorCode)
{
// TODO: Add your specialized code here and/or call the base class
CSocket::OnReceive(nErrorCode);
do
{
CMessg temp;
temp.Serialize(*m_aSessionIn);
m_dlg->m_sMsgList+=temp.m_strText;
m_dlg->SetDlgItemText(IDC_SHOWTEXT,m_dlg->m_sMsgList);
if(temp.m_tag==1&&m_dlg->m_willchating==FALSE)
//如果有聲音過來並且本機的聲音設備已經准備好了則首先在本機發出聲音
{
memcpy(m_dlg->m_sound.m_cBufferOut,temp.m_buffer,MAX_BUFFER_SIZE);
}
int linenum=((CEdit *)(m_dlg->GetDlgItem(IDC_SHOWTEXT)))->GetLineCount();
((CEdit *)(m_dlg->GetDlgItem(IDC_SHOWTEXT)))->LineScroll(linenum);
if(!m_dlg->m_bClient)
{
for(POSITION pos=m_dlg->m_connectionList.GetHeadPosition();pos!=NULL;)
{
CClientSocket * t = (CClientSocket*)m_dlg->m_connectionList.GetNext(pos);
if(t->m_hSocket!=this->m_hSocket)
{
t->SendMessage(&temp);
}
}
}
}
while (!m_aSessionIn->IsBufferEmpty());
}
void CClientSocket::Init(CExample2_ChatRoomDlg * dlg)
{
m_sfSocketFile= new CSocketFile(this);
m_aSessionIn=new CArchive(m_sfSocketFile,CArchive::load);
m_aSessionOut=new CArchive(m_sfSocketFile,CArchive::store);
m_bClose=false;
this->m_dlg=dlg;
}
BOOL CClientSocket::SendMessage(CMessg * msg)
{
if (m_aSessionOut != NULL)
{
msg->Serialize(*m_aSessionOut);
m_aSessionOut->Flush();
return TRUE;
}
else
{
//對方關閉了連接
m_bClose=true;
CloseSocket();
m_dlg->CloseSessionSocket();
return FALSE;
}
}
void CClientSocket::CloseSocket()
{
if(m_aSessionIn)
{
delete m_aSessionIn;
m_aSessionIn=NULL;
}
if(m_aSessionOut)
{
delete m_aSessionOut;
m_aSessionOut=NULL;
}
if(m_sfSocketFile)
{
delete m_aSessionOut;
m_sfSocketFile=NULL;
}
Close();
m_bInit=false;
m_bClose=true;
}
void CClientSocket::OnClose(int nErrorCode)
{
// TODO: Add your specialized code here and/or call the base class
m_bClose=true;
CloseSocket();
m_dlg->CloseSessionSocket();
CSocket::OnClose(nErrorCode);
}
int CClientSocket::GetLocalHostName(CString &sHostName) //獲得本地計算機名稱
{
char szHostName[256];
int nRetCode;
nRetCode=gethostname(szHostName,sizeof(szHostName));
if(nRetCode!=0)
{
//產生錯誤
sHostName=_T("沒有取得");
return GetLastError();
}
sHostName=szHostName;
return 0;
}
int CClientSocket::GetIpAddress(const CString &sHostName, CString &sIpAddress)//獲得本地IP
{
struct hostent FAR * lpHostEnt=gethostbyname(sHostName);
if(lpHostEnt==NULL)
{
//產生錯誤
sIpAddress=_T("");
return GetLastError();
}
//獲取IP
LPSTR lpAddr=lpHostEnt->h_addr_list[0];
if(lpAddr)
{
struct in_addr inAddr;
memmove(&inAddr,lpAddr,4);
//轉換為標准格式
sIpAddress=inet_ntoa(inAddr);
if(sIpAddress.IsEmpty())
sIpAddress=_T("沒有取得");
}
return 0;
}
int CClientSocket::GetIpAddress(const CString &sHostName, BYTE &f0,BYTE &f1,BYTE &f2,BYTE &f3)//獲得本地IP
{
struct hostent FAR * lpHostEnt=gethostbyname(sHostName);
if(lpHostEnt==NULL)
{
//產生錯誤
f0=f1=f2=f3=0;
return GetLastError();
}
//獲取IP
LPSTR lpAddr=lpHostEnt->h_addr_list[0];
if(lpAddr)
{
struct in_addr inAddr;
memmove(&inAddr,lpAddr,4);
f0=inAddr.S_un.S_un_b.s_b1;
f1=inAddr.S_un.S_un_b.s_b2;
f2=inAddr.S_un.S_un_b.s_b3;
f3=inAddr.S_un.S_un_b.s_b4;
}
return 0;
}
3.程序附加功能
以下功能不是核心功能,目的是讓程序更方便使用。
(1)設置功能
開始聊天之前需要進行相關設置,本程序采用全局變量的方式將服務器IP、端口,聊天昵稱等參數存儲在全局變量中供主程序調用。
全局變量聲明:
//Global.h
extern CString Set_nicname;
extern CString ip;
extern bool IsClient;
extern int port;
extern int count;
extern bool IsSetted;
設置功能主要實現代碼如下:
void GlobalSetting::OnSave()
{
// TODO: Add your control notification handler code here
//extern CString Set_nicname;
if(IsDlgButtonChecked(IDC_RADIO_HOST))
IsClient=false;
else
IsClient=true;
GetDlgItemText(IDC_NICNAMETEXT,Set_nicname);
BYTE f0,f1,f2,f3;
extern CString Set_nicname;
((CIPAddressCtrl *)(GetDlgItem(IDC_IPADDRESS)))->GetAddress(f0,f1,f2,f3);
extern CString ip;
ip.Format("%d.%d.%d.%d",f0,f1,f2,f3);
extern int port;
port=GetDlgItemInt(IDC_PORT);
extern bool IsSetted;
IsSetted = true;
CDialog::OnOK();
}
void GlobalSetting::OnRadioHost()
{
// TODO: Add your control notification handler code here
SetDlgItemText(IDC_TEXT_HOST,"本機IP");
SetDlgItemText(IDC_NICNAMETEXT,"服務器");
BYTE f0,f1,f2,f3;
CString name;
CClientSocket::GetLocalHostName(name);
CClientSocket::GetIpAddress(name,f0,f1,f2,f3);
((CIPAddressCtrl *)(GetDlgItem(IDC_IPADDRESS)))->SetAddress(f0,f1,f2,f3);
//GetDlgItem(IDC_IPADDRESS)->ModifyStyle(0,WS_DISABLED);
GetDlgItem(IDC_IPADDRESS)->EnableWindow(FALSE);
}
void GlobalSetting::OnRadioClient()
{
// TODO: Add your control notification handler code here
SetDlgItemText(IDC_TEXT_HOST,"輸入服務器的IP");
//GetDlgItem(IDC_IPADDRESS)->ModifyStyle(WS_DISABLED,0);
GetDlgItem(IDC_IPADDRESS)->EnableWindow(TRUE);
SetDlgItemText(IDC_NICNAMETEXT,"客戶端");
}
(2)消息時間功能
每條消息中包含發送此消息的時間。使用getSystemTime()獲取系統時間,具體代碼如下
//本函數用於獲取當前系統的時間,使用前請傳入獲取時間的string的引用
void getSystemTime(CString & stime){
time_t t = time( 0 );
char tmp[64];
strftime( tmp, sizeof(tmp), "%Y/%m/%d %X ",localtime(&t) );
stime=tmp;
}
然后加入到消息內容中發送出去,接收方接受之后進行相應的處理之后顯示出來,從而達到顯示消息時間的目的
(3)當前運行狀態顯示
獲取當前狀態並顯示在主窗口的底部及標題欄上。主要通過獲取當前全局變量的值來實現,具體代碼如下:
SetWindowText("局域網聊天程序-客戶端:"+Set_nicname);
SetDlgItemText(IDC_STATUS,"客戶端| 當前連接的服務器:"+ip+ "|端口:" + portStr +"\r\n"+"昵稱:"+Set_nicname);
SetWindowText("局域網聊天程序-服務器:"+Set_nicname);
SetDlgItemText(IDC_STATUS,"服務器|當前IP:"+ ip +"|端口:" + portStr +"\r\n" + "昵稱:"+Set_nicname);
(4)回車鍵發送消息功能
一般常見的聊天程序都可以直接使用回車鍵直接發送消息,方便快捷。本程序也設計了回車發送消息的功能,具體實現代碼如下:
void CExample2_ChatRoomDlg::OnInputText()
{
if(IsDlgButtonChecked(IDC_ENTERCHECK))
{
if(!m_bInit)
{
AfxMessageBox("未連接服務器……");
return;
}
CString in;
CString nicname;
CMessg msg;
extern CString Set_nicname;
GetDlgItemText(IDC_INPUTTEXT,in);
nicname = Set_nicname;
getSystemTime(nowtime); //得到系統時間
if(in.GetLength()<1)
{
return;
}
if(in.GetAt(in.GetLength()-1)=='\n') //判斷是否輸入回車,是則發送消息
{
in.TrimRight(" ");
SetDlgItemText(IDC_INPUTTEXT,"");
if(in.GetLength()>2)
{
m_sMsgList+="我("+nicname+")"+nowtime+"\r\n"+in+" \r\n";
SetDlgItemText(IDC_SHOWTEXT,m_sMsgList);
int m_iLineCurrentPos=((CEdit *)(GetDlgItem(IDC_SHOWTEXT)))->GetLineCount();
((CEdit *)(GetDlgItem(IDC_SHOWTEXT)))->LineScroll(m_iLineCurrentPos);
msg.m_strText=nicname+" "+nowtime+"\r\n"+in+" \r\n";
if(!m_bClient)
{
POSITION pos;
for(pos=m_connectionList.GetHeadPosition();pos!=NULL;)
{
CClientSocket * t= (CClientSocket *)m_connectionList.GetNext(pos);
t->SendMessage(&msg);
}
}
else
{
m_clientsocket.SendMessage(&msg);
}
}
}
}//if(IsDlgButtonChecked(IDC_ENTERCHECK))
}
4.程序運行
程序部分運行截圖如下
連接參數設置、服務器建立與狀態顯示
客戶端連接成功與加入聊天室全局提示
多人在線聊天與退出提示
從一開始的使用recv()與send()函數實現windows控制台聊天應用到使用Csocket套接字方式實現win32窗口聊天程序是一個質的飛越。在這過程中也遇到了很多困難,比如字符轉換、設置參數的傳送、系統時間的獲取、語音的錄制與播放以及進入退出聊天室的提示……不過這些問題都基本被一一克服了,最后終於開發出了一個有模有樣的聊天室程序還是挺有成就感的。
程序基本功能已經實現,並且已經可以進行局域網內多人實時在線聊天,但是也有一些不足之處:如語音聊天的回聲問題一直沒能得到很好的解決,還有既然實現了局域網內的聊天,能不能擴展到廣域網呢?這些都是還需要繼續思考的問題!
