处理流数据
关于socket缓冲区的小说明
一个基于流的传输,比如TCP/IP, 接收的数据存储在一个socket接收缓冲区里,
不幸的是,这个缓冲区不是基于报文而是基于字节。
这就意味着,即使你发送了2个消息作为2个独立的报文,操作系统不会认为它们是2个报文
而是一堆字节。
因此,没有机制保证你读的内容是远程程序所写的。
例如,让我们假设TCP/IP栈接受了3个数据包。
三个数据报文正如它们被发送的,因为流协议很有可能从程序来看,以片段的方式读取。
三个报文分离进入4个缓冲区。
因此,一个接收的部分,不考虑服务端还是客户端,应该重新组织数据被程序理解。
例如,下面的数据
四个缓冲区的数据成为3个报文。
第一个解决方案
让我们回到TIME客户端的例子,我们有同样的问题,
一个32位的整数非常小,很可能不会被分开。
尽管如此,问题是它可以被分片,分片会导致流量增加。
简单的解决方案就是:增加一个内部的渐增的缓冲区,等待知道4个字节都接收到了内部的缓冲区。
下面的是修改过的客户端的例子来解决问题。
package io.netty.example.time;
import java.util.Date;
public class TimeClientHandler extends ChannelHandlerAdapter {
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();
}
}
一个ChannelHandler 有两个生命周期监听方法:handlerAdded() and handlerRemoved().
你可以建立一个强制性的初始化任务只要它不耗费很长时间。
第一:所有接收的数据应该都进入缓冲区。
那个时候,handler必须检查是否buf有足够的数据,4个字节这里,处理真实的业务逻辑。
否则,netty将调用channelRead()方法当更多的数据到来,最终所有的4个数据都有了。
第2种解决方案
尽管第一种解决方案已经解决了问题,代码不怎么好看。
想象一下,一个更复杂的协议包含多个字段,比如各种长度字段。
你的ChannelHandler实现将会很复杂。
你可以增加多个ChannelHandler to a ChannelPipeline, 因此,你可以分裂一个整体ChannelHandler为多个部分。
来降低复杂度。例如,你可以分成两部分。
TimeDecoder处理分片,并且最初的简化版
幸运的,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)
}
}
ByteToMessageDecoder是一个ChannelHandler的实现,让你更简单的处理分片数据。
ByteToMessageDecoder 调用decode() 方法通过内部的缓冲区当新的数据来到时。
decode() 决定是否什么都不做当没有足够的数据
ByteToMessageDecoder将在有更多的数据来到时调用decode().
如果decode()增加一个对象到out,这就意味着,decoder 解码了一个消息,
ByteToMessageDecoder将忽略累积缓冲区的读部分,
记住:你没有必要decode多个消息,
ByteToMessageDecoder将保持调用decode()方法直到它添加nothing到out.
既然我们有另外的handler来加入到ChannelPipeline, 我们应该修改ChannelInitializer的实现。
b.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new TimeDecoder(), new TimeClientHandler());
}
});
如果你是一个冒险的人,你也许想试试ReplayingDecoder,它简化了decoder.
你将需要查看API文档获取更多消息。
public class TimeDecoder extends ReplayingDecoder<Void> {
@Override
protected void decode(
ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
out.add(in.readBytes(4));
}
}
另外,NETTY提供out-of-the-box decoders(可以让你来实现大多数协议,帮助你避免一些意外)
下面有更多信息
io.netty.example.factorial for a binary protocol, and
io.netty.example.telnet for a text line-based protocol.
翻译 | ok | ok | ok |
理解 | ok | ok | ok |
最后推荐一篇好文章 http://www.dozer.cc/2014/12/netty-long-connection/