LanChatRoom局域网聊天室

CPP已经结课,我提交的项目是Qt的入门项目,局域网聊天室LanChatRoom。

这个代码重构了很多遍。第一遍是照着明哥推荐到书,把代码抄了一遍。

但抄下来之后,各种问题,而且是清朝老代码。抄了一遍之后,对代码的业务逻辑已经有了一个大体的了解。

整个开发周期持续了一周,其实最开始两天就已经能跑了。但我觉得远古代码太丑陋了,所以我扔掉了了书本,选择重写。

重写的过程也是曲折的,而且每次都遇到新的或旧的问题。这些问题以及解决方案将在接下来的内容中分享给大家。希望可以帮助到有需要的同学。

IDE的选择

如果是跟我一样的新手的话,第一遍建议是去找书,抄项目代码。当然是理解地抄,而不是单纯的Ctrl+CV。

IDE建议开始选择Qt自带的QtCreater。因为这涉及到对ui的操作,以及信号槽机制。这对没有qt经验的同学来说很不友好。

但是QtCreater太丑陋了,而且代码补全也不好用。

所以我当时是已经熟悉了ui的各项操作之后,就转到clion里了。

熟悉信号槽之后,就可以考虑转到clion了。

而且clion默认配置的cmakelist文件也更加清晰。

我一开始是去书栈网找Qt的教程,但它们很少用到ui文件,而是直接用代码控制元素。实际上很多对象的属性和方法,是不需要去记的,直接用designer编辑ui文件就可以。

消息广播

消息广播利用的是传输层协议UDP。

消息广播需要将消息发送给同一局域网内的所有设备。如果使用TCP协议,则需要在每个设备上都建立连接,这会增加网络开销。而UDP协议是无连接的协议,只需要设置源IP地址、源端口、目标IP地址和目标端口即可发送数据,因此可以提高传输效率。

UDP协议也存在一些缺点,例如数据传输不保证可靠性。在局域网聊天室中,如果某个设备没有接收到消息,则不会影响其他设备的正常使用。

文件传输

文件传输用的是传输层协议TCP。

TCP具有可靠性、有序性和流量控制等特性,可以保证文件传输的顺利进行。

而且文件的发送也利用了qt的信号槽机制。触发readyread或byteswritten信号之后,才传输下一部分文件。能够正常进入事件循环。这样不会堵塞当前线程,实现类似多线程的效果。

如果用循环的话,会卡在循环内,无法进入事件循环,在传输结束之前,显示"无响应"。

文件收发有很多共有的部分,比如界面元素、进度条更新。这些共有的部分可以单独封装,交给子类实现。这属于软件设计模式中的策略模式。
QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy);这是我每次重写都遇到的问题,需要指定代理方式,这可能跟我一直开着系统代理有关。

cpp 复制代码
QNetworkProxy::setApplicationProxy(QNetworkProxy::NoProxy);
connect(ui->selectFileButton, &QPushButton::clicked, this, &FileTransfer::selectFile);
connect(ui->transferFileButton, &QPushButton::clicked, this, &FileTransfer::transferFile);
connect(ui->cancelButton, &QPushButton::clicked, this, [=]() {
    this->close();
});

connect是qt特有的信号槽方法。使用的话需要继承QObject类,或者他的派生类。
selectFiletransferFile是纯虚函数,具体的策略在子类中实现。这里必须要用纯虚函数,交由子类实现。
cancelButton触发"取消"事件,通过lambda表达式实现。无论是接收还是发送,点击取消按钮的结果,都是关闭窗口,因此选择直接使用lambda表达式简化代码。

文件图标

我是在Clion中构建的的cmake项目。

需要在构建目录中添加.rc资源文件,并在.rc资源文件中指定IDI_ICON1 ICON "resources/icons/beer.ico"

后面的路径是相对于构建目录的,如果不确定写相对还是写绝对,可以都试一试。

回车发送消息

实现原理就是重写eventFilter方法。

如果检测到键盘事件,先判断是不是回车,如果是回车就发送消息,如果是CTRL+回车,就插入换行符。

如果是粘贴事件,就尝试插入图片。插入图片有两种可能:

  • 在粘贴板的元数据中
  • 粘贴板存放的是文件地址url

把这两种情况都尝试一遍,如果能获取到图片,那就插入到输入框。

还创建了一个自定义工具类,实现一个静态工具方法imageToBase64。用于将image对象转换为base64格式的字符串,嵌入到html中。

构建多个可执行文件

一个项目构建多个可执行文件,而不是为每一个可执行文件创建新的项目。

这需要修改CmakeList文件,为每一个构建目标指定文件。

添加自定义目标add_custom_target,允许一次编译所有可执行文件。

添加可执行文件add_executable,允许一个项目编译生成多个可执行文件。

括号内,第一个参数LanChatRoom是构建后的可执行文件名。

后面的所有参数,都是参与构建这个可执行文件的源代码文件,包括头文件、源文件、资源文件。之后可能还会导入更多。

条件编译

每次切换debug和release两种状态的时候,都增删代码,是不现实的。

这样项目中每一处需要修改的地方都需要修改。

