Tcp多人聊天窗口:MFC客户端(2)

简介

上一篇说明了LinuxTcp服务端,这篇说一下,Windows客户端,MFC。

环境

Windows系统:Win10
IDE:VS2008,MFC

思路

客户端思路如下:

  • 登录:客户端Tcp连接服务端成功后,可以登录服务端,登录成功
  • 主界面:类似于QQ,能看到朋友,聊天群组
  • 聊天:单人聊天:点击聊天的ID,创建聊天窗口,不可以点击自己。多人聊天:点击群组ID,进入群聊天界面
    大致编程思路也是遵循,MVC,对外网络协议有单独的协议,界面层有自己单独界面结构体,控制层负责逻辑,进行交互。
    代码结构大概如下:
    代码结构

代码片段

看上诉代码结构,可以看出来,代码分为:单人聊天模块,群组聊天模块,配置文件读取,登录界面,主界面,网络模块,网络模块里边分为UDP,TCP,协议模块,多线程模块,入口函数模块,Other为系统自带资源文件,预编译文件
资源视图
UI文件如下,单人聊天界面,群组聊天界面,登录界面,主界面
下面按模块来看代码:

  • AloneTalk
    AloneTalk界面
按界面来说,分为了消息输入区,消息显示区,发送按钮。
界面控件都为:IDET_BOX,只不过消息显示区做了一个禁止输入操作,都支持多行操作。

代码如下:
自定义函数只有四个:

	afx_msg void OnBnClickedButton1(); //发送消息
	void ShowRecvMsg(char * msg,unsigned short usID);//显示接收消息
	AloneTalking * GetWindPaint();//得到窗口指针
	void SetSendID(unsigned short usID);//设置发送消息ID
	unsigned short usSendID;//定义发方ID
void AloneTalking::OnBnClickedButton1()
{
	// TODO: Add your control notification handler code here
	CString m_msg;
	CString buf_msg;
	CString strID;
	strID.Format("%d:\r\n\t",UsingID);
	GetDlgItemText(IDC_EDIT3,m_msg);
	GetDlgItem(IDC_EDIT3)->SetWindowText("");
	msg_show.GetWindowText(buf_msg);
	buf_msg+="\r\n";
	buf_msg+=strID;
	buf_msg+=m_msg;
	msg_show.SetWindowText(buf_msg);
	msg_show.LineScroll(msg_show.GetLineCount()-1,0);
	SendMeg(m_msg);
}

void AloneTalking::ShowRecvMsg(char * msg,unsigned short usID)
{
	CString m_msg(msg);
	CString buf_msg;
	CString strID;
	strID.Format("%d:\r\n\t",usID);
	msg_show.GetWindowText(buf_msg);
	buf_msg+="\r\n";
	buf_msg+=strID;
	buf_msg+=m_msg;
	msg_show.SetWindowText(buf_msg);
	msg_show.LineScroll(msg_show.GetLineCount()-1,0); //自动索引到最后一行
}

AloneTalking * AloneTalking::GetWindPaint()
{
	return this;
}

void AloneTalking::SendMeg(CString msg)
{
	Msg_pack m_pack = {0};
	m_pack.m_Msg_Header.usMsgID = AloneMsg;
	m_pack.m_Msg_Header.usSendID = UsingID;
	m_pack.m_Msg_Header.usRecvID = usSendID;

	int len = msg.GetLength();
	//	if(len>Msg_MaxLen) msg.
	memcpy(m_pack.strMsg,msg,msg.GetLength());
	m_pack.m_Msg_Header.usMsgLen = sizeof(m_pack);
	char SendMsg[PACK_MAXLEN] = {0};
	memcpy(SendMsg,&m_pack,sizeof(Msg_pack));
	m_TcpNetWork.ClientSend(SendMsg,sizeof(SendMsg)); //调用客户端发送
}

void AloneTalking::SetSendID(unsigned short usID)
{
	usSendID = usID;
}
  • 多人聊天
    多人聊天界面
    比单人聊天界面,多了一个LIST,用来显示群组人员。
    基础逻辑和单人聊天一样,
	void ClubNumberInit(std::vector<unsigned short> vec_list); //人员列表初始化
	afx_msg void OnBnClickedButton1();
	void ShowRecvMsg(char * msg,unsigned short usID);
	void SendMeg(CString msg); //发送消息
	void SetClubID(unsigned short usID);
	unsigned short usClubID;
	CEdit msg_show;
