Netty を使用してカスタム通信プロトコルをデコードする方法を説明する記事

ネットワークプロトコルの基本要素

完全なネットワーク プロトコルの基本要素は何ですか

  1. マジック ナンバー: マジック ナンバーは、通信当事者によってネゴシエートされる秘密コードであり、通常は固定バイト数で表されます。マジック ナンバーの機能は、誰かがサーバー ポートにデータを自由に送信できないようにすることです。
  2. プロトコルのバージョン番号: ビジネス要件が変化すると、プロトコルの構造やフィールドを変更する必要が生じる場合があり、プロトコルのバージョンに応じて解析方法も異なります。したがって、実稼働レベルのプロジェクトではプロトコルのバージョン番号フィールドを予約することを強くお勧めします。
  3. シリアル化アルゴリズム: 要求されたオブジェクトをバイナリに変換するためにデータ送信者がどのメソッドを使用する必要があるか、およびバイナリをオブジェクトに変換する方法を示します。
  4. パケット タイプ: パケットにはさまざまなタイプがある場合があります。たとえば、RPC フレームワークにはリクエスト、応答、ハートビート メッセージがあり、IM シナリオにはログイン、グループ チャットの作成、メッセージの送信、メッセージの受信、およびグループ チャットの終了に関するメッセージがあります。
  5. 長さフィールド field : 要求されたデータの長さを表し、受信者は長さフィールド フィールドに従って完全なメッセージを取得します。
  6. リクエストデータ: 通常、シリアル化後に取得されたバイナリストリーム
  7. ステータス: ステータス フィールドは、リクエストが正常かどうかを識別するために使用されます。通常は呼び出し先によって設定されます。たとえば、RPC 呼び出しが失敗した場合、サービス プロバイダーによってステータス フィールドが異常ステータスに設定される可能性があります。
  8. 予約フィールド: 予約フィールドはオプションであり、プロトコルアップグレードの可能性に対処するために、緊急用に数バイトの予約フィールドを予約できます。
 
 

ルア

コードをコピーする

+---------------------------------------------------------------+ ​ | 魔数 2byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte | ​ +---------------------------------------------------------------+ ​ | 状态 1byte |       保留字段 4byte     |     数据长度 4byte     | ​ +---------------------------------------------------------------+ ​ |                   数据内容 (长度不定)                         | ​ +---------------------------------------------------------------+

例は次のとおりです。

画像.png

カスタム通信プロトコルを実装する方法

非常に優れたネットワーク通信フレームワークとして、Netty は非常に豊富なコーデック抽象基本クラスを提供しており、これらの抽象基本クラス拡張に基づいてカスタム プロトコルをより簡単に実装するのに役立ちます。Netty の一般的なエンコーダのタイプ:

  • MessageToByteEncoder オブジェクトはバイト ストリームにエンコードされます。

  • MessageToMessageEncoder メッセージ タイプを別のメッセージ タイプにエンコードします。

Netty の一般的なデコーダのタイプ:

  • ByteToMessageDecoder/ ReplayingDecoder は、バイト ストリームをメッセージ オブジェクトにデコードします。

  • MessageToMessageDecoder は、あるメッセージ タイプを別のメッセージ タイプにデコードします。

コーデックはプライマリ デコーダとセカンダリ デコーダに分けることができ、プライマリ デコーダは TCP のアンパッキング/スティッキング問題を解決し、プロトコルに従って解析して得られるバイト データを解決するために使用されます。解析されたバイト データのオブジェクト モデルを変換する必要がある場合は、この時点で 2 次デコーダを使用する必要があり、同様にエンコーダのプロセスが逆になります。1 回のコーデック: MessageToByteEncoder/ByteToMessageDecoder。セカンダリ コーデック: MessageToMessageEncoder/MessageToMessageDecoder。

抽象コーディングクラス

ChannelOutboundHandler.png

抽象エンコーディング クラスの継承図から、エンコーディング クラスが ChannelOutboundHandler の抽象クラス実装であり、特定の操作が Outbound アウトバウンド データであることがわかります。

MessageToByteEncoder

MessageToByteEncoder は、オブジェクトをバイト ストリームにエンコードするために使用されます。MessageToByteEncoder は、唯一のエンコード抽象メソッドを提供します。カスタム エンコードを完了するには、encode メソッドを実装するだけで済みます。エンコーダの実装は非常に簡単で、アンパック/スティッキングの問題に注意を払う必要はありません次の例は、文字列型のデータを ByteBuf インスタンスに書き込む方法を示しています。ByteBuf インスタンスは、ChannelPipeline リンク リスト内の次の ChannelOutboundHandler に渡されます。

 
 

