基于C++、MFC和Windows套接字实现的简单聊天室程序开发

news2024/10/6 10:35:14

一、一个简单的聊天室程序

该程序由服务器端客户端两个项目组成,这两个项目均基于对话框的程序。服务器端项目负责管理客户端的上线、离线状态,以及转发客户端发送的信息。客户端项目则负责向服务器发送信息,并接收来自服务器的信息,并将接收到的信息显示在客户端界面上。

1.1创建服务器端界面
1.创建项目

打开 VS2022 软件,选择创建 MFC 项目,项目名称为ChatServer。选择应用程序类型为基于对话框。在创建项目的过程中,在“高级功能”选项中勾选“Windows 套接字”。具体步骤如下所示:

  1. 启动 VS2022 软件。
  2. 在“启动窗口”中,选择“创建新项目”。
  3. 在“创建新项目”窗口中,搜索并选择“MFC 应用程序”,然后点击“下一步”。
  4. 在“配置新项目”窗口中,将项目名称设置为“ChatServer”,并选择项目的保存位置。
  5. 点击“创建”按钮。
  6. 在“创建 MFC 应用程序”向导中,选择“基于对话框”的应用程序类型。
  7. 在“高级功能”选项中,勾选“Windows 套接字”。
  8. 完成所有设置后,点击“完成”按钮,VS2022 将生成一个基于对话框的 MFC 项目,并启用 Windows 套接字功能。

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

最终进入的界面如下所示:
在这里插入图片描述

2.编辑对话框资源

