上一篇:
首先先说一下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-通过反射实现 序列化,反序列化