Netty从入门到精通(六)

二次编解码(数据的序列化反序列化)

概念:

  • 我们把解决半包粘包问题的常用三种解码器叫一次解码器,其作用是将原始数据流(可能会出现粘包和半包的数据流) 转换为用户数据(ByteBuf中存储),但仍然是字节数据,所以我们需要二次解码器将字节数组转换为java对象,或者 将将一种格式转化为另一种格式,方便上层应用程序使用。
  • 一次解码器继承自:ByteToMessageDecoder;二次解码器继承自MessageToMessageDecoder;但他们的本质 都是继承ChannelInboundHandlerAdapter

 二次编解码方式:

  1. Java 序列化:不推荐使用,占用空间大,也只有java语言能用
  2. Marshaling:比java序列化稍好
  3. XML :可读性好,但是占用空间大
  4. JSON :可读性也好,空间较小
  5. MessagePack :占用空间比JSON小,可读性不如JSON,但也还行
  6. Protobuf :性能高,体积小,但是可读性差  hessian :跨语言、高效的二进制序列化协议,整体性能和protobuf差不多。
  7. 其他
常用的二次编解码器:

StringDecoder & StringEncoder
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
//添加编码器
//lengthFieldLength is 1,2,3,4,8
pipeline.addLast(new LengthFieldPrepender(2));
pipeline.addLast(new StringEncoder());
pipeline.addLast(new LengthFieldBasedFrameDecoder(65536,0,2,0,2));
pipeline.addLast(new StringDecoder());
//添加客户端channel对应的handler
pipeline.addLast(new ClientInboundHandler1());
pipeline.addLast(new ClientSimpleInboundHandler2());
}
StringDecoder和StringEncoder的使用,可以使handler中接收或者发送的的数据直接转化为String,不必在用bytebuf转化
@Override
public void channelRead(ChannelHandlerContext ctx,Object msg) throws Exception {
String message = (String) msg;
count++;
log.info("---服务端收到的第{}个数据:{}",count,message);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
log.info("ClientInboundHandler1 channelActive begin send data");
//批量发送数据
UserInfo userInfo;
for (int i=0;i<100;i++) {
userInfo = new UserInfo(i,"name"+i,i+1,(i%2==0) ? "男":"女","北京");
ctx.writeAndFlush(userInfo.toString());
 }
}

Protostuff编解码

编写 ProtostuffUtil 工具类

import io.protostuff.LinkedBuffer;
import io.protostuff.ProtostuffIOUtil;
import io.protostuff.Schema;
import io.protostuff.runtime.RuntimeSchema;
import lombok.extern.slf4j.Slf4j;

import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

/**
 * @description
 * @author: ts
 * @create:2021-04-08 10:31
 */
@Slf4j
public class ProtostuffUtil {

	//存储因为无法直接序列化/反序列化 而需要被包装的类型Class
	private static final Set<Class<?>> WRAPPER_SET = new HashSet<Class<?>>();

	static {
		WRAPPER_SET.add(List.class);
		WRAPPER_SET.add(ArrayList.class);
		WRAPPER_SET.add(CopyOnWriteArrayList.class);
		WRAPPER_SET.add(LinkedList.class);
		WRAPPER_SET.add(Stack.class);
		WRAPPER_SET.add(Vector.class);
		WRAPPER_SET.add(Map.class);
		WRAPPER_SET.add(HashMap.class);
		WRAPPER_SET.add(TreeMap.class);
		WRAPPER_SET.add(LinkedHashMap.class);
		WRAPPER_SET.add(Hashtable.class);
		WRAPPER_SET.add(SortedMap.class);
		WRAPPER_SET.add(Object.class);
	}

	//注册需要使用包装类进行序列化的Class对象
	public static void registerWrapperClass(Class<?> clazz) {
		WRAPPER_SET.add(clazz);
	}

	/**
	 * 将对象序列化为字节数组
	 * @param t
	 * @param useWrapper 为true完全使用包装模式 为false则选择性的使用包装模式
	 * @param <T>
	 * @return
	 */
	public static <T> byte[] serialize(T t,boolean useWrapper) {
		Object serializerObj = t;
		if (useWrapper) {
			serializerObj = SerializeDeserializeWrapper.build(t);
		}
		return serialize(serializerObj);
	}

	/**
	 * 将对象序列化为字节数组
	 * @param t
	 * @param <T>
	 * @return
	 */
	public static <T> byte[] serialize(T t) {
		//获取序列化对象的class
		Class<T> clazz = (Class<T>) t.getClass();
		Object serializerObj = t;
		if (WRAPPER_SET.contains(clazz)) {
			serializerObj = SerializeDeserializeWrapper.build(t);//将原始序列化对象进行包装
		}
		return doSerialize(serializerObj);
	}


