现在的位置: 首页 > 综合 > 正文

c++/MFC CSocket仿QQ聊天软件,实现1对1聊天,群聊 c++/MFC 极为简单的socket实例

2018年03月21日 ⁄ 综合 ⁄ 共 16429字 ⁄ 字号 评论关闭

学习,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;
    }

走完上面的几个步骤,这样,服务端,就能和客户端接受和发送消息了。大家可能会很奇怪,上面那个怎么没有绑定端口,直接listen了。。。这个我那个简单socket里有介绍,因为,Create 方法已经包含了 Bind 方法,如果是以 Create 方法创建socket的前提下不能再调用 Bind ,要不一定出错。

然后重载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); //减去第一个的位置
	}
}

用户端更新好了,就应该更新客户端的了。。客户端的更新,肯定是通过发送消息来更新了。。发送消息,又要用到,开始说的HEADER结构体,这种协议了。

而且,这次发送的主要内容是,所有用户的字符串。

 //跟新所有在线用户
 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;
     }

如果是聊天消息,就是OnMSGTranslate方法了。这个方法相对来说,要比登陆还要简单点,这个不必更新服务端什么东西,就是一个转发消息的过程。

//转发消息
 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);			//然后发布内容
		}
	 }
 }

实现,群聊和点对点,关键就在于这个方法。to_user  收件人  from_user  发送人。服务端发送也是发送2次,先一个头部,然后主体。这里的协议都是和客户端商量好的。send发送出去,现在就是客户端接收的事了。服务端也到此结束了。下面就是服务端的事了。。。上面有些东西,我注释了一次,就不再注释了。不然太多废话了。高手看到都烦。。我主要是给新手做入门用,也巩固一下自己的这方面学习。

////////////////////////////////////////////////////////////////////////////////////////////////客户端 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;
    }

大家,可能发先,这里只是创建了。socket却没有看到连接socket。连接,是点击登陆才操作连接的。在登陆那里能指定ip,其实端口也可以手动指定的。但是为了方便,就不指定了。在这里,我给大家分享一个我遇到的一个很奇怪的错误。我有一个Cstring字符串,一直想通过我的WChar2MByte这个方法进行转换char。但是一直转换不成功,比如“你好”  但是如果是“a你好” 这样就能转换成功了。我一直很纳闷,这个是为什么了。后来我感觉到问题了,这个问题,可能是我自己的理解,我那样操作也是对的。以后也能成功了。因为,纯中文字符串用CString用GetLength()*2来取占用空间大小虽然是准确的,但是转换成char
需要一个空间来存放结束符号"\0",这样就会有内存溢出。所以,在申请空间的时候,需要在GetLength()*2的基础上加1  除非你本来申请的空间足够大。这个搞了我很郁闷。。。还是没经验的原因。

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);

切记不要和我一样开始做蠢事,在构造函数里设置。。。一定要在OnInitDialog里。执行构造函数的时候,都还没有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;
//顺便把每个用户里,都加上“群聊”strInfo = _T("群聊#") + strInfo;while(!strInfo.IsEmpty()){int n = strInfo.Find('#');if(n==-1)break;strTmp = strInfo.Left(n);if(strTmp!=m_strUserName){pBox->AddString(strTmp);}strInfo = strInfo.Right(strInfo.GetLength()-n-1);}}

这样一个过程,整个登陆就完成了。发送消息其实也是一样。先看看,点击“发送”事件。

//发送消息
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;
}

send发送之后,就等待服务端的了。服务端接受到参数后,又会做转发操作。。。可以再上去看看,服务端接受了参数,然后做了什么操作。

服务端操作过来,,又到了。

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调试的时候,就出一个实例,其他的实例,在虚拟机里出来。。。。。。本人新手,如果写的不好,还请多给建议、。。。。。

抱歉!评论已关闭.