初次接触netty,很多迷茫,也很多坑,踩了,自己记下,分享到博客,以免后来人继续踩坑,喜欢就点赞或评论,因为刚开始写博客,谢谢了。
我假如你已经了解netty的一些知识,比如他的NIO是多么的好,ByteBuf是多么的好,所以你才选择用此框架,那么,就我和一起开始学习netty吧。(不过即使你不知道netty有多好,但是既然你来看这篇博客,就说明你想了解netty,那我们不说屁话,开始吧。)
首先、Netty我并没有开发客户端,而只开发服务端。
客户端有两个,一个是app,通过http请求我服务端接口(这个本篇博客不做说明);一个是硬件,通过tcp和建立连接,进行通讯(这里的tcp通讯报文数据格式(十六进制)采用:字头head(1个字节)+业务类型type(1个字节)+校验位(2个字节)+定长length(2个字节)+实体body(N个字节)+字尾cc(1个字节))。
其次,开始写代码:
其中有几个注意点:
1、流式编程,不断向 channel.pipeline().addLast()填东西
2、填的第一个就是心跳检测
new IdleStateHandler(READ_IDEL_TIME_OUT, WRITE_IDEL_TIME_OUT, ALL_IDEL_TIME_OUT, TimeUnit.SECONDS)
3、填的第二个是编码方式的类(这里很重要,我在这里踩了坑)
new ClientMessageDecoder(MAX_FRAME_LENGTH, LENGTH_FIELD_OFFSET, LENGTH_FIELD_LENGTH, LENGTH_ADJUSTMENT, INITIAL_BYTES_TO_STRIP, false)
ClientMessageDecoder()这个类继承 LengthFieldBasedFrameDecoder(这是netty用来处理tcp报文数据格式为head+type+校验码+length+body+cc粘包的一个类,只需要传入四个参数,就可以很好解决tcp粘包和拆包的问题)
4、填的第三个是解码方式的类(通过第3点,我们把ByteBuf缓存中的数据(客户端传来的数据放到这个缓存中),编码成我们想要的格式,现在就可以在这里解码,并交给handler去处理了)
channel.pipeline().addLast("encoder", new ClientMessageEncoder());
5、填的第四个是handler处理类了(TcpServerInboundHandler)。由他进行数据的处理,完成我们想要的业务吧。
6、后面还是有一些配置,主要设置连接数,长连接,是否将收到的数据理解发送处理的配置,不做讲解。
7、我将在你看完代码后讲解:ClientMessageDecoder,ClientMessageDecoder,TcpServerInboundHandler,这个三个类,很重要。
耐心看代码和注释
public class TcpServer {
private static final String TAG = TcpServer.class.getName();
private static final int READ_IDEL_TIME_OUT = 200; // 读超时
private static final int WRITE_IDEL_TIME_OUT = 205;// 写超时
private static final int ALL_IDEL_TIME_OUT = 210; // 所有超时
private static final int MAX_FRAME_LENGTH = 1024 * 1024;
// lengthFieldLength = 4;//长度字段占的长度
private static final int LENGTH_FIELD_LENGTH = 4;
// lengthFieldOffset = 0;//长度字段的偏差
private static final int LENGTH_FIELD_OFFSET = 2;
// lengthAdjustment = 0;//添加到长度字段的补偿值
private static final int LENGTH_ADJUSTMENT = 0;
// initialBytesToStrip = 0。//从解码帧中第一次去除的字节数
private static final int INITIAL_BYTES_TO_STRIP = 0;
/**
* (ChannelOption.SO_BACKLOG, 1024)//连接数
* (ChannelOption.SO_KEEPALIVE, true); //长连接
* (ChannelOption.TCP_NODELAY, true); //不延迟,消息立即发送
* @param port
*/
public static void start(int port) {
String output = "Server start at:" + port;
LogUtil.info(TAG, output);
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup).channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel channel) throws Exception {
channel.pipeline().addLast(new IdleStateHandler(READ_IDEL_TIME_OUT, WRITE_IDEL_TIME_OUT, ALL_IDEL_TIME_OUT, TimeUnit.SECONDS));
channel.pipeline().addLast("decoder", new ServerMessageDecoder(MAX_FRAME_LENGTH, LENGTH_FIELD_OFFSET, LENGTH_FIELD_LENGTH, LENGTH_ADJUSTMENT, INITIAL_BYTES_TO_STRIP, false));
channel.pipeline().addLast("encoder", new ServerMessageEncoder());
channel.pipeline().addLast(new TcpServerInboundHandler());
}
}).option(ChannelOption.SO_BACKLOG, 1024)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
.childOption(ChannelOption.TCP_NODELAY, true);
ChannelFuture future = serverBootstrap.bind(port).sync();
future.channel().closeFuture().sync();
} catch (Exception e) {
LogUtil.error(TAG, e);
} finally {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
}
}
ClientMessageDecoder:
负责将客户端传来数据按照我们指定的方式进行编码,同时这里配置几个参数,解决了tcp粘包和拆包问题。本文只介绍继承LengthFieldBasedFrameDecoder这个类,处理tcp报文数据格式为head+type+校验码+length+body+cc粘包拆包问题,其他如报文数据格式定长啊,报文数据采用换行分隔符这里不做介绍。
首先明白 客户端传来的数据放到ByteBuf中,我们读取来就OK了
public class ClientMessageDecoder extends LengthFieldBasedFrameDecoder {
// 判断传送客户端传送过来的数据是否按照协议传输,头部信息的大小应该是 byte+byte+int = 1+1+4 = 6
private static final int HEADER_SIZE = 6;
private String head;
private String type;
private int length;
private static final String TAG = ClientMessageDecoder.class.getName();
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
LogUtil.error(TAG, cause);
}
/**
* @param maxFrameLength 解码时,处理每个帧数据的最大长度
* @param lengthFieldOffset 该帧数据中,存放该帧数据的长度的数据的起始位置
* @param lengthFieldLength 记录该帧数据长度的字段本身的长度
* @param lengthAdjustment 修改帧数据长度字段中定义的值,可以为负数
* @param initialBytesToStrip 解析的时候需要跳过的字节数
* @param failFast 为true,当frame长度超过maxFrameLength时立即报TooLongFrameException异常,为false,读取完整个帧再报异常
*/
public ClientMessageDecoder(int maxFrameLength, int lengthFieldOffset, int lengthFieldLength, int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
super(maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip, failFast);
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
ServerMessage customMsg = null;
ByteBuf buf = null;
try {
if (in == null) {
throw new Exception("in is null");
}
if (in.readableBytes() < HEADER_SIZE) {
throw new Exception("Readable smaller");
}
// 注意在读的过程中,readIndex的指针也在移动
head = Utils.byteToHexString(in.readByte()) ;
type = Utils.byteToHexString(in.readByte());
String valide = "";
for (int i = 0; i < 4; i++) {
valide = valide + Utils.byteToHexString(in.readByte());
}
if (Utils.isNullOrEmpty(valide) || valide.length() < 4)
throw new Exception("valide error,valide:" + valide);
length = Integer.parseInt(valide.substring(4), 16);
if ((length > 24 && length < 500 && in.readerIndex() < 500) || length == 9) {
if (in.readableBytes() < length) {
throw new Exception("The length is " + length + ",length is wrong" );
}
buf = in.readBytes(length);
byte[] body = new byte[buf.readableBytes()];
buf.readBytes(body);
if (body.length < 1) {
throw new Exception("body.length is " + body.length);
}
String realBody = null;
realBody = Utils.bytesToHexString(Utils.interceptBytes(body, 0, body.length - 1));
customMsg = new ServerMessage(head, type, length, realBody);
} else {
throw new Exception("The length is " + length + ",length is not in range");
}
} catch (Exception e) {
throw e;
} finally {
try {
if (buf != null) {
buf.clear();
buf.release();
}
if (in != null) {
in.clear();
}
} catch (Exception e) {
// TODO: handle exception
throw e;
}
}
return customMsg;
}
}
ClientMessageDecoder
public class ServerMessageEncoder extends MessageToByteEncoder<ServerMessage> {
private static final String TAG = ServerMessageEncoder.class.getName();
@Override
protected void encode(ChannelHandlerContext ctx, ServerMessage msg, ByteBuf out) throws Exception {
if (null == msg) {
throw new Exception("msg is null");
}
try {
if (msg.getBody().length() > 24 && msg.getBody().length() < 256)
if ("00".equals(msg.getType()))
byte[] bodyBytes = msg.getBody().getBytes("utf-8");
if (out != null && out.isWritable() && ctx.channel().isActive() && ctx.channel().isWritable()) {
out.writeBytes(Utils.hexStringToBytes(msg.getHead()));
out.writeBytes(Utils.hexStringToBytes(msg.getType()));
out.writeInt(bodyBytes.length);
out.writeBytes(bodyBytes);
}
} catch (Exception e) {
// TODO: handle exception
LogUtil.error(TAG, e);
ctx.close();
}
}
}
小结:经过以上两步(编码,解码),我们已经收到了通过tcp协议传来的数据,并在编码的时候通过LengthFieldBasedFrameDecoder这个类,解决我们规定格式的报文的粘包拆包问题,现在我们可以通过
TcpServerInboundHandler 去处理业务了(好开心)
该类继承ChannelInboundHandlerAdapter,重写其中的channelRead(ChannelHandlerContext ctx, Object msg)方法,就可以了
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// TODO Auto-generated method stub
try {
ServerMessage customMsg = (ServerMessage) msg;
String body = customMsg.getBody();
if (!Utils.isNullOrEmpty(body) && body.length() > 24) {
//带着我们custoMsg去分发到各处吧
TypeDispatcher.dispatcher(ctx, customMsg);
} else if (ctx != null) {
ctx.close();
}
} catch (Exception e) {
// TODO: handle exception
LogUtil.error(TAG, e);
if (ctx != null) {
ctx.close();
}
}
}
总结
至此,我们已经完成了netty服务端的一个简单开发,但是对于ClientMessageDecoder继承LengthFieldBasedFrameDecoder时,传入的五个参数,他们是我们准确拿到客户端数据的关键,大家可以去看看netty权威指南2,进一步加深理解。