《序列化与自定义Request、Response编解码器》

序列化是如何实现的

对象的序列化主要有两种用途:

1) 把对象的字节序列永久地保存到硬盘上,通常存放在一个文件中;

2) 在网络上传送对象的字节序列。

  在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有 10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到硬盘中,等要用了,再把保存在硬盘中的对象还原到内存中。
  当两个进程在进行远程通信时,彼此可以发送各种类型的数据。无论是何种类型的数据,都会以二进制序列的形式在网络上传送。发送方需要把这个Java对象转换为字节序列,才能在网络上传送;接收方则需要把字节序列再恢复为Java对象。
  java序列化是java编解码技术中的一种,不同的编解码的优缺点,参考之前的博客。https://blog.csdn.net/weixin_41262453/article/details/88980701

序列化三种底层实现方式

使用JDK的ByteArrayOutputStream序列化(需要自己手写大小端转字节序列的函数)

  ByteArrayOutputStream类实现了将数据写入字节数组的输出流当数据写入缓冲区时,缓冲区会自动增长。ByteArrayOutputStream.write(int b) 将指定的字节写入此字节数组输出流。 此方式需要自己手写大小端转字节序列的函数,麻烦。

public class Test1 {
	public static void main(String[] args) throws IOException {
		int id = 101;
		int age = 21;
		
		ByteArrayOutputStream arrayOutputStream = new ByteArrayOutputStream();
		arrayOutputStream.write(int2bytes(id));
		arrayOutputStream.write(int2bytes(age));
		
		byte[] byteArray = arrayOutputStream.toByteArray();
		System.out.println(Arrays.toString(byteArray));
		
		ByteArrayInputStream arrayInputStream = new ByteArrayInputStream(byteArray);
		byte[] idBytes = new byte[4];
		arrayInputStream.read(idBytes);
		System.out.println("id:" + bytes2int(idBytes));
		
		byte[] ageBytes = new byte[4];
		arrayInputStream.read(ageBytes);
		System.out.println("age:" + bytes2int(ageBytes));
		
	}	
	/**
	 * 大端字节序列(先写高位,再写低位)
	 * 百度下 大小端字节序列
	 * @param i
	 * @return
	 */
	public static byte[] int2bytes(int i){
		byte[] bytes = new byte[4];
		bytes[0] = (byte)(i >> 3*8);
		bytes[1] = (byte)(i >> 2*8);
		bytes[2] = (byte)(i >> 1*8);
		bytes[3] = (byte)(i >> 0*8);
		return bytes;
	}	
	/**
	 * 大端
	 * @param bytes
	 * @return
	 */
	public static int bytes2int(byte[] bytes){
		return (bytes[0] << 3*8) |
				(bytes[1] << 2*8) |
				(bytes[2] << 1*8) |
				(bytes[3] << 0*8);
	}
}

运行结果:
                在这里插入图片描述

用NIO中的Bytebuf(不能自动扩容)

  使用NIO中的字节缓冲区类Bytebuf,提供了许多序列化方法。使用方便,但是在使用时要定义大小,不能自动扩容。

public class Test2 {

	public static void main(String[] args) {
		int id = 101;
		int age = 21;
		
		ByteBuffer buffer = ByteBuffer.allocate(8);
		buffer.putInt(id);
		buffer.putInt(age);
		byte[] array = buffer.array();
		System.out.println(Arrays.toString(buffer.array()));
		
		//反序列化
		ByteBuffer buffer2 = ByteBuffer.wrap(array);
		System.out.println("id:"+buffer2.getInt());
		System.out.println("age:"+buffer2.getInt());
	}
}

运行结果:
           在这里插入图片描述

使用Netty中的ChannelBuffer

  使用时需要导入Netty的jar包。

public class Test3 {
	public static void main(String[] args) {

		ChannelBuffer buffer = ChannelBuffers.dynamicBuffer();
		buffer.writeInt(101);
		buffer.writeDouble(80.1);

		byte[] bytes = new byte[buffer.writerIndex()];
		buffer.readBytes(bytes);
		
		System.out.println(Arrays.toString(bytes));
		
		"abc".getBytes();
		
		//=====================反序列化===========================
		ChannelBuffer wrappedBuffer = ChannelBuffers.wrappedBuffer(bytes);
		System.out.println(wrappedBuffer.readInt());
		System.out.println(wrappedBuffer.readDouble());		
	}
}