	/**
	 * 执行序列化
	 * @param t
	 * @param <T>
	 * @return
	 */
	public static <T> byte[] doSerialize(T t) {
		//获取序列化对象的class
		Class<T> clazz = (Class<T>) t.getClass();
		//获取Schema
		// RuntimeSchema<T> schema = RuntimeSchema.createFrom(clazz);//根据给定的class创建schema
		/**
		 * this is lazily created and cached by RuntimeSchema
		 * so its safe to call RuntimeSchema.getSchema() over and over The getSchema method is also thread-safe
		 */
		Schema<T> schema = RuntimeSchema.getSchema(clazz);//内部有缓存机制
		/**
		 * Re-use (manage) this buffer to avoid allocating on every serialization
		 */
		LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
		byte[] protostuff = null;
		try {
			protostuff = ProtostuffIOUtil.toByteArray(t, schema, buffer);
		} catch (Exception e){
			//log.error("protostuff serialize error,{}",e.getMessage());
		}finally {
			buffer.clear();
		}
		return protostuff;
	}


	/**
	 * 反序列化
	 * @param data
	 * @param clazz
	 * @param <T>
	 * @return
	 */
	public static <T> T deserialize(byte[] data,Class<T> clazz) {
		//判断是否经过包装
		if (WRAPPER_SET.contains(clazz)) {
			SerializeDeserializeWrapper<T> wrapper = new SerializeDeserializeWrapper<T>();
			ProtostuffIOUtil.mergeFrom(data,wrapper,RuntimeSchema.getSchema(SerializeDeserializeWrapper.class));
			return wrapper.getData();
		}else {
			Schema<T> schema = RuntimeSchema.getSchema(clazz);
			T newMessage = schema.newMessage();
			ProtostuffIOUtil.mergeFrom(data,newMessage,schema);
			return newMessage;
		}
	}


	private static class SerializeDeserializeWrapper<T> {
		//被包装的数据
		T data;

		public static <T> SerializeDeserializeWrapper<T> build(T data){
			SerializeDeserializeWrapper<T> wrapper = new SerializeDeserializeWrapper<T>();
			wrapper.setData(data);
			return wrapper;
		}

		public T getData() {
			return data;
		}

		public void setData(T data) {
			this.data = data;
		}
	}
}
2 、编写编码器和解码器
import com.itheima.netty.pojo.UserInfo;
import com.itheima.netty.util.ProtostuffUtil;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageEncoder;
import lombok.extern.slf4j.Slf4j;

import java.util.List;

@Slf4j
public class ProtoStuffEncoder extends MessageToMessageEncoder<UserInfo> {

    @Override
    protected void encode(ChannelHandlerContext ctx, UserInfo msg, List<Object> out) throws Exception {
        try {
            byte[] bytes = ProtostuffUtil.serialize(msg);
            ByteBuf buffer = ctx.alloc().buffer(bytes.length);
            buffer.writeBytes(bytes);
            out.add(buffer);
        } catch (Exception e) {
            //log.error("protostuff encode error,msg={}",e.getMessage());
            throw new RuntimeException(e);
        }
    }
}
import com.itheima.netty.pojo.UserInfo;
import com.itheima.netty.util.ProtostuffUtil;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToMessageDecoder;
import lombok.extern.slf4j.Slf4j;

import java.util.List;

@Slf4j
public class ProtoStuffDecoder extends MessageToMessageDecoder<ByteBuf> {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf msg, List<Object> out) throws Exception {
        try {
            int length = msg.readableBytes();
            byte[] bytes = new byte[length];
            msg.readBytes(bytes);
            UserInfo userInfo = ProtostuffUtil.deserialize(bytes, UserInfo.class);
            out.add(userInfo);
        } catch (Exception e) {
            //log.error("protostuff decode error,msg={}",e.getMessage());
            throw new RuntimeException(e);
        }
    }
}

服务端handler直接用对象接收

public class TcpStickHalfHandler1 extendsChannelInboundHandlerAdapter {
private static final Logger log =LoggerFactory.getLogger(TcpStickHalfHandler1.class);
int count =0;
@Override
public void channelRead(ChannelHandlerContext ctx,Object msg) throws Exception {
UserInfo userInfo = (UserInfo) msg;//直接用UserInfo接收
count++;
log.info("---服务端收到的第{}个数据:{}",count,userInfo);
 }
}

客户端handler直接发送对象

@Override
public void channelActive(ChannelHandlerContext ctx)throws Exception {
log.info("ClientInboundHandler1 channelActive beginsend data");
//批量发送数据
UserInfo userInfo;
  for (int i=0;i<100;i++) {
    userInfo = new UserInfo(i,"name"+i,i+1,(i%2==0) ? "男":"女","北京");
    ctx.writeAndFlush(userInfo);
   }
}
服务端添加对应的编解码器
protected void initChannel(SocketChannel ch) throws Exception {
//获取与该channel绑定的pipeline
ChannelPipeline pipeline = ch.pipeline();
//测试tcp stick or half pack
pipeline.addLast(new LengthFieldPrepender(2));
pipeline.addLast(new ProtostuffEncoder());
pipeline.addLast(new LengthFieldBasedFrameDecoder(65536,0,2,0,2));
pipeline.addLast(new ProtostuffDecoder());
pipeline.addLast(new TcpStickHalfHandler1());
}

