5. 手写一个聊天室(单聊、群聊、文件传输)

一、前言

鉴于很多人都想给聊天室添加单聊的功能,所以这里就再出一篇功能更全的聊天室

一直啃旧东西也没意思,所以本篇文章再给大家介绍一个新的窗口框架:WTL

本文的聊天室客户端将基于WTL进行开发,主要有三个程序:windows客户端windows服务器以及linux服务器

由于有两个不同平台的服务器,分别开发就太麻烦了,因此本文选择使用boost中的asio库进行跨平台开发。

二、前置知识点

1.相关文章

在学习本文前,你需要至少阅读以下四篇文章:

  1. 模板
  2. Boost asio库使用详解
  3. C++开源项目配置(vcpkg)
  4. Linux系统入门

其中第一点,是WTL的底层实现逻辑,不懂的话,后面用起来你可能就会觉得非常奇怪。

然后是第二点,由于我们的服务器必须能运行到windowslinux双平台,如果分别进行开发就太麻烦了,所以这里采用C++准标准库中的asio库进行跨平台开发。

即我们只需要写一份代码,就可以在多个平台直接编译运行

然后是第三点,因为我们需要用到第三方库,所以就必须得学会如何在自己的项目中使用第三方库。

但本文实际上并不需要,因为我直接提供了完整的项目解决方案,包括用到的WTL与boost第三方库,我也直接放在了项目中。

至于最后一点,则是为了让我们能够直接在VS中编译运行linux服务器。

2.WTL的基本使用流程

MFC一样,WTL也是一个图形框架,是由微软员工写出来的。

之所以出现WTL,主要还是因为MFC很笨重,不跨平台,闭源,而且似乎已经很多年没有更新过了

一个最简单的MFC程序,默认生成就有数兆大小,一旦使用MFC出现bug,排错就会非常麻烦,因为没有源码。

WTL就可以很好的解决这些问题,因为WTL是直接给我们提供框架源代码的,你可以任意修改、扩展里面的内容。

而且因为其小巧,其生成的文件非常小,本文完成的这个聊天室,最终生成的可执行文件只有160k

但缺点肯定也是有的,就是不跨平台,使用起来也较为麻烦。

不过我已经总结出了一套基本使用流程,所以不必过于担心,只要你会MFCWTL也能很快学会。

WTL很多函数接口都是直接模仿的MFC,所以如果你对WTL中的某个控件不知道怎么用,你就可以直接查询MFC的文档,可以点击这里查看官方MFC文档。

首先,我们需要建立一个桌面窗口空项目:

随便取个名字:

在这里插入图片描述

然后选择桌面应用程序,以及勾选空项目:

在这里插入图片描述

如果用的vcpkg,那么下面两个配置的步骤就不需要了。

这样我们的客户端项目就创建好了,现在你就可以将WTL源代码复制进这个项目文件夹中:

在这里插入图片描述

然后将这个文件添加到项目的包含目录中即可,不再赘述:

在这里插入图片描述

现在,我们就可以正式写代码了。

首先新建两个文件:stdafx.hmain.cpp

在这里插入图片描述

然后在stdafx.h头文件中包含如下头文件:

#pragma once
#include<atlbase.h> //使用ATL的基本文件,必须包含
#include<atlapp.h> //使用WTL的基本文件
#include<atlwin.h>  //ATL封装的窗口
#include<atlframe.h> //WTL封装的窗口框架类
#include<atlmisc.h>  //WTL封装的工具类
#include<atlctrls.h> //WTL封装的控件类
#include<atlcrack.h> //WTL封装的增强消息宏
#include<atldlgs.h> //WTL封装的各种对话框类
#include"resource.h" //资源头文件,当使用到了资源,会自动生成该头文件

也就是说,这个头文件是用来包含库的头文件的,方便我们以后使用。

这里写出的便是我们所需要的所有头文件了,注意最好不要更改各个头文件之间的顺序,以后其它文件直接包含这个stdafx.h头文件就行了。

然后我们就可以像MFC那样,在资源视图中,新建一个对话框窗口资源:

在这里插入图片描述

注意这里的对话框资源ID,可以按 F4或者右键跳转到属性界面进行修改,后面要用到,比如这里我就将其修改成为了IDD_DLG_MAIN,即用这个窗口资源作为我们的主对话框。

完成了这一步之后,我们还需要为它绑定一个类。

