简介
上一篇说明了LinuxTcp服务端,这篇说一下,Windows客户端,MFC。
环境
Windows系统:Win10
IDE:VS2008,MFC
思路
客户端思路如下:
- 登录:客户端Tcp连接服务端成功后,可以登录服务端,登录成功
- 主界面:类似于QQ,能看到朋友,聊天群组
- 聊天:单人聊天:点击聊天的ID,创建聊天窗口,不可以点击自己。多人聊天:点击群组ID,进入群聊天界面
大致编程思路也是遵循,MVC,对外网络协议有单独的协议,界面层有自己单独界面结构体,控制层负责逻辑,进行交互。
代码结构大概如下:
代码片段
看上诉代码结构,可以看出来,代码分为:单人聊天模块,群组聊天模块,配置文件读取,登录界面,主界面,网络模块,网络模块里边分为UDP,TCP,协议模块,多线程模块,入口函数模块,Other为系统自带资源文件,预编译文件
UI文件如下,单人聊天界面,群组聊天界面,登录界面,主界面
下面按模块来看代码:
- 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的工程了。
源码正在审核中,随后上传。