运行结果:
          在这里插入图片描述

序列化对象

使用继承Serializer抽象类

  Serializer底层的实现就是两个ChannelBuffer, writeBuffer和readBuffer。
  把对象转换为字节序列的过程称为对象的序列化;把字节序列恢复为对象的过程称为对象的反序列化。需要序列化的两个类:Player 和Resource ,继承Serializer抽象类要做的事情就是重写write和read方法。

public class Player extends Serializer{
	
	private long playerId;
	
	private int age;
	
	private List<Integer> skills = new ArrayList<>();
	
	private Resource resource = new Resource();
	public Resource getResource() {
		return resource;
	}
	public void setResource(Resource resource) {
		this.resource = resource;
	}
	public long getPlayerId() {
		return playerId;
	}
	public void setPlayerId(long playerId) {
		this.playerId = playerId;
	}
	public int getAge() {
		return age;
	}
	public void setAge(int age) {
		this.age = age;
	}
	public List<Integer> getSkills() {
		return skills;
	}
	public void setSkills(List<Integer> skills) {
		this.skills = skills;
	}
	@Override
	protected void read() {
		this.playerId = readLong();
		this.age = readInt();
		this.skills = readList(Integer.class);
		this.resource = read(Resource.class);
	}
	@Override
	protected void write() {
		writeLong(playerId);
		writeInt(age);
		writeList(skills);
		writeObject(resource);
	}
}
public class Resource extends Serializer {
	
	private int gold;
	

	public int getGold() {
		return gold;
	}

	public void setGold(int gold) {
		this.gold = gold;
	}

	@Override
	protected void read() {
		this.gold = readInt();
	}

	@Override
	protected void write() {
		writeInt(gold);
	}
}

测试序列化对象:

public class Test4 {

	public static void main(String[] args) {
		
		Player player = new Player();
		player.setPlayerId(10001);
		player.setAge(22);
		player.getSkills().add(101);
		player.getResource().setGold(99999);
		
		byte[] bytes = player.getBytes();
		
		System.out.println(Arrays.toString(bytes));
		
		//==============================================
		
		Player player2 = new Player();
		player2.readFromBytes(bytes);
		System.out.println(player2.getPlayerId() + "   "+player2.getAge() + "     "+ Arrays.toString(player2.getSkills().toArray())+"   " +player2.getResource().getGold());

	}
}

运行结果:
在这里插入图片描述

采用Protobuf序列化框架

  Protobuf系列化操作参考https://blog.csdn.net/weixin_41262453/article/details/88980701#Google_Protobuf_451 ,将上面的player序列化,进行测试。
  player.proto配置文件参考
              在这里插入图片描述
序列化后得到的PlayerModule有1000多行就不放了,测试代码如下:

import java.util.Arrays;
import com.proto.PlayerModule.PBPlayer;
import com.proto.PlayerModule.PBPlayer.Builder;
public class PB2Bytes {
	public static void main(String[] args) throws Exception {
		byte[] bytes = toBytes();
		toPlayer(bytes);

	}
	/**
	 * 序列化
	 */
	public static byte[] toBytes(){
		//获取一个PBPlayer的构造器
		Builder builder = PlayerModule.PBPlayer.newBuilder();
		//设置数据
		builder.setPlayerId(101).setAge(20).setName("peter").addSkills(1001);
		//构造出对象
		PBPlayer player = builder.build();
		//序列化成字节数组
		byte[] byteArray = player.toByteArray();
		
		System.out.println(Arrays.toString(byteArray));
		
		return byteArray;
	}	
	/**
	 * 反序列化
	 * @param bs
	 * @throws Exception 
	 */
	public static void toPlayer(byte[] bs) throws Exception{
		
		 PBPlayer player = PlayerModule.PBPlayer.parseFrom(bs);
		 
		 System.out.println("playerId:" + player.getPlayerId());
		 System.out.println("age:" + player.getAge());
		 System.out.println("name:" + player.getName());
		 System.out.println("skills:" + (Arrays.toString(player.getSkillsList().toArray())));
	}
}