void ClubTalk::ClubNumberInit(std::vector<unsigned short> vec_list)
{
	//人员列表初始化
	int len = vec_list.size();
	for (int i = 0;i<len;i++)
	{	
		m_ClubList.AddString((LPCTSTR)ListItemName(vec_list[i]));
	}
}
  • 初始化登录界面
    登录界面
    界面比较简单:用户ID输入窗口,ip地址输入窗口,登录按钮,退出按钮
    功能不做简介了,说一个逻辑
	bool TcpConnect(); //Tcp连接
	CwTalkDlg dlg; //主对话框比那辆
	bool connectFlag;//连接标志位
	afx_msg void OnBnClickedBtnlog(); //登录按钮响应事件
	void LogingMsg();//登录消息发送

构造函数的处理:Tcp连接

LogDlg::LogDlg(CWnd* pParent /*=NULL*/)
	: CDialog(LogDlg::IDD, pParent)
{
	connectFlag=TcpConnect();
}
bool LogDlg::TcpConnect()
{	
	if(m_TcpNetWork.NetworkInit()){
		m_TcpNetWork.PortInit();
		if(!m_TcpNetWork.ConnectServer()){
			//	MessageBox(NULL,"服务器连接失败!\n请检查服务器是否开启或者检查网络连接情况!","警告",0);
			return false;
		}else{
			return true;
		}
	}else
	{
		//		AfxMessageBox("套接字初始化失败!");
		return false;
	}	
}

// LogDlg message handlers

void LogDlg::LogingMsg()
{
	CString csID;
	CString csip;
	LogStruct m_logstruct = {0};

	GetDlgItemText(IDC_EDIT1,csID);
	m_logstruct.ID = _ttoi(csID);
	UsingID =  _ttoi(csID);
	GetDlgItemText(IDC_IPADDRESS1,csip);
	memcpy(m_logstruct.ip,csip,csip.GetLength());
	ResDel m_ResDel;

	m_ResDel.m_MsgHeader.usMsgLen = sizeof(ResDel);
	m_ResDel.m_MsgHeader.usMsgID = Res;
	m_ResDel.m_MsgHeader.usSendID = UsingID;
	m_ResDel.m_MsgHeader.usRecvID = ServerID;
	m_ResDel.usID = m_logstruct.ID;
	memcpy(m_ResDel.strIp,m_logstruct.ip,sizeof(m_logstruct.ip));
	char Buf[128] = {0};
	memcpy(Buf,&m_ResDel,sizeof(ResDel));
	//	sprintf(Buf,"%d%d%d%d%d%s",m_ResDel.m_MsgHeader.usMsgID,m_ResDel.m_MsgHeader.usMsgLen,m_ResDel.m_MsgHeader.usSendID,m_ResDel.m_MsgHeader.usRecvID,m_ResDel.usID,m_ResDel.strIp);
	m_TcpNetWork.ClientSend(Buf,sizeof(Buf));
}

void LogDlg::OnBnClickedBtnlog()
{
	// TODO: Add your control notification handler code here
	if(connectFlag==true){
		LogingMsg();
		LogDlg::OnCancel();
		dlg.DoModal();
	}else{
	}
}

需要注意的地方:按下登录按钮,Tcp连接成功,登录消息发送,登录界面隐藏,主界面唯一实例化。如果测试,可以把登录界面隐藏,主界面显示放出来。不进行判断。

	if(connectFlag==true){
		LogingMsg();
		LogDlg::OnCancel();
		dlg.DoModal();
	}
  • 主界面
    主界面
    主界面比较简单,上方List是用户ID显示区,下方List是群组ID显示区
    主界面的逻辑稍微多一些。
    定时器定义,TcpRecv接收多线程函数
#define UDP_TIMER 1
#define TCP_TIMER 2

DWORD WINAPI TcpRecv(LPVOID lpParamter);
	void InitTimer(); //定时器初始化
	void UDPInit();//udp初始化
	bool closeflag;//关闭标志位
	config m_config;
	AloneTalking * p_AloneTalking; //单人对话框指针
	ClubTalk * p_ClubTalk; //多人对话框指针
	afx_msg void OnTimer(UINT_PTR nIDEvent); //定时器响应函数
	afx_msg void OnClose();
	afx_msg void OnLbnDblclkList2();//用户窗口双击消息事件
	afx_msg void OnLbnDblclkList1();//群组窗口双击响应事件
	private:
	UDPMsg m_Udpmsg; //UDP 消息变量
	UDPNetwork m_UdpNetwork;//UDP 变量
	CString strID;	
	void ListViewInit(); //List初始化
	
	void CreatAloneDlg(unsigned short usID); //创建单人聊天窗口
	void CreatClubDlg(unsigned short usClubID);//创建多人聊天窗口
	bool elemfind(unsigned short usID,std::vector<unsigned short> m_list); //元素寻找
	CListBox m_ListBox;//用户List
	CListBox m_ClubListbox;//群组List

	std::vector<unsigned short> m_NumberListBuf;//用户ID缓冲区
	std::vector<unsigned short> m_ClubListBuf;//群组ID缓冲区
	std::map<unsigned short,AloneTalking *> m_AloneWindow;//用户聊天窗口,用户ID map
	std::map<unsigned short,ClubTalk *> m_ClubWindow;//群组聊天窗口,群组ID map

	AloneTalking * GetAlonePaint(unsigned short usID); //得到用户聊天窗口指针
	ClubTalk * GetClubPaint(unsigned short usID);//得到群组聊天窗口指针
	void Msg_Pro();//Tcp消息初步处理
	void Msg_Process(unsigned short usMsgID,int len,char * RecvBuf);	//Tcp消息详细解析

