跳过正文

epoll/kqueue 文件传输服务器和客户端

·1903 字·4 分钟· loading · loading ·
加绒
作者
加绒
融雪之前,牧神搭上春色的火车,而日光在我们之间。
目录
小米营军训记 - 这篇文章属于一个选集。
§ 2: 本文

使用 epoll 开发一个文件传输的服务器和客户端

要求:

  • 客户端申请指定的文件名,服务器返回对应的文件数据,客户端将收到的数据写入本地文件

  • 异常情况处理:无指定文件则返回“not file”字符串

  • 完成基础的文件传输

  • 断点续传:见 FileHandler 类的 appendAndVerifyFilesaveAndVerifyFile 方法,Client 类的 transferFileresumeTransfer 方法

  • 文件校验:见 FileHandler 类的 calculateMD5 方法

目录结构
#

.
├── README.md
├── assets
│   └── ...
├── include
│   ├── file_handler.h
│   ├── logger.h
│   └── md5.hpp
├── src
│   ├── client.cpp
│   └── server.cpp
└── xmake.lua

编译和运行
#

使用 xmake 来编译项目:

xmake

运行服务器:

xmake run server

运行客户端:

xmake run client

其中,客户端需要携带服务器 IP 的参数,已经写在 xmake.lua 中,默认为 127.0.0.1。由于我使用的是 macOS 系统,它缺少 epoll,所以我使用了 #ifdef 来区分不同的操作系统,确保代码的可移植性。起初我是分不同的文件编写不同平台的代码,但后来发现只需要简单地使用 #ifdef 来区分即可。 所以在 xmake.lua 中,编译选项如下:

target("server")
    set_kind("binary")
    add_files("src/server.cpp")
    set_languages("c++17")
    set_rundir("$(projectdir)")
    add_includedirs("include")

target("client")
    set_kind("binary")
    add_files("src/client.cpp")
    set_languages("c++17")
    set_runargs("127.0.0.1")
    add_includedirs("include")

运行截图
#

output

epoll 和 select/poll 的区别
#

epoll 是 Linux 内核提供的一种高效的 I/O 事件通知机制,相较于 selectpoll,它有以下优点:

  1. 使用事件驱动模型
    • selectpoll 需要每次调用时都传递整个文件描述符集合,效率较低。
    • epoll 使用事件通知机制,只需关注发生变化的文件描述符,在内核中维护了一个事件表,应用程序只需关注感兴趣的事件,提高了性能。
  2. 支持大规模文件描述符
    • select 限制了文件描述符的数量(通常为 1024),而 pollepoll 没有这个限制。
    • 使用红黑树实现,epoll 可以高效地管理大量文件描述符。
  3. 较低的内存消耗
    • epoll 内部使用内存映射文件(mmap)来减少内存拷贝,提高性能。

在 macOS 和 BSD 系统中,epoll 不可用,通常使用 kqueue 作为替代。

实现
#

实现上参考了 aceld/libevent,但最终效果和它的代码实现是不一样的,也借助了一些 AI 的力量,最终实现了这个文件传输的服务端和客户端。有点坎坷的,但还是收获颇丰。

服务端
#

flowchart

服务端的核心是 NetworkServer 类,UML 类图如下:

uml

进入主函数后,服务端会初始化一个 NetworkServer 对象,并调用其 run 方法开始监听客户端的连接请求。初始化过程包括创建 socket、绑定地址、监听端口等操作。

run 方法是一个死循环,不断地调用 epoll_wait 来等待事件的发生。

当有新的客户端连接时,服务端使用 handleNewConnection 方法会接受连接并将其添加到 epoll 监听列表中。当有数据可读时,服务端会使用 handleClientRead 方法读取数据并进行相应的处理。

handleNewConnection 方法的实现和 socket-sample 中的类似,主要是使用 accept 接受新的连接,并将其添加到 epoll 监听列表中。handleClientRead 方法会读取客户端发送的文件名,并根据文件名查找对应的文件。如果文件存在,服务端会将文件内容发送给客户端,其余错误处理由 FileHandler 类来完成。

FileHandler 类
#

uml

FileHandler 类其实只是把一系列文件操作的函数进行封装,提供了一个接口。

  • calculateMD5:计算文件的 MD5 值

    使用的头文件来自 zaphoyd/websocketpp,它修改自 苹果的 md5.c 实现

  • getFileSize:获取文件大小

  • sendFile:发送文件内容

    主要的执行流程如下图所示:

    flowchart

    如果文件不存在,或者文件无法打开,或者文件读取失败,都会返回相应的错误信息,比如 not file

  • createSafeFilename:创建安全的文件名,主要是因为带有路径 / 的文件名并不一定能被正确处理,所以需要将其转换为安全的文件名。

  • appendAndVerifyFile:断点续传的实现,主要是通过在文件末尾追加数据来实现的,同时会校验文件的 MD5 值是否一致。

  • saveAndVerifyFile:保存文件并校验 MD5 值,主要用于第一次传输文件时使用。

    这两个方法其实很像,只是 appendAndVerifyFile 是在文件末尾追加数据,而 saveAndVerifyFile 是直接覆盖文件内容。流程如下图所示:

    flowchart

客户端
#

flowchart

客户端的核心是 FileTransferClient 类和 NetworkClient 类,UML 类图如下:

uml

至于为什么要分成两个类,主要是因为客户端的功能相对简单,FileTransferClient 只需要处理文件传输相关的逻辑,而 NetworkClient 只需要处理网络相关的逻辑即可。

进入主函数后,客户端会初始化一个 FileTransferClient 对象,并调用其 connect 方法开始与服务端进行通信。连接的方法同样是使用 socketconnect 等操作,和服务端类似。

连接成功后,客户端可以不断地向服务端发送文件名,请求文件数据。客户端会使用 transferFile 或者 resumeTransfer方法来请求文件传输。transferFile 方法用于第一次传输文件,而 resumeTransfer 方法用于断点续传,它会检查已经存在的文件,然后给 transferFile 方法传递一个 fileSize 参数,表示文件的大小即其偏移量,再交由 transferFile 方法来处理。

流程如下图所示:

flowchart

在这里面调用了 NetworkClientreceiveFileData 方法来接收文件数据,并将其写入本地文件中。文件每次读取 1024 字节,直到文件传输完成。

flowchart

文件接收后,交由 FileHandler 类来进行 MD5 校验,确保文件传输的正确性。

Logger
#

实际上也只是定义了 LogLevel 枚举和一个简单的日志输出函数,方便在调试过程中查看日志信息。

uml

小米营军训记 - 这篇文章属于一个选集。
§ 2: 本文