故障修复说明.txt 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839
  1. chat_room 故障复盘、学习过程与修复说明
  2. 说明:
  3. 这份文档不是只写“最后修好了什么”,而是按学习过程来写,尽量把这次排查中遇到的每一个关键问题都讲清楚:
  4. 1. 现象是什么
  5. 2. 为什么会出现这种现象
  6. 3. 我是怎么一步一步定位的
  7. 4. 每一步排查时学到了什么
  8. 5. 代码层面具体改了什么
  9. 6. 部署层面具体改了什么
  10. 7. 最后怎么验证问题真的被解决了
  11. 一、问题背景
  12. 这个项目是一个基于 WebSocket 的简易聊天室,分为两部分:
  13. 1. 服务端
  14. 路径:D:\Network\chat_room\chat
  15. 技术:Java + Gradle + Java-WebSocket
  16. 作用:接收客户端连接,处理登录、广播、私聊、在线用户等逻辑。
  17. 2. 客户端
  18. 路径:D:\Network\chat_room\user
  19. 技术:Android Java
  20. 作用:手机端聊天界面,负责连接服务器、发送消息、接收消息、展示消息。
  21. 线上部署方式:
  22. 1. Java 聊天服务实际监听 8080。
  23. 2. Nginx 监听公网 80。
  24. 3. Nginx 把 WebSocket 请求反向代理到 127.0.0.1:8080。
  25. 4. 所以客户端默认连接地址是:
  26. ws://124.220.100.95
  27. 二、最开始用户看到的现象
  28. 当时用户看到的是:
  29. 1. App 可以打开。
  30. 2. 顶部状态一直显示“登录中”。
  31. 3. 输入框可以输入文字。
  32. 4. 发送按钮是灰色,点不动。
  33. 5. 两台手机之间不能像 QQ / 微信 那样正常互发消息。
  34. 6. 有时看起来像是“已经连上服务器了”,但消息并没有真正互通。
  35. 从用户角度看,这个问题很容易理解成:
  36. 1. 是不是按钮坏了
  37. 2. 是不是前端界面写错了
  38. 3. 是不是服务器地址没填对
  39. 4. 是不是端口没开
  40. 但真正的问题并不是单点故障,而是一个完整链路上的组合问题。
  41. 三、第一次判断时为什么容易误判
  42. 这次排查里,最容易误判的地方在于:
  43. 1. WebSocket 连接本身确实能建立。
  44. 2. Nginx 也确实在转发请求。
  45. 3. 所以看起来像是“网络是通的”。
  46. 为什么这会误导人?
  47. 因为很多人会把下面两件事混为一谈:
  48. 1. TCP / WebSocket 连接成功
  49. 2. 业务登录成功
  50. 实际上这是两层不同的事情:
  51. 1. WebSocket 连接成功,只说明底层链路通了。
  52. 2. 业务登录成功,说明服务端已经正确处理了 LOGIN,并把登录确认回给客户端。
  53. 这次问题里,第一层是通的,第二层是不通的。
  54. 这也是这次学习过程中最重要的一点之一:
  55. 排查实时通信问题时,不能只看“连上没连上”,一定要继续问:
  56. 1. 连接建立后,第一条业务消息有没有发出去
  57. 2. 服务端有没有收到
  58. 3. 服务端有没有返回确认
  59. 4. 客户端有没有收到确认
  60. 5. 客户端有没有根据确认更新自己的状态
  61. 四、发送按钮为什么会是灰色
  62. 这个问题必须先从客户端逻辑理解。
  63. 客户端新版本的发送按钮不是单纯由界面样式控制,而是由业务状态控制。
  64. 按钮可点的条件是:
  65. 1. WebSocket 已连接
  66. 2. 登录已完成
  67. 也就是类似:
  68. 1. isConnected == true
  69. 2. isLoggedIn == true
  70. 只要第二个条件不成立,按钮就会保持禁用状态。
  71. 所以发送按钮灰色,并不一定说明按钮组件坏了,而更可能说明:
  72. 1. 客户端认为自己还没完成登录
  73. 2. 客户端在等待服务端的登录确认
  74. 这就是为什么我后来没有去直接“强行点亮按钮”,因为那样只是把表面效果改了,真正的业务状态依然是错的。
  75. 学习点:
  76. 前端控件状态如果由业务状态驱动,就应该顺着业务状态去找问题,而不是直接改样式。
  77. 五、真正卡住的位置到底在哪里
  78. 问题真正卡住的位置是:
  79. 客户端发送 LOGIN 之后,没有正确收到服务端的登录确认回包。
  80. 正常链路应该是:
  81. 1. 客户端建立 WebSocket 连接
  82. 2. onOpen 触发
  83. 3. 客户端自动发送 LOGIN
  84. 4. 服务端处理 LOGIN
  85. 5. 服务端返回:
  86. - Welcome
  87. - joined the chat
  88. - ONLINE
  89. 6. 客户端收到这些消息后,把自己状态改为“已登录”
  90. 7. 发送按钮解锁
  91. 但当时实际发生的是:
  92. 1. 客户端建立了 WebSocket 连接
  93. 2. 客户端确实发出了 LOGIN
  94. 3. 服务端也确实收到了 LOGIN
  95. 4. 但服务端没有把欢迎消息和在线列表正确返回给客户端
  96. 5. 客户端一直停在“登录中”
  97. 6. 发送按钮一直保持灰色
  98. 所以,故障真正卡住的位置不是“按钮组件”,而是“服务端登录确认链路”。
  99. 六、我是怎么一步一步定位这个问题的
  100. 这一部分很重要,因为这是完整的学习过程。
  101. 第一步:看本地代码结构
  102. 我先检查了本地仓库,确认项目到底有哪些部分。
  103. 当时先看到的是:
  104. 1. D:\Network\chat_room\chat
  105. 通过代码搜索,我确认了这部分是服务端,核心信息包括:
  106. 1. 使用 Java-WebSocket
  107. 2. 默认监听 8080
  108. 3. 有 LOGIN / ALL / PRIVATE / ONLINE 等消息类型
  109. 这一阶段学到的是:
  110. 先不要急着猜问题,要先搞清楚项目结构,知道“谁是服务端、谁是客户端、协议长什么样、消息怎么流转”。
  111. 第二步:确认真正客户端在哪里
  112. 一开始用户口头说客户端是 usera,但目录里实际上没有 usera。
  113. 我继续检查 D:\Network\chat_room,发现真实目录是:
  114. 1. D:\Network\chat_room\chat
  115. 2. D:\Network\chat_room\user
  116. 后面确认 user 才是真正的 Android 客户端工程。
  117. 这一阶段学到的是:
  118. 用户口述路径不一定和实际路径完全一致,排查时应该以实际文件系统为准。
  119. 第三步:检查客户端逻辑
  120. 我看了客户端的 MainActivity,重点关注:
  121. 1. 什么时候连接服务器
  122. 2. 什么时候发送 LOGIN
  123. 3. 发送按钮什么时候启用
  124. 4. 收到什么消息后才算登录成功
  125. 由此确认:
  126. 1. 客户端会自动连接服务器
  127. 2. onOpen 后会自动发送 LOGIN
  128. 3. 发送按钮依赖登录状态,不是单纯依赖连接状态
  129. 这一步的结论非常关键:
  130. 按钮灰色不是 UI 渲染错误,而是客户端有意识地禁止发送。
  131. 第四步:检查服务端本地逻辑
  132. 我继续读服务端代码,重点检查:
  133. 1. ChatApplication.java
  134. 2. ChatWebSocketServer.java
  135. 3. ChatMessageRouter.java
  136. 4. ChatRoomManager.java
  137. 通过这些文件,我确认了服务端登录流程大致是:
  138. 1. 收到 LOGIN
  139. 2. register 用户
  140. 3. 给当前用户发送 Welcome
  141. 4. 向所有人广播 joined
  142. 5. 给当前用户发送 ONLINE
  143. 到这里,逻辑表面上看是对的。
  144. 但“表面上看是对的”不等于“实现细节就是对的”。
  145. 第五步:本地启动服务端做联通测试
  146. 我在本地启动了服务端,并用脚本模拟两个 WebSocket 客户端。
  147. 这里做了几轮测试:
  148. 1. 先测单用户 LOGIN
  149. 2. 再测双用户 LOGIN
  150. 3. 再测 PRIVATE
  151. 4. 再测 ALL
  152. 这一步发现了一个重要现象:
  153. 1. 群聊和私聊主链路大体是能走通的
  154. 2. 但是登录阶段会出现异常状态消息
  155. 当时我就开始怀疑:
  156. 服务端的“已注册会话”和“当前处理中的 sender 对象”可能不是同一个状态。
  157. 第六步:直接连公网地址做协议测试
  158. 为了确认问题到底在本地代码、部署、还是线上环境,我没有只看本地,而是直接连公网:
  159. 1. 连接 ws://124.220.100.95
  160. 2. 手动发送 LOGIN JSON
  161. 3. 观察有没有回 Welcome / ONLINE
  162. 测试结果是:
  163. 1. WebSocket 能建立连接
  164. 2. 但服务端没有回任何登录确认消息
  165. 然后我又做了第二轮公网测试:
  166. 1. 两个客户端同时连接
  167. 2. 两边都发 LOGIN
  168. 3. 再由一边发 ALL
  169. 结果是:
  170. 1. 登录没有回包
  171. 2. 群聊也没有回包
  172. 这一步的意义是:
  173. 把问题从“客户端界面问题”彻底缩小成“线上服务端业务处理或部署版本问题”。
  174. 七、服务端代码里真正的 bug 是什么
  175. 问题文件是:
  176. chat\src\main\java\com\campus\chat\server\ChatMessageRouter.java
  177. 问题函数是:
  178. handleLogin()
  179. 原来的问题逻辑:
  180. 1. 收到 LOGIN 时,当前 sender 其实是一个临时会话对象:
  181. SessionInfo(null, conn)
  182. 2. 代码调用 roomManager.register(username, sender.getConnection())
  183. 3. register 会新建一个真正的已登录 SessionInfo(username, conn),并放进 sessionsBySocket / sessionsByUsername
  184. 4. 但是后续代码继续用旧的 sender 发 Welcome 和 ONLINE
  185. 5. 旧 sender 里的 username 仍然是 null
  186. 为什么这会出问题?
  187. 因为从 register 之后开始,真正应该代表当前用户的对象已经不是旧的 sender,而是 ChatRoomManager 里新注册进去的那个 SessionInfo。
  188. 如果继续拿旧 sender 发消息,就会出现:
  189. 1. 登录状态对象不一致
  190. 2. 某些地方认为已经登录
  191. 3. 某些地方仍然认为未登录
  192. 4. 最终客户端收不到完整稳定的登录确认
  193. 这一点本质上是“状态源不一致”的问题。
  194. 学习点:
  195. 注册用户、绑定连接、会话对象管理这类逻辑里,最怕的不是 if 写错,而是“系统里同时存在两个代表当前用户的对象”。
  196. 八、服务端 bug 是怎么修的
  197. 修复方式非常明确:
  198. 1. 先 register(username, sender.getConnection())
  199. 2. 再从 roomManager.findBySocket(sender.getConnection()) 取回真正已注册的 SessionInfo
  200. 3. 用这个 registeredSender 发 Welcome
  201. 4. 用这个 registeredSender 发 ONLINE
  202. 也就是说:
  203. 1. register
  204. 2. refetch session
  205. 3. use registered session for all post-login actions
  206. 修完后:
  207. 1. 登录后的消息发送使用的是统一状态对象
  208. 2. 服务端能正确回 Welcome
  209. 3. 服务端能正确回 ONLINE
  210. 4. 客户端能真正完成登录
  211. 九、为什么“不能发消息”和“对方手机收不到消息”会一起出现
  212. 这两个现象的根因其实是一条线上的。
  213. 原因链条是:
  214. 1. 客户端没完成登录
  215. 2. 发送按钮不解锁
  216. 3. 即使绕过按钮,有些消息也会因为服务端认为 sender 未登录而处理失败
  217. 4. 广播逻辑依赖 sender 已正确注册
  218. 5. 所以最终表现为“不能发、不能收、对方看不到”
  219. 学习点:
  220. 在即时通信系统里,“登录是否成功”不是独立的小问题,而是整个消息链路的前置条件。
  221. 十、客户端本身有哪些设计问题
  222. 除了服务端 bug,这次还顺手暴露出客户端原来设计上的一些问题。
  223. 问题 1:聊天主界面像调试面板
  224. 现象:
  225. 1. 服务器地址输入框直接放在聊天页
  226. 2. 昵称输入框也直接放在聊天页
  227. 3. 界面像“开发测试工具”,不像“普通聊天软件”
  228. 为什么这是问题?
  229. 因为:
  230. 1. 普通用户不应该每次进入聊天页都看到服务器地址
  231. 2. 昵称这种配置项更适合首次设置或设置页
  232. 3. 主聊天页应该尽量只保留会话相关元素
  233. 解决:
  234. 1. 去掉服务器地址输入框
  235. 2. 去掉昵称输入框
  236. 3. 服务器地址改内置
  237. 4. 昵称改成首次弹窗设置
  238. 5. 设置按钮用来修改昵称
  239. 问题 2:默认昵称固定为 guest
  240. 现象:
  241. 1. 两台设备可能都叫 guest
  242. 2. 第二台会被服务端判定为重名
  243. 3. 用户会误以为“好像已经连上了,只是发不出去”
  244. 为什么会这样?
  245. 1. 默认值写死,没考虑多设备测试
  246. 2. 用户又不一定第一时间主动改昵称
  247. 解决:
  248. 1. 默认昵称改成根据设备信息自动生成
  249. 2. 首次进入弹窗要求确认昵称
  250. 3. 昵称保存到本地
  251. 4. 遇到 Username already in use. 时强制重新设置
  252. 问题 3:消息重复显示风险
  253. 现象:
  254. 1. 客户端发送时本地先插一条自己的消息
  255. 2. 服务端广播回来又显示一条
  256. 为什么会这样?
  257. 因为客户端做了 optimistic UI,而服务端也会回广播。
  258. 解决:
  259. 1. 改成主要依赖服务端真实广播回包来展示消息
  260. 这样做的好处:
  261. 1. 自己发出去的消息和别人收到的消息展示逻辑一致
  262. 2. 更接近真实的最终状态
  263. 十一、客户端代码修改过程中遇到的问题
  264. 这一部分是纯学习过程,记录的是“在写和改代码过程中遇到的技术问题”。
  265. 问题 1:原文件中文文案乱码,导致精确补丁容易失败
  266. 现象:
  267. 1. MainActivity.java 里有些中文字符串显示成乱码
  268. 2. 按原字符串做上下文匹配时补丁不稳定
  269. 原因:
  270. 1. 文件原有编码和内容已经存在混乱
  271. 解决:
  272. 1. 放弃小块 patch
  273. 2. 改成整文件替换 MainActivity、activity_main.xml、strings.xml
  274. 学习点:
  275. 当文件上下文已经不可靠时,整文件替换有时比硬做小补丁更稳。
  276. 问题 2:旧客户端构建报告里有 JSONException 编译错误
  277. 现象:
  278. 1. 构建报告里曾有未处理 JSONException 的编译错误
  279. 原因:
  280. 1. 旧版消息解析流程里异常处理不完整
  281. 解决:
  282. 1. 重写 MainActivity 的消息处理逻辑
  283. 2. 重新编译 assembleDebug
  284. 问题 3:illegal forward reference
  285. 现象:
  286. 1. 重写 MainActivity 后,编译器提示 illegal forward reference
  287. 原因:
  288. 1. Runnable 字段在初始化时直接依赖了还没完全建立好的成员字段
  289. 解决:
  290. 1. 把 reconnectRunnable 改成 this::runReconnect
  291. 2. 真正逻辑放进 runReconnect() 方法中
  292. 学习点:
  293. Java 字段初始化顺序是非常具体的,复杂字段逻辑最好延后到方法里。
  294. 问题 4:匿名回调里 client 变量“可能尚未初始化”
  295. 现象:
  296. 1. 编译器提示 client 可能尚未初始化
  297. 原因:
  298. 1. 匿名内部类中引用了正在构造过程中的 client 本身
  299. 解决:
  300. 1. 使用 final ChatWebSocketClient[] clientRef = new ChatWebSocketClient[1]
  301. 2. 构造完成后再赋值 clientRef[0] = client
  302. 学习点:
  303. 这类问题不是业务错误,而是 Java 语言层面的闭包/初始化限制。
  304. 问题 5:修完客户端后,按钮还是灰的
  305. 现象:
  306. 1. 新客户端装上后,按钮一度还是灰的
  307. 为什么会这样?
  308. 1. 客户端逻辑已经修了
  309. 2. 但手机实际连接的还是线上旧服务端
  310. 3. 旧服务端仍然不给 Welcome / ONLINE
  311. 解决:
  312. 1. 不继续在按钮逻辑上瞎改
  313. 2. 继续追服务器部署状态
  314. 学习点:
  315. 本地代码改对了,不代表线上就已经生效。一定要把“代码状态”和“部署状态”分开看。
  316. 十二、部署排查过程中遇到的问题
  317. 这一部分是部署学习过程。
  318. 问题 1:最开始容易怀疑云防火墙没放 8080
  319. 现象:
  320. 1. 服务端默认监听 8080
  321. 2. 云控制台截图里没有明确放开 8080
  322. 为什么容易误判?
  323. 因为很多人看到“端口不一致”就会先怀疑防火墙。
  324. 怎么解决?
  325. 1. 登录服务器查看 nginx -T
  326. 2. 查看 ss -ltnp
  327. 3. 确认真实链路是:
  328. 80 -> nginx -> 127.0.0.1:8080
  329. 结果:
  330. 1. 证明不是必须公网直接放 8080
  331. 2. 这不是主因
  332. 问题 2:需要确认线上到底跑的是哪个版本
  333. 现象:
  334. 1. 本地代码修了
  335. 2. 但公网行为没变
  336. 为什么会这样?
  337. 因为本地代码和线上部署不是同一个概念。
  338. 怎么解决?
  339. 1. 通过 SSH 登录服务器
  340. 2. 查看 /etc/systemd/system/chat.service
  341. 3. 查看 /opt/chat 目录
  342. 4. 查看 Java 启动命令
  343. 结果:
  344. 1. 确认线上 chat.service 启动的是 /opt/chat/bin/chat
  345. 2. 确认线上真正运行的 jar 在 /opt/chat/lib/chat-0.0.1-SNAPSHOT.jar
  346. 问题 3:如何确认服务端“收到消息但没回消息”
  347. 现象:
  348. 1. 客户端拿不到回包
  349. 为什么不能只靠客户端猜?
  350. 因为必须区分:
  351. 1. 消息有没有发到服务器
  352. 2. 服务器有没有收到
  353. 3. 服务器收到后有没有处理
  354. 4. 处理后有没有发送回包
  355. 怎么解决?
  356. 1. 查 systemctl status chat --no-pager
  357. 2. 看日志里的 Raw message received
  358. 结果:
  359. 1. 确认线上 Java 进程确实收到了 LOGIN 和 ALL
  360. 2. 问题不是“消息没到服务器”,而是“服务器收到后没正确回包”
  361. 问题 4:scp 上传部署包卡住
  362. 现象:
  363. 1. 直接 scp 上传时超时
  364. 为什么会这样?
  365. 1. scp 在等待交互输入密码
  366. 2. 自动化环境没法手工输入
  367. 怎么解决?
  368. 1. 改用 Python 的 paramiko + SFTP
  369. 2. 在脚本里显式提供用户名和密码
  370. 结果:
  371. 1. 部署包能稳定上传
  372. 问题 5:服务器本机没有 websockets Python 库
  373. 现象:
  374. 1. 想在服务器本机做 Python WebSocket 回环测试
  375. 2. 报错 ModuleNotFoundError: No module named 'websockets'
  376. 为什么会这样?
  377. 1. 系统 Python 里没装这个依赖
  378. 怎么解决?
  379. 1. 没有为了诊断去污染服务器环境安装新库
  380. 2. 改用已有证据链:
  381. - 公网直连测试
  382. - systemd 日志
  383. - nginx 配置
  384. - Java 进程状态
  385. 结果:
  386. 1. 即使没有这条回环测试,也足够完成定位
  387. 学习点:
  388. 排查时不一定每个辅助工具都必须装齐,关键是证据链能闭环。
  389. 问题 6:线上跑的是旧版本 /opt/chat
  390. 现象:
  391. 1. 本地修好了
  392. 2. 客户端修好了
  393. 3. 公网测试还是旧行为
  394. 为什么会这样?
  395. 1. 服务器上 /opt/chat 还没替换成新构建
  396. 2. chat.service 仍然在跑旧 jar
  397. 怎么解决?
  398. 1. 本地重新构建 chat
  399. 2. 打包部署目录
  400. 3. 上传服务器
  401. 4. 停掉 chat.service
  402. 5. 备份旧的 /opt/chat 为 /opt/chat.bak
  403. 6. 解压新包到 /opt/chat
  404. 7. chown 给 chat:chat
  405. 8. 启动 chat.service
  406. 结果:
  407. 1. 线上服务真正切换到修复版
  408. 十三、线上部署实际执行了哪些事情
  409. 这是整个部署修复的完整过程。
  410. 1. 在本地执行:
  411. gradlew clean installDist test
  412. 目的:
  413. 1. 重新编译服务端
  414. 2. 生成新的运行目录
  415. 3. 跑服务端测试,确认本地构建没问题
  416. 2. 在本地把 build/install/chat 打成部署 tar 包
  417. 目的:
  418. 1. 把可运行目录整体封装,便于上传
  419. 3. 用 paramiko SFTP 上传到服务器
  420. 目的:
  421. 1. 避开 scp 交互密码问题
  422. 4. 在服务器上停止 chat.service
  423. 目的:
  424. 1. 避免覆盖运行中的目录
  425. 5. 备份旧目录
  426. 动作:
  427. 1. /opt/chat -> /opt/chat.bak
  428. 目的:
  429. 1. 保留旧版本,必要时可回滚
  430. 6. 把新 tar 包解压到 /opt/chat
  431. 目的:
  432. 1. 替换旧部署目录
  433. 7. 修正目录属主为 chat:chat
  434. 目的:
  435. 1. 保证 systemd 用 chat 用户运行时对目录有权限
  436. 8. 启动 chat.service
  437. 目的:
  438. 1. 让线上开始跑修复后的版本
  439. 9. 查看 systemctl status chat
  440. 目的:
  441. 1. 确认服务已经真正启动成功
  442. 十四、最终是怎么验证真的修好了
  443. 这一步非常重要,因为“感觉修好了”和“证明确实修好了”是两回事。
  444. 验证 1:公网登录验证
  445. 1. 直接连 ws://124.220.100.95
  446. 2. 手动发 LOGIN
  447. 3. 成功收到:
  448. - Welcome, verify_user.
  449. - verify_user joined the chat.
  450. - ONLINE
  451. 说明:
  452. 1. 登录确认链路已经恢复
  453. 验证 2:双端消息验证
  454. 1. 建两个公网 WebSocket 客户端
  455. 2. 分别登录 alice_fix 和 bob_fix
  456. 3. alice_fix 发 hello fixed
  457. 4. alice_fix 自己收到广播
  458. 5. bob_fix 也收到广播
  459. 说明:
  460. 1. 群聊广播链路恢复
  461. 2. 对方手机应该也能看到消息
  462. 验证 3:客户端行为验证
  463. 修好之后,客户端预期行为应该是:
  464. 1. 顶部状态从“登录中”切换为“在线”
  465. 2. 发送按钮解锁
  466. 3. 输入消息后可发送
  467. 4. 对方客户端能看到消息
  468. 十五、这次学习过程中最重要的收获
  469. 1. 连接成功不等于业务成功。
  470. WebSocket 建立成功,只说明链路通;是否能聊天,要看 LOGIN、ONLINE、ALL 这些业务消息是否真正完成闭环。
  471. 2. UI 状态很多时候只是业务状态的表象。
  472. 按钮灰色,并不一定是按钮自身的问题,往往是后面的业务状态没有变。
  473. 3. 状态管理最怕“对象不一致”。
  474. 这次服务端 bug 的本质不是语法错误,而是 register 之后继续使用旧 SessionInfo,导致状态源错位。
  475. 4. 本地修复不等于线上生效。
  476. 本地代码、构建产物、服务器部署目录、systemd 服务,都是不同层。每一层都可能是问题点。
  477. 5. 实时通信故障要按链路排查。
  478. 最稳的方法不是猜,而是逐段验证:
  479. - 客户端有没有发
  480. - 服务端有没有收
  481. - 服务端有没有回
  482. - 客户端有没有处理回包
  483. 6. 部署问题和代码问题常常同时存在。
  484. 这次不是“只修代码就完事”,也不是“只改服务器就完事”,而是两边都要改,最后再重新部署。
  485. 十六、最终结果
  486. 到本次修复结束时,主链路已经恢复:
  487. 1. 客户端可以连接公网地址 ws://124.220.100.95
  488. 2. 客户端可以收到 Welcome / ONLINE
  489. 3. 顶部状态会从“登录中”切换为“在线”
  490. 4. 发送按钮会解锁
  491. 5. 一台设备发消息,另一台设备可以收到
  492. 6. 线上服务已替换为新版本
  493. 7. 旧版本保留在 /opt/chat.bak 作为回滚备份
  494. 这说明:
  495. 1. 不是只修了界面
  496. 2. 不是只修了本地代码
  497. 3. 而是把“客户端逻辑 + 服务端逻辑 + 线上部署”这一整条链路修通了