构造函数,析构函数

CwTalkDlg::CwTalkDlg(CWnd* pParent /*=NULL*/)
	: CDialog(CwTalkDlg::IDD, pParent)
{
	m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
	m_Udpmsg.iNo = 0;
	m_config.InfoInit();
}

void CwTalkDlg::DoDataExchange(CDataExchange* pDX)
{
	KillTimer(UDP_TIMER);
	CDialog::DoDataExchange(pDX);
	DDX_Control(pDX, IDC_LIST2, m_ListBox);
	DDX_Control(pDX, IDC_LIST1, m_ClubListbox);
}
void CwTalkDlg::InitTimer()
{
	SetTimer(UDP_TIMER,1000,NULL);
	SetTimer(TCP_TIMER,200,NULL);
}

void CwTalkDlg::UDPInit()
{
	m_UdpNetwork.SocketInit();
	InitTimer();
}
//定时器消息处理函数
void CwTalkDlg::OnTimer(UINT_PTR nIDEvent)
{
	// TODO: Add your message handler code here and/or call default
	char  buf_msg[6];
	int len = 0;
	switch(nIDEvent)
	{
	case 1: //UDP消息发送
		m_Udpmsg.usID = 10001;
		m_Udpmsg.iNo = 1;
		len = sizeof(UDPMsg);		

		memset(&buf_msg,0,len);
		memcpy(&buf_msg,&m_Udpmsg,len);

	//	m_UdpNetwork.SocketSendMsg(buf_msg,len);
		break;
	case 2:
		Msg_Pro();//接收消息解析
		break;
	default:
		break;

	}
	CDialog::OnTimer(nIDEvent);
}

void CwTalkDlg::OnClose()
{
	// TODO: Add your message handler code here and/or call default
	closeflag = true;
	CDialog::OnClose();
}

CString GetListItemName(unsigned short usID)
{
	char s[6];
	sprintf(s,"%d",usID);
	CString linename(s);
	return linename;
}

void CwTalkDlg::ListViewInit()
{
	/*人员列表初始化*/
	int len = m_config.vec_NumberList.size();
	for (int i = 0;i<len;i++)
	{	
		m_ListBox.AddString((LPCTSTR)GetListItemName(m_config.vec_NumberList[i]));
	}
	/*群组列表初始化*/
	int iClublen = m_config.map_Club.size();
	std::map<unsigned short,std::vector<unsigned short>>::iterator it;
	for ( it = m_config.map_Club.begin();it != m_config.map_Club.end();it++)
	{
		m_ClubListbox.AddString((LPCTSTR)GetListItemName(it->first));
	}

}
//双击,对应的群组ID相,创建群组聊天窗口
void CwTalkDlg::OnLbnDblclkList2()
{
	// TODO: Add your control notification handler code here
	int iSel = m_ListBox.GetCurSel();
	unsigned short usID = m_config.vec_NumberList[iSel];
	if(!elemfind(usID,m_NumberListBuf))	{
		m_NumberListBuf.push_back(usID);
		CreatAloneDlg(usID);
	}else{
		AloneTalking * p = GetAlonePaint(usID);
		p->ShowWindow(SW_SHOW);
	}	
}
//创建单人聊天窗口
void CwTalkDlg::CreatAloneDlg(unsigned short usID)
{
	p_AloneTalking = new AloneTalking;
	p_AloneTalking->Create(AloneTalking::IDD,this);
	CString AloneTitle;
	AloneTitle.Format("与%d的对话",usID);
	p_AloneTalking->SetSendID(usID);
	p_AloneTalking->SetWindowText(AloneTitle);
	p_AloneTalking->ShowWindow(SW_SHOWNORMAL);
	std::pair<unsigned short,AloneTalking *> buf(usID,p_AloneTalking);
	m_AloneWindow.insert(buf);
}
//创建群组聊天窗口,根据对应ID
void CwTalkDlg::CreatClubDlg(unsigned short usClubID)
{
	p_ClubTalk = new ClubTalk;
	p_ClubTalk->Create(ClubTalk::IDD,this);
	CString AloneTitle;
	AloneTitle.Format("%d群组对话",usClubID);
	p_ClubTalk->SetClubID(usClubID);
	p_ClubTalk->SetWindowText(AloneTitle);
	std::vector<unsigned short>m_ClubList = m_config.map_Club[usClubID];
	p_ClubTalk->ClubNumberInit(m_ClubList);
	p_ClubTalk->ShowWindow(SW_SHOWNORMAL);
	std::pair<unsigned short,ClubTalk *> buf(usClubID,p_ClubTalk);
	m_ClubWindow.insert(buf);
}
//双击,对应群组ID相,有,则显示,无,则创建
void CwTalkDlg::OnLbnDblclkList1()
{
	// TODO: Add your control notification handler code here
	int iItem = m_ClubListbox.GetCurSel();
	CString sText;
	m_ClubListbox.GetText(iItem,sText);
	unsigned short usClubID = atoi(sText);
	if(!elemfind(usClubID,m_ClubListBuf)){
		m_ClubListBuf.push_back(usClubID);
		CreatClubDlg(usClubID);
	}else{
		ClubTalk * p =GetClubPaint(usClubID);
		p->ShowWindow(SW_SHOWNORMAL);
	}
}

