Skip to main content
  1. Posts/

12h慢通大学生《网络编程》考试

本文阅读量
·19 mins·
Windows 网络编程
udcode
Author
udcode
程序员A-DU
Table of Contents

12h慢通大学生《网络编程》考试
#

  • 项目代码就不上传github,放在这边,留给有需要的人。
  • 相关视频在B站,https://space.bilibili.com/1873697345
  • 需要一对一编程培训请联系:
    Img

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("]"));
	}
}

Related

Win11 24H2 远程桌面客户端不定时断连
·1 min
Windows Rdp
冒险岛N web3链游
·2 mins
Game 冒险岛 Web3
淑玉NEXT工具箱
·1 min
Windows 淑玉NEXT工具箱
Windows系统运行库
·3 mins
Windows 系统运行库 MSVCRXXX.dll DirectX .NET
安装OpenWrt软路由前置篇
·1 min
软路由 OpenWrt
速通OpenWrt-24.10软路由实操篇
·4 mins
软路由 OpenWrt ImmortalWRT N100 Itx