除了一个一个文件的添加,我们这里还可以直接添加类的:

在这里插入图片描述

然后取名为CMainDlg

在这里插入图片描述

然后在该类的头文件中修改为如下代码:

#include"stdafx.h"
class CMainDlg :public CDialogImpl<CMainDlg> {
public:
	enum {
		IDD = IDD_DLG_MAIN
	};

	BEGIN_MSG_MAP(CMainDlg)
	END_MSG_MAP();
};

这里用到的就是C++模板实现多态的特性,我们继承自模板类CDialogImpl,并传入了自己的类名。

接着写一个枚举变量等于要绑定的窗口资源ID即可:

	enum {
		IDD = IDD_DLG_MAIN
	};

最后就是消息映射,因为windows是消息驱动的,MFC里面同样也是用消息映射来处理事件,这里同样如此:

	BEGIN_MSG_MAP(CMainDlg)
	END_MSG_MAP();

注意BEGIN_MSG_MAP后面需要填写的是本类类名,也就是给本类添加消息映射。

至此,我们就完成了类与窗口资源的绑定工作。

最后,我们还需要在main.cpp中写下我们的启动代码:

#include"stdafx.h"
#include"CMainDlg.h"

CAppModule app; //保存主线程id和消息循环的实例
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) {
	app.Init(NULL, hInstance);

	CMainDlg dlg;
	dlg.DoModal();

	app.Term();
}

WinMain函数是窗口程序的入口函数,直接复制即可,这里主要是多出了CAppModule ,如果你对MFC熟悉的话,应该知道MFC程序中也有一个CWinApp类代表着当前整个应用程序实例。

在这里,我们只需要用它声明一个全局变量,然后在代码中初始化它与销毁它即可:

app.Init(NULL, hInstance);
//代码
app.Term();

然后在这两行代码中间,填入我们想要执行的代码,所以这里只需要声明一个我们上面那个窗口类,然后调用其DoModal函数即可,

现在运行程序,你就发现可以正常运行了。

上面所有的步骤都是最基本且必要的步骤。

如果要深究原理的话,实际步骤会比上面的内容还要复杂的多,这里用到的是我自己总结出的一套能够快速用WTL库进行开发的方法,只要你熟悉MFC,就能很快上手。

但你会发现,运行出来的窗口是无法关闭的,那时因为我们没有响应对应的关闭消息。

这时,我们只需要来到资源对话框窗口,按F4进入其属性页面,点击上方的消息,添加对窗口关闭消息的处理:

在这里插入图片描述

这就和MFC非常像了,在生成的代码中,写一个EndDialog(0)函数即可退出当前的对话框:

在这里插入图片描述

这个EndDialog函数便是我们继承下来的,专门用于退出当前对话框窗口。

如果你想要看我们继承下来了哪些函数,可以使用this->,VS就会帮我们列举出来。

除了关闭消息,我们最常使用的还有初始化消息,方法与上面一样:

在这里插入图片描述

还有按钮,你也可以直接双击它们,添加点击事件处理函数,也可以选择它们按F4,选择其它可用的事件:

在这里插入图片描述

添加控件的方法和MFC一模一样,在工具箱中拖出需要的控件,放在对话框上面即可。

至此,我们就过了一遍WTL开发程序的基本步骤,来总结一下。

  1. 新建stdafx.h头文件,用于存放WTL库的头文件。
  2. 添加一个资源对话框窗口。
  3. 添加一个类,通过其枚举IDD绑定窗口的ID,并添加消息映射宏。
  4. main.cpp中启动整个程序。
  5. 可以在窗口视图中,选择对应的窗口、控件,按F4进入属性页面,添加对应的消息、事件处理函数。

然后之后的开发,就和MFC基本相同了。

不过还有一点,我们想要在代码中直接操作控件,MFC中曾经提到过数种方法,其中使用ID的方法仍然是可以的。

即,用GetDlgItem与控件id来获取控件实例,进而调用相应的函数:

GetDlgItem(IDOK).EnableWindow(false);

但每次都这样弄就比较麻烦,其实我们也可以将其绑定在一个变量上:

首先添加一个对应的类型(WTL中的控件类名与MFC中的基本保持一致):

在这里插入图片描述

然后在窗口的初始化函数中调用Attach方法,直接将这个控件与变量进行绑定:

m_btn.Attach(GetDlgItem(IDOK));