bool CwTalkDlg::elemfind(unsigned short usID,std::vector<unsigned short> m_list)
{
	std::vector<unsigned short>::iterator iter_find = std::find(m_list.begin(),m_list.end(),usID);
	if (iter_find != m_list.end())	{
		return true;
	}else{
		return false;
	}
}

AloneTalking * CwTalkDlg::GetAlonePaint(unsigned short usID)
{
	AloneTalking * p ;
	//收方ID,寻找对应的窗口指针,有,则显示,无则创建
	std::map<unsigned short,AloneTalking *>::iterator it = m_AloneWindow.find(usID);
	if(it != m_AloneWindow.end()){
		p = it->second;		
	}else{
		CreatClubDlg(usID);
		p = m_AloneWindow[usID];
	}
	return p;
}

ClubTalk * CwTalkDlg::GetClubPaint(unsigned short usID)
{
	ClubTalk * p;
	//收方ID,寻找对应的窗口指针,有,则显示,无则创建
	std::map<unsigned short,ClubTalk *>::iterator it = m_ClubWindow.find(usID);
	if(it != m_ClubWindow.end()){
		p = it->second;		
	}else{
		CreatAloneDlg(usID);
		p = m_ClubWindow[usID];
	}
	return p;
}
//对消息缓冲区的消息进行处理
void CwTalkDlg::Msg_Pro()
{
	int vec_msglen = vec_msg.size();
	MsgHeader m_MsgHeader;
	char Buf_Recv[280] = {0};
	if(vec_msglen>0)
	{
		for(int i =0 ;i<vec_msglen;i++)
		{
			memcpy(Buf_Recv,vec_msg[i],280);
			memcpy(&m_MsgHeader,Buf_Recv,sizeof(MsgHeader));
			Msg_Process(m_MsgHeader.usMsgID,m_MsgHeader.usMsgLen,Buf_Recv);
		}
		vec_msg.clear();
	}else{  

	}
}

void CwTalkDlg::Msg_Process(unsigned short usMsgID,int len,char * RecvBuf)
{
	Msg_pack m_Msg_pack = {0};
	CString msg;
	AloneTalking * p;
	switch(usMsgID)
	{
	case Res:
		break;
	case Del:
		break;
	case AloneMsg: //单人消息显示
		memcpy(&m_Msg_pack,RecvBuf,sizeof(Msg_pack));
		GetAlonePaint(m_Msg_pack.m_Msg_Header.usSendID)->ShowRecvMsg(m_Msg_pack.strMsg,m_Msg_pack.m_Msg_Header.usSendID);
		break;
	case ClubMsg://群组消息显示
		memcpy(&m_Msg_pack,RecvBuf,sizeof(Msg_pack));
		//得到消息窗口指针,显示消息
		GetClubPaint(m_Msg_pack.m_Msg_Header.usRecvID)->ShowRecvMsg(m_Msg_pack.strMsg,m_Msg_pack.m_Msg_Header.usSendID);
		break;
	case CreatCmd:

		break;
	case DelCmd:
		break;
	default:
		break;
	}
}

