学习,c++有2个星期了。本来,本人是做php出身的。做php快2年了,最近身边多了很多高手。让自己对c开始感兴趣了,就开始学习c++了。首先接触的就是mfc。前几天,看到了一个博文,是有关,mfc网络编程的。可对方,的实例只能是多对多,出于兴趣,自己改写了下它的程序,实现了点对点的聊天。所以,本实例并非纯原创的。这个还请大家见谅,尤其是作者。我在他程序基础上,增加了1对1的聊天,同时还保留了群聊。而且,最关键的是,我增加了很多备注。很适合新手学习。。。本人也是新手,还请各位高手提出宝贵建议。。。先谢谢大家了。
如果要转载请注明原地址:http://blog.csdn.net/open520yin/article/details/8222279
实例下载地址:http://download.csdn.net/detail/open520yin/4808903(为了自己能有点下载积分,客户端和服务端一起打包5个积分不算贵吧。。呵呵。。。)
大家要是想看懂这个可能还需要先了解一下mfc的socket的一些基本使用规则我也有一篇博文写了
c++/MFC 极为简单的socket实例:http://blog.csdn.net/open520yin/article/details/8202465
MFC的CSocket编程,利用CSocket实现一个基于TCP实现一个QQ聊天程序。
///////////////////////////////////////////////////////////////////////// 服务端 start ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
先讲讲服务端,一切先从服务端开始:
首先就是要使用AfxSocketInit初始化winsocket,
//初始化winSock库,成功则返回非0否则返回0 WSAData wsData; if(!AfxSocketInit(&wsData)) { AfxMessageBox(_T("Socket 库初始化出错!")); return false; }
m_iSocket 是一个 CServerSocket*的 指针 ,CServerSocket类是一个我们自己的类我会在后面给出相应代码,他继承于CSocket类。
//创建服务器端Socket、采用TCP m_iSocket = new CServerSocket(); if(!m_iSocket) { AfxMessageBox(_T("动态创建服务器套接字出错!")); return false; }
实例socket好了,就要创建套接字了。。这里的端口要和客户端连接的端口一致,不然就连接不上。而且,这个端口,要和服务器的其他软件端口不能冲突,怎么去判断冲突,可以自己谷歌一下,很简单的。我这里就直接写死了,这个端口一般不会被占用的。。
//端口使用8989 if(!m_iSocket->Create(8989)) { AfxMessageBox(_T("创建套接字错误!")); m_iSocket->Close(); return false; }
创建好了就要,开始监听这个端口了。这个是一般的,socket必须建立的几个过程。。
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类,这个是用来,服务端接收消息用的。开启了监听这里的OnAccept()方法就会一直被循环调用。这个方法其实是重写CSocket类的OnAccept()方法。只要socket开启了监听,OnAccept就会被循环调用,我那个简单的socket实例,是开启一个while进行死循环来达到这个目的。大家不要介意,我也是新手。这里OnAccept方法为什么能一直被循环执行,我到现在也没弄明白,如果有高手知道请告诉我下。但是我知道,这里就是如果服务器有收到消息就会调用这里,提示ClientSocket接受消息。
void CServerSocket::OnAccept(int nErrorCode) { //接受到一个连接请求 CClientSocket* theClientSock(0); //初始化在初始化里把m_listSockets赋值到m_pList里 theClientSock = new CClientSocket(&m_listSockets); if(!theClientSock) { AfxMessageBox(_T("内存不足,客户连接服务器失败!")); return; } Accept(*theClientSock); //接受 //加入list中便于管理,这个很关键 m_listSockets.AddTail(theClientSock); CSocket::OnAccept(nErrorCode); }OnAccept收到消息,就会实例CClientSocket类,这里其实主要是,服务端发送消息和接受消息的主要部分。也是服务端,最核心的部分。OnAccept收到消息后,就会通知CClientSocket来接受消息。注意了,CserverSocket是接收消息而CClientSocket是接收消息。接收,接受还是有区别的。这个关系我们要理解清楚。这个是我自己的理解,不时候是否有错误。还请高手赐教。。我学c++最多不过半个月,有些东西,都真是靠自己的理解。下面的接受消息OnReceive方法怎么调用的,我也有点模糊,这个方法好像是重写Socket的。就有数据来,他就会自动调用。m_listSockets.AddTail(theClientSock);这个很关键,m_listSockets是CPtrList类型,我对这个也还不太了解,经过我一些认识,这个是存放socket连接,成功一个就会加入这个,是一个链表。用来存放所有连接到服务器的socket连接的,这个后面会经常用到。
下面的HEADER是一个结构体,定义如下
typedef struct tagHeader{ int type ;//协议类型 int nContentLen; //将要发送内容的长度 char to_user[20]; char from_user[20]; }HEADER ,*LPHEADER;这个结构体,要和客户端保持一致,不然我担心会有问题。就算没有问题,我估计转换也麻烦。尽量保持一直吧,这个也算是一种协议吧。客户端传输的时候,也传递这样的结构体。下面的方法,具体就看看备注吧。我在备注里讲解了。但是注意的是,我们客户端发送消息,是一次发送2个消息,先发送一个头部消息,这个头部消息是一个结构体,是服务端和客户端一种自定义的协议。这样的好处是,能节约资源,并且提前知道内容的长度进行申请内存空间。能先知道对应的消息类型,然后再进行转换和读取。这个头部接受好了,然后再去接受正式的数据。这个,可能你去看看。服务端可能会更容易了解。
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); //收到内容,并赋值到pHead中,指定接受的空间大小 //以下是将接收大结构体进行强制转换成我们的结构体, head.type = ((LPHEADER)pHead)->type; head.nContentLen = ((LPHEADER)pHead)->nContentLen; //head.to_user 是char[]类型,如果不进行初始化,可能会有乱码出现 memset(head.to_user,0,sizeof(head.to_user)); //讲接受的数据转换过后并赋值到head.to_user,以下同 strcpy(head.to_user,((LPHEADER)pHead)->to_user); memset(head.from_user,0,sizeof(head.from_user)); strcpy(head.from_user,((LPHEADER)pHead)->from_user); 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; } ////////////根据消息类型,处理数据,这个也是和客户端进行对应的。。下面的MSG_LOGOIN,MSG_SEND是定义好的常量,可以F12看看//////////////////// switch(head.type) { case MSG_LOGOIN: //登陆消息 OnLogoIN(pHead, head.nContentLen,head.from_user); break; case MSG_SEND: //发送消息 OnMSGTranslate(pHead, head.nContentLen,head.to_user,head.from_user); break; default : break; } delete pHead; CSocket::OnReceive(nErrorCode); }这上面的注释应该非常清楚了。我建议要看懂这个程序,新手的多看几遍,尤其是先看看客服端怎么发送数据的,然后再来看服务端怎么接受的。这样可能更清楚点。
假如,上面的head.type 是 MSG_LOGOIN 也就是,客户端发送的是一个,登陆的消息过来。就会调用OnLogoIN
//登录 void CClientSocket::OnLogoIN(char* buff, int nlen,char from_user[20]) { //对得接收到的用户信息进行验证 //... (为了简化这步省略) //登录成功 CTime time; time = CTime::GetCurrentTime(); //获取现在时间 CString strTime = time.Format("%Y-%m-%d %H:%M:%S "); CString strTemp(buff); strTime = strTime + strTemp + _T(" 登录...\r\n"); //记录日志 //将内容在NetChatServerDlg里的控件显示 ((CNetChatServerDlg*)theApp.GetMainWnd())->DisplayLog(strTime); m_strName = strTemp; //更新服务列表,这个是更新服务器端的在线名单 //str1 返回的是所有用户字符串 CString str1 = this->UpdateServerLog(); //更新在线所有客服端,from_user 是为了不更新自己的在线列表, //自己跟自己聊天没多大意思吧,其实更自己聊也问题不大,我只是为了学习,加了这么一个工程 this->UpdateAllUser(str1,from_user); }这里主要做的是接受的用户参数整理,并且尤这里去更新一些操作。
下面这句话,我虽然知道什么意思,就是调用CNetChatServerDlg的DisplayLog方法,这样调用,我以前还真没看到。有高手能解释下吗?
((CNetChatServerDlg*)theApp.GetMainWnd())->DisplayLog(strTime);UpdateServerLog更新服务端在线列表UpdateAllUser更新用户在线列表
//跟新服务器在线名单 // 返回在线用户列表的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; }这个很简单,唯一让我费事的是。m_pList这个变量,和个类型我很不熟悉。但是经过几天的探索这个程序,知道这个是存放所有socket连接的,发送消息到对应的人那里,也是靠这个连接、、、
他通过,while循环,把所有的用户准换成Cstring类型,并且用#隔开,方便下一次读取在UpdateUserInfo就是更新,界面显示。UpdateUserInfo方法,在代码里注释很清楚,我这里就不多说了。
void CNetChatServerDlg::UpdateUserInfo( CString strUserInfo) const { CString strTemp; //CStringArray strArray; CListBox* pList = (CListBox*)GetDlgItem(IDC_LT_ONLINE); pList->ResetContent(); //清除所有的数据 while(!strUserInfo.IsEmpty()) { int n = strUserInfo.Find('#'); //查找第一次出现#的位置,读取第一次数据 strTemp = strUserInfo.Left(n); //strArray.Add(strTemp); pList->AddString(strTemp); //加入数据 strUserInfo = strUserInfo.Right(strUserInfo.GetLength()-n-1); //减去第一个的位置 } }
而且,这次发送的主要内容是,所有用户的字符串。
//跟新所有在线用户 void CClientSocket::UpdateAllUser(CString strUserInfo,char from_user[20]) { HEADER _head; _head.type = MSG_UPDATE; _head.nContentLen = strUserInfo.GetLength()+1; memset(_head.from_user, 0, sizeof(_head.from_user)); strcpy(_head.from_user,from_user); char *pSend = new char[_head.nContentLen]; memset(pSend, 0, _head.nContentLen*sizeof(char)); //因为我用的是vs2010 所以是用unicode字符,转换的时候很多 //网站上转换方法不行,必须用WideCharToMultiByte准换 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; }
这里又要回到 CClientSocket::OnReceive 这里接收到客户端的头部,的head.type如果是MSG_SEND那么就是,服务端接收到一个,客户端发送给别人的聊天消息。
这个switch就是CClientSocket::OnReceive里的
switch(head.type) { case MSG_LOGOIN: //登陆消息 OnLogoIN(pHead, head.nContentLen,head.from_user); break; case MSG_SEND: //发送消息 OnMSGTranslate(pHead, head.nContentLen,head.to_user,head.from_user); break; default : break; }
//转发消息 void CClientSocket::OnMSGTranslate(char* buff, int nlen,char to_user[20],char from_user[20]) { //建立头部信息,准备发送 HEADER head; head.type = MSG_SEND; head.nContentLen = nlen; strcpy(head.to_user,to_user); strcpy(head.from_user,from_user); POSITION ps = m_pList->GetHeadPosition(); //取得,所有用户的队列 CString str(buff); int i = strcmp(head.to_user,"群聊"); while(ps!=NULL) { CClientSocket* pTemp = (CClientSocket*)m_pList->GetNext(ps); //只发送2个人, 一个是发送聊天消息的人和接收聊天消息的人。 //如果,接收聊天消息的人是“群聊”那么就发送所有用户,实现群聊和一对一关键就在于此 if(pTemp->m_strName==head.to_user || pTemp->m_strName==head.from_user || i==0 ) { pTemp->Send(&head,sizeof(HEADER)); //先发送头部 pTemp->Send(buff, nlen); //然后发布内容 } } }
////////////////////////////////////////////////////////////////////////////////////////////////客户端 start////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
客户端在最开始的工作和服务端差不多,就是建立socket连接。但是和服务端相比就没那么复杂了。
//初始化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; }
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); //inet_ntoa返回一个char *,而这个char *的空间是在inet_ntoa里面静态分配 CString strIP(inet_ntoa(addr)); //开始只是创建了,并没有连接,这里连接socket,这个8989端口要和服务端监听的端口一直,否则监听不到的。 if(!pSock->Connect(strIP.GetBuffer(0),8989)) { AfxMessageBox(_T("连接服务器失败!")); return ; } CString Cm_strUser = m_strUser; char from_user[20]; memset(from_user,0,sizeof(from_user)); WideCharToMultiByte(CP_OEMCP,0,(LPCTSTR)m_strUser,-1,from_user,260,0,false); 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)) //头部空间,和头部长度 用户名 用户名长度加1 发送者用户名 pSock->LogoIn(pBuff, m_strUser.GetLength()+1,from_user); delete pBuff;CDialogEx::OnCancel();}上面是把,你登陆的用户名发送给服务端。。先调用CClientSocket的Logoin,
CClientSocket* pSock = theApp.GetMainSocket();这句话,又出现了。大概知道是那个意思,但是不知道这个属于什么。注意看这个头部协议,一定要和服务端对应。
//用户登陆 BOOL CClientSocket::LogoIn(LPSTR lpBuff, int nlen,char from_user[20]) { HEADER _head; _head.type = MSG_LOGOIN; //头部类型 _head.nContentLen = nlen; //长度 memset(_head.to_user,0,20); memset(_head.from_user,0,20); strcpy(_head.from_user,from_user); //_head.to_user = ""; 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; }
CLogoInDlg* pLogoinDlg; pLogoinDlg = new CLogoInDlg(); CString m_strUser; if(pLogoinDlg->DoModal()==IDOK) { //不登录 delete pLogoinDlg; m_pSocket->Close(); return false; } else { m_strUser = pLogoinDlg->m_strUser; //读取用户名 delete pLogoinDlg; } CNetChatClientDlg dlg; //将用户名传入下一个对话框 dlg.m_caption = m_strUser + _T(" 的聊天对话框"); m_pMainWnd = &dlg; INT_PTR nResponse = dlg.DoModal();点击登陆之后,先读取用户名,然后弹出我们主窗口。并讲用户名传入其中。
在CNetChatClientDlg 里 的OnInitDialog方法中,设置弹出框的标题。
SetWindowText(m_caption);
在来看看CClientSocket,这个建立以后,只要有消息过来,就会掉用我们重载的OnReceive方法,为什么,我也不知道。这里还是我的模糊地带,有清楚的高手能告诉我一下吗?我只知道是这样。
时间过去那么久了。我们登陆了,从上面的服务器端代码可以看出来,我们登陆后,服务器端会发送消息给我们。告诉我们什么用户上线了,要更新在线列表,先来看看重载的OnReceive方法,这个是客户端最核心的部分
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)); //接受并赋值到pHead里 //强制转换 head.type =((LPHEADER)pHead)->type; head.nContentLen = ((LPHEADER)pHead)->nContentLen; strcpy(head.from_user,((LPHEADER)pHead)->from_user); 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); //这里和服务端类似,根据head.type判断是什么消息 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); }
这里和服务器端类似,如果服务端懂了。这里应该很容易明白。刚刚我们登陆了,,这次服务器端应该发送的是一个head.type为 MSG_UPDATE的。不信你看看服务端的CClientSocket::UpdateAllUser方法定义的head.type。
把接受到的数据,传递到CNetChatClientDlg::UpdateUserInfo进行更新界面
void CNetChatClientDlg::UpdateUserInfo(CString strInfo) //显示所有用户 { CString strTmp; CListBox* pBox = (CListBox*)GetDlgItem(IDC_LB_ONLINE); pBox->ResetContent();//清除所有的数据 //获取自己的用户名,如果是自己的则不需要在自己的列表里显示CString m_strUserName = theApp.GetMainSocket()->m_strUserName;
//发送消息 void CNetChatClientDlg::OnBnClickedBtnSend() { //发送消息 UpdateData(); if(m_strSend.IsEmpty()) { AfxMessageBox(_T("发送类容不能为空!")); return ; } //获取选中内容,并赋值to_user,下面长度加1上面有讲 CListBox* pList = (CListBox*)GetDlgItem(IDC_LB_ONLINE); CString tep(_T("")); INT nIndex = 0 ; nIndex = pList->GetCurSel(); if(LB_ERR == nIndex) { AfxMessageBox(_T("请选择聊天对象!")); return ; } pList->GetText( nIndex, tep ) ; char* to_user = new char[tep.GetLength()*2+1]; memset(to_user, 0, tep.GetLength()*2+1); WChar2MByte(tep.GetBuffer(0), to_user, tep.GetLength()*2+1); CString m_strUserName = theApp.GetMainSocket()->m_strUserName; char from_user[20]; memset(from_user,0,sizeof(from_user)); WChar2MByte(m_strUserName.GetBuffer(0), from_user, m_strUserName.GetLength()*2); CString temp; CTime time = CTime::GetCurrentTime(); temp = time.Format("%H:%M:%S"); //姓名 +_T("\n\t") 时间 m_strSend = theApp.GetMainSocket()->m_strUserName+_T(" 发送给 ") + to_user + _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,to_user,from_user); delete pBuff; m_strSend.Empty(); UpdateData(0); }这个过程,其实主要是准备数据,真正的发送在CClientSocket::SendMSG里。这个里面,定义一下头部,然后分2次发送。服务端的接受,你们可以再去看看
BOOL CClientSocket::SendMSG(LPSTR lpBuff, int nlen,char to_user[20],char from_user[20]) { //生成协议头 HEADER head; head.type = MSG_SEND; head.nContentLen = nlen; strcpy(head.to_user,to_user); strcpy(head.from_user,from_user); int i =Send(&head, sizeof(HEADER)); if(i==SOCKET_ERROR) { AfxMessageBox(_T("发送错误!")); return FALSE; }; if(Send(lpBuff, nlen)==SOCKET_ERROR) { AfxMessageBox(_T("发送错误!")); return FALSE; }; return TRUE; }
服务端操作过来,,又到了。
CClientSocket::OnReceive来接受,同样,还是看head.type这次服务端发送的是MSG_SEND了。这次就更简单了。直接显示接受的内容就可以了。。。如果是MSG_SEND
case MSG_SEND: { //显示接收到的消息 CString str(pBuff); ((CNetChatClientDlg*)(AfxGetApp()->GetMainWnd()))->UpdateText(str); break; }
void CNetChatClientDlg::UpdateText(CString &strText) { ((CEdit*)GetDlgItem(IDC_ET_TEXT))->ReplaceSel(strText); }
而且,我还有一个奇怪问题,不是知道是我系统问题,还是怎么了。。。。我是win7系统,我在本机运行了一个服务端,多个客户端,这样,有时候会出现,有的客户端,只能发消息,不能收,有的只能收不能发。
尤其是通过vs运行出来的实例。。。如果不通过vs运行出来的,还没什么什么。我在虚拟机了,运行多个实例就没有问题了。不管多少都ok。我担心是不是vs运行出来的实例,占用的端口有问题。。。还是其他什么问题。
看下你们的测试,是否也有这样的问题。所以,建议你们,vs调试的时候,就出一个实例,其他的实例,在虚拟机里出来。。。。。。本人新手,如果写的不好,还请多给建议、。。。。。