ショック!これはおそらく、私がこれまでに得た低レベルのプログラミング体験の中で最も近いものです。
1. nettyは何ができるのか
まず、netty は、NIO (真のノンブロッキング IO) に基づいた、高性能で適切にカプセル化された柔軟なオープンソース フレームワークです。手書き Web サーバー、TCP サーバーなどに使用できます。一般的に使用される HTTP/HTTPS/WEBSOCKET などの豊富なプロトコルをサポートし、非常に柔軟で、必要に応じて DIV サーバーを調整できる多数のメソッドを提供します。あなた自身のニーズ。
netty を使用して TCP サーバー/クライアントを作成します
。 1. 次のように独自のデータ送信プロトコルを設計できます。
2. エンコード ルールとデコード ルールをカスタマイズできます
。 3. クライアントとサーバー間のデータ インタラクションの詳細をカスタマイズでき、ソケットフロー攻撃、TCP スティッキー パケット、および解凍の問題
2.クイックスタート
- サードパーティの Web サーバーに依存せずに通常の Maven プロジェクトを作成し、main メソッドを使用して実行するだけです。
- POM 依存関係を追加する
<!--netty的依赖集合,都整合在一个依赖里面了-->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>
<!--这里使用jackson反序列字节码-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.7</version>
</dependency>
<!--加入log4j 便于深入学习整合运行过程的一些细节-->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
- TCP に基づいた一連のデータ送信プロトコルを設計する
public class TcpProtocol {
private byte header=0x58;
private int len;
private byte [] data;
private byte tail=0x63;
public byte getTail() {
return tail;
}
public void setTail(byte tail) {
this.tail = tail;
}
public TcpProtocol(int len, byte[] data) {
this.len = len;
this.data = data;
}
public TcpProtocol() {
}
public byte getHeader() {
return header;
}
public void setHeader(byte header) {
this.header = header;
}
public int getLen() {
return len;
}
public void setLen(int len) {
this.len = len;
}
public byte[] getData() {
return data;
}
public void setData(byte[] data) {
this.data = data;
}
}
ここで、プロトコルの開始ビットと終了ビットは16進数で表され、0x58が開始、0x63が終了を表し、いずれも1バイトで表されます。
- TCPサーバーの起動クラス
public class TcpServer {
private int port;
private Logger logger = Logger.getLogger(this.getClass());
public void init(){
logger.info("正在启动tcp服务器……");
NioEventLoopGroup boss = new NioEventLoopGroup();//主线程组
NioEventLoopGroup work = new NioEventLoopGroup();//工作线程组
try {
ServerBootstrap bootstrap = new ServerBootstrap();//引导对象
bootstrap.group(boss,work);//配置工作线程组
bootstrap.channel(NioServerSocketChannel.class);//配置为NIO的socket通道
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel ch) throws Exception {
//绑定通道参数
ch.pipeline().addLast("logging",new LoggingHandler("DEBUG"));//设置log监听器,并且日志级别为debug,方便观察运行流程
ch.pipeline().addLast("encode",new EncoderHandler());//编码器。发送消息时候用过
ch.pipeline().addLast("decode",new DecoderHandler());//解码器,接收消息时候用
ch.pipeline().addLast("handler",new BusinessHandler());//业务处理类,最终的消息会在这个handler中进行业务处理
}
});
bootstrap.option(ChannelOption.SO_BACKLOG,1024);//缓冲区
bootstrap.childOption(ChannelOption.SO_KEEPALIVE,true);//ChannelOption对象设置TCP套接字的参数,非必须步骤
ChannelFuture future = bootstrap.bind(port).sync();//使用了Future来启动线程,并绑定了端口
logger.info("启动tcp服务器启动成功,正在监听端口:"+port);
future.channel().closeFuture().sync();//以异步的方式关闭端口
}catch (InterruptedException e) {
logger.info("启动出现异常:"+e);
}finally {
work.shutdownGracefully();
boss.shutdownGracefully();//出现异常后,关闭线程组
logger.info("tcp服务器已经关闭");
}
}
public static void main(String[] args) {
new TcpServer(8777).init();
}
public TcpServer(int port) {
this.port = port;
}
}
nettyベースのサーバーである限り、bootstrap
ワーカースレッドグループ、 Class 、およびユーザー DIV のさまざまなクラスをバインドするために使用されますchannel
。その際、データの流れの順序とハンドラーを追加する順序に注意してください。カスタム ハンドラーの追加は一貫しています。つまり、上から下に、基礎となるバイト ストリームのデコード/エンコード ハンドラー、およびビジネス処理ハンドラーとなる必要があります。pipeline
handler
pipeline
- エンコーダエンコーダは、サーバーがプロトコル形式、継承されたコード
に従ってデータをクライアントに返すときに呼び出されます。MessageToByteEncoder
public class EncoderHandler extends MessageToByteEncoder {
private Logger logger = Logger.getLogger(this.getClass());
protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
if (msg instanceof TcpProtocol){
TcpProtocol protocol = (TcpProtocol) msg;
out.writeByte(protocol.getHeader());
out.writeInt(protocol.getLen());
out.writeBytes(protocol.getData());
out.writeByte(protocol.getTail());
logger.debug("数据编码成功:"+out);
}else {
logger.info("不支持的数据协议:"+msg.getClass()+"\t期待的数据协议类是:"+TcpProtocol.class);
}
}
}
- デコーダ デコーダ
は比較的コアな部分です。カスタム デコード プロトコル、スティッキー パケット、アンパッキングなどはすべてデコーダに実装されており、継承されています。実際、ByteToMessageDecoder の内部は、アンパッキング/スティッキングの問題に対処するのにすでに役立っていますByteToMessageDecoder
。パケットの場合は、次のデコード メソッドを実装する設計原則に従ってください。
public class DecoderHandler extends ByteToMessageDecoder {
//最小的数据长度:开头标准位1字节
private static int MIN_DATA_LEN=6;
//数据解码协议的开始标志
private static byte PROTOCOL_HEADER=0x58;
//数据解码协议的结束标志
private static byte PROTOCOL_TAIL=0x63;
private Logger logger = Logger.getLogger(this.getClass());
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
if (in.readableBytes()>MIN_DATA_LEN){
logger.debug("开始解码数据……");
//标记读操作的指针
in.markReaderIndex();
byte header=in.readByte();
if (header==PROTOCOL_HEADER){
logger.debug("数据开头格式正确");
//读取字节数据的长度
int len=in.readInt();
//数据可读长度必须要大于len,因为结尾还有一字节的解释标志位
if (len>=in.readableBytes()){
logger.debug(String.format("数据长度不够,数据协议len长度为:%1$d,数据包实际可读内容为:%2$d正在等待处理拆包……",len,in.readableBytes()));
in.resetReaderIndex();
/*
**结束解码,这种情况说明数据没有到齐,在父类ByteToMessageDecoder的callDecode中会对out和in进行判断
* 如果in里面还有可读内容即in.isReadable为true,cumulation中的内容会进行保留,,直到下一次数据到来,将两帧的数据合并起来,再解码。
* 以此解决拆包问题
*/
return;
}
byte [] data=new byte[len];
in.readBytes(data);//读取核心的数据
byte tail=in.readByte();
if (tail==PROTOCOL_TAIL){
logger.debug("数据解码成功");
out.add(data);
//如果out有值,且in仍然可读,将继续调用decode方法再次解码in中的内容,以此解决粘包问题
}else {
logger.debug(String.format("数据解码协议结束标志位:%1$d [错误!],期待的结束标志位是:%2$d",tail,PROTOCOL_TAIL));
return;
}
}else {
logger.debug("开头不对,可能不是期待的客服端发送的数,将自动略过这一个字节");
}
}else {
logger.debug("数据长度不符合要求,期待最小长度是:"+MIN_DATA_LEN+" 字节");
return;
}
}
}
1つ目はスティッキーパケットの問題で、
通常のデータ送信は図に示すようにデータAのように1パケットで完結するはずですが、1パケットの中に複数のデータが含まれるなど異常な場合もあります。デフォルトではByteToMessageDecoder
バイナリバイトコードはbyteBufに置かれるので、コーディングする際にはそのような場面があることを知っておく必要があります。スティッキー パケットの問題は、実際には解決する必要はありませんが、 callDecode で手書きデコーダをコールバックするメソッドのソース コードを
以下に示します。ByteToMessageDecoder
decode
protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
try {
while (in.isReadable()) {
//buf中是否还有数据
int outSize = out.size();//标记out的size,解析成功的数据会添加的out中
if (outSize > 0) {
fireChannelRead(ctx, out, outSize);//这个是回调业务handler的channelRead方法
out.clear();
if (ctx.isRemoved()) {
break;
}
outSize = 0;//清空了out,将标记size清零
}
int oldInputLength = in.readableBytes();//这里开始准备调用decode方法,标记了解码前的可读内容
decode(ctx, in, out);//对应DecoderHandler中的decode方法
if (ctx.isRemoved()) {
break;
}
if (outSize == out.size()) {
//相等说明,并没有解析出来新的object到out中
if (oldInputLength == in.readableBytes()) {
//这里很重要,若相
等说明decode中没有读取任何内容出来,这里一般是发生拆包后,将ByteBuf
的指针手动重置。重置后从这个方法break出来。让ByteToMessageDecoder
去处理拆包问题。这里就体现了要按照netty的设计原则来写代码
break;
} else {
continue;//这里直接continue,是考虑让开发者去跳过某些字节,比如收到了socket攻击时,数据不按照协议体来的时候,就直接跳过这些字节
}
}
if (oldInputLength == in.readableBytes()) {
//这种情况属于,没有按
照netty的设计原则来。要么是decode中没有任何逻辑代码,要么是在out中添加了
内容后,调用了byteBuf的resetReaderIndex重置的读操作的指针
throw new DecoderException(
StringUtil.simpleClassName(getClass()) +
".decode() did not read anything but decoded a message.");
}
if (isSingleDecode()) {
//默认为false,用来设置只解析一条数据
break;
}
//这里结束后,继续wile循环,因为bytebuf仍然有可读的内容,将会继续调用
decode方法解析bytebuf中的字节码,以此解决了粘包问题
}
} catch (DecoderException e) {
throw e;
} catch (Throwable cause) {
throw new DecoderException(cause);
}
}
上記のソース コードを分析した結果、decode メソッドが while ループ内にあることがわかりました。つまり、bytebuf にコンテンツがある限り、常に decode メソッドを呼び出してデコード操作を実行します。パケットの場合は、通常のプロセスに従うだけでよく、プロトコルの先頭、データ バイト、および終了フラグを解析した後、データを out リストに追加します。スティッキー パケット テスト用のデータは後で作成されます。
- 解凍の問題
受け取ったデータが不完全で、1 つのパッケージ内のデータが複数の部分に分割されて送信される場合があります。この場合、データが大きすぎるため、何回かに分割して送信している可能性があります。たとえば、データ パケット B は送信のために 2 つの部分に分割されます。
アンパックの問題もByteToMessageDecoder
解決され、netty の設計原則に従ってデコード コードを記述するだけで済みます。
まず、開梱の問題を自分で解決する必要があるとします。どうすればそれを達成できるでしょうか?
まず問題を分析しましょう。必要なのはデータ B ですが、データ B_1 しか受信していません。この時点では、残りのデータ B_2 が到着するのを待つ必要があります。受信したデータ B_1 はアキュムレータに格納する必要があります。B_2 のとき2 つのデータ パケットが結合されてデコードされます。
したがって、問題は、データが不完全であることを ByteToMessageDecoder にどのように知らせるかです。DecoderHandler.decode
次のようなコードが存在します。
if (len>=in.readableBytes()){
logger.debug(String.format("数据长度不够,数据协议len长度为:%1$d,数据包实际可读内容为:%2$d正在等待处理拆包……",len,in.readableBytes()));
in.resetReaderIndex();
/*
**结束解码,这种情况说明数据没有到齐,在父类ByteToMessageDecoder的callDecode中会对out和in进行判断
* 如果in里面还有可读内容即in.isReadable为true,cumulation中的内容会进行保留,,直到下一次数据到来,将两帧的数据合并起来,再解码。
* 以此解决拆包问题
*/
return;
}
プロトコルの len が bytebuf の読み取り可能な内容より大きいことが読み取られた場合、データが不完全でアンパックが発生したことを意味します。resetReaderIndex を呼び出して読み取り操作ポインターをリセットし、メソッドを終了します。親クラスの CallDecode メソッドのコードを見てください。
if (outSize == out.size()) {
//相等说明,并没有解析出来新的object到out中
if (oldInputLength == in.readableBytes()) {
//这里很重要,若相等说明decode中没有读取任何内容出来,这里一般是发生拆包后,将ByteBuf的指针手动重置。重置后从这个方法break出来。让ByteToMessageDecoder去处理拆包问题。这里就体现了要按照netty的设计原则来写代码
break;//退出该方法
} else {
continue;//这里直接continue,是考虑让开发者去跳过某些字节,比如收到了socket攻击时,数据不按照协议体来的时候,就直接跳过这些字节
}
}
を終了した後callDecode
、次の場所に戻りますchannelRead
。
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof ByteBuf) {
CodecOutputList out = CodecOutputList.newInstance();
try {
ByteBuf data = (ByteBuf) msg;
first = cumulation == null;
if (first) {
cumulation = data;
} else {
cumulation = cumulator.cumulate(ctx.alloc(), cumulation, data);
}
callDecode(ctx, cumulation, out);//注意这里传入的不是data,而是cumulator,这个对象相当于一个累加器,也就是说每次调用callDecode的时候传入的byteBuf实际上是经过累加后的cumulation
} catch (DecoderException e) {
throw e;
} catch (Throwable t) {
throw new DecoderException(t);
} finally {
if (cumulation != null && !cumulation.isReadable()) {
//这里若是数据被读取完,会清空累加器cumulation
numReads = 0;
cumulation.release();
cumulation = null;
} else if (++ numReads >= discardAfterReads) {
// We did enough reads already try to discard some bytes so we not risk to see a OOME.
// See https://github.com/netty/netty/issues/4275
numReads = 0;
discardSomeReadBytes();
}
int size = out.size();
decodeWasNull = !out.insertSinceRecycled();
fireChannelRead(ctx, out, size);
out.recycle();
}
} else {
ctx.fireChannelRead(msg);
}
}
channelRead メソッドは、データのパケットを受信した後に 1 回呼び出されます。これまでのところ、netty は解凍の問題を完璧に解決してくれました。彼の設計原則に従う必要があるのは、len>byteBuf.readableBytes の場合、読み取りポインタをリセットしてデコードを終了することだけです。
- ビジネス処理ハンドラー クラス
この層のデータは完全に解析されており、直接使用できます。
public class BusinessHandler extends ChannelInboundHandlerAdapter {
private ObjectMapper objectMapper= ByteUtils.InstanceObjectMapper();
private Logger logger = Logger.getLogger(this.getClass());
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof byte []){
logger.debug("解码后的字节码:"+new String((byte[]) msg,"UTF-8"));
try {
Object objectContainer = objectMapper.readValue((byte[]) msg, DTObject.class);
if (objectContainer instanceof DTObject){
DTObject data = (DTObject) objectContainer;
if (data.getClassName()!=null&&data.getObject().length>0){
Object object = objectMapper.readValue(data.getObject(), Class.forName(data.getClassName()));
logger.info("收到实体对象:"+object);
}
}
}catch (Exception e){
logger.info("对象反序列化出现问题:"+e);
}
}
}
}
デコードではバイトコードがオブジェクトに逆シリアル化されないため、さらに逆シリアル化が必要です。データを転送するときは、複数のタイプのオブジェクトが渡される可能性があるため、この問題は逆シリアル化でも考慮する必要があります。解決策は、転送されたオブジェクトを再パッケージし、フルネームのクラス情報を含めることです。
public class DTObject {
private String className;
private byte[] object;
}
このように、デシリアライゼーションを使用して
Class.forName()
クラスを取得すると、デシリアライズされたオブジェクトのクラスを判断するための大量の if ループの記述が回避されます。前提として、クラス名とパッケージのパスは正確に一致する必要があります。
- 次に、TCP クライアントを作成して、
スタートアップ クラスの init メソッドをテストします。
public void init() throws InterruptedException {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group);
bootstrap.channel(NioSocketChannel.class);
bootstrap.option(ChannelOption.SO_KEEPALIVE,true);
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast("logging",new LoggingHandler("DEBUG"));
ch.pipeline().addLast(new EncoderHandler());
ch.pipeline().addLast(new EchoHandler());
}
});
bootstrap.remoteAddress(ip,port);
ChannelFuture future = bootstrap.connect().sync();
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
group.shutdownGracefully().sync();
}
}
クライアントハンドラー:
public class EchoHandler extends ChannelInboundHandlerAdapter {
//连接成功后发送消息测试
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
User user = new User();
user.setBirthday(new Date());
user.setUID(UUID.randomUUID().toString());
user.setName("冉鹏峰");
user.setAge(22);
DTObject dtObject = new DTObject();
dtObject.setClassName(user.getClass().getName());
dtObject.setObject(ByteUtils.InstanceObjectMapper().writeValueAsBytes(user));
TcpProtocol tcpProtocol = new TcpProtocol();
byte [] objectBytes=ByteUtils.InstanceObjectMapper().writeValueAsBytes(dtObject);
tcpProtocol.setLen(objectBytes.length);
tcpProtocol.setData(objectBytes);
ctx.write(tcpProtocol);
ctx.flush();
}
}
このハンドラーは、TCP 接続が確立された後、テストのためにサーバーにデータのパケットを送信することをシミュレートし、チャネルを通じてデータを送信します。エンコーダーがスタートアップ クラスで構成されているwrite
限り、オブジェクトを直接渡すことができます。メソッド内では自動的にバイトコードに変換されて bytebuf に入れられます。TcpClient
EncoderHandler
tcpProtocol
EncoderHandler
encode
- 通常のデータ送信テスト:
結果:
2019-01-14 16:30:34 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 16:30:34 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 16:30:34 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 16:30:34 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{
"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA0OjMwOjE0IiwidWlkIjoiOGY0OTM0OGEtMWNmMy00ZTEyLWEzZTAtY2M1ZTJjZTkzMDdlIn0="}
2019-01-14 16:30:34 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{
name='冉鹏峰', age=24, UID='8f49348a-1cf3-4e12-a3e0-cc5e2ce9307e', birthday=Mon Jan 14 04:30:00 CST 2019}
最終的なエンティティ オブジェクト User が正常に解析されたことがわかります。
デバッグ モードでは、次のようなテーブル出力もコンソールに表示されます。
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 58 00 00 00 b5 7b 22 63 6c 61 73 73 4e 61 6d 65 |X....{
"className|
|00000010| 22 3a 22 70 6f 6a 6f 2e 55 73 65 72 22 2c 22 6f |":"pojo.User","o|
|00000020| 62 6a 65 63 74 22 3a 22 65 79 4a 75 59 57 31 6c |bject":"eyJuYW1l|
|00000030| 49 6a 6f 69 35 59 61 4a 36 62 6d 50 35 62 4f 77 |Ijoi5YaJ6bmP5bOw|
|00000040| 49 69 77 69 59 57 64 6c 49 6a 6f 79 4e 43 77 69 |IiwiYWdlIjoyNCwi|
|00000050| 59 6d 6c 79 64 47 68 6b 59 58 6b 69 4f 69 49 79 |YmlydGhkYXkiOiIy|
|00000060| 4d 44 45 35 4c 7a 41 78 4c 7a 45 30 49 44 41 30 |MDE5LzAxLzE0IDA0|
|00000070| 4f 6a 4d 77 4f 6a 45 30 49 69 77 69 64 57 6c 6b |OjMwOjE0IiwidWlk|
|00000080| 49 6a 6f 69 4f 47 59 30 4f 54 4d 30 4f 47 45 74 |IjoiOGY0OTM0OGEt|
|00000090| 4d 57 4e 6d 4d 79 30 30 5a 54 45 79 4c 57 45 7a |MWNmMy00ZTEyLWEz|
|000000a0| 5a 54 41 74 59 32 4d 31 5a 54 4a 6a 5a 54 6b 7a |ZTAtY2M1ZTJjZTkz|
|000000b0| 4d 44 64 6c 49 6e 30 3d 22 7d 63 |MDdlIn0="}c |
+--------+-------------------------------------------------+----------------+
これは実際のデータキャプチャ表示に相当し、データはバイトコードに変換された後、バイナリ形式で TCP バッファに送信されます。しかし、2 進数では長すぎるため、通常は 16 進数表示に変換され、テーブルには 1 バイトのデータが表示され、データは位置から上位、左から右、上から下の順に配置されます。このうち開始フラグが設定され、0x58
データの長さになります。int型なので4バイトを占めます。内容は送信するデータ内容、最後は設定された終了フラグビットです。TcpProtocol
00 00 00 b5
7b--7d
0x63
TcpProtocol
- スティッキー パッケージのテスト
スティッキー パッケージをシミュレートするには、まずスタートアップ クラスTcpClient
で構成されたエンコーダーをコメント アウトしますEncoderHandler
。
bootstrap.handler(new ChannelInitializer() {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast("logging",new LoggingHandler("DEBUG"));
//ch.pipeline().addLast(new EncoderHandler()); 因为需要在byteBuf中手动模拟粘包的场景
ch.pipeline().addLast(new EchoHandler());
}
});
次に、3 つのデータ フレームを意図的に 1 つのパケットで送信し、次のように EchoHanlder で送信します。
public void channelActive(ChannelHandlerContext ctx) throws Exception {
User user = new User();
user.setBirthday(new Date());
user.setUID(UUID.randomUUID().toString());
user.setName("冉鹏峰");
user.setAge(24);
DTObject dtObject = new DTObject();
dtObject.setClassName(user.getClass().getName());
dtObject.setObject(ByteUtils.InstanceObjectMapper().writeValueAsBytes(user));
TcpProtocol tcpProtocol = new TcpProtocol();
byte [] objectBytes=ByteUtils.InstanceObjectMapper().writeValueAsBytes(dtObject);
tcpProtocol.setLen(objectBytes.length);
tcpProtocol.setData(objectBytes);
ByteBuf buffer = ctx.alloc().buffer();
buffer.writeByte(tcpProtocol.getHeader());
buffer.writeInt(tcpProtocol.getLen());
buffer.writeBytes(tcpProtocol.getData());
buffer.writeByte(tcpProtocol.getTail());
//模拟粘包的第二帧数据
buffer.writeByte(tcpProtocol.getHeader());
buffer.writeInt(tcpProtocol.getLen());
buffer.writeBytes(tcpProtocol.getData());
buffer.writeByte(tcpProtocol.getTail());
//模拟粘包的第三帧数据
buffer.writeByte(tcpProtocol.getHeader());
buffer.writeInt(tcpProtocol.getLen());
buffer.writeBytes(tcpProtocol.getData());
buffer.writeByte(tcpProtocol.getTail());
ctx.write(buffer);
ctx.flush();
}
操作結果:
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 16:44:51 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{
"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA0OjQ0OjE0IiwidWlkIjoiODFkZTU5YWUtMzQ4Mi00ZDFhLWJjNDMtN2NjMTJmOTI1ZTUxIn0="}
2019-01-14 16:44:51 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{
name='冉鹏峰', age=24, UID='81de59ae-3482-4d1a-bc43-7cc12f925e51', birthday=Mon Jan 14 04:44:00 CST 2019}
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 16:44:51 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{
"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA0OjQ0OjE0IiwidWlkIjoiODFkZTU5YWUtMzQ4Mi00ZDFhLWJjNDMtN2NjMTJmOTI1ZTUxIn0="}
2019-01-14 16:44:51 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{
name='冉鹏峰', age=24, UID='81de59ae-3482-4d1a-bc43-7cc12f925e51', birthday=Mon Jan 14 04:44:00 CST 2019}
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 16:44:51 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 16:44:51 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{
"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA0OjQ0OjE0IiwidWlkIjoiODFkZTU5YWUtMzQ4Mi00ZDFhLWJjNDMtN2NjMTJmOTI1ZTUxIn0="}
2019-01-14 16:44:51 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{
name='冉鹏峰', age=24, UID='81de59ae-3482-4d1a-bc43-7cc12f925e51', birthday=Mon Jan 14 04:44:00 CST 2019}
サーバーは 3 フレームのデータを正常に解析し、BusinessHandler
メソッドchannelRead
が 3 回呼び出されました。
そして、キャプチャされたデータ パケットは、実際に 1 つのパケットに詰まった 3 つのデータ フレームをシミュレートしたものです。
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 58 00 00 00 b5 7b 22 63 6c 61 73 73 4e 61 6d 65 |X....{
"className|
|00000010| 22 3a 22 70 6f 6a 6f 2e 55 73 65 72 22 2c 22 6f |":"pojo.User","o|
|00000020| 62 6a 65 63 74 22 3a 22 65 79 4a 75 59 57 31 6c |bject":"eyJuYW1l|
|00000030| 49 6a 6f 69 35 59 61 4a 36 62 6d 50 35 62 4f 77 |Ijoi5YaJ6bmP5bOw|
|00000040| 49 69 77 69 59 57 64 6c 49 6a 6f 79 4e 43 77 69 |IiwiYWdlIjoyNCwi|
|00000050| 59 6d 6c 79 64 47 68 6b 59 58 6b 69 4f 69 49 79 |YmlydGhkYXkiOiIy|
|00000060| 4d 44 45 35 4c 7a 41 78 4c 7a 45 30 49 44 41 30 |MDE5LzAxLzE0IDA0|
|00000070| 4f 6a 51 30 4f 6a 45 30 49 69 77 69 64 57 6c 6b |OjQ0OjE0IiwidWlk|
|00000080| 49 6a 6f 69 4f 44 46 6b 5a 54 55 35 59 57 55 74 |IjoiODFkZTU5YWUt|
|00000090| 4d 7a 51 34 4d 69 30 30 5a 44 46 68 4c 57 4a 6a |MzQ4Mi00ZDFhLWJj|
|000000a0| 4e 44 4d 74 4e 32 4e 6a 4d 54 4a 6d 4f 54 49 31 |NDMtN2NjMTJmOTI1|
|000000b0| 5a 54 55 78 49 6e 30 3d 22 7d 【63】 58 00 00 00 b5 |ZTUxIn0="}cX....|
|000000c0| 7b 22 63 6c 61 73 73 4e 61 6d 65 22 3a 22 70 6f |{
"className":"po|
|000000d0| 6a 6f 2e 55 73 65 72 22 2c 22 6f 62 6a 65 63 74 |jo.User","object|
|000000e0| 22 3a 22 65 79 4a 75 59 57 31 6c 49 6a 6f 69 35 |":"eyJuYW1lIjoi5|
|000000f0| 59 61 4a 36 62 6d 50 35 62 4f 77 49 69 77 69 59 |YaJ6bmP5bOwIiwiY|
|00000100| 57 64 6c 49 6a 6f 79 4e 43 77 69 59 6d 6c 79 64 |WdlIjoyNCwiYmlyd|
|00000110| 47 68 6b 59 58 6b 69 4f 69 49 79 4d 44 45 35 4c |GhkYXkiOiIyMDE5L|
|00000120| 7a 41 78 4c 7a 45 30 49 44 41 30 4f 6a 51 30 4f |zAxLzE0IDA0OjQ0O|
|00000130| 6a 45 30 49 69 77 69 64 57 6c 6b 49 6a 6f 69 4f |jE0IiwidWlkIjoiO|
|00000140| 44 46 6b 5a 54 55 35 59 57 55 74 4d 7a 51 34 4d |DFkZTU5YWUtMzQ4M|
|00000150| 69 30 30 5a 44 46 68 4c 57 4a 6a 4e 44 4d 74 4e |i00ZDFhLWJjNDMtN|
|00000160| 32 4e 6a 4d 54 4a 6d 4f 54 49 31 5a 54 55 78 49 |2NjMTJmOTI1ZTUxI|
|00000170| 6e 30 3d 22 7d 【63】 58 00 00 00 b5 7b 22 63 6c 61 |n0="}cX....{"cla|
|00000180| 73 73 4e 61 6d 65 22 3a 22 70 6f 6a 6f 2e 55 73 |ssName":"pojo.Us|
|00000190| 65 72 22 2c 22 6f 62 6a 65 63 74 22 3a 22 65 79 |er","object":"ey|
|000001a0| 4a 75 59 57 31 6c 49 6a 6f 69 35 59 61 4a 36 62 |JuYW1lIjoi5YaJ6b|
|000001b0| 6d 50 35 62 4f 77 49 69 77 69 59 57 64 6c 49 6a |mP5bOwIiwiYWdlIj|
|000001c0| 6f 79 4e 43 77 69 59 6d 6c 79 64 47 68 6b 59 58 |oyNCwiYmlydGhkYX|
|000001d0| 6b 69 4f 69 49 79 4d 44 45 35 4c 7a 41 78 4c 7a |kiOiIyMDE5LzAxLz|
|000001e0| 45 30 49 44 41 30 4f 6a 51 30 4f 6a 45 30 49 69 |E0IDA0OjQ0OjE0Ii|
|000001f0| 77 69 64 57 6c 6b 49 6a 6f 69 4f 44 46 6b 5a 54 |widWlkIjoiODFkZT|
|00000200| 55 35 59 57 55 74 4d 7a 51 34 4d 69 30 30 5a 44 |U5YWUtMzQ4Mi00ZD|
|00000210| 46 68 4c 57 4a 6a 4e 44 4d 74 4e 32 4e 6a 4d 54 |FhLWJjNDMtN2NjMT|
|00000220| 4a 6d 4f 54 49 31 5a 54 55 78 49 6e 30 3d 22 7d |JmOTI1ZTUxIn0="}|
|00000230|【63】 |c |
+--------+-------------------------------------------------+----------------+
確かに尾が 3 つあることがわかります [63]
netty4.x バージョンでは、スティッキー パケットの問題は、netty の ByteToMessageDecoder の CallDecode メソッドによって実際に処理されます。
- アンパック問題
この時間のTcpClient
エンコーダーをコメントアウトしEncoderHandler
、EchoHandler の channelActive でデータアンパック問題をシミュレートします。
public void channelActive(ChannelHandlerContext ctx) throws Exception {
User user = new User();
user.setBirthday(new Date());
user.setUID(UUID.randomUUID().toString());
user.setName("冉鹏峰");
user.setAge(24);
DTObject dtObject = new DTObject();
dtObject.setClassName(user.getClass().getName());
dtObject.setObject(ByteUtils.InstanceObjectMapper().writeValueAsBytes(user));
TcpProtocol tcpProtocol = new TcpProtocol();
byte [] objectBytes=ByteUtils.InstanceObjectMapper().writeValueAsBytes(dtObject);
tcpProtocol.setLen(objectBytes.length);
tcpProtocol.setData(objectBytes);
ByteBuf buffer = ctx.alloc().buffer();
buffer.writeByte(tcpProtocol.getHeader());
buffer.writeInt(tcpProtocol.getLen());
buffer.writeBytes(Arrays.copyOfRange(tcpProtocol.getData(),0,tcpProtocol.getLen()/2));//只发送二分之一的数据包
//模拟拆包
ctx.write(buffer);
ctx.flush();
Thread.sleep(3000);//模拟网络延时
buffer = ctx.alloc().buffer();
buffer.writeBytes(Arrays.copyOfRange(tcpProtocol.getData(),tcpProtocol.getLen()/2,tcpProtocol.getLen()));//将剩下的二分之和尾巴发送过去
buffer.writeByte(tcpProtocol.getTail());
ctx.write(buffer);
ctx.flush();
}
実行結果:
まず、クライアント側:
2019-01-14 17:08:33 DEBUG [DEBUG] [id: 0x3b8cbbbb, L:/127.0.0.1:51138 - R:/127.0.0.1:8777] WRITE: 95B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 58 00 00 00 b5 7b 22 63 6c 61 73 73 4e 61 6d 65 |X....{
"className|
|00000010| 22 3a 22 70 6f 6a 6f 2e 55 73 65 72 22 2c 22 6f |":"pojo.User","o|
|00000020| 62 6a 65 63 74 22 3a 22 65 79 4a 75 59 57 31 6c |bject":"eyJuYW1l|
|00000030| 49 6a 6f 69 35 59 61 4a 36 62 6d 50 35 62 4f 77 |Ijoi5YaJ6bmP5bOw|
|00000040| 49 69 77 69 59 57 64 6c 49 6a 6f 79 4e 43 77 69 |IiwiYWdlIjoyNCwi|
|00000050| 59 6d 6c 79 64 47 68 6b 59 58 6b 69 4f 69 49 |YmlydGhkYXkiOiI |
+--------+-------------------------------------------------+----------------+
2019-01-14 17:08:33 DEBUG [DEBUG] [id: 0x3b8cbbbb, L:/127.0.0.1:51138 - R:/127.0.0.1:8777] FLUSH
2019-01-14 17:08:36 DEBUG [DEBUG] [id: 0x3b8cbbbb, L:/127.0.0.1:51138 - R:/127.0.0.1:8777] WRITE: 92B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 79 4d 44 45 35 4c 7a 41 78 4c 7a 45 30 49 44 41 |yMDE5LzAxLzE0IDA|
|00000010| 31 4f 6a 41 34 4f 6a 45 30 49 69 77 69 64 57 6c |1OjA4OjE0IiwidWl|
|00000020| 6b 49 6a 6f 69 4f 57 45 79 5a 6a 49 35 4d 6d 4d |kIjoiOWEyZjI5MmM|
|00000030| 74 4d 6a 4d 35 4f 43 30 30 5a 6a 6b 77 4c 57 46 |tMjM5OC00ZjkwLWF|
|00000040| 6b 5a 57 59 74 5a 6d 46 6c 4e 44 45 7a 5a 6a 55 |kZWYtZmFlNDEzZjU|
|00000050| 35 4e 32 45 33 49 6e 30 3d 22 7d 63 |5N2E3In0="}c |
+--------+-------------------------------------------------+----------------+
2019-01-14 17:08:36 DEBUG [DEBUG] [id: 0x3b8cbbbb, L:/127.0.0.1:51138 - R:/127.0.0.1:8777] FLUSH
確かにデータは2つのパケットに分割されて送信されます。
サーバーの出力ログを確認します。
2019-01-14 17:08:33 DEBUG [DEBUG] [id: 0x8e5811b3, L:/127.0.0.1:8777 - R:/127.0.0.1:51138] RECEIVED: 95B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 58 00 00 00 b5 7b 22 63 6c 61 73 73 4e 61 6d 65 |X....{
"className|
|00000010| 22 3a 22 70 6f 6a 6f 2e 55 73 65 72 22 2c 22 6f |":"pojo.User","o|
|00000020| 62 6a 65 63 74 22 3a 22 65 79 4a 75 59 57 31 6c |bject":"eyJuYW1l|
|00000030| 49 6a 6f 69 35 59 61 4a 36 62 6d 50 35 62 4f 77 |Ijoi5YaJ6bmP5bOw|
|00000040| 49 69 77 69 59 57 64 6c 49 6a 6f 79 4e 43 77 69 |IiwiYWdlIjoyNCwi|
|00000050| 59 6d 6c 79 64 47 68 6b 59 58 6b 69 4f 69 49 |YmlydGhkYXkiOiI |
+--------+-------------------------------------------------+----------------+
2019-01-14 17:08:33 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 17:08:33 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 17:08:33 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据长度不够,数据协议len长度为:181,数据包实际可读内容为:90正在等待处理拆包……
2019-01-14 17:08:36 DEBUG [DEBUG] [id: 0x8e5811b3, L:/127.0.0.1:8777 - R:/127.0.0.1:51138] RECEIVED: 92B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 79 4d 44 45 35 4c 7a 41 78 4c 7a 45 30 49 44 41 |yMDE5LzAxLzE0IDA|
|00000010| 31 4f 6a 41 34 4f 6a 45 30 49 69 77 69 64 57 6c |1OjA4OjE0IiwidWl|
|00000020| 6b 49 6a 6f 69 4f 57 45 79 5a 6a 49 35 4d 6d 4d |kIjoiOWEyZjI5MmM|
|00000030| 74 4d 6a 4d 35 4f 43 30 30 5a 6a 6b 77 4c 57 46 |tMjM5OC00ZjkwLWF|
|00000040| 6b 5a 57 59 74 5a 6d 46 6c 4e 44 45 7a 5a 6a 55 |kZWYtZmFlNDEzZjU|
|00000050| 35 4e 32 45 33 49 6e 30 3d 22 7d 63 |5N2E3In0="}c |
+--------+-------------------------------------------------+----------------+
2019-01-14 17:08:36 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 17:08:36 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 17:08:36 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 17:08:36 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{
"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA1OjA4OjE0IiwidWlkIjoiOWEyZjI5MmMtMjM5OC00ZjkwLWFkZWYtZmFlNDEzZjU5N2E3In0="}
2019-01-14 17:08:36 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{
name='冉鹏峰', age=24, UID='9a2f292c-2398-4f90-adef-fae413f597a7', birthday=Mon Jan 14 05:08:00 CST 2019}
最初のデータパケットで、bytebuf の読み取り可能な内容が十分ではないと判断された場合、デコードは終了し、親クラスの callDecode 内の while ループが中断され、次のデータパケットが到着すると、親クラスのchannelRead、2つのパケットデータが結合され、再度デコードされます。
- 最後に、解凍と貼り付けのシーンを同時にテストする
か、TcpClient
でエンコーダをコメントアウトしてEncoderHandler
から、EchoHandler の ChannelActive メソッドでエンコーダをコメントアウトします。
public void channelActive(ChannelHandlerContext ctx) throws Exception {
User user = new User();
user.setBirthday(new Date());
user.setUID(UUID.randomUUID().toString());
user.setName("冉鹏峰");
user.setAge(24);
DTObject dtObject = new DTObject();
dtObject.setClassName(user.getClass().getName());
dtObject.setObject(ByteUtils.InstanceObjectMapper().writeValueAsBytes(user));
TcpProtocol tcpProtocol = new TcpProtocol();
byte [] objectBytes=ByteUtils.InstanceObjectMapper().writeValueAsBytes(dtObject);
tcpProtocol.setLen(objectBytes.length);
tcpProtocol.setData(objectBytes);
ByteBuf buffer = ctx.alloc().buffer();
buffer.writeByte(tcpProtocol.getHeader());
buffer.writeInt(tcpProtocol.getLen());
buffer.writeBytes(Arrays.copyOfRange(tcpProtocol.getData(),0,tcpProtocol.getLen()/2));//拆包,只发送一半的数据
ctx.write(buffer);
ctx.flush();
Thread.sleep(3000);
buffer = ctx.alloc().buffer();
buffer.writeBytes(Arrays.copyOfRange(tcpProtocol.getData(),tcpProtocol.getLen()/2,tcpProtocol.getLen())); //拆包发送剩余的一半数据
buffer.writeByte(tcpProtocol.getTail());
//模拟粘包的第二帧数据
buffer.writeByte(tcpProtocol.getHeader());
buffer.writeInt(tcpProtocol.getLen());
buffer.writeBytes(tcpProtocol.getData());
buffer.writeByte(tcpProtocol.getTail());
//模拟粘包的第三帧数据
buffer.writeByte(tcpProtocol.getHeader());
buffer.writeInt(tcpProtocol.getLen());
buffer.writeBytes(tcpProtocol.getData());
buffer.writeByte(tcpProtocol.getTail());
ctx.write(buffer);
ctx.flush();
}
最後に、サーバーの出力を直接表示します。
2019-01-14 17:19:25 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 17:19:25 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 17:19:25 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据长度不够,数据协议len长度为:181,数据包实际可读内容为:90正在等待处理拆包……
2019-01-14 17:19:28 DEBUG [DEBUG] [id: 0xc46234aa, L:/127.0.0.1:8777 - R:/127.0.0.1:51466] RECEIVED: 466B
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 17:19:28 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{
"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA1OjE5OjE0IiwidWlkIjoiODE2Zjg2ZDItNDBhMS00MDRkLTgwMWItZmY1NzgxMTJhNjFmIn0="}
2019-01-14 17:19:28 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{
name='冉鹏峰', age=24, UID='816f86d2-40a1-404d-801b-ff578112a61f', birthday=Mon Jan 14 05:19:00 CST 2019}
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 17:19:28 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{
"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA1OjE5OjE0IiwidWlkIjoiODE2Zjg2ZDItNDBhMS00MDRkLTgwMWItZmY1NzgxMTJhNjFmIn0="}
2019-01-14 17:19:28 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{
name='冉鹏峰', age=24, UID='816f86d2-40a1-404d-801b-ff578112a61f', birthday=Mon Jan 14 05:19:00 CST 2019}
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 开始解码数据……
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据开头格式正确
2019-01-14 17:19:28 DEBUG [org.wisdom.server.decoder.DecoderHandler] 数据解码成功
2019-01-14 17:19:28 DEBUG [org.wisdom.server.business.BusinessHandler] 解码后的字节码:{
"className":"pojo.User","object":"eyJuYW1lIjoi5YaJ6bmP5bOwIiwiYWdlIjoyNCwiYmlydGhkYXkiOiIyMDE5LzAxLzE0IDA1OjE5OjE0IiwidWlkIjoiODE2Zjg2ZDItNDBhMS00MDRkLTgwMWItZmY1NzgxMTJhNjFmIn0="}
2019-01-14 17:19:28 INFO [org.wisdom.server.business.BusinessHandler] 收到实体对象:User{
name='冉鹏峰', age=24, UID='816f86d2-40a1-404d-801b-ff578112a61f', birthday=Mon Jan 14 05:19:00 CST 2019}
要約する
アンパックとスティッキングについては、コードが netty の設計原則に従って実装されている限り、問題なく簡単に解決できます。この例ではDTObject
データをパックしますが、デコード中にオブジェクト タイプが追加されるたびに if 判定を追加するという厄介な作業を回避できます。ただし、リストとマップを転送するときのシーンはまだ処理できません。次の記事では、リスト、マップ、および共通オブジェクトのシナリオに対処する方法を紹介します。