在最开始的时候,我就是这么做的。把一些调试信息显示在ui上。比如,本来这个标签是显示文件路径的,我现在显示TcpSocket的错误信息。

前面也提了,这个代码重构了很多遍,每次重构的原因,都包括:这一编写的太丑了,乱七八糟的。

重构很多遍之后,才想起来软件设计师备考时学的:软件设计模式。

这种工科的概念,如果脱离实践,那么只是空洞的文字。就算接触到了,也需要重复很多遍才能把认识和实践联系起来。
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DDEBUG")

上面这行代码是写在CmakeList中的,它的意思是,在预处理阶段,添加宏DEBUG

在代码中需要调试的地方,用#ifdef DEBUG,进行条件编译。

窗口程序,不显示cmd

这需要在CmakeList中添加:

set(CMAKE_WIN32_EXECUTABLE TRUE)

否则会携带一个控制台窗口。

动态链接库

这一部分的作用是在编译时链接动态链接库。

并在编译后,把动态链接库.dll复制到目标目录中。

cmake 复制代码
target_link_libraries(LanChatRoom
        Qt::Core
        Qt::Gui
        Qt::Widgets
        Qt::Network
)
target_link_libraries(FileSender
        Qt::Core
        Qt::Gui
        Qt::Widgets
        Qt::Network
)
target_link_libraries(FileReceiver
        Qt::Core
        Qt::Gui
        Qt::Widgets
        Qt::Network
)
if (WIN32 AND NOT DEFINED CMAKE_TOOLCHAIN_FILE)
    set(DEBUG_SUFFIX)
    if (MSVC AND CMAKE_BUILD_TYPE MATCHES "Debug")
        set(DEBUG_SUFFIX "d")
    endif ()
    set(QT_INSTALL_PATH "${CMAKE_PREFIX_PATH}")
    if (NOT EXISTS "${QT_INSTALL_PATH}/bin")
        set(QT_INSTALL_PATH "${QT_INSTALL_PATH}/..")
        if (NOT EXISTS "${QT_INSTALL_PATH}/bin")
            set(QT_INSTALL_PATH "${QT_INSTALL_PATH}/..")
        endif ()
    endif ()
    if (EXISTS "${QT_INSTALL_PATH}/plugins/platforms/qwindows${DEBUG_SUFFIX}.dll")
        add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
                COMMAND ${CMAKE_COMMAND} -E make_directory
                "$<TARGET_FILE_DIR:${PROJECT_NAME}>/plugins/platforms/")
        add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
                COMMAND ${CMAKE_COMMAND} -E copy
                "${QT_INSTALL_PATH}/plugins/platforms/qwindows${DEBUG_SUFFIX}.dll"
                "$<TARGET_FILE_DIR:${PROJECT_NAME}>/plugins/platforms/")
    endif ()
    foreach (QT_LIB Core Gui Widgets Network)
        add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
                COMMAND ${CMAKE_COMMAND} -E copy
                "${QT_INSTALL_PATH}/bin/Qt6${QT_LIB}${DEBUG_SUFFIX}.dll"
                "$<TARGET_FILE_DIR:${PROJECT_NAME}>")
    endforeach (QT_LIB)
endif ()

实际上,可以只保留target_link_libraries部分。

因为后面一大段的if,作用是导入动态链接库文件,导入的这些仍然是不完整的。

最后需要用windeployqt来补充依赖。用法就是windeployqt [文件名],比如:windeployqt lanchatroom.exe。win环境下是大小写都可以的。

使用windeployqt需要预先将所在目录添加到环境变量中,以我的电脑为例,windeployqt在目录C:\Tools\Qt\6.6.1\mingw_64\bin下。

也就是Qt版本文件夹下的mingw_64\bin

软件设计模式

我最开始接触,是前段时间准备软考的时候。

重写了这么多编,才对软件设计模式有稍微浅薄的理解。

这里面也用到了策略、状态等模式。

如果没有软件设计模式,那么整个项目将非常混乱。我觉得,从事软件工程,软件设计模式是必须的。

软件设计模式(Design pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。

下面是当时汇报的PPT,对其他组的作品也算是降维打击了,哈哈。
LanChatRoom - yuque.pptx

相关推荐
轩辰~6 分钟前
网络协议入门
linux·服务器·开发语言·网络·arm开发·c++·网络协议
lxyzcm25 分钟前
C++23新特性解析:[[assume]]属性
java·c++·spring boot·c++23
蜀黍@猿44 分钟前
C/C++基础错题归纳
c++
雨中rain1 小时前
Linux -- 从抢票逻辑理解线程互斥
linux·运维·c++
就爱学编程1 小时前
重生之我在异世界学编程之C语言小项目:通讯录
c语言·开发语言·数据结构·算法
北国无红豆2 小时前
【CAN总线】STM32的CAN外设
c语言·stm32·嵌入式硬件
单片机学习之路2 小时前
【C语言】结构
c语言·开发语言·stm32·单片机·51单片机
ALISHENGYA2 小时前
全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(实战项目二)
数据结构·c++·算法
arong_xu2 小时前
现代C++锁介绍
c++·多线程·mutex
汤姆和杰瑞在瑞士吃糯米粑粑2 小时前
【C++学习篇】AVL树
开发语言·c++·学习