簡介
在之前的文章中我們提到了,對於NioSocketChannel來說,它不接收最基本的string消息,只接收ByteBuf和FileRegion。但是ByteBuf是以二進制的形式進行處理的,對於程序員來說太不直觀了,處理起來也比較麻煩,有沒有可能直接處理java簡單對象呢?本文將會探討一下這個問題。
decode和encode
比如我們需要直接向channel中寫入一個字符串,在之前的文章中,我們知道這是不可以的,會報下面的錯誤:
DefaultChannelPromise@57f5c075(failure: java.lang.UnsupportedOperationException: unsupported message type: String (expected: ByteBuf, FileRegion))
也就說ChannelPromise只接受ByteBuf和FileRegion,那么怎么做呢?
既然ChannelPromise只接受ByteBuf和FileRegion,那么我們就需要把String對象轉換成ByteBuf即可。
也就是說在寫入String之前把String轉換成ByteBuf,當要讀取數據的時候,再把ByteBuf轉換成String。
我們知道ChannelPipeline中可以添加多個handler,並且控制這些handler的順序。
那么我們的思路就出來了,在ChannelPipeline中添加一個encode,用於數據寫入的是對數據進行編碼成ByteBuf,然后再添加一個decode,用於在數據寫出的時候對數據進行解碼成對應的對象。
encode,decode是不是很熟悉?對了,這就是對象的序列化。
對象序列化
netty中對象序列化是要把傳輸的對象和ByteBuf直接互相轉換,當然我們可以自己實現這個轉換對象。但是netty已經為我們提供了方便的兩個轉換類:ObjectEncoder和ObjectDecoder。
先看ObjectEncoder,他的作用就是將對象轉換成為ByteBuf。
這個類很簡單,我們對其分析一下:
public class ObjectEncoder extends MessageToByteEncoder<Serializable> {
private static final byte[] LENGTH_PLACEHOLDER = new byte[4];
@Override
protected void encode(ChannelHandlerContext ctx, Serializable msg, ByteBuf out) throws Exception {
int startIdx = out.writerIndex();
ByteBufOutputStream bout = new ByteBufOutputStream(out);
ObjectOutputStream oout = null;
try {
bout.write(LENGTH_PLACEHOLDER);
oout = new CompactObjectOutputStream(bout);
oout.writeObject(msg);
oout.flush();
} finally {
if (oout != null) {
oout.close();
} else {
bout.close();
}
}
int endIdx = out.writerIndex();
out.setInt(startIdx, endIdx - startIdx - 4);
}
}
ObjectEncoder繼承了MessageToByteEncoder,而MessageToByteEncoder又繼承了ChannelOutboundHandlerAdapter。為什么是OutBound呢?這是因為我們是要對寫入的對象進行轉換,所以是outbound。
首先使用ByteBufOutputStream對out ByteBuf進行封裝,在bout中,首先寫入了一個LENGTH_PLACEHOLDER字段,用來表示stream中中Byte的長度。然后用一個CompactObjectOutputStream對bout進行封裝,最后就可以用CompactObjectOutputStream寫入對象了。
對應的,netty還有一個ObjectDecoder對象,用於將ByteBuf轉換成對應的對象,ObjectDecoder繼承自LengthFieldBasedFrameDecoder,實際上他是一個ByteToMessageDecoder,也是一個ChannelInboundHandlerAdapter,用來對數據讀取進行處理。
我們看下ObjectDecoder中最重要的decode方法:
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
ByteBuf frame = (ByteBuf) super.decode(ctx, in);
if (frame == null) {
return null;
}
ObjectInputStream ois = new CompactObjectInputStream(new ByteBufInputStream(frame, true), classResolver);
try {
return ois.readObject();
} finally {
ois.close();
}
}
上面的代碼可以看到,將輸入的ByteBuf轉換為ByteBufInputStream,最后轉換成為CompactObjectInputStream,就可以直接讀取對象了。
使用編碼和解碼器
有了上面兩個編碼解碼器,直接需要將其添加到client和server端的ChannelPipeline中就可以了。
對於server端,其核心代碼如下:
//定義bossGroup和workerGroup
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.handler(new LoggingHandler(LogLevel.INFO))
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(
// 添加encoder和decoder
new ObjectEncoder(),
new ObjectDecoder(ClassResolvers.cacheDisabled(null)),
new PojoServerHandler());
}
});
// 綁定端口,並准備接受連接
b.bind(PORT).sync().channel().closeFuture().sync();
同樣的,對於client端,我們其核心代碼如下:
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline p = ch.pipeline();
p.addLast(
// 添加encoder和decoder
new ObjectEncoder(),
new ObjectDecoder(ClassResolvers.cacheDisabled(null)),
new PojoClientHandler());
}
});
// 建立連接
b.connect(HOST, PORT).sync().channel().closeFuture().sync();
可以看到上面的邏輯就是將ObjectEncoder和ObjectDecoder添加到ChannelPipeline中即可。
最后,就可以在客戶端和瀏覽器端通過調用:
ctx.write("加油!");
直接寫入字符串對象了。
總結
有了ObjectEncoder和ObjectDecoder,我們就可以不用受限於ByteBuf了,程序的靈活程度得到了大幅提升。
本文的例子可以參考:learn-netty4
本文已收錄於 http://www.flydean.com/08-netty-pojo-buf/
最通俗的解讀,最深刻的干貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!
歡迎關注我的公眾號:「程序那些事」,懂技術,更懂你!