/*多线程接收*/
HANDLE p_RecvEvent;
DWORD WINAPI TcpRecv(LPVOID lpParamter)
{	
	char RecvMsg[PACK_MAXLEN];
	while(1)
	{
		m_TcpNetWork.ClientRecv(RecvMsg);
		if(strlen(RecvMsg)>0)
		{
			char Buf_Msg[PACK_MAXLEN] ={0};
			memcpy(Buf_Msg,RecvMsg,sizeof(RecvMsg));
			vec_msg.push_back(Buf_Msg);
			memset(RecvMsg,0,sizeof(RecvMsg));
		}else{
			
		}
	}	
}
  • 网络相关(Tcp,Udp的初始化不做讲解)
  • TCP
#include <WinSock2.h>
#pragma comment(lib,"WS2_32.lib")

#define MAXLEN 1024
#define SendPort 2000

class TcpNetWork
{
public:
	TcpNetWork(void);
	~TcpNetWork(void);
	bool NetworkInit();//套接字初始化;Tcp
	void PortInit(); //端口初始化
	bool ConnectServer(); //连接服务器
	char * ClientRecv(char * Buf_Recv);//客户端接收
	void ClientSend(char * Buf_Recv,int Msg_len);//客户端发送
private:
	WSADATA data;
	SOCKET ClientSocket;
	SOCKADDR_IN ClientAddr;
	SOCKADDR_IN ServerAddr;
};
bool TcpNetWork::NetworkInit()
{
	bool connectstate = 0;
	WSAStartup(MAKEWORD(2,2),&data);
	ClientSocket = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP);
	if(INVALID_SOCKET == ClientSocket){
		return false;
	}else{
		return true;
	}
}

void TcpNetWork::PortInit()
{
	ServerAddr.sin_family = AF_INET;
	ServerAddr.sin_port = htons(3000);
	ServerAddr.sin_addr.S_un.S_addr = inet_addr("10.30.20.138");
}

bool TcpNetWork::ConnectServer()
{
	int res = connect(ClientSocket,(SOCKADDR *)&ServerAddr,sizeof(SOCKADDR_IN));
	if(res == SOCKET_ERROR)	{
		return false;
	}else{
		return true;
	}
}

char * TcpNetWork::ClientRecv(char * Buf_Recv)
{
	int iRet = recv(ClientSocket,Buf_Recv,MAXLEN,0);
	return Buf_Recv;
}

void TcpNetWork::ClientSend(char * Buf_Recv,int Msg_len)
{
	int iRet = send(ClientSocket,Buf_Recv,Msg_len,0);
	if(iRet == 0)
	{
		printf("消息发送超时!\n");
	}
}
  • UDP
#pragma comment(lib, "wsock32.lib")

#define UdpSendPort 2001
#define UdpRecvPort 3001

#define LOCALIP 10.30.20.137

class UDPNetwork
{
public:
	UDPNetwork(void);
	~UDPNetwork(void);

	void SocketInit();
	void SocketSendMsg(char * strMsg,int ilen);

private:

	WSADATA data;
	SOCKET SendSocket;
	SOCKADDR_IN addr;
	SOCKET TargetSocket;
	SOCKADDR_IN TargetAddr;
};
void UDPNetwork::SocketInit()
{
	WSAStartup(MAKEWORD(2,2),&data);
	SendSocket = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);
	addr.sin_family = AF_INET;
	addr.sin_port = htons(UdpSendPort);
	addr.sin_addr.S_un.S_addr=inet_addr("10.30.20.137");
	bind(SendSocket,(SOCKADDR *)&addr,sizeof(addr));

	TargetAddr.sin_family = AF_INET;
	TargetAddr.sin_port =htons(UdpRecvPort);
	TargetAddr.sin_addr.S_un.S_addr = inet_addr("10.30.20.138");
}

void UDPNetwork::SocketSendMsg(char * strMsg,int ilen)
{
	int len = sizeof(TargetAddr);
	int flag = sendto(SendSocket,strMsg,ilen,0,(const SOCKADDR*)&TargetAddr,len);
}

不足:

说一下不足,因为时间关系,还有最初进行设计的时候,考虑不周,导致最后功能模块全部实现,但是不是预初的设计。比如:只实现了主体功能,一些,用户注册,用户登录,用户注销,列表的动态维护,创键群组,删除群组,等其他功能,虽然只是简单的周边的功能,调用现有函数均可实现,Tcp服务端的TCP网络模型的使用,是Tcp服务端容错率,收发效率更高。

源码

我应经将工程转为VS2017的工程了。
源码正在审核中,随后上传。

  • 0
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值