ジャワ

コードをコピーする

public class StringToByteEncoder extends MessageToByteEncoder<String> {    @Override    protected void encode(ChannelHandlerContext channelHandlerContext, String data, ByteBuf byteBuf) throws Exception {        byteBuf.writeBytes(data.getBytes());   } }

エンコードが呼び出されるタイミング

MessageToByteEncoder は ChannelOutboundHandler の write() メソッドを書き換えます。その主なロジックは次のステップに分かれています。

  1. acceptOutboundMessage は、一致するメッセージ タイプがあるかどうかを判断します。一致する場合は、エンコード プロセスを実行する必要があります。一致しない場合は、次の ChannelOutboundHandler に直接渡されます。

  2. デフォルトでオフヒープ メモリを使用して、ByteBuf リソースを割り当てます。

  3. サブクラスによって実装された encode メソッドを呼び出して、データのエンコードを完了します。メッセージが正常にエンコードされると、ReferenceCountUtil.release(cast); を呼び出すことによって自動的に解放されます。

  4. ByteBuf が読み取り可能な場合は、データが正常にエンコードされ、ChannelHandlerContext に書き込まれて次のノードに渡されたことを意味します。ByteBuf が読み取り不可能な場合は、ByteBuf リソースが解放され、空の ByteBuf オブジェクトが渡されます。

 
 

ジャワ

コードをコピーする