Netty-HTTP 编解码
HTTP服务器在我们日常开发中,常见的实现方式就是实现一个Java Web项目,基Nginx+Tomcat的方式就可以提供HTTP服务。 但是很多场景是非Web容器的场景,这个时候再使用Tomcat就大材小用了。这个时候就可以使用基于Netty的HTTP协议。而且基 于Netty开发的HTTP服务器有如下优势:
Ø Netty的线程模型和异步非阻塞特性能够支持高并发
Ø 相比于Tomcat HTTP,Netty HTTP更加轻量、小巧、可靠,占用资源更少

Keepalive 与 idle监测

TCP Keepalive

Ø net.ipv4.tcp_keepalive_time = 7200
Ø net.ipv4.tcp_keepalive_intvl = 75
Ø net.ipv4.tcp_keepalive_probes = 9
Ø 当启用(默认关闭)keepalive 时,TCP 在连接没有数据通过的7200秒后 发送keepalive 探测消息,当探测没有确认时,按75秒的重试频率重发,一直发9 个探测包都没有确认,就认定连接失效。  所以总耗时一般为:2 小时11 分钟(7200 秒+ 75 秒* 9 次)

应用层 Keepalive

除了在tcp网络层开启keepalive之外,我们普遍还需要在应用层启动keepalive,一般称之为:应用心跳(心跳机制 ),原因如下:

1、协议分层,各层关注点不同,网络传输层关注网络是否可达,应用层关注是否能正常提供服务

2、tcp的keepalive默认关闭,并且经过路由等中转设备后keepalive包有可能被丢弃

3、tcp层的keepalive时间太长,默认>2小时,虽然可改,但是属于系统参数一旦改动影响该机器上的所有应用

另外需要注意:
http虽然属于应用层协议,因此会经常听到 HTTP 的头信息:Connection: Keep-Alive,HTTP/1.1 默认使用Connection:keep-alive进行长连接。在一次 TCP 连接中可以完成多个 HTTP 请求,但是对每个请求仍然 要单独发 header,Keep-Alive不会永久保持连接,它有一个保持时间,可以在不同的服务器软件(如Apache)中 设定这个时间。这种长连接是一种“伪链接”,而且只能由客户端发送请求,服务端响应。 HTTP协议的长连接和短连接,实质上是TCP协议的长连接和短连接

idle监测
Idle 监测,只是负责诊断,诊断后,做出不同的行为,决定Idle 监测的最终用途,一般用来配合keepalive ,减少 keepalive 消息
idle配合keepalive的发展阶段
1:定时keepalive 消息,keepalive 消息与服务器正常消息交换完全不关联,定时就发送;
2:空闲监测+ 判定为Idle 时才发keepalive,有其他数据传输的时候,不发送keepalive ,无数据传输超过一定 时间,判定为Idle,再发keepalive
作用:
  1. 快速释放损坏的、恶意的、很久不用的连接,让系统时刻保持最好的状态
  2. 实际应用中:结合起来使用。按需keepalive ,保证不会空闲,如果空闲,关闭连接

案例:

请服务器添加read idle check,10s接收不到channel数据就断掉连接,保护自己,瘦身。客户端添加write idle check + keepalive,5s不发送数据就发送一个keepalive,避免连接被断,也避免频繁
keepalive

服务端编写用于idle监测的handler

public class ServerReaderIdleHandler extends IdleStateHandler {
    public ServerReaderIdleHandler() {
        super(10, 0, 0, TimeUnit.SECONDS);
    }

    @Override
    protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
        super.channelIdle(ctx, evt);
        if (evt == IdleStateEvent.FIRST_READER_IDLE_STATE_EVENT) {
            //ctx.channel().close();
            ctx.close();
        }

    }
}

服务端配置handler

pipeline.addLast("readerIdleHandler",new ServerReaderIdleHandler());

客户端编写用于idle监测的handler

public class ClientWriterIdleHandler extends IdleStateHandler {
    public ClientWriterIdleHandler() {
        super(0, 5, 0, TimeUnit.SECONDS);
    }

    @Override
    protected void channelIdle(ChannelHandlerContext ctx, IdleStateEvent evt) throws Exception {
        super.channelIdle(ctx, evt);
        if (evt == IdleStateEvent.FIRST_WRITER_IDLE_STATE_EVENT) {
            //写心跳数据
            UserInfo keepalive = new UserInfo();
            keepalive.setName("this is a keepalive");
            ctx.channel().writeAndFlush(keepalive);
        }
    }
}
pipeline.addLast(new ClientWriterIdleHandler());

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值