[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"i-ri:search-line":3,"i-ri:moon-line":8,"i-ri:sun-line":10,"i-ri:menu-3-line":12,"i-ri:close-line":14,"notes-module-items-im":16,"notes-page-im-02-connection-protocol\u002F02-request-response":66,"i-ri:arrow-left-s-line":243,"i-ri:menu-2-line":245,"i-ri:arrow-right-s-line":247},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":7},0,24,false,"\u003Cpath fill=\"currentColor\" d=\"m18.031 16.617l4.283 4.282l-1.415 1.415l-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9s9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617m-2.006-.742A6.98 6.98 0 0 0 18 11c0-3.867-3.133-7-7-7s-7 3.133-7 7s3.133 7 7 7a6.98 6.98 0 0 0 4.875-1.975z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":9},"\u003Cpath fill=\"currentColor\" d=\"M10 7a7 7 0 0 0 12 4.9v.1c0 5.523-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2h.1A6.98 6.98 0 0 0 10 7m-6 5a8 8 0 0 0 15.062 3.762A9 9 0 0 1 8.238 4.938A8 8 0 0 0 4 12\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":11},"\u003Cpath fill=\"currentColor\" d=\"M12 18a6 6 0 1 1 0-12a6 6 0 0 1 0 12m0-2a4 4 0 1 0 0-8a4 4 0 0 0 0 8M11 1h2v3h-2zm0 19h2v3h-2zM3.515 4.929l1.414-1.414L7.05 5.636L5.636 7.05zM16.95 18.364l1.414-1.414l2.121 2.121l-1.414 1.414zm2.121-14.85l1.414 1.415l-2.121 2.121l-1.414-1.414zM5.636 16.95l1.414 1.414l-2.121 2.121l-1.414-1.414zM23 11v2h-3v-2zM4 11v2H1v-2z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":13},"\u003Cpath fill=\"currentColor\" d=\"M3 4h18v2H3zm6 7h12v2H9zm-6 7h18v2H3z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":15},"\u003Cpath fill=\"currentColor\" d=\"m12 10.587l4.95-4.95l1.414 1.414l-4.95 4.95l4.95 4.95l-1.415 1.414l-4.95-4.95l-4.949 4.95l-1.414-1.415l4.95-4.95l-4.95-4.95L7.05 5.638z\"\u002F>",[17,22,26,30,34,38,42,46,50,54,58,62],{"path":18,"title":19,"meta":20},"\u002Fnotes\u002Fim\u002F01-message-state\u002F01-offline-sync","连接恢复了，离线消息为什么还没补齐？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},"2026-11-05T09:47:15.000Z",{"path":23,"title":24,"meta":25},"\u002Fnotes\u002Fim\u002F01-message-state\u002F02-read-cursor","手机读过了，电脑为何又显示未读？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":27,"title":28,"meta":29},"\u002Fnotes\u002Fim\u002F01-message-state\u002F03-pagination-search","聊天搜索跳回旧消息后，怎样继续翻页？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":31,"title":32,"meta":33},"\u002Fnotes\u002Fim\u002F01-message-state\u002F04-unread-refresh","消息没停，未读角标就不更新？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":35,"title":36,"meta":37},"\u002Fnotes\u002Fim\u002F02-connection-protocol\u002F01-stream-parsing","一次接收回调，为什么不是一条消息？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":39,"title":40,"meta":41},"\u002Fnotes\u002Fim\u002F02-connection-protocol\u002F02-request-response","长连接上的请求，怎样等到自己的响应？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":43,"title":44,"meta":45},"\u002Fnotes\u002Fim\u002F02-connection-protocol\u002F03-connection-lifecycle","登录一直在连接中，到底在等什么？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":47,"title":48,"meta":49},"\u002Fnotes\u002Fim\u002F03-message-content\u002F01-file-download","附件下载完了，为什么还是打不开？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":51,"title":52,"meta":53},"\u002Fnotes\u002Fim\u002F03-message-content\u002F02-image-send","图片上传到 100%，为什么还没发出去？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":55,"title":56,"meta":57},"\u002Fnotes\u002Fim\u002F03-message-content\u002F03-mentions","聊天 @提及怎样保存身份和位置？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":59,"title":60,"meta":61},"\u002Fnotes\u002Fim\u002F04-client-release\u002F01-flutter-build","Release 包为什么仍连着测试环境？",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"path":63,"title":64,"meta":65},"\u002Fnotes\u002Fim","即时通信",{"date":-1,"tags":-1,"summary":-1,"lastUpdated":21},{"id":67,"type":68,"sourcePath":69,"routePath":39,"path":39,"title":40,"description":70,"updatedAt":21,"meta":71,"body":79,"toc":235},"bm90ZTpub3Rlcy9pbS8wMi1jb25uZWN0aW9uLXByb3RvY29sLzAyLXJlcXVlc3QtcmVzcG9uc2UubWQ","note","notes\u002Fim\u002F02-connection-protocol\u002F02-request-response.md","HTTP 调用通常把请求与响应包装在一起。自己实现长连接协议时，发出的 XML 进入发送队列，服务端的数据稍后从另一条接收事件流到达，调用方没有现成的响应可以 await。",{"id":67,"type":68,"sourcePath":69,"slug":72,"title":40,"updatedAt":21,"description":70,"tags":73,"date":76,"comment":77,"summary":78,"lastUpdated":21},"im\u002F02-connection-protocol\u002F02-request-response",[74,75],"dart","IM","2026-08-13",true,"多个请求共用接收流时，先监听再发送只是第一步。还要精确匹配响应，并统一处理发送失败、连接关闭和超时清理。",{"type":80,"children":81},"root",[82,90,95,100,107,112,117,122,128,150,155,160,166,179,184,189,195,211,216,230],{"type":83,"tag":84,"props":85,"children":87},"element","h1",{"id":86},"长连接上的请求怎样等到自己的响应",[88],{"type":89,"value":40},"text",{"type":83,"tag":91,"props":92,"children":93},"p",{},[94],{"type":89,"value":70},{"type":83,"tag":91,"props":96,"children":97},{},[98],{"type":89,"value":99},"这里的 Dart XMPP 客户端需要等待认证、资源绑定和心跳等结果。资源绑定是在初始化中分配当前客户端资源标识的步骤。这些请求共享连接，却必须各自认领自己的响应。",{"type":83,"tag":101,"props":102,"children":104},"h2",{"id":103},"怎么把事件流包装成一次等待",[105],{"type":89,"value":106},"怎么把事件流包装成一次等待？",{"type":83,"tag":91,"props":108,"children":109},{},[110],{"type":89,"value":111},"可以建立请求表，由统一接收器按请求 ID 分发；也可以为一次请求注册临时监听，匹配成功后完成 Future。项目目前采用后者，用 Completer 保存稍后完成的结果。",{"type":83,"tag":91,"props":113,"children":114},{},[115],{"type":89,"value":116},"顺序是先注册监听，再发送请求。如果先发送、等发送方法返回后才监听，服务端响应可能已经到达，而这一层没有替新监听器保留它。",{"type":83,"tag":91,"props":118,"children":119},{},[120],{"type":89,"value":121},"Completer 完成前检查是否已经结束，可以防止重复完成。但成功、发送失败、连接关闭和超时如何竞争，仍要由封装定义，Completer 本身不会决定谁优先。",{"type":83,"tag":101,"props":123,"children":125},{"id":124},"响应里出现请求-id就算匹配了吗",[126],{"type":89,"value":127},"响应里出现请求 ID，就算匹配了吗？",{"type":83,"tag":91,"props":129,"children":130},{},[131,133,140,142,148],{"type":89,"value":132},"不够。项目当前支持元素名、额外条件和预期 ID，但 ID 检查使用字符串包含。",{"type":83,"tag":134,"props":135,"children":137},"code",{"className":136},[],[138],{"type":89,"value":139},"request-1",{"type":89,"value":141}," 可能匹配到 ",{"type":83,"tag":134,"props":143,"children":145},{"className":144},[],[146],{"type":89,"value":147},"request-10",{"type":89,"value":149},"，一块数据里也可能同时有两条响应。",{"type":83,"tag":91,"props":151,"children":152},{},[153],{"type":89,"value":154},"更可靠的思路是先解析完整协议元素，再按 ID、类型与命名空间关联请求。目前这里接收的却是每块解码后的字符串，数据先进入这条事件流，之后才被追加到聊天消息缓冲。",{"type":83,"tag":91,"props":156,"children":157},{},[158],{"type":89,"value":159},"因此等待层可能拿到半个标签或半个 ID。后面的聊天消息解析不能替这条原始监听路径补齐输入，匹配规则与输入边界需要一起调整，当前尚未完成这一步。",{"type":83,"tag":101,"props":161,"children":163},{"id":162},"发送已经失败为什么还要等到超时",[164],{"type":89,"value":165},"发送已经失败，为什么还要等到超时？",{"type":83,"tag":91,"props":167,"children":168},{},[169,171,177],{"type":89,"value":170},"等待响应和等待发送结果是两段工作。项目调用发送方法后，没有等待它返回的 ",{"type":83,"tag":134,"props":172,"children":174},{"className":173},[],[175],{"type":89,"value":176},"Future\u003Cbool>",{"type":89,"value":178},"；即使发送层已经给出 false，外层仍可能继续等响应。",{"type":83,"tag":91,"props":180,"children":181},{},[182],{"type":89,"value":183},"改进时可以保持先监听的顺序，同时处理发送结果。但需要定义：发送明确失败时是否立即结束，如果响应已经先到、迟到的发送错误又如何处理。",{"type":83,"tag":91,"props":185,"children":186},{},[187],{"type":89,"value":188},"只加“完成过就不再完成”的判断能挡住重复调用，不能替业务确定优先级。验证应故意让响应先于发送返回，再试发送失败与两条响应乱序，而不是只测正常收发。",{"type":83,"tag":101,"props":190,"children":192},{"id":191},"超时后原请求还在不在",[193],{"type":89,"value":194},"超时后，原请求还在不在？",{"type":83,"tag":91,"props":196,"children":197},{},[198,200,209],{"type":89,"value":199},"项目超时会清理监听并抛异常。Dart 的 ",{"type":83,"tag":201,"props":202,"children":206},"a",{"href":203,"rel":204},"https:\u002F\u002Fapi.dart.dev\u002Fdart-async\u002FFuture\u002Ftimeout.html",[205],"nofollow",[207],{"type":89,"value":208},"Future.timeout",{"type":89,"value":210}," 为等待加上时间限制，源 Future 仍可能稍后完成；它也不会自动移除发送队列里的内容。",{"type":83,"tag":91,"props":212,"children":213},{},[214],{"type":89,"value":215},"服务端可能已处理请求，所以重试需要保留请求身份，防止上一轮迟到结果被下一轮过宽的匹配条件接走。超时表达的是这次等待没有按时完成，不能代替远端状态查询。",{"type":83,"tag":91,"props":217,"children":218},{},[219,221,228],{"type":89,"value":220},"成功、超时和捕获异常时都会取消订阅。按照 ",{"type":83,"tag":201,"props":222,"children":225},{"href":223,"rel":224},"https:\u002F\u002Fapi.dart.dev\u002Fdart-async\u002FStreamSubscription\u002Fcancel.html",[205],[226],{"type":89,"value":227},"StreamSubscription.cancel",{"type":89,"value":229},"，取消后不再接收事件，返回的 Future 表示底层清理何时完成。当前只是发起取消，若后续依赖资源已释放，还需要等待它。",{"type":83,"tag":91,"props":231,"children":232},{},[233],{"type":89,"value":234},"监听尚未显式处理错误和结束事件，连接关闭后仍可能等到 timeout。",{"title":236,"searchDepth":237,"depth":237,"links":238},"",2,[239,240,241,242],{"id":103,"depth":237,"text":106},{"id":124,"depth":237,"text":127},{"id":162,"depth":237,"text":165},{"id":191,"depth":237,"text":194},{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":244},"\u003Cpath fill=\"currentColor\" d=\"m10.828 12l4.95 4.95l-1.414 1.415L8 12l6.364-6.364l1.414 1.414z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":246},"\u003Cpath fill=\"currentColor\" d=\"M3 4h18v2H3zm0 7h12v2H3zm0 7h18v2H3z\"\u002F>",{"left":4,"top":4,"width":5,"height":5,"rotate":4,"vFlip":6,"hFlip":6,"body":248},"\u003Cpath fill=\"currentColor\" d=\"m13.172 12l-4.95-4.95l1.414-1.413L16 12l-6.364 6.364l-1.414-1.415z\"\u002F>"]