打开对话框资源,并进行以下操作以设置对话框的标题和添加所需的控件:

  1. 设置对话框标题和边框样式

    • 在解决方案资源管理器中,展开“资源文件夹”并找到“Dialog”资源。
    • 双击主对话框资源(IDD_CHATSERVER_DIALOG`)以打开对话框编辑器或如上图已经打开对话框编辑器。
    • 右键点击对话框的空白区域,选择“属性”(Properties)。
    • 在属性窗口中,找到“描述文字”属性,将其值设置为“聊天室服务器端”。
    • 找到“边框”属性,将其值设置为“对话框外框”。
  2. 删除对话框中原有的控件

    • 选择对话框中默认生成的所有控件(如静态文本、按钮等),然后按 Delete 键或按鼠标右键显示删除按钮删除这些控件。
  3. 添加新的控件

    • 在工具箱中找到并选择需要的控件,然后将其拖放到对话框上进行布局。
    • 具体控件和布局请根据需求进行设置,例如:
      • 编辑框(Edit Control):用于显示和输入文本信息。
      • 按钮(Button):用于执行启动、停止服务器等操作。
      • 列表框(List Box)或列表控件(List Control):用于显示客户端连接信息或聊天记录。

在这里插入图片描述

按照上述设置添加如下所示的控件:

在这里插入图片描述

点击控件并右键选择“属性”选项,即可更改控件的设置,例如描述文字等。按照下表中的设置进行调整:

控件名ID其他属性
编辑框(已连接客户端右侧)IDC_EDIT_CLIENTS只读:true
编辑框(中间较大的编辑框)IDC_ENIT_MSG只读:true;多行:true;垂直滚动:true
IP地址IDC_IPADDRESS
编辑框(PORT右侧)IDC_EDIT_PORT
按钮(启动服务器)IDC_START文字描述:启动服务器
按钮(退出程序)IDC_EXIT文字描述:退出程序

最终效果如下所示:

在这里插入图片描述

3.为控件添加关联变量

为了方便获取和设置对话框中控件的值,可以为对话框的部分控件添加关联变量。在解决方案资源管理器中,右击项目名称ChatServer,在弹出的快捷菜单中选择“类向导”,打开“类向导”对话框。在类向导对话框中,确保项目名称是ChatServer,在“类名”下方选择CChartServerDlg,单击“成员变量”标签,流程如下所示:

在这里插入图片描述

在这里插入图片描述

下面为控件IDC_EDIT_CLIENTS添加关联变量。在上图中,选中IDC_EDIT_CLIENTS,单击“添加变量”按钮,出现“添加控制变量”对话框。在“类别”下方选择“值”“名称”下方输入m_nClients“变量类型”下方输入unsigned int,单击“完成”按钮,返回到“类向导”对话框,控件IDC_EDIT_CLIENTS的关联变量m_nClients添加完毕。流程如下所示:

在这里插入图片描述
在这里插入图片描述

按照上面同样的方法为其他控件添加关联变量,最终控件的关联变量以及类型如下表所示:

ID类别变量类型变量名
IDC_IPADDRESS控件CIPAddressCtrlm_ipServer
IDC_EDIT_PORTunsigned intm_nPort
IDC_EDIT_CLIENTSunsigned intm_nClients
IDC_EDIT_MSG控件CEditm_edtMsg

添加完之后如下所示:

在这里插入图片描述

4.对话框初始化

为了避免每次运行服务器端程序都要输入IP地址、端口等数据,可为这两个控件设置初值。打开文件ChatServerDlg.cpp,找到对话框初始化函数OnInitDialog(),在“//TODO:”一行的后面输入以下代码:

	// TODO: 在此添加额外的初始化代码
	m_ipServer.SetAddress(127, 0, 0, 1);
	m_nPort = 8888;
	m_nClients = 0;
	UpdateData(false);

在这里插入图片描述

接下来编译运行程序:

在这里插入图片描述

程序运行结果如下所示:

在这里插入图片描述

上面添加的初始化代码,为IP地址控件设置初值“127.0.0.1”,为端口编辑框关联变量m_nPort赋值8888,为已连接客户端编辑框关联变量m_nClients赋值0,最后调用函数UpdateData(false),将控件关联变量的值显示在对应的控件中。如果函数UpdateData()的参数为true,则将控件中显示的值保存在对应的关联变量中。

1.2创建客户端界面
1.创建项目

与ChatServer项目类似,创建MFC项目ChatClient,应用程序类型选择基于对话框,在“高级功能”中勾选“Windows套接字”

2.编辑对话框资源

打开对话框资源,将对话框的标题(文字描述)设置为“聊天室客户端”,边框选择“对话框外框”。删除对话框中原有的控件,添加如下图所示的控件:

在这里插入图片描述

除了静态控件,对话框中其他控件的ID以及属性如下表:

控件名ID其他属性
编辑框(中间较大的编辑框)IDC_EDIT_MSG只读:true;多行:true;垂直滚动:true
编辑框(左下角)IDC_EDIT_SEND
IP地址IDC_IPADDRESS
编辑框(服务器端口)IDC_EDIT_PORT
按钮IDC_LOGIN文字描述:登录
按钮IDC_EXIT文字描述:退出程序
按钮IDC_SEND文字描述:发送
3.为控件添加关联变量

与服务器项目类似,为客户端对话框的部分部件添加关联变量,具体添加方法与服务器端对话框相同,添加的关联变量如下表:

ID变量类型变量类型变量名
IDC_IPADDRESS控件CIPAddressCtrlm_ipServer
IDC_EDIT_PORTunsigned intm_nPort
IDC_EDIT_SENDCStringm_strSend
IDC_EDIT_MSG控件CEditm_edtMsg

在这里插入图片描述

4.对话框初始化

与服务器对话框类似,在客户端对话框的初始化函数中为IP地址和端口提供初始化值。打开文件ChatClientDlg.cpp,找到对话框初始化函数OnInitDialog(),在“//TODO:”一行的后面输入以下代码:

	// TODO: 在此添加额外的初始化代码
	m_ipServer.SetAddress(127, 0, 0, 1);
	m_nPort = 8888;
	UpdateData(false);

编译运行程序,程序运行结果如下所示:

在这里插入图片描述

1.3服务器端编程
1.对话框类中添加数据成员

因为在对话框类中CChatServerDlg中需要用到vector,则在ChatServerDlg.h文件的开始部分加入以下两行代码:

#include<vector>
using namespace std;

在服务器端要记录每个连接客户端的信息,首先要定义一个保存客户端信息的类ClientItem,之后用vector保存所有连接客户端的信息

在ChatServerDlg.h文件中,找到服务器端对话框类CChatServerDlg的定义处,在其上方添加ClientItem类的定义:

class CChatServerDlg;
class ClientItem {
public:
	SOCKET cltSocket;
	CString cltIP;
	int cltPort;
	CChatServerDlg* pChatDlg;
	ClientItem(int cltPort=8888, SOCKET cltSocket=INVALID_SOCKET, CChatServerDlg*  pChatDlg=nullptr)
		:cltPort(cltPort),cltSocket(cltSocket), pChatDlg(pChatDlg){}
};

从上面的代码可以看到,需要保存的客户端信息有对应的socket、IP地址、端口。为了能在ClientItem类中访问对话框类的成员,可以再ClientItem类中使用属性pChatDlg保存服务器端对话框的地址。

在CChatServerDlg类中加入以下两个属性:

	//监听客户端连接请求的socket以及保存连接用户端的信息
	SOCKET m_listenSocket;
	vector<ClientItem*> m_clients;

第一个属性是监听客户端连接请求的socket;第二个属性是一个向量,用于保存连接客户端的信息

在构造函数中将加入以下代码:

CChatServerDlg::CChatServerDlg(CWnd* pParent /*=nullptr*/)
	: CDialogEx(IDD_CHATSERVER_DIALOG, pParent)
	, m_nClients(0)
	, m_nPort(0)
{
	m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
	m_listenSocket = INVALID_SOCKET;
}

将m_listenSocket 初始化为 INVALID_SOCKET。

2.添加“启动服务器”按钮的消息响应函数

使用类向导添加“启动服务器”按钮的消息响应函数,打开“类向导”对话框,选择项目ChatServer,选择类CChatServerDlg,然后选择“命令”标签,找到并选中IDC_START,在“消息”中选择BN_CLICKED,如下图所示:

在这里插入图片描述

在上图中,单击“添加处理程序”按钮,完成“启动服务器”按钮的消息响应函数OnClickedStart()的添加,在函数OnClickedStart()中添加如下代码:

void CChatServerDlg::OnClickedStart()
{
	// TODO: 在此添加控件通知处理程序代码
	UpdateData(true);							//将对话框控件中的值保存到控件关联变量中
	DWORD dwIP;									//32位无符号整数表示的IP
	m_ipServer.GetAddress(dwIP);				//获取控件中的IP地址
	m_listenSocket = socket(AF_INET, SOCK_STREAM, 0);
	if (m_listenSocket == INVALID_SOCKET) {
		AfxMessageBox(_T("创建socket失败"));
		return;
	}
	//绑定端口和IP
	sockaddr_in serverAddr = { 0 };
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(m_nPort);
	serverAddr.sin_addr.s_addr = htonl(dwIP);
	if (bind(m_listenSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) {
		AfxMessageBox(_T("绑定失败"));
		return;
	}
	//开始监听(listen())
	if (INVALID_SOCKET == listen(m_listenSocket, 5)) {
		AfxMessageBox(_T("监听失败"));
		return;
	}
	m_edtMsg.SetWindowTextW(_T("服务器已启动!\r\n"));
	GetDlgItem(IDC_START)->EnableWindow(false);
	//创建监听线程,监听线程函数名为AccpThreadProc
	HANDLE h = CreateThread(nullptr, 0, AccpThreadProc, this, 0, nullptr);
	if (h != nullptr)
		CloseHandle(h);
	return;
}
3.创建监听线程函数

下面给出监听线程函数的定义。将监听线程函数定义为类的静态成员函数,首先在CChatServerDlg类中给出函数AccpThreadProc()的声明,代码如下:

static DWORD WINAPI AccpThreadProc(LPVOID pParam);

然后给出函数的定义,代码如下:

//用于监听的线程函数,监听线程函数作为类的成员函数必须需定义为静态函数
DWORD WINAPI CChatServerDlg::AccpThreadProc(LPVOID pParam) {
	CChatServerDlg* thisDlg = (CChatServerDlg*)pParam;
	while (true) {
		sockaddr_in clientAddr = { 0 };
		int iLen = sizeof(sockaddr_in);
		//accept()函数取出客户端请求连接队列中最前面的请求,并创建与该客户端通信的套接字
		SOCKET accSock = accept(thisDlg->m_listenSocket, (sockaddr*)&clientAddr, &iLen);
		if (accSock == INVALID_SOCKET) {
			AfxMessageBox(_T("监听失败! "));
			return 0;
		}
		ClientItem* pClient = new ClientItem();		//创建一个CClient对象
		pClient->cltSocket = accSock;
		char IP[20];			//点分十进制表示的IP
		inet_ntop(AF_INET, (void*)&clientAddr.sin_addr, IP, 16);
		pClient->cltIP.Format(_T("%S"), IP);			//IP地址
		pClient->cltPort = clientAddr.sin_port;		//端口
		pClient->pChatDlg = thisDlg;					//服务器对话框
		thisDlg->m_clients.push_back(pClient);			//将pClient添加到向量m_clients中
		++thisDlg->m_nClients;							//登录客户端数加1
		CString msgIP(pClient->cltIP);
		CString msgPort;
		msgPort.Format(_T(":%d"), pClient->cltPort);
		CString msg = msgIP + msgPort + "登录";		//smg格式为“IP:端口 登录”
		//想对话框发送消息,更新界面信息
		::SendMessage(thisDlg->GetSafeHwnd(), WM_UPDATE_MSG, NULL, (LPARAM)&msg);
		//创建接受客户端消息的线程
		HANDLE h = CreateThread(nullptr, 0, RecvThreadProc, pClient, 0, nullptr);
		if (h != nullptr) {
			CloseHandle(h);
		}
	}
	return 0;
}
4.创建接收数据线程函数

下面定义接受客户端消息的线程函数RecvThreadProc(),在CChatServerDlg类中添加函数的声明,代码如下:

static DWORD WINAPI RecvThreadProc(LPVOID pParam);

RecvThreadProc()函数的定义如下:

//接收数据并处理,每个客户端对应一个线程
DWORD WINAPI CChatServerDlg::RecvThreadProc(LPVOID pParam) {
	ClientItem* thisClient = (ClientItem*)pParam;
	while (true) {
		TCHAR buffer[256] = { 0 };
		int nRecv = recv(thisClient->cltSocket, (char*)buffer, sizeof(buffer), 0);
		if (nRecv <= 0) {				//-1:网络出错	0:对方关闭连接	,从向量m_clients中删除thisClient
			removeClient(thisClient->pChatDlg->m_clients, thisClient);
			return 0;
		}
		//在消息buffer的前面加上发送客户端的IP地址和端口,然后转发给每一个客户端
		CString strPort;
		strPort.Format(_T("%d"), thisClient->cltPort);
		CString msg = thisClient->cltIP + "-" + strPort + ":" + buffer;
		TCHAR sendBuf[256] = { 0 };
		_tcscpy_s(sendBuf, 256, msg);
		auto it = thisClient->pChatDlg->m_clients.begin();
		for (; it != thisClient->pChatDlg->m_clients.end(); ++it) {
			ClientItem* pClient = *it;
			int bufLen = _tcslen(sendBuf) * sizeof(TCHAR);
			int len = send(pClient->cltSocket, (char*)sendBuf, bufLen, 0);
			if (len <= 0) {		//发送失败,从向量m_clients中删除pClient
				removeClient(thisClient->pChatDlg->m_clients, pClient);
			}
		}
	}
	return 0;
}
5.添加removeCilent函数

为了使用方便,将removeClient()函数也定义为静态函数,首先在类中声明,代码如下:

static void removeClient(vector<ClientItem*>clients, ClientItem* theClient);

removeClient()函数的定义如下:

//在向量clients中,将theClient删除
void CChatServerDlg::removeClient(vector<ClientItem*>clients, ClientItem* theClient) {
	--(theClient->pChatDlg->m_nClients);			//登录客户端数减1
	//向对话框发送消息,通知客户端离线
	CString msgIP(theClient->cltIP);
	CString msgPort;
	msgPort.Format(_T(":%d"), theClient->cltPort);
	CString msg = msgIP + msgPort + "离开";
	//向对话框发送消息,显示有客户端离线的消息
	::SendMessage(theClient->pChatDlg->GetSafeHwnd(), WM_UPDATE_MSG, NULL, (LPARAM)&msg);
	//将客户端从客户端向量中删除
	vector<ClientItem*>::iterator it;
	for (it = clients.begin(); it != clients.end(); ++it) {
		ClientItem* pClient = *it;
		if (pClient->cltSocket == theClient->cltSocket) {			//找到要删除的客户端
			clients.erase(it);
			break;
		}
	}
	delete theClient;			//删除客户端
}
6.对话框自定义消息响应函数

要发送自定义消息,需要定义消息宏、添加消息响应函数以及完成消息映射

(1)消息宏的定义。在ChatServerDlg.h文件的开始部分加入消息宏定义,代码如下:

#define WM_UPDATE_MSG WM_USER+100

为了防止用户定义的消息ID与系统的消息ID冲突,Microsoft定义了一个宏WM_USER,小于WM_USER的ID被系统使用,大于WM_USER的ID被用户使用。因此,这里定义的宏WM_UPDATE_MSG的值是WM_USER+100。

(2)添加消息响应函数。在CChatServerDlg类中添加消息响应函数的声明,代码如下:

LRESULT OnUpdateMsg(WPARAM w, LPARAM l);

在CCHatServerDlg.cpp文件中给出OnUpdateMsg()函数的定义,代码如下:

LRESULT CChatServerDlg::OnUpdateMsg(WPARAM w, LPARAM l) {
	CString* strMsg = (CString*)l;
	int len = m_edtMsg.GetWindowTextLength();		//原来编辑框中文本的长度
	m_edtMsg.SetSel(len, len);
	m_edtMsg.ReplaceSel(*strMsg + _T("\r\n"));		//在原来文本的后面增加文本strMsg
	UpdateData(false);
	return 0;
}

需要显示在对话框中的消息由函数的第二个参数传入,将消息追加在编辑框原来的内容之后。最后调用函数UpdateData(false),将已登陆客户端的数量更新

(3)完成消息映射。在ChatServerDlg.cpp文件中,找到BEGIN_MESSAGE_MAP(CChatServerDlg,CDialogEx)和END_MESSAGE_MAP()之间的代码块,在这段代码块中加入消息映射。添加消息映射后的代码如下:

BEGIN_MESSAGE_MAP(CChatServerDlg, CDialogEx)
	ON_WM_SYSCOMMAND()
	ON_WM_PAINT()
	ON_WM_QUERYDRAGICON()
	ON_BN_CLICKED(IDC_BUTTON2, &CChatServerDlg::OnBnClickedButton2)
	ON_BN_CLICKED(IDC_START, &CChatServerDlg::OnClickedStart)
	ON_MESSAGE(WM_UPDATE_MSG, OnUpdateMsg)
	ON_BN_CLICKED(IDC_EXIT, &CChatServerDlg::OnClickedExit)
END_MESSAGE_MAP()

ON_MESSAGE(WM_UPDATE_MSG, OnUpdateMsg)的作用是,当街收到消息WM_UPDATE_MSG时,就会调用函数OnUpdateMsg()

1.4客户端编程
1.对话框类中添加数据成员

在ChatClientDlg类中加入负责与服务器端通信的socket属性,代码如下:

SOCKET m_Socket;

在构造函数中将加入以下代码,对属性m_Socket初始化:

CChatClientDlg::CChatClientDlg(CWnd* pParent /*=nullptr*/)
	: CDialogEx(IDD_CHATCLIENT_DIALOG, pParent)
	, m_nPort(0)
	, m_strSend(_T(""))
{
	m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
	m_Socket = INVALID_SOCKET;
}
2.添加“登录”按钮的消息响应函数

使用类向导为对话框类CChatClientDlg添加“登录”按钮的消息响应函数,方法与前面为服务器端对话框类添加“启动服务器”按钮的消息响应函数相同,在函数中添加如下代码:

void CChatClientDlg::OnClickedLogin()
{
	// TODO: 在此添加控件通知处理程序代码
	UpdateData(true);						//将对话框控件中的值保存到控件关联变量中
	DWORD dwIP;								//32位无符号正式表示的IP
	m_ipServer.GetAddress(dwIP);			//获取IP地址
	m_Socket = socket(AF_INET, SOCK_STREAM, 0);
	if (m_Socket == INVALID_SOCKET) {
		AfxMessageBox(_T("创建socket失败"));
		return;
	}
	//连接服务器(绑定socket到服务器的IP地址和端口)
	sockaddr_in serverAddr;
	serverAddr.sin_family = AF_INET;
	serverAddr.sin_port = htons(m_nPort);			//函数htons()将参数转换为网络字节序
	serverAddr.sin_addr.s_addr = htonl(dwIP);		//函数htonl()将参数转换为网络字节序
	if (connect(m_Socket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == -1) {
		AfxMessageBox(_T("连接失败"));
		return;
	}
	m_edtMsg.SetWindowTextW(_T("登陆成功!\r\n"));
	GetDlgItem(IDC_LOGIN)->EnableWindow(false);		//禁用“登录”按钮
	GetDlgItem(IDC_SEND)->EnableWindow(true);		//启用“发送”按钮
	//创建接收服务器的线程
	HANDLE h = CreateThread(nullptr, 0, RecvThreadProc, this, 0, nullptr);
	if (h != nullptr)
		CloseHandle(h);
}
3.创建接收数据线程函数

下面给出接受信息线程函数的定义。将线程函数定义为类的静态成员函数,首先在CCHatServerDlg类中给出函数AccpThreadProc()的声明,代码如下:

static DWORD WINAPI RecvThreadProc(LPVOID pParam);

然后给出函数的定义,代码如下:

DWORD WINAPI CChatClientDlg::RecvThreadProc(LPVOID pParam) {
	CChatClientDlg* thisDlg = (CChatClientDlg*)pParam;
	while (true) {
		TCHAR buffer[256] = { 0 };
		int nRecv = recv(thisDlg->m_Socket, (char*)buffer, sizeof(buffer), 0);
		if (nRecv <= 0) {		//接收数据失败
			closesocket(thisDlg->m_Socket);										//关闭socket
			thisDlg->GetDlgItem(IDC_LOGIN)->EnableWindow(true);					//启用”登录“按钮
			thisDlg->GetDlgItem(IDC_SEND)->EnableWindow(false);					//禁用“发送”按钮
			AfxMessageBox(_T("服务器失去连接,请稍后再试!"));
			break;
		}
		CString msg(buffer);
		//向对话框发送自定义消息
		::SendMessage(thisDlg->GetSafeHwnd(), WM_UPDATE_MSG, NULL, (LPARAM)&msg);
	}
	return 0;
}
4.对话框自定义消息响应函数

上面的程序向对话框发送了自定义消息,下面就实现在对话框类中接收消息并处理

(1)消息宏的定义。在ChatClientDlg.h文件的开始部分加入消息宏定义,代码如下:

#define WM_UPDATE_MSG WM_USER+100

(2)添加消息响应函数。在CCHatServerDlg类中添加消息响应函数的声明,代码如下:

LRESULT OnUpdateMsg(WPARAM w, LPARAM l);

在ChatClientDlg.cpp文件中给出OnUpdateMsg()函数的定义,代码如下:

//自定义消息响应函数,将参数l中的信息显示在m_edtMsg中
LRESULT CChatClientDlg::OnUpdateMsg(WPARAM w, LPARAM l) {
	CString* strMsg = (CString*)l;
	int len = m_edtMsg.GetWindowTextLength();
	m_edtMsg.SetSel(len, len);
	m_edtMsg.ReplaceSel(*strMsg + _T("\r\n"));
	return 0;
}

需要显示在对话框中的消息由函数的第二个参数传入,将该消息的内容追加在编辑框原来的内容之后

(3)完成消息映射。在ChatClientDlg.cpp文件中,找到BEGIN_MESSAGE_MAP(CChaClientDlg,CDialogEx)和END_MESSAGE_MAP()之间的代码块,在这段代码块中加入消息映射。添加消息映射后的代码如下:

BEGIN_MESSAGE_MAP(CChatClientDlg, CDialogEx)
	ON_WM_SYSCOMMAND()
	ON_WM_PAINT()
	ON_WM_QUERYDRAGICON()
	ON_BN_CLICKED(IDC_LOGIN, &CChatClientDlg::OnClickedLogin)
	ON_MESSAGE(WM_UPDATE_MSG,OnUpdateMsg)
	ON_BN_CLICKED(IDC_SEND, &CChatClientDlg::OnClickedSend)
	ON_BN_CLICKED(IDC_EXIT, &CChatClientDlg::OnClickedExit)
END_MESSAGE_MAP()

5.客户端发送信息

为“发送”按钮添加消息响应函数,在函数中添加如下代码:

void CChatClientDlg::OnClickedSend()
{
	// TODO: 在此添加控件通知处理程序代码
	UpdateData(true);
	TCHAR buffer[256] = { 0 };
	_tcscpy_s(buffer, 256, m_strSend);
	if (_tcslen(buffer) == 0) {
		return;
	}
	int len = send(m_Socket, (char*)buffer, _tcslen(buffer) * sizeof(TCHAR), 0);
	if (len < 0) {		//发送失败,与接收失败处理相同
		closesocket(m_Socket);										//关闭socket
		GetDlgItem(IDC_LOGIN)->EnableWindow(true);					//启用”登录“按钮
		GetDlgItem(IDC_SEND)->EnableWindow(false);					//禁用“发送”按钮
		AfxMessageBox(_T("服务器失去连接,请稍后再试!"));
	}
	m_strSend = "";
	UpdateData(false);
}

首先将发送文本框的内容复制到buffer缓冲区,如果内容为空,则返回;如果不为空,调用send()函数向服务器发送buffer的内容

如果发送失败,则关闭socket,启用”登录“按钮,禁用”发送“按钮,并显示“服务器失去连接,稍后再试!”的消息框。

最后将文本框清空。

1.5完善其他功能
1.完善客户端程序

客户端需要完善的程序有退出程序的功能以及析构函数要完成的一些清理工作

(1)添加“退出程序”按钮的消息响应函数。使用类向导添加“退出程序”按钮的消息响应函数,在函数中添加如下代码:

void CChatClientDlg::OnClickedExit()
{
	// TODO: 在此添加控件通知处理程序代码
	CDialogEx::OnCancel();
}

(2)完善对话框类的析构函数。在CChatClientDlg类的析构函数中,添加如下代码:

CChatClientDlg::~CChatClientDlg() {
	if (m_Socket != INVALID_SOCKET)
		closesocket(m_Socket);
}

如果m_Socket不是INVALID_SOCKET,则将其关闭释放资源。

2.完善服务端程序

服务器端需要完善的程序与客户端类似,也是退出程序的功能以及析构函数的要完成的一些清理工作

(1)添加”退出程序“按钮的消息响应函数。使用类向导添加”退出程序“按钮的消息响应函数,在函数中添加如下代码:

void CChatServerDlg::OnClickedExit()
{
	// TODO: 在此添加控件通知处理程序代码
	CDialogEx::OnCancel();
}

(2)完善对话框类的析构函数。在CChatClientDlg类的析构函数中,添加如下代码:

CChatServerDlg::~CChatServerDlg() {
	if (m_listenSocket != INVALID_SOCKET)
		closesocket(m_listenSocket);		//关闭监听socket
	vector <ClientItem*>::iterator it;
	for (it = m_clients.begin(); it != m_clients.end(); ++it) {
		ClientItem* pClient = *it;
		delete pClient;					//将向量中每个元素指定的对象删除
	}	
	m_clients.clear();					//将向量中的元素删除
}

首先将监听socket关闭,然后删除vector的m_clients中的所有元素。

删除向量m_clients中的所有元素分为两个步骤,第一步通过循环将每个元素指向的对象删除,第二步将所有元素移除。

至此聊天室程序全部完成,可以编译运行,查看程序运行效果。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1829020.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

不一样的SYSTEM APP(SYSTEM flag和system_prop区别)

1.问题引入 在Android开发中, 1)Framework中PackageManager扫包后,会把app归类为SYSTEM, SYSTEM_EXT, PRIVILEGED 类别. 2)同样的, SeAndroid也会把APP归类程platform_app, system_app, untrusted_app(甚至还有其他,mediaprovider,gmscore_app). flag SYSTEM和system_app我们…

IDEA配置JavaFX

一、下载SDK &#x1f4ce;javafx-sdk-18.zip 二、配置依赖包 三、复制一个javafx代码 import javafx.application.Application; import javafx.scene.Scene; import javafx.scene.layout.VBox; import javafx.scene.shape.Line; import javafx.stage.Stage;public class Java…

基于Java和SSM框架的多人命题系统

你好呀&#xff0c;我是计算机学长猫哥&#xff01;如果你对多人命题系统感兴趣或者有相关开发需求&#xff0c;文末可以找到我的联系方式。 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;Java SSM框架 工具&#xff1a;Eclipse、MySQL Workbench、…

树莓派4B_OpenCv学习笔记10:调整视频帧大小

今日继续学习树莓派4B 4G&#xff1a;&#xff08;Raspberry Pi&#xff0c;简称RPi或RasPi&#xff09; 本人所用树莓派4B 装载的系统与版本如下: 版本可用命令 (lsb_release -a) 查询: Opencv 版本是4.5.1&#xff1a; 今日学习一下如何降低视频帧像素 文章提供测试代码讲解…

USB转I2C转SPI芯片CH341与CH347比较

1. 芯片中文资料&#xff1a; USB转I2C转SPI芯片CH341 高速USB转接芯片CH347转9M双串口转I2C转SPI转JTAG转SWD USB2.0高速转接芯片CH347应用开发手册 2. CH341与CH347比较&#xff1a; 类别CH341CH347备注串口速度2M9MCH347的串口速度更快设置CH341的I2C或SPI不能与串口同…

DAY24 回溯算法part01 77. 组合 216.组合总和III 17.电话号码的字母组合

理论基础 #什么是回溯法 回溯法也可以叫做回溯搜索法&#xff0c;它是一种搜索的方式。 在二叉树系列中&#xff0c;我们已经不止一次&#xff0c;提到了回溯&#xff0c;例如二叉树&#xff1a;以为使用了递归&#xff0c;其实还隐藏着回溯 (opens new window)。 回溯是递…

系统集成项目管理工程师第9章思维导图发布

今天发布系统集成项目管理工程师新版第9章脑图的图片版

移动硬盘接入mac无法复制文件进去怎么办,mac里的文件如何存进移动硬盘

如果要传输的文件数据量比较大&#xff0c;相比于使用U盘&#xff0c;移动硬盘是更多的选择。移动硬盘可存储量大、传输速度快&#xff0c;是实用性比较强的储存设备。不仅是Windows设备&#xff0c;Mac电脑也经常使用到移动硬盘。但有时候&#xff0c;移动硬盘在Mac上不能传文…

Python合并文件(dat、mdf、mf4)

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

揭秘最强气象武器的库,SPEI-Python不可思议之处.

spei-python是一个专门用于计算标准化降水蒸散指数&#xff08;Standardized Precipitation Evapotranspiration Index,SPEI&#xff09;的Python库.SPEI是一种综合考虑降水和潜在蒸散发的干旱指数,用于评估干旱的严重程度和持续时间. 安装 ## 可以使用 pip 来安装 spei-pyth…

C | 在ubuntu22下开发的一些配置

目录 VScode设置 要下载的插件&#xff1a; 卸载VScode的话就是哪装的哪删。 浅用gcc 预处理指令 使用gcc 语言编译过程 1. 预处理&#xff08;Preprocessing&#xff09; 2. 编译&#xff08;Compilation&#xff09; 3. 汇编&#xff08;Assembly&#xff09; 4. …

光明网发稿投稿流程与要求,光明日报如何投稿?附光明网多少钱(价格表)

对于想要在光明网发稿的作者来说&#xff0c;媒介多多网发稿平台是一个绝佳的投稿选择。光明网作为国内一流的新闻媒体平台&#xff0c;其严谨的文章审核标准和广泛的读者基础吸引着无数作者。然而&#xff0c;由于其严格的发稿标准&#xff0c;一些作者可能会遇到一些困难&…

盛世古董乱世金-数据库稳定到底好不好?

是不是觉得这个还用问&#xff1f; 是的要问。因为这个还是一个有争议的问题。但是争议双方都没有错。这就像辩论&#xff0c;有正反双方。大家都说的有道理&#xff0c;但是很难说谁对谁错。 正方观点&#xff1a;数据库稳定好 其实这个是用户的观点&#xff0c;应用开发人…

目标检测算法SSD与FasterRCNN

目标检测算法SSD与FasterRCNN SSD:&#xff08; Single Shot MultiBox Detector&#xff09;特点是在不同特征尺度上预测不同尺度的目标。 SSD网络结构 首先对网络的特征进行说明&#xff1a;输入的图像是300x300的三通道彩色图像。 网络的第一个部分贯穿到Vgg16模型 Conv5的…

大数据入门实践一:mac安装Hadoop,Hbase

一、安装Hadoop 安装hadoop参考此文&#xff0c;关键点是安装JDK和Hadoop的配置&#xff0c;为避免引用文章变收费&#xff0c;我把关键信息摘录如下&#xff1a; jdk安装和配置就不说了&#xff0c;hadoop安装过程&#xff1a; 1.设置SSH免密 1. 打开系统偏好设置&#xf…

芯片验证分享8 —— 代码审查2

大家好&#xff0c;我是谷公子&#xff0c;上节课给大家讲了代码审查中的代码正向检查&#xff0c;今天我们来讲代码审查的其他方法。 今天介绍的检查方法有&#xff1a; 代码反向检查 桌面检查 同行评审 可用性验证 这些验证方法可以应用在芯片开发的任何阶段。代码审查…

【网络安全的神秘世界】AppScan安装及使用指南

&#x1f31d;博客主页&#xff1a;泥菩萨 &#x1f496;专栏&#xff1a;Linux探索之旅 | 网络安全的神秘世界 | 专接本 https://www.hcl-software.com/appscan AppScan是一种综合型漏洞扫描工具&#xff0c;采用SaaS解决方案&#xff0c;它将所以测试功能整合到一个服务中&a…

Day 24:100301. 构成整天的下标对数目II

Leetcode 100301. 构成整天的下标对数目II 给你一个整数数组 hours&#xff0c;表示以 **小时 **为单位的时间&#xff0c;返回一个整数&#xff0c;表示满足 i < j 且 hours[i] hours[j] 构成 **整天 **的下标对 i, j 的数目。 **整天 **定义为时间持续时间是 24 小时的 *…

FreeRTOS队列(queue)

队列(queue)可以用于"任务到任务"、 "任务到中断"、 "中断到任务"直接传输信息。 1、队列的特性 1、1常规操作 队列的简化操如下图所示&#xff0c;从此图可知&#xff1a; 队列中可以包含若干数据&#xff1a;队列中有若干项&#xff0c;这…

2024上半年软考---江苏考区最先公布成绩

经历了考试之后&#xff0c;最期待的就是考试成绩的公布了&#xff0c;最好的成绩是45、45、45.只要过了分数线就满足了。下面我们来看看各大考区的分数的公布时间。 提前说下江苏考区的时间比较早&#xff0c;我就是江苏考区的&#xff0c;希望本次可以顺利通过考试。 2024年…