运行结果:
        在这里插入图片描述

序列化协议在编解码器中的应用

  此处提供了一个自定义的编解码器用于传输我们已经序列化的对象,可用于了解常见通信框架的编解码器底层序列化工作原理。

如何自定义写一个编解码器应用在数据传输中

先实现数据包

  首先设计数据包的格式:

 * 数据包格式
 * +——----——+——-----——+——----——+——----——+——-----——+
 * | 包头	| 模块号  | 命令号 |  长度  |   数据  |
 * +——----——+——-----——+——----——+——----——+——-----——+
 * </pre>
 * 包头4字节
 * 模块号2字节short
 * 命令号2字节short
 * 长度4字节(描述数据部分字节长度)

  因此我们要定义一个包头,首先采用一个不常用的4字节数据作为包头:

public interface ConstantValue {	
	/**
	 * 包头
	 */
	public static final int FLAG = -32523523;

}

  其次定义请求,请求中包括请求模块、命令号、数据

public class Request {	
	/**
	 * 请求模块
	 */
	private short module;	
	/**
	 * 命令号
	 */
	private short cmd;	
	/**
	 * 数据部分
	 */
	private byte[] data;
	public short getModule() {
		return module;
	}
	public void setModule(short module) {
		this.module = module;
	}
	public short getCmd() {
		return cmd;
	}
	public void setCmd(short cmd) {
		this.cmd = cmd;
	}
	public byte[] getData() {
		return data;
	}
	public void setData(byte[] data) {
		this.data = data;
	}
	public int getDataLength(){
		if(data == null){
			return 0;
		}
		return data.length;
	}
}

响应的有响应:Response

public class Response {
	/**
	 * 请求模块
	 */
	private short module;	
	/**
	 * 命令号
	 */
	private short cmd;	
	/**
	 * 状态码
	 */
	private int stateCode;	
	/**
	 * 数据部分
	 */
	private byte[] data;
	public short getModule() {
		return module;
	}
	public void setModule(short module) {
		this.module = module;
	}
	public short getCmd() {
		return cmd;
	}
	public void setCmd(short cmd) {
		this.cmd = cmd;
	}
	public int getStateCode() {
		return stateCode;
	}
	public void setStateCode(int stateCode) {
		this.stateCode = stateCode;
	}
	public byte[] getData() {
		return data;
	}
	public void setData(byte[] data) {
		this.data = data;
	}
	public int getDataLength(){
		if(data == null){
			return 0;
		}
		return data.length;
	}
}

返回的状态字:

public interface StateCode {	
	/**
	 * 成功
	 */
	public static int SUCCESS  = 0;
	
	/**
	 * 失败
	 */
	public static int FAIL  =  1;
}

Request的编解码器

RequestEncoder

  RequestEncoder首先需要继承Netty3.10.5中OneToOneEncoder,RequestEncoder将请求Request对象实现序列化到ChannelBuffer ,返回ChannelBuffer 缓存区buffer。

/**
 * 请求编码器
 * <pre>
 * 数据包格式
 * +——----——+——-----——+——----——+——----——+——-----——+
 * | 包头          | 模块号        | 命令号      |  长度        |   数据       |
 * +——----——+——-----——+——----——+——----——+——-----——+
 * </pre>
 * 包头4字节
 * 模块号2字节short
 * 命令号2字节short
 * 长度4字节(描述数据部分字节长度)
 */
public class RequestEncoder extends OneToOneEncoder{
	@Override
	protected Object encode(ChannelHandlerContext context, Channel channel, Object rs) throws Exception {
		Request request = (Request)(rs);		
		ChannelBuffer buffer = ChannelBuffers.dynamicBuffer();
		//包头
		buffer.writeInt(ConstantValue.FLAG);
		//module
		buffer.writeShort(request.getModule());
		//cmd
		buffer.writeShort(request.getCmd());
		//长度
		buffer.writeInt(request.getDataLength());
		//data
		if(request.getData() != null){
			buffer.writeBytes(request.getData());
		}		
		return buffer;
	}
}

