GSoC 2022 Series - 3

系列导航

设计与实现

mentor 希望在 rtorrent 中引入 websocket 的同时还保留 scgi。现在 rtorrent 有 3 个线程,我的想法是在一个独立的线程来运行 websocket 服务,届时整个进程中将会有 4 个线程。引入 RpcThreadManager 类来管理 scgi 和 websocket 线程,类中的成员变量是 ThreadWorker 和 WebsocketsThread 的指针,成员方法是 ThreadWorker 类中暴露的 public 方法,这样只需要将 global.h 中的全局变量 worker_thread 的类型从 ThreadWorker 改为 RpcThreadManager 从而不需要改变调用接口的定义。

利用 uWebsockets 来实现 websocket 服务,所有的逻辑都放在 WebsocketsThread 这个类中,主要的成员变量有:

  • std::unique_ptr<std::thread> m_websockets_thread:
    当前运行 websocket 服务的线程指针,析构时 join 掉线程
  • uWS::App* m_websockets_app:
    websocket App 对象,后续调用其 publish 方法向 client 推送通知
  • std::pair<std::string, int>* listen_info:
    websocket 服务监听方式(unix domain socket 还是 tcp:ip port 以及监听的地址信息
  • std::vector<uWS::WebSocket<false, true, ConnectionData>*> all_connection:
    保存与所有 client 的连接,析构时 close

主要的成员变量有:

  • void start_thread();
    初始化 uWS::App 对象并创建线程运行 websocket 服务
  • void publish_ws_topic(std::string_view topic, std::string_view message);
    外部向 client 推送通知的接口
  • void handle_request(const std::string_view&);
    收到 client 的 JSON-RPC 命令后分派给 RpcManager 处理然后返回处理结果
  • ~WebsocketsThread();
    关闭掉所有 ws 连接,清理资源,join 掉当前线程

遇到的问题

如何优雅结束进程?

每次退出的时候都会阻塞在 m_websockets_thread->join(); 上,按照 uWebsockets 文档的描述,清理掉 App 对象依赖的资源后其 run 方法会自动退出,可是执行了 us_listen_socket_close(0, m_listen_socket); 之后 run 方法还是没有返回。后来尝试了在关闭 socket 之前 close 掉与所有 client 的连接后 run 方法就成功返回了,进而 m_websockets_thread 就能顺利 join 掉了。

bazel build

我是在 windows 上用 wsl 环境开发的。项目中用到了 bazel 构建系统所以我也得支持这种构建方式,运行 bazel build rtorrent 后报错 ERROR: /home/flash/.cache/bazel/_bazel_flash/deb97f29152e01999a06e8171eedc056/external/cares/BUILD.bazel:115:8: Executing genrule @cares//:configure failed: (Exit 127): bash failed: error executing command /bin/bash -c … (remaining 1 argument skipped) /bin/bash: $'\r': command not found。这是 wsl 的锅,参考这里用 dos2unix 把有问题的文件格式化之后重新执行 bazel build rtorrent 就成功了

推送通知到 client 时程序就 Segmentation fault 了

一开始还以为是不同线程之间内存的访问可见性问题,但是线程之间是共享内存的呀,在创建好 uWS::App 对象后就通过指针保存到 m_websockets_app,而且在 websocket 线程中和外部调用线程中 m_websockets_app 打印出来的指针值都是一样的,为什么一调用方法就出错???这个问题困扰了我好久,后来突然想到是不是 publish 的时候 App 还没初始化好?结果还真是!!!知道了原因之后就很好解决了:publish的时候对 m_websockets_app 判空就 OK 了。我在 debug 的时候看到 m_websockets_app 为 NULL 的时候才想到这一点,如果没有想到的话肯定会走不少弯路(其实已经走了不少了)

想到在 leetcode 用 C++ 做题的时候,如果访问了空指针会给出报错原因

如果当时 IDE 在我访问还没初始化的 m_websockets_app 也能给出这样的提示那么我马上就知道问题在哪了。后来 Google 到了 sanitize 编译选项,在访问空指针的测试代码中用
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=null -fsanitize=leak -fsanitize=address") 这些选项编译就能有类似的提示。但是在 rtorrent 中加入这些编译选项后还是没有,不知道为什么。。。

hash string 乱码

我想在返回消息中带上当前 torrent 的 hash string 给 client 作为判断是针对哪个 torrent 的事件通知,看代码应该是 download->info()->hash().str() 这个方法,结果 json 无法解析,打印出来一看是乱码。一开始还在想会不会又是 wsl 的锅,后来放到虚拟机上跑还是一样。最后是从 rpc 命令入手的:发现有一条 “d.hash” 的命令可以返回 torrent 的 hash string,那么 rtorrent 中肯定有处理这条命令的代码,通过在 IDE 中搜索 “d.hash” 找到 src/command_download.cc line 802,发现原来还要 transform 一下才可以:torrent::utils::transform_hex_str(download->info()->hash().str())

scgi 和 websocket 监听同一个 socket

给 mentor 看完代码后 mentor 说能不能让 scgi 和 websocket 监听同一个 socket,后面我去了解到了 SO_REUSEPORT 这个 socket option,在 scgi 中开启后程序可以启动,但是 scgi 服务却不能正常运行,接下来尝试了让一个 tcp echo server 和 websocket 监听在同一个 port 上,发现了消息错乱的现象:websocket 的第一个 http 报文有可能被 tcp echo server 收了,返回的内容自然不是 websocket client 能处理就报错了,二者都是运行在 tcp 之上,不能保证 client 发来的消息被正确的 server 收到。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!