netty(2)

处理基于流的传输

套接字缓冲区的一个小警告

在基于流的传输(如TCP/IP)中,接收的数据存储在套接字接收缓冲区中。不幸的是,基于流的传输的缓冲区不是数据包队列,而是字节队列。这意味着,即使我们将两条消息作为两个独立的数据包发送,操作系统也不会将它们视为两条消息,而是将它们视为一堆字节。因此,无法保证我们所读的内容与远程对等方所写的内容完全相同。例如,假设操作系统的TCP/IP堆栈已接收到三个数据包:

由于基于流的协议的这一一般属性,在应用程序中很有可能以以下碎片形式读取它们:

因此,接收部分,无论是服务器端还是客户端,都应该将接收到的数据碎片整理成一个或多个有意义的帧,应用程序逻辑很容易理解这些帧。在上述示例的情况下,接收到的数据应如下所示:

第一个解决方案

现在让我们回到时间Client示例(在netty(1))。我们这里也有同样的问题。32位整数是一个非常小的数据量,不太可能经常被分割。但是,问题是它可能会被分割,并且随着流量的增加,分割的可能性会增加。

最简单的解决方案是创建一个内部累积缓冲区,并等待所有4个字节都接收到内部缓冲区。以下是修复该问题的修改后的TimeClientHandler实现:

import java.util.Date;

public class TimeClientHandler extends ChannelInboundHandlerAdapter {
    private ByteBuf buf;
    
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) {
        buf = ctx.alloc().buffer(4); // (1)
    }
    
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) {
        buf.release(); // (1)
        buf = null;
    }
    
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ByteBuf m = (ByteBuf) msg;
        buf.writeBytes(m); // (2)
        m.release();
        
        if (buf.readableBytes() >= 4) { // (3)
            long currentTimeMillis = (buf.readUnsignedInt() - 2208988800L) * 1000L;
            System.out.println(new Date(currentTimeMillis));
            ctx.close();
        }
    }
    
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}

1、ChannelHandler有两个生命周期侦听器方法:handlerAdded()和handlerRemoved()。我们可以执行任意(取消)初始化任务,只要它不会长时间阻塞。

2、首先,所有接收到的数据应累积到buf中。

3、然后,处理程序必须检查buf是否有足够的数据(本例中为4字节),然后继续执行实际的业务逻辑。否则,当更多数据到达时,Netty将再次调用channelRead()方法,最终所有4个字节都将累积。

第二种解决方案

尽管第一个解决方案解决了时间Client的问题,但修改后的处理程序看起来并不干净。想象一个更复杂的协议,它由多个字段组成,例如可变长度字段。我们的ChannelInboundHandler实现将很快变得无法维护。

正如我们可能已经注意到的,我们可以向ChannelPipeline添加多个ChannelHandler,因此,我们可以将一个单一ChannelHandler拆分为多个模块化ChannelHandler,以降低应用程序的复杂性。例如,我们可以将TimeClientHandler拆分为两个处理程序:

  • 处理碎片问题的时间解码器
  • TimeClientHandler最初的简单版本。

幸运的是,Netty提供了一个可扩展类,可以帮助我们编写开箱即用的第一个类:

package io.netty.example.time;

public class TimeDecoder extends ByteToMessageDecoder { // (1)
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) { // (2)
        if (in.readableBytes() < 4) {
            return; // (3)
        }
        
        out.add(in.readBytes(4)); // (4)
    }
}

1、ByteToMessageDecoder是ChannelInboundHandler的一个实现,它可以轻松处理碎片问题。

2、每当接收到新数据时,ByteToMessageDecoder使用内部维护的累积缓冲区调用decode()方法。

3、当累积缓冲区中没有足够的数据时,decode()可以决定不向out添加任何内容。当接收到更多数据时,ByteToMessageDecoder将再次调用decode()。

4、如果decode()将对象添加到out,则表示解码器已成功解码消息。ByteToMessageDecoder将丢弃累积缓冲区的读取部分。请记住,我们不需要解码多条消息。ByteToMessageDecoder将继续调用decode()方法,直到它不向out添加任何内容。

 现在我们有另一个处理程序要插入到ChannelPipeline中,我们应该修改TimeClient中的ChannelInitializer实现:

b.handler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
        ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
    }
});

此外,Netty还提供了开箱即用的解码器,使我们能够非常轻松地实现大多数协议,并帮助我们避免最终使用无法维护的单一处理程序实现。

用POJO而不是ByteBuf

到目前为止,我们回顾的所有示例都使用ByteBuf作为协议消息的主要数据结构。在本节中,我们将改进时间协议Client和服务器示例,以使用POJO而不是ByteBuf。

在ChannelHandler中使用POJO的优势是显而易见的;通过将从ByteBuf中提取信息的代码从处理程序中分离出来,处理程序变得更易于维护和重用。在TIME客户端和服务器示例中,我们只读取一个32位整数,直接使用ByteBuf并不是一个主要问题。但是,我们会发现,在实现实际协议时,有必要进行分离。

首先,让我们定义一个名为UnixTime的新类型。

import java.util.Date;

public class UnixTime {

    private final long value;
    
    public UnixTime() {
        this(System.currentTimeMillis() / 1000L + 2208988800L);
    }
    
    public UnixTime(long value) {
        this.value = value;
    }
        
    public long value() {
        return value;
    }
        
    @Override
    public String toString() {
        return new Date((value() - 2208988800L) * 1000L).toString();
    }
}

我们现在可以修改时间译码器来生成UnixTime而不是ByteBuf。

@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    if (in.readableBytes() < 4) {
        return;
    }

    out.add(new UnixTime(in.readUnsignedInt()));
}

随着解码器的更新,TimeClientHandler不再使用ByteBuf:

@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
    UnixTime m = (UnixTime) msg;
    System.out.println(m);
    ctx.close();
}

更简单,更优雅,对吗?同样的技术也可以应用于服务器端。这次让我们首先更新TimeServerHandler:

@Override
public void channelActive(ChannelHandlerContext ctx) {
    ChannelFuture f = ctx.writeAndFlush(new UnixTime());
    f.addListener(ChannelFutureListener.CLOSE);
}

现在,唯一缺少的部分是编码器,它是ChannelOutboundHandler的一个实现,它将UnixtTime转换回ByteBuf。它比编写解码器简单得多,因为在编码消息时不需要处理数据包碎片和组装。

public class TimeEncoder extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
        UnixTime m = (UnixTime) msg;
        ByteBuf encoded = ctx.alloc().buffer(4);
        encoded.writeInt((int)m.value());
        ctx.write(encoded, promise); // (1)
    }
}

1、这一行有很多重要的东西。

首先,我们按原样传递原始ChannelPromise,以便Netty在编码数据实际写入到导线时将其标记为成功或失败。

第二,我们没有给ctx.flush()。有一个单独的处理程序方法void flush(ChannelHandlerContext ctx),用于重写flush()操作。

为了进一步简化,我们可以使用MessageToByteEncoder:

public class TimeEncoder extends MessageToByteEncoder<UnixTime> {
    @Override
    protected void encode(ChannelHandlerContext ctx, UnixTime msg, ByteBuf out) {
        out.writeInt((int)msg.value());
    }
}

剩下的最后一项任务是在TimeServerHandler之前将TimeEncoder插入服务器端的ChannelPipeline,这只是一个简单的练习。

关闭应用程序

关闭 Netty 应用程序通常就像我们通过 shutdownGracefully() 关闭创建的所有 EventLoopGroups 一样简单。 当 EventLoopGroup 完全终止并且属于该组的所有 Channel 都已关闭时,它会返回一个 Future 通知我们。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值