RequestDecoder

  RequestDecoder同样要继承Netty3.10.5中FrameDecoder,Request解码器要做的事情就是从接收到的字节流缓存区读取出数据,进行判断,若包头正确,可读长度大于基本长度,则将字节流依次读取出Request类的各个成员变量,返回Request对象。

/**
 * 请求解码器
 * <pre>
 * 数据包格式
 * +——----——+——-----——+——----——+——----——+——-----——+
 * | 包头          | 模块号        | 命令号      |  长度        |   数据       |
 * +——----——+——-----——+——----——+——----——+——-----——+
 * </pre>
 * 包头4字节
 * 模块号2字节short
 * 命令号2字节short
 * 长度4字节(描述数据部分字节长度)
 * 
 *
 */
public class RequestDecoder extends FrameDecoder{	
	/**
	 * 数据包基本长度
	 */
	public static int BASE_LENTH = 4 + 2 + 2 + 4;
	@Override
	protected Object decode(ChannelHandlerContext arg0, Channel arg1, ChannelBuffer buffer) throws Exception {		
		//可读长度必须大于基本长度
		if(buffer.readableBytes() >= BASE_LENTH){
			//防止socket字节流攻击
			if(buffer.readableBytes() > 2048){
				buffer.skipBytes(buffer.readableBytes());
			}			
			//记录包头开始的index
			int beginReader;
			
			while(true){
				beginReader = buffer.readerIndex();
				buffer.markReaderIndex();
				if(buffer.readInt() == ConstantValue.FLAG){
					break;
				}				
				//未读到包头,略过一个字节
				buffer.resetReaderIndex();
				buffer.readByte();
				
				//长度又变得不满足
				if(buffer.readableBytes() < BASE_LENTH){
					return null;
				}
			}			
			//模块号
			short module = buffer.readShort();
			//命令号
			short cmd = buffer.readShort();
			//长度
			int length = buffer.readInt();
			
			//判断请求数据包数据是否到齐
			if(buffer.readableBytes() < length){
				//还原读指针
				buffer.readerIndex(beginReader);
				return null;
			}			
			//读取data数据
			byte[] data = new byte[length];
			buffer.readBytes(data);			
			Request request = new Request();
			request.setModule(module);
			request.setCmd(cmd);
			request.setData(data);			
			//继续往下传递 
			return request;			
		}
		//数据包不完整,需要等待后面的包来
		return null;
	}
}

response编解码器

ResponseEncoder

  ResponseEncoder与RequestEncoder实现原理类似。

/**
 * 请求编码器
 * <pre>
 * 数据包格式
 * +——----——+——-----——+——----——+——----——+——-----——+——-----——+
 * | 包头          | 模块号        | 命令号       |  状态码    |  长度          |   数据       |
 * +——----——+——-----——+——----——+——----——+——-----——+——-----——+
 * </pre>
 * 包头4字节
 * 模块号2字节short
 * 命令号2字节short
 * 长度4字节(描述数据部分字节长度)
 */
public class ResponseEncoder extends OneToOneEncoder{
	@Override
	protected Object encode(ChannelHandlerContext context, Channel channel, Object rs) throws Exception {
		Response response = (Response)(rs);
		
		ChannelBuffer buffer = ChannelBuffers.dynamicBuffer();
		//包头
		buffer.writeInt(ConstantValue.FLAG);
		//module
		buffer.writeShort(response.getModule());
		//cmd
		buffer.writeShort(response.getCmd());
		//状态码
		buffer.writeInt(response.getStateCode());
		//长度
		buffer.writeInt(response.getDataLength());
		//data
		if(response.getData() != null){
			buffer.writeBytes(response.getData());
		}	
		return buffer;
	}
}

ResponseDecoder

  ResponseDecoder相比RequestDecoder,还会多解析一个状态码。

/**
 * response解码器
 * <pre>
 * 数据包格式
 * +——----——+——-----——+——----——+——----——+——-----——+——-----——+
 * | 包头          | 模块号        | 命令号       |  状态码    |  长度          |   数据       |
 * +——----——+——-----——+——----——+——----——+——-----——+——-----——+
 * </pre>
 * 包头4字节
 * 模块号2字节short
 * 命令号2字节short
 * 长度4字节(描述数据部分字节长度)
 */
