我们制造了一个即使在电梯中也能工作的信使*

*实际上,我们只会编写该协议的原型。

也许您遇到过类似的情况-坐在您最喜欢的信使中,与朋友聊天,进入电梯/隧道/马车,互联网似乎仍然存在,但是什么也发不出去? 有时,您的通信提供商错误地配置了网络,并且50%的数据包消失了,也没有任何作用。 也许您现在正在考虑-嗯,您可能可以以某种方式做些什么,以便在连接不良的情况下仍可以发送所需的一小段文字? 你并不孤单。

图片来源

在本文中,我将讨论实现基于UDP的协议的想法,在这种情况下可以提供帮助。

TCP / IP问题


当我们的(移动)连接较差时,很大一部分数据包开始丢失(或者延时很长),TCP / IP协议可能会认为这是网络拥塞的信号,如果一切正常,一切都会开始缓慢工作一般而言。 建立连接(尤其是TLS)需要发送和接收多个数据包并不会增加喜悦,即使很小的损失也会严重影响其操作。 在建立连接之前,它通常还需要访问DNS –还有两个额外的数据包。

总而言之,连接不良的典型基于TCP / IP的REST API的问题:

  • 对数据包丢失的响应不良(速度降低,超时时间过长)
  • 建立连接需要交换数据包(+3个数据包)
  • 通常,您需要“额外” DNS查询来查找IP服务器(+2个数据包)
  • 经常需要TLS(最少+2个数据包)

总的来说,这意味着仅用于连接服务器,我们需要发送3-7个数据包,并且丢失率很高,连接可能要花费大量时间,而我们甚至还没有发送任何内容。

实施思路


这个想法是这样的:我们只需要发送一个UDP数据包到服务器的预先连接的IP地址,其中包含必要的授权数据和消息文本,然后得到答案。 所有数据都可以额外加密(这不在原型中)。 如果答案没有在一秒钟内到达,则我们认为请求已丢失,请尝试再次发送。 服务器应该能够删除重复的消息,因此重新发送不会造成问题。

生产就绪实施的可能陷阱


以下内容(绝不是全部)是您在“战斗”条件下使用类似内容之前需要考虑的事项:

  1. 提供商可以“切断” UDP-您需要能够通过TCP / IP工作
  2. UDP与NAT不友好-通常很少(〜30秒)时间来响应客户端请求
  3. 服务器必须能够抵抗攻击 -您需要确保响应数据包不超过请求数据包
  4. 加密很困难,而且如果您不是安全专家,则几乎没有机会正确实施加密
  5. 如果您错误地设置了重传间隔(例如,而不是每秒重试一次,而又不停止尝试重试一次),那么您所做的可能比TCP / IP差很多
  6. 由于缺乏UDP反馈和无休止的重试,更多的流量可能开始流向您的服务器
  7. 服务器可以有多个IP地址,并且它们可以随时间变化,因此您需要能够更新缓存(Telegram运作良好:))

实作


我们将编写一个服务器,该服务器将通过UDP发送响应,并向响应中发送到达它的请求的编号(该请求看起来像“ request-ts消息文本”),以及接收响应的时间戳:

//  Go. //      buf := make([]byte, maxUDPPacketSize) //   UDP addr, _ := net.ResolveUDPAddr("udp", fmt.Sprintf("0.0.0.0:%d", serverPort)) conn, _ := net.ListenUDP("udp", addr) for { //   UDP,      n, uaddr, _ := conn.ReadFromUDP(buf) req := string(buf[0:n]) parts := strings.SplitN(req, " ", 2) //          curTs := time.Now().UnixNano() clientTs, _ := strconv.Atoi(parts[0]) //       -      //   conn.WriteToUDP([]byte(fmt.Sprintf("%d %d", curTs, clientTs)), uaddr) } 

现在最棘手的部分是客户。 我们将一次发送一条消息,并等待服务器响应后再发送下一条消息。 我们将发送当前时间戳和一条文本-时间戳将用作请求标识符。

 //   addr, _ := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", serverIP, serverPort)) conn, _ := net.DialUDP("udp", nil, addr) //  UDP      ,     . resCh := make(chan udpResult, 10) go readResponse(conn, resCh) for i := 0; i < numMessages; i++ { requestID := time.Now().UnixNano() send(conn, requestID, resCh) } 

功能代码:

 func send(conn *net.UDPConn, requestID int64, resCh chan udpResult) { for { //     ,       . conn.Write([]byte(fmt.Sprintf("%d %s", requestID, testMessageText))) if waitReply(requestID, time.After(time.Second), resCh) { return } } } //   ,  . //      ,   ,   // ,        , //   . func waitReply(requestID int64, timeout <-chan time.Time, resCh chan udpResult) (ok bool) { for { select { case res := <-resCh: if res.requestTs == requestID { return true } case <-timeout: return false } } } //    type udpResult struct { serverTs int64 requestTs int64 } //           . func readResp(conn *net.UDPConn, resCh chan udpResult) { buf := make([]byte, maxUDPPacketSize) for { n, _, _ := conn.ReadFromUDP(buf) respStr := string(buf[0:n]) parts := strings.SplitN(respStr, " ", 2) var res udpResult res.serverTs, _ = strconv.ParseInt(parts[0], 10, 64) res.requestTs, _ = strconv.ParseInt(parts[1], 10, 64) resCh <- res } } 

我还基于(或多或少)标准REST实现了相同的事情:使用HTTP POST,我们发送相同的requestT和消息文本,并等待响应,然后转到下一个。 上诉是通过域名提出的,系统中没有禁止DNS缓存。 没有使用HTTPS来使比较更真实(原型中没有加密)。 超时设置为15秒:TCP / IP已经转发了丢失的数据包,用户很可能不会等待超过15秒。

测试结果


在测试原型时,会测量以下内容(全部以毫秒为单位 ):

  1. 第一响应时间(第一)
  2. 平均响应时间(平均)
  3. 最大响应时间(最大)
  4. H / U-“ HTTP时间” /“ UDP时间”的比率-使用UDP时延迟减少了多少倍

发出了100个系列的10个请求-我们模拟了一种情况,当您只需要发送少量消息时,并且在正常的Internet(例如,地铁中的Wi-Fi或街道上的3G / LTE)可用之后。

测试的通讯类型:


  1. 网络链接调节器中配置“非常严重的网络”(10%丢失,500 ms延迟,1 Mbps)-“非常严重”
  2. EDGE,冰箱中的电话(“电梯”)-冰箱
  3. 边缘
  4. 3G
  5. LTE
  6. 无线上网

结果(以毫秒为单位):




CSV格式相同

结论


以下是可以从结果中得出的结论:

  1. 除了LTE异常外,发送第一个消息的差异越大,连接越差(平均快2-3倍)
  2. 随后通过HTTP发送消息的速度并不慢-平均慢了1.3倍,但是在稳定的Wi-Fi上根本没有区别
  3. 基于UDP的响应时间要稳定得多,这可以通过最大延迟间接看到-也减少了1.4-1.8倍

换句话说,在适当的(“不良”)条件下,我们的协议会更好地工作,尤其是在发送第一个消息时(通常这就是所有需要发送的消息)。

原型实现


原型发布在github上 。 不要在生产中使用它!

在电话或计算机上启动客户端的命令:
 instant-im -client -num 10 
。 服务器仍在运行:)。 首先必须在第一个答案的时间以及最大延迟时查看。 所有这些数据都打印在末尾。

电梯启动示例


Source: https://habr.com/ru/post/zh-CN435026/


All Articles