@Override public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {    ByteBuf buf = null;    try {        if (acceptOutboundMessage(msg)) { // 1. 消息类型是否匹配            @SuppressWarnings("unchecked")            I cast = (I) msg;            buf = allocateBuffer(ctx, cast, preferDirect); // 2. 分配 ByteBuf 资源            try {                encode(ctx, cast, buf); // 3. 执行 encode 方法完成数据编码           } finally {                ReferenceCountUtil.release(cast);           }            if (buf.isReadable()) {                ctx.write(buf, promise); // 4. 向后传递写事件           } else {                buf.release();                ctx.write(Unpooled.EMPTY_BUFFER, promise);           }            buf = null;       } else {            ctx.write(msg, promise);       }   } catch (EncoderException e) {        throw e;   } catch (Throwable e) {        throw new EncoderException(e);   } finally {        if (buf != null) {            buf.release();       }   } }

メッセージからメッセージエンコーダ

MessageToMessageEncoder は MessageToByteEncoder に似ており、encode メソッドを実装するだけで済みます。

MessageToMessageEncoder の一般的な実装サブクラスStringEncoderには、LineEncoderBase64Encoderなどが含まれます。

StringEncoderの使用法を例に挙げますMessageToMessageEncoder

ソースコード例は以下の通りです。CharSequence型(String、StringBuilder、StringBufferなど)をByteBuf型に変換し、StringDecoderを組み合わせることでString型データのエンコード、デコードを直接実現します。

 
 

ジャワ

コードをコピーする

@Override protected void encode(ChannelHandlerContext ctx, CharSequence msg, List<Object> out) throws Exception {    if (msg.length() == 0) {        return;   }    out.add(ByteBufUtil.encodeString(ctx.alloc(), CharBuffer.wrap(msg), charset)); }

抽象デコードクラス

デコード クラスは、受信データを操作する ChannelInboundHandler の抽象クラス実装です。デコーダはアンパッキング/スティッキングの問題を考慮する必要があるため、デコーダの実装はエンコーダよりもはるかに困難です。

受信者は完全なメッセージを受信できない可能性があるため、デコード フレームワークは完全なメッセージが取得されるまで受信データをバッファリングする必要があります。

ChannelOutboundHandler.png

ByteToMessageDecoder

ByteToMessageDecoder を使用すると、Netty は自動的にメモリを解放するため、メモリ管理ロジックについてあまり心配する必要はありません。まず、ByteToMessageDecoder によって定義された抽象メソッドを見てみましょう。

 
 

ジャワ

コードをコピーする

public abstract class ByteToMessageDecoder extends ChannelInboundHandlerAdapter {    protected abstract void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception;    protected void decodeLast(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {        if (in.isReadable()) {            decodeRemovalReentryProtection(ctx, in, out);       }   } }

必要なのはdecode()メソッドを実装することだけです。ここでの in は渡された時点ですでに ByteBuf 型になっていることがわかります。したがって、それを強制する必要はなくなりました。3 番目のパラメータは型です。デコードされた結果をこのオブジェクトに追加ListListます、結果を次のハンドラーに自動的に渡すことができるため、デコード ロジック ハンドラーが実現されました。

d966e7714f17f19cf3606ddae7a0b6ed.png

デコードされたデータにアクセスするために List を使用する理由

TCP スティッキー パケットの問題により、ByteBuf には複数の有効なパケットが含まれているか、完全なパケットには十分ではない可能性があります。

Netty は、新しい完全なメッセージをデコードして List に追加できなくなるか、ByteBuf に読み取り可能なデータがなくなるまで、繰り返し decode() メソッドをコールバックします。

この時点で List の内容が空でない場合は、ChannelPipeline の次の ChannelInboundHandler に渡されます。

 
 

ジャワ

コードをコピーする

static void fireChannelRead(ChannelHandlerContext ctx, CodecOutputList msgs, int numElements) {    for (int i = 0; i < numElements; i ++) {        //循环传播 有多少调用多少        ctx.fireChannelRead(msgs.getUnsafe(i));   } }

デコード最後

ByteToMessageDecoder は、decodeLast() メソッドも定義します。抽象デコーダにはエンコーダよりも decodeLast() メソッドが 1 つ多いのはなぜですか?

decodeLast は Channel が閉じられた後に一度呼び出されるため、主に ByteBuf の最後に残ったバイト データを処理するために使用されます。Netty の decodeLast のデフォルト実装は、単純に decode() メソッドを呼び出します。特別なビジネス要件がある場合は、decodeLast() メソッドをオーバーライドすることでカスタム ロジックを拡張できます。

再生デコーダ

ByteToMessageDecoder のもう 1 つの抽象サブクラスは ReplayingDecoder です。バッファの管理がカプセル化されているため、バッファ データを読み取るときにバイト長をチェックする必要がありません十分な長さのバイト データがない場合、ReplayingDecoder はデコード操作を終了するためです。ReplayingDecoder のパフォーマンスは ByteToMessageDecoder を直接使用するよりも遅いため、ほとんどの場合、ReplayingDecoder の使用はお勧めできません。

メッセージからメッセージデコーダ

ByteToMessageDecoder とは異なり、MessageToMessageDecoder はデータ パケットをキャッシュしません。主にメッセージ モデルを変換するために使用されます。推奨される方法は、ByteToMessageDecoder を使用して TCP プロトコルを解析し、パケットのアンパック/スティッキングの問題を解決することです。有効な ByteBuf データは解析によって取得され、データ オブジェクト変換のために後続の MessageToMessageDecoder に渡されます。具体的なプロセスを次の図に示します。

画像.png

ケースは次のとおりです。

 
 

ジャワ

コードをコピーする

public class MyTcpDecoder extends ByteToMessageDecoder { ​    @Override    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {        // 检查ByteBuf数据是否完整        if (in.readableBytes() < 4) {            return;       } ​        // 标记ByteBuf读取索引位置        in.markReaderIndex(); ​        // 读取数据包长度        int length = in.readInt(); ​        // 如果ByteBuf中可读字节数不足一个数据包长度,则将读取索引位置恢复到标记位置,等待下一次读取        if (in.readableBytes() < length) {            in.resetReaderIndex();            return;       } ​        // 读取数据        ByteBuf data = in.readBytes(length); ​        // 将数据传递给下一个解码器进行转换,转换后的数据对象添加到out中        ctx.fireChannelRead(data);   } } ​ public class MyDataDecoder extends MessageToMessageDecoder<ByteBuf> { ​    @Override    protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {        // 将读取到的ByteBuf数据转换为自定义的数据对象        MyData data = decode(msg);        if (data != null) {            // 将转换后的数据对象添加到out中,表示解码成功            out.add(data);       }   } ​    private MyData decode(ByteBuf buf) {        // 实现自定义的数据转换逻辑        // ...        return myData;   } }

実際の事例

ByteBuf に完全なメッセージがあるかどうかを判断するにはどうすればよいですか? 最も一般的な方法は、メッセージ長 dataLength を読み取って判断することです。ByteBuf の読み取り可能なデータ長が dataLength 未満の場合、ByteBuf では完全なメッセージを取得するのに十分ではないことを意味します。プロトコルの前のメッセージ ヘッダーには、マジック ナンバー、プロトコルのバージョン番号、データ長などの固定フィールドが含まれており、合計 14 バイトになります。固定フィールド長とデータ長は、メッセージの整合性を判断するための基礎として使用できます。具体的なエンコーダ実装ByteToMessageDecoderロジックの例は次のとおりです。

 
 

ジャワ

コードをコピーする

/* +---------------------------------------------------------------+ | 魔数 2byte | 协议版本号 1byte | 序列化算法 1byte | 报文类型 1byte | +---------------------------------------------------------------+ | 状态 1byte | 保留字段 4byte | 数据长度 4byte | +---------------------------------------------------------------+ | 数据内容 (长度不定) | +---------------------------------------------------------------+ */ @Override public final void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { // 判断 ByteBuf 可读取字节 if (in.readableBytes() < 14) { return; } // 标记 ByteBuf 读指针位置 in.markReaderIndex(); // 跳过魔数 in.skipBytes(2); // 跳过协议版本号 in.skipBytes(1); byte serializeType = in.readByte(); // 跳过报文类型 in.skipBytes(1); // 跳过状态字段 in.skipBytes(1); // 跳过保留字段 in.skipBytes(4); // 验证报文长度,不对的话就重置指针位置 int dataLength = in.readInt(); if (in.readableBytes() < dataLength) { in.resetReaderIndex(); // 重置 ByteBuf 读指针位置,这一步很重要 return; } byte[] data = new byte[dataLength]; in.readBytes(data); // 方式一:在解码器中就将数据解码成具体的对象 SerializeService serializeService = getSerializeServiceByType(serializeType); Object obj = serializeService.deserialize(data); if (obj != null) { out.add(obj); } // 方式二:这一步可以不在解码器中处理,将请求数据读取到一个新的byteBuf然后丢给handler处理 // 创建新的 ByteBuf 对象来存储有效负载数据 ByteBuf payload = Unpooled.buffer((int) dataSize); // 读取有效负载数据并写入到 payload 中 in.readBytes(payload); if (payload.isReadable()) { out.add(payload); } }

拡大

バイトオーダーとは何ですか

バイト順序は、16 進数表現 0x12345678 を使用したメモリ内のデータの格納順序を指します。この数値をメモリに保存するには 2 つの方法があります。

違いは、表現される特定の値について、値の下位ビットを下位アドレスに格納するか、値の上位ビットを下位アドレスに格納するかにあります。

バイトオーダーの分類

バイトを配置するには 2 つの方法があります。たとえば、マルチバイト オブジェクトの下位ビットが小さいアドレスに配置され、上位ビットが大きいアドレスに配置される場合、それはリトル エンディアンと呼ばれ、それ以外の場合はビッグ エンディアンと呼ばれます一般的な状況は、整数がメモリに格納される方法 (リトル エンディアン/ホスト バイト オーダー) とネットワーク送信の送信順序 (ビッグ エンディアン/ネットワーク バイト オーダー) です。

1. ネットワーク順序 (ネットワーク順序): 各層の TCP/IP プロトコルはバイト順序をビッグ エンディアン (ビッグ エンディアン) として定義しているため、 TCP/IP プロトコルで使用されるバイト順序は通常ネットワーク バイト順序と呼ばれます。

  • したがって、2 つのホストが TCP/IP プロトコルを通じて通信する必要がある場合、対応する関数を呼び出して、ホスト シーケンス (リトル エンディアン) とネットワーク シーケンス (ビッグ エンディアン) を変換する必要があります。これにより、CPUやOSに依存せず、ネットワーク通信の標準化を実現します。

2. ホストの順序:整数がメモリに格納される順序は、リトル エンディアン ルールに従います (必ずしもホストの CPU アーキテクチャに依存するわけではありませんが、ほとんどの順序はリトル エンディアンです)。

  • 同じモデルのコンピュータで書かれたプログラムは、同じシステム上で問題なく実行できます。

結論は

Java の仮想マシンはビッグ エンディアンとスモール エンディアンの問題を保護します。Java 間の通信であれば考慮する必要はありませんが、言語を越えた通信の場合のみビッグ エンディアンとスモールエンディアンの問題に対処する必要があります。

この記事の要点に戻りますが、エンコードとデコードの際には、ビッグ エンディアンとスモール エンディアンの問題にも注意する必要があります。一般的に、リトル エンディアンの場合、Netty を使用して値を取得するときは LE 終了メソッドを使用する必要があります。 。

おすすめ

転載: blog.csdn.net/wdj_yyds/article/details/131767523