public class ResponseDecoder extends FrameDecoder{	
	/**
	 * 数据包基本长度
	 */
	public static int BASE_LENTH = 4 + 2 + 2 + 4;
	@Override
	protected Object decode(ChannelHandlerContext arg0, Channel arg1, ChannelBuffer buffer) throws Exception {		
		//可读长度必须大于基本长度
		if(buffer.readableBytes() >= BASE_LENTH){
		
			//记录包头开始的index
			int beginReader = buffer.readerIndex();
			
			while(true){
				if(buffer.readInt() == ConstantValue.FLAG){
					break;
				}
			}
			//模块号
			short module = buffer.readShort();
			//命令号
			short cmd = buffer.readShort();
			//状态码
			int stateCode = buffer.readInt();
			//长度
			int length = buffer.readInt();
			
			if(buffer.readableBytes() < length){
				//还原读指针
				buffer.readerIndex(beginReader);
				return null;
			}			
			byte[] data = new byte[length];
			buffer.readBytes(data);			
			Response response = new Response();
			response.setModule(module);
			response.setCmd(cmd);
			response.setStateCode(stateCode);
			response.setData(data);			
			//继续往下传递 
			return response;			
		}
		//数据包不完整,需要等待后面的包来
		return null;
	}
}

测试自定义编解码器

  发送的对象模型:FightRequest

public class FightRequest extends Serializer{	
	/**
	 * 副本id
	 */
	private int fubenId;	
	/**
	 * 次数
	 */
	private int count;

	public int getFubenId() {
		return fubenId;
	}
	public void setFubenId(int fubenId) {
		this.fubenId = fubenId;
	}
	public int getCount() {
		return count;
	}
	public void setCount(int count) {
		this.count = count;
	}
	@Override
	protected void read() {
		this.fubenId = readInt();
		this.count = readInt();
	}
	@Override
	protected void write() {
		writeInt(fubenId);
		writeInt(count);
	}
}

  返回的对象模型:

public class FightResponse extends Serializer{
	/**
	 * 获取金币
	 */
	private int gold;
	public int getGold() {
		return gold;
	}
	public void setGold(int gold) {
		this.gold = gold;
	}
	@Override
	protected void read() {
		this.gold = readInt();
	}
	@Override
	protected void write() {
		writeInt(gold);
	}
}

  测试代码:服务端Client和HiHandler ,由于Request和Response的Encoder和Decoder都是继承了Netty中的类进行设计的,所以可以直接pipeline.addLast,因此兼容原有的Netty框架。并且在channel.write(request)时也是直接write request对象,request对象中的data就是需要发送的fightRequest模型的序列化字节码。

public class Client {
	public static void main(String[] args) throws InterruptedException {		
		//服务类
		ClientBootstrap bootstrap = new  ClientBootstrap();
		
		//线程池
		ExecutorService boss = Executors.newCachedThreadPool();
		ExecutorService worker = Executors.newCachedThreadPool();		
		//socket工厂
		bootstrap.setFactory(new NioClientSocketChannelFactory(boss, worker));
		
		//管道工厂
		bootstrap.setPipelineFactory(new ChannelPipelineFactory() {			
			@Override
			public ChannelPipeline getPipeline() throws Exception {
				ChannelPipeline pipeline = Channels.pipeline();
				pipeline.addLast("decoder", new ResponseDecoder());
				pipeline.addLast("encoder", new RequestEncoder());
				pipeline.addLast("hiHandler", new HiHandler());
				return pipeline;
			}
		});		
		//连接服务端
		ChannelFuture connect = bootstrap.connect(new InetSocketAddress("127.0.0.1", 30000));
		Channel channel = connect.sync().getChannel();		
		System.out.println("client start");		
		Scanner scanner = new Scanner(System.in);
		while(true){
			System.out.println("请输入");
			int fubenId = Integer.parseInt(scanner.nextLine());
			int count = Integer.parseInt(scanner.nextLine());
			
			FightRequest fightRequest = new FightRequest();
			fightRequest.setFubenId(fubenId);
			fightRequest.setCount(count);
			
			Request request = new Request();
			request.setModule((short) 1);
			request.setCmd((short) 1);
			request.setData(fightRequest.getBytes());
			//发送请求
			channel.write(request);
		}
	}
}

