网络聊天程序设计(文字+语音)
掌握基于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窗口聊天程序是一个质的飞越。在这过程中也遇到了很多困难,比如字符转换、设置参数的传送、系统时间的获取、语音的录制与播放以及进入退出聊天室的提示……不过这些问题都基本被一一克服了,最后终于开发出了一个有模有样的聊天室程序还是挺有成就感的。
程序基本功能已经实现,并且已经可以进行局域网内多人实时在线聊天,但是也有一些不足之处:如语音聊天的回声问题一直没能得到很好的解决,还有既然实现了局域网内的聊天,能不能扩展到广域网呢?这些都是还需要继续思考的问题!