
おそらく、あなたは好きなメッセンジャーに座って、友人とチャットし、エレベーター/トンネル/馬車に乗り、インターネットはまだキャッチしているように見えますが、送信することはできませんか? または、通信プロバイダーがネットワークを誤って構成し、パケットの50%が消えて、何も機能しない場合があります。 おそらく、あなたはこの瞬間に考えていたのでしょう。まあ、どうにかして何かをすることができるので、接続が悪い場合でも、必要なテキストを送信できますか? あなたは一人ではありません。
画像ソース
この記事では、この状況で役立つUDPに基づいたプロトコルを実装するためのアイデアについて説明します。
TCP / IPの問題
貧弱な(モバイル)接続がある場合、パケットの大部分が失われ始めます(または非常に長い遅延が発生します)、TCP / IPプロトコルはこれがネットワークが混雑していることを信号として認識し、すべてがうまくいくとすべてがゆっくりと動作し始めます一般的に。 接続(特にTLS)の確立に複数のパケットの送受信が必要であることは喜びではなく、わずかな損失でさえその動作に非常に悪い影響を与えます。 また、多くの場合、接続を確立する前にDNSにアクセスする必要があります(さらに2、3個の余分なパケット)。
全体的に、接続不良の典型的なTCP / IPベースのREST APIの問題:
- パケット損失に対する不適切な応答(急激な速度低下、長いタイムアウト)
- 接続を確立するには、パケット交換が必要です(+3パケット)
- 多くの場合、IPサーバーを見つけるために「追加の」DNSクエリが必要です(+2パケット)
- 多くの場合、TLSが必要です(+2パケット以上)
合計すると、これはサーバーに接続するためだけに3〜7パケットを送信する必要があり、損失の割合が高いと、接続にかなりの時間がかかり、まだ何も送信していないことを意味します。
実装アイデア
アイデアは次のとおりです。必要な認証データとメッセージテキストを含む事前に配線されたサーバーのIPアドレスに1つのUDPパケットを送信し、それに対する回答を取得するだけです。 すべてのデータを追加で暗号化できます(これはプロトタイプには含まれていません)。 回答が1秒以内に届かない場合は、リクエストが失われたと判断し、再度送信を試みます。 サーバーは重複したメッセージを削除できる必要があるため、再送信しても問題は発生しません。
実稼働対応の実装で起こりうる落とし穴
以下は、「戦闘」状態でこのようなものを使用する前に熟考する必要がある(決してすべてではない)ことです。
- UDPはプロバイダーによって「カット」される可能性があります-TCP / IPで作業できる必要があります
- UDPはNATに対応していません-通常、クライアント要求に応答する時間はほとんどありません(約30秒)
- サーバーは攻撃を受けないようにする必要があります-応答パケットが要求パケット以下であることを確認する必要があります
- 暗号化は難しく、セキュリティの専門家でない場合、正しく実装する機会はほとんどありません。
- 再送信間隔を誤って設定した場合(たとえば、毎秒再試行する代わりに、停止せずに再試行する)、TCP / IPよりもはるかに悪いことができます。
- UDPでのフィードバックの欠如と無限の再試行により、より多くのトラフィックがサーバーに到達し始める可能性があります
- サーバーは複数の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) }
さて、トリッキーな部分はクライアントです。 メッセージを1つずつ送信し、サーバーが応答するのを待ってから次のメッセージを送信します。 現在のタイムスタンプとテキストを送信します-タイムスタンプはリクエスト識別子として機能します。
// 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に基づいて同じことを実装しました。HTTPPOSTを使用して、同じrequestTとメッセージテキストを送信し、応答を待ってから、次のものに進みます。 この呼びかけはドメイン名によって行われ、DNSキャッシュはシステムで禁止されていませんでした。 HTTPSは、比較をより正直にするために使用されませんでした(プロトタイプには暗号化はありません)。 タイムアウトは15秒に設定されました。TCP/ IPはすでに失われたパケットを転送しているため、ユーザーは15秒以上待つことはほとんどありません。
テスト、結果
プロトタイプをテストするときに、次のことが測定されました(すべてミリ秒単位 )。
- 最初の応答時間(最初)
- 平均応答時間(平均)
- 最大応答時間(最大)
- H / U-「HTTP時間」/「UDP時間」の比率-UDP使用時の遅延の回数
10リクエストの100シリーズが作成されました-数個のメッセージを送信する必要がある状況をシミュレートし、その後、通常のインターネット(たとえば、地下鉄のWi-Fi、路上での3G / LTE)が利用可能になります。
テスト済みの通信の種類:
- ネットワークリンクコンディショナーのプロファイル「非常に悪いネットワーク」(10%の損失、500ミリ秒の遅延、1 Mbps)-「非常に悪い」
- EDGE、冷蔵庫内の電話(「エレベーター」)-冷蔵庫
- エッジ
- 3G
- LTE
- Wifi
結果(ミリ秒単位):

( CSV形式でも同じ )
結論
結果から導き出せる結論は次のとおりです。
- LTEの異常は別として、最初のメッセージの送信の差は大きく、接続が悪い(平均で2〜3倍高速)
- HTTPでのメッセージのその後の送信はそれほど遅くありません-平均で1.3倍遅くなりますが、安定したWi-Fiではまったく違いはありません
- UDPベースの応答時間ははるかに安定しており、これは間接的に最大遅延によって見られます-また、1.4〜1.8倍短くなります
言い換えると、適切な(「悪い」)条件下で、特に最初のメッセージを送信する場合(多くの場合、これが送信する必要があるのはこれだけです)、私たちのプロトコルは非常によく機能します。
プロトタイプ実装
プロトタイプはgithubに投稿されています。 本番環境では使用しないでください!
電話またはコンピューターでクライアントを起動するコマンド:
。 サーバーはまだ実行中です:)。 まず最初の回答の時間と最大遅延を調べる必要があります。 このデータはすべて最後に印刷されます。instant-im -client -num 10
エレベーターの打ち上げの例