public class HiHandler extends SimpleChannelHandler {
	/**
	 * 接收消息
	 */
	@Override
	public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
			Response message = (Response)e.getMessage();

			if(message.getModule() == 1){
				
				if(message.getCmd() == 1){
					FightResponse fightResponse = new FightResponse();
					fightResponse.readFromBytes(message.getData());
					
					System.out.println("gold:" + fightResponse.getGold());
					
				}else if(message.getCmd() == 2){
					
				}
				
			}else if (message.getModule() == 1){
				
				
			}
	}
	/**
	 * 捕获异常
	 */
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
		System.out.println("exceptionCaught");
		super.exceptionCaught(ctx, e);
	}
	/**
	 * 新连接
	 */
	@Override
	public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
		System.out.println("channelConnected");
		super.channelConnected(ctx, e);
	}
	/**
	 * 必须是链接已经建立,关闭通道的时候才会触发
	 */
	@Override
	public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
		System.out.println("channelDisconnected");
		super.channelDisconnected(ctx, e);
	}
	/**
	 * channel关闭的时候触发
	 */
	@Override
	public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
		System.out.println("channelClosed");
		super.channelClosed(ctx, e);
	}
}

  客户端Server 和HelloHandler:messageReceived中接收的消息此时就是Request对象,从Request对象的data取出的数据是序列化的fightRequest 字节码,要再经过该类中的readFromBytes方法转换,才能完全转成发送过来的fightRequest 。

public class Server {
	public static void main(String[] args) {
		//服务类
		ServerBootstrap bootstrap = new ServerBootstrap();		
		//boss线程监听端口,worker线程负责数据读写
		ExecutorService boss = Executors.newCachedThreadPool();
		ExecutorService worker = Executors.newCachedThreadPool();		
		//设置niosocket工厂
		bootstrap.setFactory(new NioServerSocketChannelFactory(boss, worker));		
		//设置管道的工厂
		bootstrap.setPipelineFactory(new ChannelPipelineFactory() {
			
			@Override
			public ChannelPipeline getPipeline() throws Exception {

				ChannelPipeline pipeline = Channels.pipeline();
				pipeline.addLast("decoder", new RequestDecoder());
				pipeline.addLast("encoder", new ResponseEncoder());
				pipeline.addLast("helloHandler", new HelloHandler());
				return pipeline;
			}
		});		
		bootstrap.bind(new InetSocketAddress(30000));		
		System.out.println("start!!!");		
	}
}

public class HelloHandler extends SimpleChannelHandler {

	/**
	 * 接收消息
	 */
	@Override
	public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
		Request message = (Request)e.getMessage();		
		if(message.getModule() == 1){
				if(message.getCmd() == 1){			
				FightRequest fightRequest = new FightRequest();
				fightRequest.readFromBytes(message.getData());
				
				System.out.println("fubenId:" +fightRequest.getFubenId() + "   " + "count:" + fightRequest.getCount());
				
				//回写数据
				FightResponse fightResponse = new FightResponse();
				fightResponse.setGold(9999);
				
				Response response = new Response();
				response.setModule((short) 1);
				response.setCmd((short) 1);
				response.setStateCode(StateCode.SUCCESS);
				response.setData(fightResponse.getBytes());
				ctx.getChannel().write(response);
			}else if(message.getCmd() == 2){				
			}			
		}else if (message.getModule() == 1){
						
		}
	}
	/**
	 * 捕获异常
	 */
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
		System.out.println("exceptionCaught");
		super.exceptionCaught(ctx, e);
	}
	/**
	 * 新连接
	 */
	@Override
	public void channelConnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
		System.out.println("channelConnected");
		super.channelConnected(ctx, e);
	}
	/**
	 * 必须是链接已经建立,关闭通道的时候才会触发
	 */
	@Override
	public void channelDisconnected(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
		System.out.println("channelDisconnected");
		super.channelDisconnected(ctx, e);
	}
	/**
	 * channel关闭的时候触发
	 */
	@Override
	public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
		System.out.println("channelClosed");
		super.channelClosed(ctx, e);
	}
}

运行测试:
              在这里插入图片描述
服务器端:
              在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_41262453/article/details/89052414