一、前言
鉴于很多人都想给聊天室添加单聊的功能,所以这里就再出一篇功能更全的聊天室
一直啃旧东西也没意思,所以本篇文章再给大家介绍一个新的窗口框架:WTL
本文的聊天室客户端将基于WTL
进行开发,主要有三个程序:windows客户端、windows服务器以及linux服务器。
由于有两个不同平台的服务器,分别开发就太麻烦了,因此本文选择使用boost
中的asio
库进行跨平台开发。
二、前置知识点
1.相关文章
在学习本文前,你需要至少阅读以下四篇文章:
其中第一点,是WTL
的底层实现逻辑,不懂的话,后面用起来你可能就会觉得非常奇怪。
然后是第二点,由于我们的服务器必须能运行到windows
、linux
双平台,如果分别进行开发就太麻烦了,所以这里采用C++准标准库中的asio
库进行跨平台开发。
即我们只需要写一份代码,就可以在多个平台直接编译运行
然后是第三点,因为我们需要用到第三方库,所以就必须得学会如何在自己的项目中使用第三方库。
但本文实际上并不需要,因为我直接提供了完整的项目解决方案,包括用到的WTL与boost第三方库,我也直接放在了项目中。
至于最后一点,则是为了让我们能够直接在VS中编译运行linux
服务器。
2.WTL的基本使用流程
和MFC
一样,WTL
也是一个图形框架,是由微软员工写出来的。
之所以出现WTL,主要还是因为MFC很笨重,不跨平台,闭源,而且似乎已经很多年没有更新过了
一个最简单的MFC程序,默认生成就有数兆大小,一旦使用MFC出现bug,排错就会非常麻烦,因为没有源码。
而WTL
就可以很好的解决这些问题,因为WTL
是直接给我们提供框架源代码的,你可以任意修改、扩展里面的内容。
而且因为其小巧,其生成的文件非常小,本文完成的这个聊天室,最终生成的可执行文件只有160k
。
但缺点肯定也是有的,就是不跨平台,使用起来也较为麻烦。
不过我已经总结出了一套基本使用流程,所以不必过于担心,只要你会MFC
,WTL
也能很快学会。
WTL很多函数接口都是直接模仿的MFC,所以如果你对WTL中的某个控件不知道怎么用,你就可以直接查询MFC的文档,可以点击这里查看官方MFC文档。
首先,我们需要建立一个桌面窗口空项目:
随便取个名字:
然后选择桌面应用程序,以及勾选空项目:
如果用的vcpkg
,那么下面两个配置的步骤就不需要了。
这样我们的客户端项目就创建好了,现在你就可以将WTL源代码复制进这个项目文件夹中:
然后将这个文件添加到项目的包含目录中即可,不再赘述:
现在,我们就可以正式写代码了。
首先新建两个文件:stdafx.h
与main.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
开发程序的基本步骤,来总结一下。
- 新建
stdafx.h
头文件,用于存放WTL
库的头文件。 - 添加一个资源对话框窗口。
- 添加一个类,通过其枚举
IDD
绑定窗口的ID
,并添加消息映射宏。 - 在
main.cpp
中启动整个程序。 - 可以在窗口视图中,选择对应的窗口、控件,按
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种消息类型。