上一篇:
首先先说一下ByteBuf
ByteBuf是比较主要的一个核心类
通过两个index实现零拷贝
当接收到来自服务端的数据时(对于服务端则是接收到客户端数据)
readIndex会相应增加
然后当你将收到的内容通过buf.readXX或buf.readXXLE(小端序)时,完成之后需要调用buf.release(),将已读部分释放
同时当你往里边写点什么东西的时候,writeIndex也会相应的增加,当将发送内容发送出去后,writeIndex会减少
当收到内容时,writeIndex会增加(可读区域扩充)
当丢弃已读内容时,readIndex会减少(清除可丢弃已读区域),writeIndex也会减少(可读区域长度没变)
0-可丢弃已读区域-readIndex-可读区域-writeIndex-可写区域-最大长度
可以简单理解成像个弹簧一样的东西,可以压缩和向上拉伸
首先
private void start(String localPoint) throws Exception {
String[] str = localPoint.split(":");
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bs = new Bootstrap();
bs.group(group) // 注册线程池
.channel(NioSocketChannel.class) // 使用NioSocketChannel来作为连接用的channel类
.remoteAddress(new InetSocketAddress(str[0], Integer.parseInt(str[1]))) // 绑定连接端口和host信息
.handler(new ChannelInitializer<SocketChannel>() { // 绑定连接初始化器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(
new CustomDecoder(ByteOrder.LITTLE_ENDIAN, Integer.MAX_VALUE, 0, 0, 0, 0, false));
ch.pipeline().addLast(new NettyClientHandler());
ch.pipeline().addLast(new CustomEncoder());
ch.pipeline().addLast(new ChunkedWriteHandler());
ch.pipeline().addLast(new IdleStateHandler(0, 0, 30, TimeUnit.SECONDS));// 心跳检测:30秒内没有读写操作时触发
ch.pipeline().addLast(new HearBeatHandler());// 继承自ChannlInboundHandlerAdapter,当没有心跳时,触发userEventTrigered方法
}
});
cf = bs.connect().sync(); // 异步连接服务器
cf.channel().closeFuture().sync(); // 异步等待关闭连接channel
} finally {
group.shutdownGracefully().sync(); // 释放线程池资源
}
}
这段是建立客户端的函数
CustomDecoder是我们自定义的解码器
NettyClientHandler是我们自定义的业务处理器
CustomEncoder是我们自定义的编码器
ChunkedWriteHandler是控制发送相关的(直接用就行了,这里不需要做任何处理)
IdleStateHandler是心跳检测上面的意思是每30秒内没有读写操作时会触发
io.netty.handler.timeout.IdleStateHandler.IdleStateHandler(long readerIdleTime, long writerIdleTime, long allIdleTime, TimeUnit unit)
这个构造函数的前两个参数和第三个参数差不多,只是第一个代表多少时间间隔内没有读操作时触发,第二个代表多少时间间隔内没写操作触发,第四个为时间单位
HearBeatHandler是自定义的心跳检测方法
在我们将这些一个一个过一遍之前,再讲一下java中的序列化和反序列化
序列化是指:
将我们的对象,转换为byte数组
而反序列化就是:
将byte转换为我们需要的对象
常见的一些序列化方法:
1.json
2.xml
3.java自带的序列化(不推荐,无法跨语言)
4.变长协议序列化
序列化将会再开一个篇幅讲一下。
1.CustomDecoder
package com.SocketLayer;
import java.nio.ByteOrder;
import com.Global.MsgType;
import com.Global.absCmd;
import com.Global.util;
import com.Global.workStation_protocol.Header;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
public class CustomDecoder extends LengthFieldBasedFrameDecoder {
/**
*
* @param ByteOrder byteOrder :大小端 高位在前还是低位在前 这里的是低位在前
* @param maxFrameLength 解码时,处理每个帧数据的最大长度 变长协议,所以是Integer.Max_Value
* @param lengthFieldOffset 该帧数据中,存放该帧数据的长度的数据的起始位置 协议头Type是个int,所以是4
* @param lengthFieldLength 记录该帧数据长度的字段本身的长度 协议长度Length的起始位置,所以是4
* @param lengthAdjustment 修改帧数据长度字段中定义的值,可以为负数 Length=8+body.length,所以是-8
* @param initialBytesToStrip 解析的时候需要跳过的字节数 0
* @param failFast 为true,当frame长度超过maxFrameLength时立即报TooLongFrameException异常,为false,读取完整个帧再报异常
*/
public CustomDecoder(ByteOrder byteOrder, int maxFrameLength, int lengthFieldOffset, int lengthFieldLength,
int lengthAdjustment, int initialBytesToStrip, boolean failFast) {
super(byteOrder, maxFrameLength, lengthFieldOffset, lengthFieldLength, lengthAdjustment, initialBytesToStrip,
failFast);
}
@Override
protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
if (in == null) {
return null;
}
if (in.readableBytes() < 8) {
return null;
}
Header header = new Header();
util.BytetoObj(header, in);
if (!in.isReadable(header.length)) {
in.resetReaderIndex();
return null;
}
// 读的过程中,readIndex的指针也在移动
MsgType msgType = MsgType.getMsgType(header.type);
if (msgType == null) {
// System.out.println("unKnownType:" + header.type);
in.readBytes(header.length).release();
in.discardReadBytes();
return null;
// throw new Exception("MsgType error: unKnownType");
}
int Length = header.length;
try {
CustomMsg customMsg = new CustomMsg(msgType, Length, in);
absCmd cmd = customMsg.CreateCmd();
if (cmd != null)
return cmd;
in.discardReadBytes();
} catch (Exception e) {
// TODO: handle exception
System.out.println(e.toString());
}
return null;
}
}
这个自定义解码器继承自LengthFieldBasedFrameDecoder (变长协议Decoder)
构造函数中的每个参数见描述
由于实际项目中的协议比较乱,所以干脆就全填0了,自己慢慢解析
util.BytetoObj(header, in);
这个是我自己写的一个反序列化函数,下一篇会详细介绍
最后返回的object是一个absCmd ,这个是解析协议的基类,根据不同msgType而执行不同子类的构造函数,并且子类均实现了某个执行方法
2.NettyClientHandler
package com.SocketLayer;
import com.Global.LogManager;
import com.Global.absCmd;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class NettyClientHandler extends SimpleChannelInboundHandler<Object> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
LogManager.writeLine(NettyClientHandler.class, ctx.channel().remoteAddress() + " Connected!");
// String sendInfo = "Client Connected!";
// ctx.writeAndFlush(Unpooled.copiedBuffer(sendInfo, CharsetUtil.UTF_8)); // 必须有flush
}
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println(ctx.channel().remoteAddress() + " Closed!" + ctx.channel().remoteAddress());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
// TODO Auto-generated method stub
absCmd cmd = (absCmd) msg;
if (cmd != null) {
cmd.Excute();
cmd.msg.buf.discardReadBytes();
}
}
}
这个没啥好说的,留意一下channelRead0
这里面的msg,正是上面自定义的解码器中返回的东西
另外两个方法就是用来知道什么时候连上服务端和断开服务端的
然后cmd.msg.buf.discardReadBytes();这一句是抛弃已读内容,防止writeIndex溢出
3.CustomEncoder
package com.SocketLayer;
import com.Global.util;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class CustomEncoder extends MessageToByteEncoder<Object> {
@Override
protected void encode(ChannelHandlerContext ctx, Object msg, ByteBuf out) throws Exception {
// TODO Auto-generated method stub
System.out.println(msg.getClass().getTypeName());
util.ObjtoBytes(msg, out);
}
}
这个继承自MessageToByteEncoder
其中
util.ObjtoBytes(msg, out);
是我自己写的一个序列化函数,下一篇幅会详细讲
这个编码器作用就是,当你要写点什么东西进去时,调用一个
ChannelFuture io.netty.channel.ChannelOutboundInvoker.writeAndFlush(Object msg)
这个方法,这个方法传入的msg就会传入Encoder中的Encode方法,在底下进一步处理
MessageToByteEncoder是一个泛型类,可以支持不同类型。但是鉴于实际使用中会比较复杂,不是只传某个数据包这么简单,所以想了想还是用object好了
同时,传入这个object后,我们要做的就是将其序列化,并且将其写入到ByteBuf中(提供的out)
4.心跳检测 IdleStateHandler+HearBeatHandler
ch.pipeline().addLast(new IdleStateHandler(0, 0, 30, TimeUnit.SECONDS));// 心跳检测:30秒内没有读写操作时触发
ch.pipeline().addLast(new HearBeatHandler());// 继承自ChannlInboundHandlerAdapter,当没有心跳时,触发userEventTrigered方法
HearBeatHandler:
package com.SocketLayer;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
public class HearBeatHandler extends ChannelInboundHandlerAdapter {
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// TODO Auto-generated method stub
if (evt instanceof IdleStateEvent) {
IdleStateEvent event = (IdleStateEvent) evt;
if (event.state() == IdleState.ALL_IDLE) {
ctx.channel().close();
} else {
super.userEventTriggered(ctx, evt);
}
}
}
}
心跳检测起作用时,进入的是我们自定义的HearBeatHandler (继承自ChannelInboundHandlerAdapter )的触发事件函数中
然后我们判断如果此时没有任何的读写操作,那么我们就将这个通道关闭(节省资源)
5.其他
直接贴代码:
package com.SocketLayer;
import java.net.InetSocketAddress;
import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
import com.Global.LogManager;
import com.Global.util;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;
public class NettyClient {
private static HashMap<String, NettyClient> ClientMap = new HashMap<String, NettyClient>();
public static void Start(final String localPoint) {
if (!ClientMap.containsKey(localPoint))
ClientMap.put(localPoint, new NettyClient());
util.CacheThreadPool.execute(new Runnable() {
public void run() {
// TODO Auto-generated method stub
LogManager.writeLine(NettyClient.class, "Client Connecting to" + localPoint);
boolean firstTime = true;
int retryTime = 0;
while (true) {
try {
if (!firstTime) {
TimeUnit.SECONDS.sleep(10);
LogManager.writeLine(NettyClient.class,
"reConnect to" + localPoint + " times:" + retryTime);
}
ClientMap.get(localPoint).start(localPoint);
retryTime = 0;
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
firstTime = false;
retryTime++;
}
}
});
}
public static void CloseAll() {
var Points = ClientMap.keySet();
Iterator<String> iterator = Points.iterator();
while (iterator.hasNext())
ClientMap.get(iterator.next()).cf.channel().close();
}
public static void Write(String localPoint, Object msg) {
ClientMap.get(localPoint).Write(msg);
}
private ChannelFuture cf;
private void Write(Object msg) {
cf.channel().writeAndFlush(msg);
}
private void start(String localPoint) throws Exception {
String[] str = localPoint.split(":");
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bs = new Bootstrap();
bs.group(group) // 注册线程池
.channel(NioSocketChannel.class) // 使用NioSocketChannel来作为连接用的channel类
.remoteAddress(new InetSocketAddress(str[0], Integer.parseInt(str[1]))) // 绑定连接端口和host信息
.handler(new ChannelInitializer<SocketChannel>() { // 绑定连接初始化器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(
new CustomDecoder(ByteOrder.LITTLE_ENDIAN, Integer.MAX_VALUE, 0, 0, 0, 0, false));
ch.pipeline().addLast(new NettyClientHandler());
ch.pipeline().addLast(new CustomEncoder());
ch.pipeline().addLast(new ChunkedWriteHandler());
ch.pipeline().addLast(new IdleStateHandler(0, 0, 30, TimeUnit.SECONDS));// 心跳检测:30秒内没有读写操作时触发
ch.pipeline().addLast(new HearBeatHandler());// 继承自ChannlInboundHandlerAdapter,当没有心跳时,触发userEventTrigered方法
}
});
cf = bs.connect().sync(); // 异步连接服务器
cf.channel().closeFuture().sync(); // 异步等待关闭连接channel
} finally {
group.shutdownGracefully().sync(); // 释放线程池资源
}
}
}
5.1当需要连接多个服务端时,对不同客户端进行统一管理
这里直接将每个服务端节点(IP:端口号)和NettyClient类本身进行映射放入HashMap中,并且每个客户端需要一个线程(不能放在同一个线程内,否则会阻塞互相影响)
这边我用的是一个线程池进行管理,在util类中
public static ExecutorService CacheThreadPool = Executors.newCachedThreadPool();
5.2当连接服务端连不上时,增加重连机制
这里在连接抛出异常时,等待一段时间后再继续重连
ClientMap.get(localPoint).start(localPoint);
执行start函数后,该线程会阻塞在此方法内(如果连接顺利的话)
5.3当需要连接多个服务端时,对不同客户端分别下发
并且通过开辟一个静态公共方法而实现对不同服务端执行写操作
cf.channel().writeAndFlush(msg);
下一篇:java踩雷系列3-通过反射实现 序列化,反序列化