在这里插入图片描述

这样以后,我们就可以直接通过操作这个变量来看控制控件了。

三、项目下载与文件介绍

由于这个项目比较复杂,所以我这里将直接提供源代码,后面也直接对这个源代码中的内容进行讲解。

只要你阅读过本站《C++实战入门到精通》的内容,我相信你是能够看得懂的。

唯一的难点可能是对这些库的使用不熟练。

本链接采用阿里云分享,点击这里跳转下载,下载后是一个可执行文件,直接点击运行就可以解压。

解压后,得到下面目录:

在这里插入图片描述

  • ChatClient:客户端代码。

  • include:本项目所用到的库文件,但由于客户端、服务器的网络部分都用到了boost库,所以我这里直接将其放在了外面,而不是在三个项目里面都放置一份。

  • LChatServer:linux服务器。

  • WChatServer:windows服务器。

  • ChatClient:生成的客户端程序

  • ChatRoom.sln:解决方案管理文件,用VS打开这个文件即可打开整个解决方案。

  • LChatServer.out:生成的linux服务器程序

  • WChatServer.exe :生成的windows服务器程序

对于Windows平台的项目,可以直接编译运行,而对于linux服务器,你就还需要配置你的linux平台,在本文最开始就提到过,不再赘述。

然后,还需要将boost库复制到该项目的include文件夹中才能正常编译:

在这里插入图片描述

这些文件夹不用你生成,只需要配置好环境后,在VS中点击编译就会自动在linux平台生成。

生成完毕后,再将你的boost文件夹复制过去,存放在一个include文件夹中即可。

我们的d盘、c盘之类的,只可以在子系统目录:/mnt下找到的,所以你可以直接使用linux复制文件命令将windows系统上的文件复制到你的子系统内。

这样就能正常编译了。

四、服务器代码讲解

服务器共有六个函数、一个类NetPacket

在这里插入图片描述

老规矩,我们还是从main函数说起:

在这里插入图片描述

首先是io_context类,这个是asio库中最基础的类,所有的io操作类都需要它作为第一个参数。

然后是acceptor类,专门用于tcp服务器来等待客户端的连接。

因为需要读网络套接字,所以它的第一个参数就是上面声明的io_context对象,然后第二个参数就是我们要监听的端口类。

endpoint类聚合了地址与端口,所以我们可以这样构造,第一个参数意思就是监听本机所有的ipv4地址,然后第二个参数就是我们要监听的端口号。

接着进入循环,注意SPSock是我自己定义的一个类型,其原型是:

shared_ptr<tcp::socket> SPSock

这部分内容如果不熟悉,可以参考本站的另一篇文章:智能指针

定义在了头文件stdafx.h中:

typedef	shared_ptr<tcp::socket> SPSock; //socket共享智能指针

之所以要用它,原因就是我们想要使用指针(便于传送),但我们不想要操心内存。

实际用到的是socket类,其主要用途就是作为一个可以和客户端通信的套接字。

接着就是等待客户端连接上来,一旦连接上来,这个sock里面就存放着与这位客户端通信的能力:

acptr.accept(*sock);

紧接着是两个变量:

vector<char> msg;
msg_id id;

这里之所以用vector<char>而不是string,主要原因就是string默认以\0作为结束,而网络传输的数据中没人能保证里面的数据中没有\0,尤其是我们想要传输文件,文件数据中里面肯定有\0

如果继续使用string,一遇到数据中的\0就会自动截断数据,导致数据丢失,这肯定不行。

vector就不存在这个问题,也就是说,这里的msg就是用来接收客户端发送来的消息的。

而这里的msg_id 是一个枚举类型,定义在了头文件stdafx.h中:

//消息包id
enum class msg_id {
	name = 0,//数据内容为客户端名称
	newuser = 1, //数据内容为新用户名称
	alluser = 2, //数据内容为当前所有在线用户名称
	exit = 4, //数据内容为当前退出聊天室的成员名称
	normal = 8, //数据内容为聊天消息
	single = 16, //数据内容为 昵称:消息
	file = 32, //文件发送,数据内容为文件数据,以文件名开头 \0 与后面的文件数据分割
	single_file = 16 | 32 //给单用户发送文件,相当于 single 与 file的组合
};

这是用来标识客户端给我们服务器发送的消息类型,本项目共用到了8种消息类型。