12h慢通大学生《网络编程》考试 #
- 项目代码就不上传github,放在这边,留给有需要的人。
- 相关视频在B站,https://space.bilibili.com/1873697345
- 需要一对一编程培训请联系:
1. 考核要求 #
运用 CAsyncSocket 类或者 CSocket 类设计聊天室程序,程序界面自行设计,程序功能要求如下。
服务器端功能要求如下
- (1)服务器开启时要绑定本地IP地址和端口号,然后才能开始侦听来自客户端的连接,可以主动断开连接,能够显示连接状态。
- (2)能够解析聊天信息,若是新用户,要获取并显示用户昵称、ip地址、端口号等信息,并显示“欢迎新人加入”的信息。若是私聊信息,则要一对一发送信息。若是公聊信息,则向所有用户转发信息。
客户机端功能要求如下
- (1)主动发出连接请求与服务器建立连接,能够向服务器发送信息,能够接收并解析服务器发来一切信息,如新用户的加入、旧用户的退出等。
- (2)能够显示并查看历史聊天信息,同时显示聊天的日期和时间。
- (3)当信息较多时,能够翻页或滚屏显示。
- (4)能够将聊天信息导出、保存到文本文件中。
其他
- 使用visual studio 2015开发工具,编程语言使用C或C++。
2. 服务端代码 #
2.1 ChatRoomServer.h #
// ChatRoomServer.h : PROJECT_NAME 应用程序的主头文件
#pragma once
#ifndef __AFXWIN_H__
#error "在包含此文件之前包含“stdafx.h”以生成 PCH 文件"
#endif
#include "resource.h" // 主符号
// CChatRoomServerApp:
// 有关此类的实现,请参阅 ChatRoomServer.cpp
//
class CChatRoomServerApp : public CWinApp
{
public:
CChatRoomServerApp();
// 重写
public:
virtual BOOL InitInstance();
// 实现
DECLARE_MESSAGE_MAP()
};
extern CChatRoomServerApp theApp;
2.2 ChatRoomServer.cpp #
// ChatRoomServer.cpp : 定义应用程序的类行为。
#include "stdafx.h"
#include "ChatRoomServer.h"
#include "ChatRoomServerDlg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
// CChatRoomServerApp
BEGIN_MESSAGE_MAP(CChatRoomServerApp, CWinApp)
ON_COMMAND(ID_HELP, &CWinApp::OnHelp)
END_MESSAGE_MAP()
// CChatRoomServerApp 构造
CChatRoomServerApp::CChatRoomServerApp()
{
// 支持重新启动管理器
m_dwRestartManagerSupportFlags = AFX_RESTART_MANAGER_SUPPORT_RESTART;
// TODO: 在此处添加构造代码,
// 将所有重要的初始化放置在 InitInstance 中
}
// 唯一的一个 CChatRoomServerApp 对象
CChatRoomServerApp theApp;
// CChatRoomServerApp 初始化
BOOL CChatRoomServerApp::InitInstance()
{
// 如果一个运行在 Windows XP 上的应用程序清单指定要
// 使用 ComCtl32.dll 版本 6 或更高版本来启用可视化方式,
//则需要 InitCommonControlsEx()。 否则,将无法创建窗口。
INITCOMMONCONTROLSEX InitCtrls;
InitCtrls.dwSize = sizeof(InitCtrls);
// 将它设置为包括所有要在应用程序中使用的
// 公共控件类。
InitCtrls.dwICC = ICC_WIN95_CLASSES;
InitCommonControlsEx(&InitCtrls);
CWinApp::InitInstance();
AfxEnableControlContainer();
// 创建 shell 管理器,以防对话框包含
// 任何 shell 树视图控件或 shell 列表视图控件。
CShellManager *pShellManager = new CShellManager;
// 激活“Windows Native”视觉管理器,以便在 MFC 控件中启用主题
CMFCVisualManager::SetDefaultManager(RUNTIME_CLASS(CMFCVisualManagerWindows));
// 标准初始化
// 如果未使用这些功能并希望减小
// 最终可执行文件的大小,则应移除下列
// 不需要的特定初始化例程
// 更改用于存储设置的注册表项
// TODO: 应适当修改该字符串,
// 例如修改为公司或组织名
SetRegistryKey(_T("应用程序向导生成的本地应用程序"));
CChatRoomServerDlg dlg;
m_pMainWnd = &dlg;
INT_PTR nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
// TODO: 在此放置处理何时用
// “确定”来关闭对话框的代码
}
else if (nResponse == IDCANCEL)
{
// TODO: 在此放置处理何时用
// “取消”来关闭对话框的代码
}
else if (nResponse == -1)
{
TRACE(traceAppMsg, 0, "警告: 对话框创建失败,应用程序将意外终止。\n");
TRACE(traceAppMsg, 0, "警告: 如果您在对话框上使用 MFC 控件,则无法 #define _AFX_NO_MFC_CONTROLS_IN_DIALOGS。\n");
}
// 删除上面创建的 shell 管理器。
if (pShellManager != NULL)
{
delete pShellManager;
}
#ifndef _AFXDLL
ControlBarCleanUp();
#endif
// 由于对话框已关闭,所以将返回 FALSE 以便退出应用程序,
// 而不是启动应用程序的消息泵。
return FALSE;
}
2.3 ChatRoomServerDlg.h #
// ChatRoomServerDlg.h : 头文件
#pragma once
#include <ctime>
#include "ChatServerSocket.h"
#include "afxcmn.h"
// CChatRoomServerDlg 对话框
class CChatRoomServerDlg : public CDialogEx
{
// 构造
public:
CChatRoomServerDlg(CWnd* pParent = NULL); // 标准构造函数
~CChatRoomServerDlg();
void LogMessage(LPCTSTR lpszMessage);
// 从list删除数据
void DelItemFromList(CString ip, USHORT port, CString nickName);
// 对话框数据
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_CHATROOMSERVER_DIALOG };
#endif
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持
// 实现
protected:
HICON m_hIcon;
// 生成的消息映射函数
virtual BOOL OnInitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
// 处理客户端断开
afx_msg LRESULT OnClientSocketClose(WPARAM wParam, LPARAM lParam);
// 处理客户端数据接收
afx_msg LRESULT OnClientSocketRecv(WPARAM wParam, LPARAM lParam);
afx_msg void OnBnClickedButtonStart();
afx_msg void OnBnClickedButtonStop();
afx_msg void OnBnClickedButtonDisconnect();
DECLARE_MESSAGE_MAP()
private:/*func*/
CString FormatCurrentTime(LPCTSTR format = _T("%Y-%m-%d %H:%M:%S"));
// 初始化List Control
void InitListControl();
// 插入数据到list
void InsertItemToList(CString ip, USHORT port, CString nickName);
// 从list读取所有会话数据
char* ReadItemsFromList(int &dataSize);
private:/*number*/
CChatServerSocket* m_pServer = nullptr;
CListCtrl m_listSessions;
};
2.4 ChatRoomServerDlg.cpp #
// ChatRoomServerDlg.cpp : 实现文件
#include "stdafx.h"
#include "ChatRoomServer.h"
#include "ChatRoomServerDlg.h"
#include "afxdialogex.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
// 用于应用程序“关于”菜单项的 CAboutDlg 对话框
class CAboutDlg : public CDialogEx
{
public:
CAboutDlg();
// 对话框数据
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_ABOUTBOX };
#endif
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持
// 实现
protected:
DECLARE_MESSAGE_MAP()
};
CAboutDlg::CAboutDlg() : CDialogEx(IDD_ABOUTBOX)
{
}
void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
}
BEGIN_MESSAGE_MAP(CAboutDlg, CDialogEx)
END_MESSAGE_MAP()
// CChatRoomServerDlg 对话框
CChatRoomServerDlg::CChatRoomServerDlg(CWnd* pParent /*=NULL*/)
: CDialogEx(IDD_CHATROOMSERVER_DIALOG, pParent)
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
CChatRoomServerDlg::~CChatRoomServerDlg()
{
if (m_pServer)
{
delete m_pServer;
m_pServer = nullptr;
}
}
void CChatRoomServerDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_LIST_SESSIONS, m_listSessions);
}
BEGIN_MESSAGE_MAP(CChatRoomServerDlg, CDialogEx)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(IDC_BUTTON_START, &CChatRoomServerDlg::OnBnClickedButtonStart)
ON_BN_CLICKED(IDC_BUTTON_STOP, &CChatRoomServerDlg::OnBnClickedButtonStop)
ON_MESSAGE(WM_SOCKET_CLOSE, &CChatRoomServerDlg::OnClientSocketClose)
ON_MESSAGE(WM_SOCKET_RECV, &CChatRoomServerDlg::OnClientSocketRecv)
ON_BN_CLICKED(IDC_BUTTON_DISCONNECT, &CChatRoomServerDlg::OnBnClickedButtonDisconnect)
END_MESSAGE_MAP()
// CChatRoomServerDlg 消息处理程序
BOOL CChatRoomServerDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
// 将“关于...”菜单项添加到系统菜单中。
// IDM_ABOUTBOX 必须在系统命令范围内。
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != NULL)
{
BOOL bNameValid;
CString strAboutMenu;
bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
ASSERT(bNameValid);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
// 设置此对话框的图标。 当应用程序主窗口不是对话框时,框架将自动
// 执行此操作
SetIcon(m_hIcon, TRUE); // 设置大图标
SetIcon(m_hIcon, FALSE); // 设置小图标
// TODO: 在此添加额外的初始化代码
LogMessage(_T("聊天服务器启动..."));
//初始化server socket
m_pServer = new CChatServerSocket(this);
if (m_pServer->InitSocket() == FALSE)
{
LogMessage(_T("Winsock初始化失败!"));
delete m_pServer;
m_pServer = nullptr;
}
else
{
LogMessage(_T("Winsock初始化成功!"));
}
//设置默认ip和端口
GetDlgItem(IDC_IPADDRESS_BIND_IP)->SetWindowText(_T("0.0.0.0"));
GetDlgItem(IDC_EDIT_PORT)->SetWindowText(_T("1080"));
//设置button
GetDlgItem(IDC_BUTTON_START)->EnableWindow(TRUE);
GetDlgItem(IDC_BUTTON_STOP)->EnableWindow(FALSE);
// 初始化List Control
InitListControl();
return TRUE; // 除非将焦点设置到控件,否则返回 TRUE
}
void CChatRoomServerDlg::OnSysCommand(UINT nID, LPARAM lParam)
{
if ((nID & 0xFFF0) == IDM_ABOUTBOX)
{
CAboutDlg dlgAbout;
dlgAbout.DoModal();
}
else
{
CDialogEx::OnSysCommand(nID, lParam);
}
}
// 如果向对话框添加最小化按钮,则需要下面的代码
// 来绘制该图标。 对于使用文档/视图模型的 MFC 应用程序,
// 这将由框架自动完成。
void CChatRoomServerDlg::OnPaint()
{
if (IsIconic())
{
CPaintDC dc(this); // 用于绘制的设备上下文
SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);
// 使图标在工作区矩形中居中
int cxIcon = GetSystemMetrics(SM_CXICON);
int cyIcon = GetSystemMetrics(SM_CYICON);
CRect rect;
GetClientRect(&rect);
int x = (rect.Width() - cxIcon + 1) / 2;
int y = (rect.Height() - cyIcon + 1) / 2;
// 绘制图标
dc.DrawIcon(x, y, m_hIcon);
}
else
{
CDialogEx::OnPaint();
}
}
//当用户拖动最小化窗口时系统调用此函数取得光标
//显示。
HCURSOR CChatRoomServerDlg::OnQueryDragIcon()
{
return static_cast<HCURSOR>(m_hIcon);
}
LRESULT CChatRoomServerDlg::OnClientSocketClose(WPARAM wParam, LPARAM lParam)
{
CChatClientSocket* pClient = (CChatClientSocket*)wParam;
if (pClient)
{
//服务端移除客户端socket
if (m_pServer)
{
m_pServer->RemoveClient(pClient);
}
}
return LRESULT();
}
LRESULT CChatRoomServerDlg::OnClientSocketRecv(WPARAM wParam, LPARAM lParam)
{
CChatClientSocket* pClient = (CChatClientSocket*)wParam;
std::string* strBuff = (std::string*)lParam;
//构建返回数据包
char tmp[128] = { 0 };
ChatProtocol* responseHdr = (ChatProtocol*)tmp;
//解析数据
ChatProtocol* hdr = (ChatProtocol*)strBuff->data();
if (hdr->cmd == ChatCmdLogin)//客户端登录
{
memcpy(responseHdr->magic, MAGIC, sizeof(responseHdr->magic));
responseHdr->size = sizeof(tmp) - sizeof(ChatProtocol);
//检查昵称是否重复
CString nickName = (TCHAR*)hdr->data;
CString strMsg;
if (m_pServer->CheckNickName(nickName)==FALSE)
{
strMsg.Format(_T("用户登录失败,昵称重复:%s"), nickName);
responseHdr->cmd = ChatCmdLoginFail;
memcpy(responseHdr->data, _T("昵称不能重复!"), 15);
pClient->m_nickName = nickName;
}
else
{
strMsg.Format(_T("用户登录成功(昵称):%s"), nickName);
responseHdr->cmd = ChatCmdLoginSuccess;
pClient->m_nickName = nickName;
//界面显示会话信息
InsertItemToList(pClient->m_strAddress, pClient->m_nPort, pClient->m_nickName);
}
//发送返回信息
int nSend = pClient->Send(tmp, sizeof(tmp));
if (nSend <= 0)
{
m_pServer->RemoveClient(pClient);
}
else
{
LogMessage(strMsg);
if (strMsg.Find(_T("用户登录成功"))>=0)
{
//群发欢迎新用户
strMsg.Empty();
strMsg.Format(_T("[服务器群发]-> 欢迎新人加入,昵称:%s"), nickName);
responseHdr->cmd = ChatCmdWelcome;
memcpy(responseHdr->data, strMsg.GetString(), strMsg.GetLength()*sizeof(TCHAR));
m_pServer->ForwardDataToAll(tmp, sizeof(tmp));
//发送会话列表
int dataSize = 0;
char* data = ReadItemsFromList(dataSize);
if (data)
{
m_pServer->ForwardDataToAll(data, dataSize);
delete[] data;
}
}
}
}
else if (hdr->cmd == ChatCmdPrivate)
{
ChatMsg* pMsg = (ChatMsg*)hdr->data;
m_pServer->ForwardDataToOne(pMsg->toNickName, (char*)strBuff->data(), strBuff->size());
m_pServer->ForwardDataToOne(pMsg->fromNickName, (char*)strBuff->data(), strBuff->size());
}
else if (hdr->cmd == ChatCmdPublic)
{
m_pServer->ForwardDataToAll((char*)strBuff->data(), strBuff->size());
}
delete strBuff;
return LRESULT();
}
void CChatRoomServerDlg::LogMessage(LPCTSTR lpszMessage)
{
CString strText;
CEdit* edit = (CEdit*)GetDlgItem(IDC_EDIT_LOGS);
edit->GetWindowText(strText);
CString strNewText;
if (strText.GetLength() > 0)
{
strNewText.Format(_T("%s\r\n[%s] %s"), strText, FormatCurrentTime(), lpszMessage);
}
else
{
strNewText.Format(_T("[%s] %s"), FormatCurrentTime(), lpszMessage);
}
edit->SetWindowText(strNewText);
edit->LineScroll(edit->GetLineCount());
}
CString CChatRoomServerDlg::FormatCurrentTime(LPCTSTR format)
{
CTime time = CTime::GetCurrentTime();
return time.Format(format);
}
void CChatRoomServerDlg::InitListControl()
{
// 设置扩展样式
m_listSessions.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER);
// 添加列
m_listSessions.InsertColumn(0, _T("昵称"), LVCFMT_LEFT, 300);
m_listSessions.InsertColumn(1, _T("IP地址"), LVCFMT_LEFT, 200);
m_listSessions.InsertColumn(2, _T("端口号"), LVCFMT_LEFT, 100);
}
void CChatRoomServerDlg::InsertItemToList(CString ip, USHORT port, CString nickName)
{
// 插入主项
int nIndex = m_listSessions.InsertItem(0, nickName);
// 设置子项
m_listSessions.SetItemText(nIndex, 1, ip);
CString strPort;
strPort.Format(_T("%d"), port);
m_listSessions.SetItemText(nIndex, 2, strPort);
}
void CChatRoomServerDlg::DelItemFromList(CString ip, USHORT port, CString nickName)
{
CString strPort;
strPort.Format(_T("%d"), port);
int nCount = m_listSessions.GetItemCount();
for (int i = 0; i < nCount; i++)
{
// 获取项数据
CString itemNickName = m_listSessions.GetItemText(i, 0); // 昵称
CString itemIp = m_listSessions.GetItemText(i, 1); // ip
CString itemPort = m_listSessions.GetItemText(i, 2); // port
if (itemIp == ip && itemPort == strPort && itemNickName == nickName)
{
m_listSessions.DeleteItem(i);
return;
}
}
}
char * CChatRoomServerDlg::ReadItemsFromList(int &dataSize)
{
int nCount = m_listSessions.GetItemCount();
if (nCount <= 0)
{
return nullptr;
}
int size = nCount * sizeof(ChatSessions) + sizeof(ChatProtocol);
dataSize = size;
char* tmp = new char[size]();
ChatProtocol* hdr = (ChatProtocol*)tmp;
hdr->cmd = ChatCmdGetSessions;
memcpy(hdr->magic, MAGIC, sizeof(hdr->magic));
hdr->size = size - sizeof(ChatProtocol);
ChatSessions* sessions = (ChatSessions*)hdr->data;
for (int i = 0; i < nCount; i++)
{
// 获取项数据
CString itemNickName = m_listSessions.GetItemText(i, 0); // 昵称
CString itemIp = m_listSessions.GetItemText(i, 1); // ip
CString itemPort = m_listSessions.GetItemText(i, 2); // port
sessions[i].port = _ttoi(itemPort);
memcpy(sessions[i].nickName, itemNickName.GetString(), itemNickName.GetLength() * sizeof(TCHAR));
memcpy(sessions[i].ip, itemIp.GetString(), itemIp.GetLength() * sizeof(TCHAR));
}
return tmp;
}
void CChatRoomServerDlg::OnBnClickedButtonStart()
{
// TODO: 在此添加控件通知处理程序代码
if (m_pServer)
{
CString bindIp;
CString port;
GetDlgItem(IDC_IPADDRESS_BIND_IP)->GetWindowText(bindIp);
GetDlgItem(IDC_EDIT_PORT)->GetWindowText(port);
if (m_pServer->Start(bindIp, _ttoi(port)) == FALSE)
{
GetDlgItem(IDC_BUTTON_START)->EnableWindow(TRUE);
GetDlgItem(IDC_BUTTON_STOP)->EnableWindow(FALSE);
}
else
{
m_listSessions.DeleteAllItems();
GetDlgItem(IDC_BUTTON_START)->EnableWindow(FALSE);
GetDlgItem(IDC_BUTTON_STOP)->EnableWindow(TRUE);
}
}
else
{
LogMessage(_T("服务器socket创建失败!"));
GetDlgItem(IDC_BUTTON_START)->EnableWindow(TRUE);
GetDlgItem(IDC_BUTTON_STOP)->EnableWindow(FALSE);
}
}
void CChatRoomServerDlg::OnBnClickedButtonStop()
{
// TODO: 在此添加控件通知处理程序代码
if (m_pServer)
{
m_pServer->Stop();
}
GetDlgItem(IDC_BUTTON_START)->EnableWindow(TRUE);
GetDlgItem(IDC_BUTTON_STOP)->EnableWindow(FALSE);
}
void CChatRoomServerDlg::OnBnClickedButtonDisconnect()
{
// TODO: 在此添加控件通知处理程序代码
CString nickName;
GetDlgItem(IDC_EDIT_NICKNAME)->GetWindowText(nickName);
if (nickName.IsEmpty())
{
LogMessage(_T("断开连接失败:用户昵称不能为空!"));
return;
}
if (m_pServer)
{
m_pServer->RomveClientByNickname(nickName);
}
}
2.5 ChatServerSocket.h #
#pragma once
#include <afxsock.h>
#include <vector>
#include <string>
#define WM_SOCKET_CLOSE (WM_USER + 100)
#define WM_SOCKET_RECV (WM_USER + 101)
#define MAGIC "udcodeok"
#define MAX_PACK_SIZE (1024*60) //60KB
//控制命令
enum ChatCmd
{
ChatCmdLogin = 1, //登录
ChatCmdPrivate, //私聊
ChatCmdPublic, //群发
ChatCmdWelcome, //服务器欢迎信息
ChatCmdLoginFail, //登录失败
ChatCmdLoginSuccess, //登录成功
ChatCmdGetSessions, //获取在线列表
ChatCmdDelSession, //删除会话
};
//实现1字节对齐
#pragma pack(push, 1)
//数据包header
typedef struct _ChatProtocol
{
char magic[8];//标志位-udcodeok
UINT32 cmd; //登录-私聊-群发-客户端断开
UINT32 size; //数据大小
char data[0]; //数据指针
}ChatProtocol;
//会话列表信息
typedef struct _ChatSessions
{
USHORT port;
TCHAR ip[16];
TCHAR nickName[256];
}ChatSessions;
//聊天信息
typedef struct _ChatMsg
{
TCHAR fromNickName[256];//消息来源
TCHAR toNickName[256]; //发送给谁
char msg[0]; //消息指针
}ChatMsg;
#pragma pack(pop)
//---------------CChatClientSocket-----------------------
class CChatClientSocket : public CSocket
{
public:
CChatClientSocket(CWnd* pParent);
virtual void OnReceive(int nErrorCode);
virtual void OnClose(int nErrorCode);
CString m_nickName;
CString m_strAddress;
UINT m_nPort = 0;
private:
CWnd* m_pNotifyWnd = nullptr;
//接收缓冲区
std::string m_recvBuff;
};
//---------------CChatServerSocket-----------------------
class CChatServerSocket : public CSocket
{
public:
CChatServerSocket(CWnd* pParent);
//初始化socket
BOOL InitSocket();
//启动服务器
BOOL Start(CString bindIp, USHORT port);
//停止服务器
void Stop();
//重写OnAccept,接受客户端连接
virtual void OnAccept(int nErrorCode);
//移除客户端
void RemoveClient(CChatClientSocket* pClient);
void RomveClientByNickname(CString nickName);
//检查客户端昵称
BOOL CheckNickName(CString nickName);
//转发数据
void ForwardDataToAll(char*data,int dataSize);
void ForwardDataToOne(CString nickName, char* data, int dataSize);
private:
CWnd* m_pWnd = nullptr;
USHORT m_port = 0;
CString m_bindIp = _T("");
//保存所有客户端
std::vector<CChatClientSocket*> m_vecClient;
};
2.6 ChatServerSocket.cpp #
#include "stdafx.h"
#include "ChatServerSocket.h"
#include "ChatRoomServerDlg.h"
//---------------CChatClientSocket-----------------------
CChatClientSocket::CChatClientSocket(CWnd * pParent)
{
m_pNotifyWnd = pParent;
}
void CChatClientSocket::OnReceive(int nErrorCode)
{
if (nErrorCode != 0)
{
CSocket::OnReceive(nErrorCode);
return;
}
//接收数据
char buff[1024 * 4] = { 0 };
int nRead = this->Receive(buff, sizeof(buff));
if (nRead <= 0)
{
CSocket::OnReceive(nErrorCode);
return;
}
m_recvBuff.append(buff, nRead);
CChatRoomServerDlg* pDlg = (CChatRoomServerDlg*)m_pNotifyWnd;
//处理tcp粘包,从缓冲区截取一个完整的数据包
while (1)
{
if (m_recvBuff.size() < sizeof(ChatProtocol))
{
break;
}
ChatProtocol* hdr = (ChatProtocol*)m_recvBuff.c_str();
//检查标志位和数据大小
if (memcmp(hdr->magic, MAGIC, sizeof(hdr->magic)) != 0 || hdr->size > MAX_PACK_SIZE)
{
//非法数据,关闭客户端
m_pNotifyWnd->PostMessage(WM_SOCKET_CLOSE, (WPARAM)this);
CString strMsg;
strMsg.Format(_T("非法数据,关闭客户端-> %s:%d"), m_strAddress, m_nPort);
pDlg->LogMessage(strMsg);
CSocket::OnReceive(nErrorCode);
return;
}
if (m_recvBuff.size() < hdr->size + sizeof(ChatProtocol))
{
break;
}
//切出完整的数据包
std::string *strBuff = new std::string();
*strBuff = m_recvBuff.substr(0, hdr->size + sizeof(ChatProtocol));
//通知窗口有完整消息
m_pNotifyWnd->PostMessage(WM_SOCKET_RECV, (WPARAM)this, (LPARAM)strBuff);
//拿出剩下的数据
m_recvBuff = m_recvBuff.substr(hdr->size + sizeof(ChatProtocol));
}
CSocket::OnReceive(nErrorCode);
}
void CChatClientSocket::OnClose(int nErrorCode)
{
if (m_pNotifyWnd)
{
m_pNotifyWnd->PostMessage(WM_SOCKET_CLOSE, (WPARAM)this);
}
CSocket::OnClose(nErrorCode);
}
//---------------CChatServerSocket-----------------------
CChatServerSocket::CChatServerSocket(CWnd* pParent)
{
m_pWnd = pParent;
}
BOOL CChatServerSocket::InitSocket()
{
if (!AfxSocketInit())
{
return FALSE;
}
return TRUE;
}
BOOL CChatServerSocket::Start(CString bindIp, USHORT port)
{
CChatRoomServerDlg* pDlg = (CChatRoomServerDlg*)m_pWnd;
// 检查ip
if (bindIp.IsEmpty())
{
pDlg->LogMessage(_T("IP地址为空!"));
return FALSE;
}
// 检查端口号
if (port < 1 || port > 65535)
{
pDlg->LogMessage(_T("无效的端口号!"));
return FALSE;
}
// 创建监听套接字
if (!this->Create(port, SOCK_STREAM, bindIp))
{
pDlg->LogMessage(_T("创建监听套接字失败!"));
return FALSE;
}
//设置Socket地址复用
BOOL bOptVal = TRUE;
int bOptLen = sizeof(BOOL);
if (!this->SetSockOpt(SO_REUSEADDR, (void *)&bOptVal, bOptLen, SOL_SOCKET))
{
pDlg->LogMessage(_T("设置套接字地址复用失败!"));
this->Close();
return FALSE;
}
// 开始监听
if (!this->Listen(SOMAXCONN))
{
pDlg->LogMessage(_T("监听失败!"));
this->Close();
return FALSE;
}
m_port = port;
m_bindIp = bindIp;
CString strMsg;
strMsg.Format(_T("服务器已启动,监听地址-> %s:%d"), m_bindIp, m_port);
pDlg->LogMessage(strMsg);
return TRUE;
}
void CChatServerSocket::Stop()
{
// 关闭所有客户端连接
for (auto& client : m_vecClient)
{
if (client)
{
client->ShutDown();
client->Close();
delete client;
}
}
m_vecClient.clear();
// 关闭服务器监听
this->Close();
CChatRoomServerDlg* pDlg = (CChatRoomServerDlg*)m_pWnd;
pDlg->LogMessage(_T("停止服务器监听!"));
}
void CChatServerSocket::OnAccept(int nErrorCode)
{
CChatRoomServerDlg* pDlg = (CChatRoomServerDlg*)m_pWnd;
if (nErrorCode == 0)
{
// 创建新的客户端套接字
CChatClientSocket* pClient = new CChatClientSocket(pDlg);
if (this->Accept(*pClient))
{
// 获取客户端地址
CString strAddr;
UINT nPort;
pClient->GetPeerName(strAddr, nPort);
pClient->m_strAddress = strAddr;
pClient->m_nPort = nPort;
// 添加到客户端列表
m_vecClient.push_back(pClient);
CString strMsg;
strMsg.Format(_T("新客户端连接-> %s:%d"), strAddr, nPort);
pDlg->LogMessage(strMsg);
}
else
{
delete pClient;
pDlg->LogMessage(_T("接受客户端连接失败!"));
}
}
CSocket::OnAccept(nErrorCode);
}
void CChatServerSocket::RemoveClient(CChatClientSocket * pClient)
{
// 从客户端列表中移除
auto it = std::find(m_vecClient.begin(), m_vecClient.end(), pClient);
if (it != m_vecClient.end())
{
CChatRoomServerDlg* pDlg = (CChatRoomServerDlg*)m_pWnd;
//列表删除对应项
pDlg->DelItemFromList(pClient->m_strAddress, pClient->m_nPort, pClient->m_nickName);
int dataSize = sizeof(ChatSessions) + sizeof(ChatProtocol);
char* data = new char[dataSize]();
ChatProtocol* hdr = (ChatProtocol*)data;
hdr->cmd = ChatCmdDelSession;
memcpy(hdr->magic, MAGIC, sizeof(hdr->magic));
hdr->size = sizeof(ChatSessions);
ChatSessions* sessions = (ChatSessions*)hdr->data;
sessions->port = pClient->m_nPort;
memcpy(sessions->nickName, pClient->m_nickName.GetString(), pClient->m_nickName.GetLength() * sizeof(TCHAR));
memcpy(sessions->ip, pClient->m_strAddress.GetString(), pClient->m_strAddress.GetLength() * sizeof(TCHAR));
CString strMsg;
strMsg.Format(_T("客户端断开连接-> %s:%d,昵称:%s"), pClient->m_strAddress, pClient->m_nPort, pClient->m_nickName);
pDlg->LogMessage(strMsg);
// 关闭并删除套接字
pClient->ShutDown();
pClient->Close();
delete pClient;
m_vecClient.erase(it);
//发送删除会话信息
this->ForwardDataToAll(data, dataSize);
delete[] data;
}
}
void CChatServerSocket::RomveClientByNickname(CString nickName)
{
CChatRoomServerDlg* pDlg = (CChatRoomServerDlg*)m_pWnd;
for (auto& client : m_vecClient)
{
if (client)
{
if (client->m_nickName == nickName)
{
pDlg->LogMessage(_T("主动断开连接成功:") + nickName);
pDlg->PostMessage(WM_SOCKET_CLOSE, (WPARAM)client);
return;
}
}
}
pDlg->LogMessage(_T("断开连接失败,没有找到指定用户:") + nickName);
}
BOOL CChatServerSocket::CheckNickName(CString nickName)
{
for (auto& client : m_vecClient)
{
if (client)
{
if(client->m_nickName == nickName)
{
return FALSE;
}
}
}
return TRUE;
}
void CChatServerSocket::ForwardDataToAll(char* data, int dataSize)
{
for (auto& client : m_vecClient)
{
if (client)
{
client->Send(data, dataSize);
}
}
}
void CChatServerSocket::ForwardDataToOne(CString nickName, char * data, int dataSize)
{
for (auto& client : m_vecClient)
{
if (client)
{
if (client->m_nickName == nickName)
{
client->Send(data, dataSize);
return;
}
}
}
}
3. 客户端代码 #
3.1 ChatClientSocket.h #
#pragma once
#include <afxsock.h>
#include <string>
#define MAGIC "udcodeok"
#define MAX_PACK_SIZE (1024*60) //60KB
#define WM_SOCKET_CLOSE (WM_USER + 100)
#define WM_SOCKET_RECV (WM_USER + 101)
//控制命令
enum ChatCmd
{
ChatCmdLogin = 1, //登录
ChatCmdPrivate, //私聊
ChatCmdPublic, //群发
ChatCmdWelcome, //服务器欢迎信息
ChatCmdLoginFail, //登录失败
ChatCmdLoginSuccess, //登录成功
ChatCmdGetSessions, //获取在线列表
ChatCmdDelSession, //删除会话
};
//实现1字节对齐
#pragma pack(push, 1)
//数据头部
typedef struct _ChatProtocol
{
char magic[8];//标志位-udcodeok
UINT32 cmd; //登录-私聊-群发-客户端断开
UINT32 size; //数据大小
char data[0]; //数据指针
}ChatProtocol;
//获取会话列表
typedef struct _ChatSessions
{
USHORT port;
TCHAR ip[16];
TCHAR nickName[256];
}ChatSessions;
//聊天信息
typedef struct _ChatMsg
{
TCHAR fromNickName[256];//消息来源
TCHAR toNickName[256]; //发送给谁
char msg[0]; //消息指针
}ChatMsg;
#pragma pack(pop)
class CChatClientSocket : public CSocket
{
public:
CChatClientSocket(CWnd* pMainWnd);
~CChatClientSocket();
BOOL InitSocket();
BOOL Login(CString ip, USHORT port, CString nickName);
virtual void OnReceive(int nErrorCode);
virtual void OnClose(int nErrorCode);
CString m_serverIp;
USHORT m_serverPort;
CString m_nickName;
private:
CWnd* m_pMainWnd = nullptr;
//接收缓冲区
std::string m_recvBuff;
};
3.2 ChatClientSocket.cpp #
#include "stdafx.h"
#include "ChatClientSocket.h"
#include "ChatRoomClientDlg.h"
CChatClientSocket::CChatClientSocket(CWnd* pParent)
{
m_pMainWnd = pParent;
}
CChatClientSocket::~CChatClientSocket()
{
}
BOOL CChatClientSocket::InitSocket()
{
if (!AfxSocketInit())
{
return FALSE;
}
return TRUE;
}
BOOL CChatClientSocket::Login(CString ip, USHORT port, CString nickName)
{
m_serverIp = ip;
m_serverPort = port;
m_nickName = nickName;
CChatRoomClientDlg* pDlg = (CChatRoomClientDlg*)m_pMainWnd;
if (m_serverIp.IsEmpty() || m_serverPort < 1 || m_serverPort > 65535)
{
pDlg->LogMessage(_T("无效的服务器地址或端口!"));
return FALSE;
}
if (m_nickName.IsEmpty())
{
pDlg->LogMessage(_T("用户昵称不能为空!"));
return FALSE;
}
if (m_nickName.GetLength()>=256)
{
pDlg->LogMessage(_T("用户昵称只能小于256个字符!"));
return FALSE;
}
// 创建客户端套接字
if (!this->Create())
{
pDlg->LogMessage(_T("创建套接字失败!"));
return FALSE;
}
// 异步连接服务器
if (!this->Connect(m_serverIp, m_serverPort))
{
int nError = GetLastError();
if (nError != WSAEWOULDBLOCK)
{
pDlg->LogMessage(_T("连接服务器失败!"));
this->Close();
return FALSE;
}
}
CString strMsg;
strMsg.Format(_T("正在连接服务器-> %s:%d ..."), m_serverIp, m_serverPort);
pDlg->LogMessage(strMsg);
//发送登录信息:包hdr + 昵称
int tSize = sizeof(ChatProtocol) + (nickName.GetLength() + 1) * sizeof(TCHAR);
char* tmp = new char[tSize]();
ChatProtocol* hdr = (ChatProtocol*)tmp;
hdr->cmd = ChatCmdLogin;
memcpy(hdr->magic, MAGIC, sizeof(hdr->magic));
hdr->size = (nickName.GetLength() + 1) * sizeof(TCHAR);
memcpy(hdr->data, nickName.GetString(), hdr->size);
int nSend = this->Send(tmp, tSize);
delete[] tmp;
if (nSend <= 0)
{
pDlg->LogMessage(_T("发送登录信息失败!"));
this->Close();
return FALSE;
}
return TRUE;
}
void CChatClientSocket::OnReceive(int nErrorCode)
{
if (nErrorCode == 0)
{
CChatRoomClientDlg* pDlg = (CChatRoomClientDlg*)m_pMainWnd;
//处理tcp粘包
char buff[1024 * 4] = { 0 };
int nRead = this->Receive(buff, sizeof(buff));
if (nRead > 0)
{
m_recvBuff.append(buff, nRead);
//拿出完整的数据包
while (1)
{
if (m_recvBuff.size() < sizeof(ChatProtocol))
{
break;
}
ChatProtocol* hdr = (ChatProtocol*)m_recvBuff.c_str();
//检查标志位和数据大小
if (memcmp(hdr->magic, MAGIC, sizeof(hdr->magic)) != 0 || hdr->size > MAX_PACK_SIZE)
{
//非法数据,关闭客户端
pDlg->PostMessage(WM_SOCKET_CLOSE, (WPARAM)this);
CString strMsg;
strMsg.Format(_T("非法数据,关闭服务器连接-> %s:%d"), m_serverIp, m_serverPort);
pDlg->LogMessage(strMsg);
CSocket::OnReceive(nErrorCode);
return;
}
if (m_recvBuff.size() < hdr->size + sizeof(ChatProtocol))
{
break;
}
//切出完整的数据包
std::string *strBuff = new std::string();
*strBuff = m_recvBuff.substr(0, hdr->size + sizeof(ChatProtocol));
//通知窗口有完整消息
m_pMainWnd->PostMessage(WM_SOCKET_RECV, (WPARAM)this, (LPARAM)strBuff);
//拿出剩下的数据
m_recvBuff = m_recvBuff.substr(hdr->size + sizeof(ChatProtocol));
}
}
}
CSocket::OnReceive(nErrorCode);
}
void CChatClientSocket::OnClose(int nErrorCode)
{
if (m_pMainWnd)
{
m_pMainWnd->PostMessage(WM_SOCKET_CLOSE, (WPARAM)this);
}
CSocket::OnClose(nErrorCode);
}
3.3 ChatRoomClient.h #
// ChatRoomClient.h : PROJECT_NAME 应用程序的主头文件
#pragma once
#ifndef __AFXWIN_H__
#error "在包含此文件之前包含“stdafx.h”以生成 PCH 文件"
#endif
#include "resource.h" // 主符号
// CChatRoomClientApp:
// 有关此类的实现,请参阅 ChatRoomClient.cpp
//
class CChatRoomClientApp : public CWinApp
{
public:
CChatRoomClientApp();
// 重写
public:
virtual BOOL InitInstance();
// 实现
DECLARE_MESSAGE_MAP()
};
extern CChatRoomClientApp theApp;
3.4 ChatRoomClient.cpp #
// ChatRoomClient.cpp : 定义应用程序的类行为。
#include "stdafx.h"
#include "ChatRoomClient.h"
#include "ChatRoomClientDlg.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
// CChatRoomClientApp
BEGIN_MESSAGE_MAP(CChatRoomClientApp, CWinApp)
ON_COMMAND(ID_HELP, &CWinApp::OnHelp)
END_MESSAGE_MAP()
// CChatRoomClientApp 构造
CChatRoomClientApp::CChatRoomClientApp()
{
// 支持重新启动管理器
m_dwRestartManagerSupportFlags = AFX_RESTART_MANAGER_SUPPORT_RESTART;
// TODO: 在此处添加构造代码,
// 将所有重要的初始化放置在 InitInstance 中
}
// 唯一的一个 CChatRoomClientApp 对象
CChatRoomClientApp theApp;
// CChatRoomClientApp 初始化
BOOL CChatRoomClientApp::InitInstance()
{
// 如果一个运行在 Windows XP 上的应用程序清单指定要
// 使用 ComCtl32.dll 版本 6 或更高版本来启用可视化方式,
//则需要 InitCommonControlsEx()。 否则,将无法创建窗口。
INITCOMMONCONTROLSEX InitCtrls;
InitCtrls.dwSize = sizeof(InitCtrls);
// 将它设置为包括所有要在应用程序中使用的
// 公共控件类。
InitCtrls.dwICC = ICC_WIN95_CLASSES;
InitCommonControlsEx(&InitCtrls);
CWinApp::InitInstance();
AfxEnableControlContainer();
// 创建 shell 管理器,以防对话框包含
// 任何 shell 树视图控件或 shell 列表视图控件。
CShellManager *pShellManager = new CShellManager;
// 激活“Windows Native”视觉管理器,以便在 MFC 控件中启用主题
CMFCVisualManager::SetDefaultManager(RUNTIME_CLASS(CMFCVisualManagerWindows));
// 标准初始化
// 如果未使用这些功能并希望减小
// 最终可执行文件的大小,则应移除下列
// 不需要的特定初始化例程
// 更改用于存储设置的注册表项
// TODO: 应适当修改该字符串,
// 例如修改为公司或组织名
SetRegistryKey(_T("应用程序向导生成的本地应用程序"));
CChatRoomClientDlg dlg;
m_pMainWnd = &dlg;
INT_PTR nResponse = dlg.DoModal();
if (nResponse == IDOK)
{
// TODO: 在此放置处理何时用
// “确定”来关闭对话框的代码
}
else if (nResponse == IDCANCEL)
{
// TODO: 在此放置处理何时用
// “取消”来关闭对话框的代码
}
else if (nResponse == -1)
{
TRACE(traceAppMsg, 0, "警告: 对话框创建失败,应用程序将意外终止。\n");
TRACE(traceAppMsg, 0, "警告: 如果您在对话框上使用 MFC 控件,则无法 #define _AFX_NO_MFC_CONTROLS_IN_DIALOGS。\n");
}
// 删除上面创建的 shell 管理器。
if (pShellManager != NULL)
{
delete pShellManager;
}
#ifndef _AFXDLL
ControlBarCleanUp();
#endif
// 由于对话框已关闭,所以将返回 FALSE 以便退出应用程序,
// 而不是启动应用程序的消息泵。
return FALSE;
}
3.5 ChatRoomClientDlg.h #
// ChatRoomClientDlg.h : 头文件
#pragma once
#include "ChatClientSocket.h"
#include "afxcmn.h"
// CChatRoomClientDlg 对话框
class CChatRoomClientDlg : public CDialogEx
{
// 构造
public:
CChatRoomClientDlg(CWnd* pParent = NULL); // 标准构造函数
~CChatRoomClientDlg();
void LogMessage(LPCTSTR lpszMessage);
void ShowChatMessage(LPCTSTR lpszMessage);
void SaveLogToFile(CString msg,CString path = _T("ChatLogs.txt"));
// 对话框数据
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_CHATROOMCLIENT_DIALOG };
#endif
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持
// 实现
protected:
HICON m_hIcon;
// 生成的消息映射函数
virtual BOOL OnInitDialog();
afx_msg void OnSysCommand(UINT nID, LPARAM lParam);
afx_msg void OnPaint();
afx_msg HCURSOR OnQueryDragIcon();
afx_msg void OnBnClickedButtonConnect();
afx_msg void OnBnClickedButtonDisconnect();
// 处理客户端断开
afx_msg LRESULT OnClientSocketClose(WPARAM wParam, LPARAM lParam);
// 处理客户端数据接收
afx_msg LRESULT OnClientSocketRecv(WPARAM wParam, LPARAM lParam);
afx_msg void OnBnClickedButtonSendMsg();
afx_msg void OnBnClickedButtonChatHistory();
DECLARE_MESSAGE_MAP()
private:
CString FormatCurrentTime(LPCTSTR format = _T("%Y-%m-%d %H:%M:%S"));
// 初始化List Control
void InitListControl();
// 插入数据到list
void InsertItemToList(CString ip, USHORT port, CString nickName);
// 从list删除数据
void DelItemFromList(CString ip, USHORT port, CString nickName);
CChatClientSocket* m_pClient = nullptr;
CListCtrl m_listSessions;
public:
afx_msg void OnBnClickedButtonExport();
};
3.6 ChatRoomClientDlg.cpp #
// ChatRoomClientDlg.cpp : 实现文件
#include "stdafx.h"
#include "ChatRoomClient.h"
#include "ChatRoomClientDlg.h"
#include "afxdialogex.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
// 用于应用程序“关于”菜单项的 CAboutDlg 对话框
class CAboutDlg : public CDialogEx
{
public:
CAboutDlg();
// 对话框数据
#ifdef AFX_DESIGN_TIME
enum { IDD = IDD_ABOUTBOX };
#endif
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV 支持
// 实现
protected:
DECLARE_MESSAGE_MAP()
};
CAboutDlg::CAboutDlg() : CDialogEx(IDD_ABOUTBOX)
{
}
void CAboutDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
}
BEGIN_MESSAGE_MAP(CAboutDlg, CDialogEx)
END_MESSAGE_MAP()
// CChatRoomClientDlg 对话框
CChatRoomClientDlg::CChatRoomClientDlg(CWnd* pParent /*=NULL*/)
: CDialogEx(IDD_CHATROOMCLIENT_DIALOG, pParent)
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
CChatRoomClientDlg::~CChatRoomClientDlg()
{
}
void CChatRoomClientDlg::LogMessage(LPCTSTR lpszMessage)
{
CString strText;
CEdit* edit = (CEdit*)GetDlgItem(IDC_EDIT_LOGS);
edit->GetWindowText(strText);
CString strNewText;
if (strText.GetLength() > 0)
{
strNewText.Format(_T("%s\r\n[%s] %s"), strText, FormatCurrentTime(), lpszMessage);
}
else
{
strNewText.Format(_T("[%s] %s"), FormatCurrentTime(), lpszMessage);
}
edit->SetWindowText(strNewText);
edit->LineScroll(edit->GetLineCount());
}
void CChatRoomClientDlg::ShowChatMessage(LPCTSTR lpszMessage)
{
CString strText;
CEdit* edit = (CEdit*)GetDlgItem(IDC_EDIT_CHAT_MSG);
edit->GetWindowText(strText);
CString strNewText;
if (strText.GetLength() > 0)
{
strNewText.Format(_T("%s\r\n[%s] %s"), strText, FormatCurrentTime(), lpszMessage);
}
else
{
strNewText.Format(_T("[%s] %s"), FormatCurrentTime(), lpszMessage);
}
edit->SetWindowText(strNewText);
edit->LineScroll(edit->GetLineCount());
CString writeStr;
writeStr.Format(_T("[%s] %s\r\n"), FormatCurrentTime(), lpszMessage);
SaveLogToFile(writeStr);
}
void CChatRoomClientDlg::SaveLogToFile(CString msg, CString path)
{
CStdioFile file;
// 追加模式打开(不覆盖原有内容)
if (file.Open(path.GetString(),
CStdioFile::modeCreate | // 如果不存在则创建
CStdioFile::modeNoTruncate | // 保留原有内容
CStdioFile::modeReadWrite)) // 读写权限(追加需要)
{
// 移动到文件末尾
ULONGLONG seek = file.SeekToEnd();
if (seek == 0)
{
const BYTE bom[] = { 0xEF, 0xBB, 0xBF };
file.Write(bom, 3);
}
CStringA utf8 = CW2A(msg, CP_UTF8);
file.Write(utf8, utf8.GetLength());
file.Close();
}
}
void CChatRoomClientDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_LIST_SESSIONS, m_listSessions);
}
BEGIN_MESSAGE_MAP(CChatRoomClientDlg, CDialogEx)
ON_WM_SYSCOMMAND()
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(IDC_BUTTON_CONNECT, &CChatRoomClientDlg::OnBnClickedButtonConnect)
ON_BN_CLICKED(IDC_BUTTON_DISCONNECT, &CChatRoomClientDlg::OnBnClickedButtonDisconnect)
ON_MESSAGE(WM_SOCKET_CLOSE, &CChatRoomClientDlg::OnClientSocketClose)
ON_MESSAGE(WM_SOCKET_RECV, &CChatRoomClientDlg::OnClientSocketRecv)
ON_BN_CLICKED(IDC_BUTTON_SEND_MSG, &CChatRoomClientDlg::OnBnClickedButtonSendMsg)
ON_BN_CLICKED(IDC_BUTTON_CHAT_HISTORY, &CChatRoomClientDlg::OnBnClickedButtonChatHistory)
ON_BN_CLICKED(IDC_BUTTON_EXPORT, &CChatRoomClientDlg::OnBnClickedButtonExport)
END_MESSAGE_MAP()
// CChatRoomClientDlg 消息处理程序
BOOL CChatRoomClientDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
// 将“关于...”菜单项添加到系统菜单中。
// IDM_ABOUTBOX 必须在系统命令范围内。
ASSERT((IDM_ABOUTBOX & 0xFFF0) == IDM_ABOUTBOX);
ASSERT(IDM_ABOUTBOX < 0xF000);
CMenu* pSysMenu = GetSystemMenu(FALSE);
if (pSysMenu != NULL)
{
BOOL bNameValid;
CString strAboutMenu;
bNameValid = strAboutMenu.LoadString(IDS_ABOUTBOX);
ASSERT(bNameValid);
if (!strAboutMenu.IsEmpty())
{
pSysMenu->AppendMenu(MF_SEPARATOR);
pSysMenu->AppendMenu(MF_STRING, IDM_ABOUTBOX, strAboutMenu);
}
}
// 设置此对话框的图标。 当应用程序主窗口不是对话框时,框架将自动
// 执行此操作
SetIcon(m_hIcon, TRUE); // 设置大图标
SetIcon(m_hIcon, FALSE); // 设置小图标
// TODO: 在此添加额外的初始化代码
LogMessage(_T("聊天室客户端启动..."));
//client socket初始化
m_pClient = new CChatClientSocket(this);
if (m_pClient->InitSocket() == FALSE)
{
LogMessage(_T("Winsock初始化失败!"));
delete m_pClient;
m_pClient = nullptr;
}
else
{
LogMessage(_T("Winsock初始化成功!"));
}
GetDlgItem(IDC_BUTTON_CONNECT)->EnableWindow(TRUE);
GetDlgItem(IDC_BUTTON_DISCONNECT)->EnableWindow(FALSE);
InitListControl();
//for debug
GetDlgItem(IDC_IPADDRESS_SERVER_IP)->SetWindowText(_T("127.0.0.1"));
GetDlgItem(IDC_EDIT_SERVER_PORT)->SetWindowText(_T("1080"));
GetDlgItem(IDC_EDIT_NICKNAME)->SetWindowText(_T("test"));
return TRUE; // 除非将焦点设置到控件,否则返回 TRUE
}
void CChatRoomClientDlg::OnSysCommand(UINT nID, LPARAM lParam)
{
if ((nID & 0xFFF0) == IDM_ABOUTBOX)
{
CAboutDlg dlgAbout;
dlgAbout.DoModal();
}
else
{
CDialogEx::OnSysCommand(nID, lParam);
}
}
// 如果向对话框添加最小化按钮,则需要下面的代码
// 来绘制该图标。 对于使用文档/视图模型的 MFC 应用程序,
// 这将由框架自动完成。
void CChatRoomClientDlg::OnPaint()
{
if (IsIconic())
{
CPaintDC dc(this); // 用于绘制的设备上下文
SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);
// 使图标在工作区矩形中居中
int cxIcon = GetSystemMetrics(SM_CXICON);
int cyIcon = GetSystemMetrics(SM_CYICON);
CRect rect;
GetClientRect(&rect);
int x = (rect.Width() - cxIcon + 1) / 2;
int y = (rect.Height() - cyIcon + 1) / 2;
// 绘制图标
dc.DrawIcon(x, y, m_hIcon);
}
else
{
CDialogEx::OnPaint();
}
}
//当用户拖动最小化窗口时系统调用此函数取得光标
//显示。
HCURSOR CChatRoomClientDlg::OnQueryDragIcon()
{
return static_cast<HCURSOR>(m_hIcon);
}
CString CChatRoomClientDlg::FormatCurrentTime(LPCTSTR format)
{
CTime time = CTime::GetCurrentTime();
return time.Format(format);
}
void CChatRoomClientDlg::OnBnClickedButtonConnect()
{
// TODO: 在此添加控件通知处理程序代码
if (m_pClient)
{
CString ip;
CString port;
CString nickName;
GetDlgItem(IDC_IPADDRESS_SERVER_IP)->GetWindowText(ip);
GetDlgItem(IDC_EDIT_SERVER_PORT)->GetWindowText(port);
GetDlgItem(IDC_EDIT_NICKNAME)->GetWindowText(nickName);
if (m_pClient->Login(ip, _ttoi(port), nickName)==FALSE)
{
GetDlgItem(IDC_BUTTON_CONNECT)->EnableWindow(TRUE);
GetDlgItem(IDC_BUTTON_DISCONNECT)->EnableWindow(FALSE);
}
else
{
m_listSessions.DeleteAllItems();
GetDlgItem(IDC_BUTTON_CONNECT)->EnableWindow(FALSE);
GetDlgItem(IDC_BUTTON_DISCONNECT)->EnableWindow(TRUE);
}
}
else
{
LogMessage(_T("客户端socket创建失败!"));
GetDlgItem(IDC_BUTTON_CONNECT)->EnableWindow(TRUE);
GetDlgItem(IDC_BUTTON_DISCONNECT)->EnableWindow(FALSE);
}
}
void CChatRoomClientDlg::OnBnClickedButtonDisconnect()
{
// TODO: 在此添加控件通知处理程序代码
if (m_pClient)
{
m_pClient->ShutDown();
m_pClient->Close();
}
LogMessage(_T("客户端socket断开连接!"));
GetDlgItem(IDC_BUTTON_CONNECT)->EnableWindow(TRUE);
GetDlgItem(IDC_BUTTON_DISCONNECT)->EnableWindow(FALSE);
}
LRESULT CChatRoomClientDlg::OnClientSocketClose(WPARAM wParam, LPARAM lParam)
{
if (m_pClient)
{
m_pClient->ShutDown();
m_pClient->Close();
}
LogMessage(_T("客户端socket断开连接!"));
GetDlgItem(IDC_BUTTON_CONNECT)->EnableWindow(TRUE);
GetDlgItem(IDC_BUTTON_DISCONNECT)->EnableWindow(FALSE);
return LRESULT();
}
LRESULT CChatRoomClientDlg::OnClientSocketRecv(WPARAM wParam, LPARAM lParam)
{
CChatClientSocket* pClient = (CChatClientSocket*)wParam;
std::string* strBuff = (std::string*)lParam;
//解析数据
ChatProtocol* hdr = (ChatProtocol*)strBuff->data();
if (hdr->cmd == ChatCmdLoginFail)//客户端登录失败
{
CString strMsg;
strMsg.Format(_T("客户端登录失败:%s"), (TCHAR*)hdr->data);
LogMessage(strMsg);
this->PostMessage(WM_SOCKET_CLOSE, (WPARAM)pClient);
}
else if (hdr->cmd == ChatCmdLoginSuccess)//客户端登录成功
{
CString strMsg;
strMsg.Format(_T("用户登录成功:%s"), pClient->m_nickName);
LogMessage(strMsg);
}
else if (hdr->cmd == ChatCmdGetSessions)
{
m_listSessions.DeleteAllItems();
ChatSessions* sessions = (ChatSessions*)hdr->data;
for (int i = 0; i < (hdr->size / sizeof(ChatSessions)); i++)
{
InsertItemToList(sessions[i].ip, sessions[i].port, sessions[i].nickName);
}
}
else if (hdr->cmd == ChatCmdDelSession)
{
ChatSessions* sessions = (ChatSessions*)hdr->data;
DelItemFromList(sessions->ip, sessions->port, sessions->nickName);
CString strMsg;
strMsg.Format(_T("[服务器群发]-> 用户退出(%s)-> %s:%d"), sessions->nickName,sessions->ip, sessions->port);
ShowChatMessage(strMsg);
}
else if (hdr->cmd == ChatCmdWelcome)
{
ShowChatMessage((TCHAR*)hdr->data);
}
else if (hdr->cmd == ChatCmdPublic)
{
ChatMsg* pMsg = (ChatMsg*)hdr->data;
CString strMsg;
strMsg.Format(_T("[%s群发]-> %s"), pMsg->fromNickName, (TCHAR*)pMsg->msg);
ShowChatMessage(strMsg);
}
else if (hdr->cmd == ChatCmdPrivate)
{
ChatMsg* pMsg = (ChatMsg*)hdr->data;
CString strMsg;
strMsg.Format(_T("[%s 私聊 %s]-> %s"), pMsg->fromNickName, pMsg->toNickName,(TCHAR*)pMsg->msg);
ShowChatMessage(strMsg);
}
delete strBuff;
return LRESULT();
}
void CChatRoomClientDlg::InitListControl()
{
// 设置扩展样式
m_listSessions.SetExtendedStyle(LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES | LVS_EX_DOUBLEBUFFER);
// 添加列
m_listSessions.InsertColumn(0, _T("昵称"), LVCFMT_LEFT, 300);
m_listSessions.InsertColumn(1, _T("IP地址"), LVCFMT_LEFT, 200);
m_listSessions.InsertColumn(2, _T("端口号"), LVCFMT_LEFT, 100);
}
void CChatRoomClientDlg::InsertItemToList(CString ip, USHORT port, CString nickName)
{
// 插入主项
int nIndex = m_listSessions.InsertItem(0, nickName);
// 设置子项
m_listSessions.SetItemText(nIndex, 1, ip);
CString strPort;
strPort.Format(_T("%d"), port);
m_listSessions.SetItemText(nIndex, 2, strPort);
}
void CChatRoomClientDlg::DelItemFromList(CString ip, USHORT port, CString nickName)
{
CString strPort;
strPort.Format(_T("%d"), port);
int nCount = m_listSessions.GetItemCount();
for (int i = 0; i < nCount; i++)
{
// 获取项数据
CString listNickName = m_listSessions.GetItemText(i, 0); // 昵称
CString listIp = m_listSessions.GetItemText(i, 1); // ip
CString listPort = m_listSessions.GetItemText(i, 2); // port
if (listIp == ip && listPort == strPort && listNickName == nickName)
{
m_listSessions.DeleteItem(i);
return;
}
}
}
void CChatRoomClientDlg::OnBnClickedButtonSendMsg()
{
// TODO: 在此添加控件通知处理程序代码
if (!m_pClient)
{
LogMessage(_T("客户端初始化失败!"));
return;
}
CString input;
GetDlgItem(IDC_EDIT_INPUT)->GetWindowText(input);
if (input.IsEmpty())
{
LogMessage(_T("输入框为空!"));
return;
}
CString toNickName;
GetDlgItem(IDC_EDIT_CHAT_NICKNAME)->GetWindowText(toNickName);
if (toNickName == m_pClient->m_nickName)
{
LogMessage(_T("不能给自己发送消息!"));
return;
}
int dataSize = sizeof(ChatProtocol) + sizeof(ChatMsg) + (input.GetLength() + 1) * sizeof(TCHAR);
char* data = new char[dataSize]();
ChatProtocol* hdr = (ChatProtocol*)data;
hdr->size = dataSize - sizeof(ChatProtocol);
memcpy(hdr->magic, MAGIC, sizeof(hdr->magic));
ChatMsg* pMsg = (ChatMsg*)hdr->data;
memcpy(pMsg->fromNickName, m_pClient->m_nickName.GetString(), m_pClient->m_nickName.GetLength()*sizeof(TCHAR));
memcpy(pMsg->msg, input.GetString(), input.GetLength() * sizeof(TCHAR));
if (toNickName.IsEmpty())
{
//群发消息
hdr->cmd = ChatCmdPublic;
}
else
{
//私聊
hdr->cmd = ChatCmdPrivate;
memcpy(pMsg->toNickName, toNickName.GetString(), toNickName.GetLength() * sizeof(TCHAR));
}
if (m_pClient->Send(data, dataSize) <= 0)
{
this->PostMessage(WM_SOCKET_CLOSE, (WPARAM)m_pClient);
}
delete[] data;
GetDlgItem(IDC_EDIT_INPUT)->SetWindowText(_T(""));
}
void CChatRoomClientDlg::OnBnClickedButtonChatHistory()
{
// TODO: 在此添加控件通知处理程序代码
WinExec("notepad ChatLogs.txt",SW_SHOW);
}
void CChatRoomClientDlg::OnBnClickedButtonExport()
{
// TODO: 在此添加控件通知处理程序代码
CString path = FormatCurrentTime(_T("%Y-%m-%d %H-%M-%S ")) + _T("ExportChatLogs.Txt");
CString msg;
GetDlgItem(IDC_EDIT_CHAT_MSG)->GetWindowText(msg);
CStdioFile file;
// 追加模式打开(不覆盖原有内容)
if (file.Open(path.GetString(),
CStdioFile::modeCreate | // 如果不存在则创建
CStdioFile::modeNoTruncate | // 保留原有内容
CStdioFile::modeReadWrite)) // 读写权限(追加需要)
{
// 移动到文件末尾
ULONGLONG seek = file.SeekToEnd();
if (seek == 0)
{
const BYTE bom[] = { 0xEF, 0xBB, 0xBF };
file.Write(bom, 3);
}
CStringA utf8 = CW2A(msg, CP_UTF8);
file.Write(utf8, utf8.GetLength());
file.Close();
LogMessage(_T("导出聊天记录成功,文件路径:[")+ path + _T("]"));
}
}