Netty

用户指南
Netty实战精髓篇
彻底理解Netty,这一篇文章就够了

常见的网络通信框架?
Netty(JBoss)
Mina(Apach)
Grizzly(sum)

前言。BIO和NIO的通信流程区别

中间多插入一个多路复用器

BIO:服务端——线程——socket——客户端           (有几个客户端就有几个线程)
NIO:服务端——线程——selector——socket——客户端 (一个线程,一个多路复用器进行轮询)

一。Netty是什么,由哪几个部分构成?

Netty是一个网络通信框架,是在NIO的基础上作封装。

主要核心组件
Channel,                  数据的输入输出,相当于事件
EventLoop                  监听和响应channel的事件(相当于线程); EventLoopGroup用来生成和管理eventLoop,(相当于线程池)    事件监听
ChannelHandler             监听到事件后你要做什么                                                                         事件处理
ChannelPip顺序
ChannelHandlerContext      数据连接,连接事件处理和事件处理顺序

channel
回调和future
事件和ChannelHandler  里面有三个方法 handlerAdded   handlerRemoved  exceptionCaught

二。应用


/*指明我这个handler可以在多个channel之间共享,意味这个实现必须线程安全的。*/
1.注解 @ChannelHandler.Sharable
2.Channel 看成Socket
   生命周期:a.被创建但是没被注册到EventLoop  b.已经注册  c.处于活动状态   d.
3.Eventloop 看成线程,控制 多线程处理 并发   ChannelEventLoop是一对多的关系
4.channelFuture 异步通知
5.ChannelOption
6.ByteBuf

7.序列化:a.内置(四句话) b.集成第三方MessagePack	  TCP粘包/半包
   a.什么是TCP粘包/半包?
   b.产生的原因? 
      应用程序写入数据的字节大小大于套接字发送缓冲区的大小
      进行MSS大小的TCP分段
      以太网的payload大于MTU进行IP分片
   c.怎么解决?
8.LengthFieldBasedFrame 参数详解  如何计算拿出纸和笔
   实际数据包长度 = 长度域中记录的数据长度 + lengthFieldOffset + lengthFieldLength + lengthAdjustment
   
9.如何对channleHandler进行单元测试
  测试出站  测试入站    异常

三。进阶与实战

1.UDP的单播和广播 (第七节课的第二)

1.UDP的单播和广播  代码包UDP
   UDP属于无连接,UDP没有粘包半包现象
   TCP就像打电话,拨出去对方要接起来才能进行对话;UDP就像发邮件
   单播只发给一个人,广播发给多个人; TCP属于单播,UDP有单播和广播
  
  单播:案例:请告诉我一句古诗  package unicast    发送端:UdpQuestionSide  接收端:UdpAnswerSide
    发送端handler和接收端handler都继承 extend simpleChannelInboundHandler<DatagramPacket>  DatagramPacket代表要在网络上发送的Udp报文,
    重写channelRead0和exceptionCaught两个方法 
   
   客户端向服务端发送信息在UdpQuestionSide,
   服务端向客户端回答问题在AnswerHandler

    测试:先启动应答端(应答服务已启动.....)  然后启动提问端

  广播:将日志信息在全网广播 package broadcast    广播端:bcside  接收端:acceptside
      日志实体类:LogMsg    日志常量语句:LogConst
      编码和解码的操作顺序都是时间 消息ID  分隔符 日志消息
      测试:先启动接收端LogEventMonitor,然后启动广播端:LogEventBroadcaster

2.服务器推送技术-Comet 有四种推送技术 (第八节课的第一个)

sse:基于http协议,而webSocket是单独的一个协议
sse轻量,基于消息的文本; webSocket基于消息的文本或者二进制数据通信

服务器推送技术-Comet 基于http长连接,无须在浏览器浏览器安装插件的“服务器堆”称为“Comet”  
    例如:弹幕、股票实时刷新、汇款页面自动跳转到支付成功页面
    这边四个案例:1.时间  2.servlet异步推送新闻  3.SSE 贵金属期货价格实时查询   4.饮料机支付         
     
   ajax短轮询和ajax长轮询区别: 短:客户端发送请求,不管服务端有没有数据响应,都迅速给客户端应答,不断往返  案例:查看服务器时间
                              长:客户端发送请求,服务端没有数据的情况,会抓住请求不放,直到有数据才响应给客户端  
   1.服务器时间——Ajax短轮询
         ajax短轮询 setInterval('save(0)', 30000);  特定:优点.服务器基本不用改造   缺点:服务器承受压力和资源的浪费  数据同步不及时          
         JSP页面:webapp/WEB-INF/views/showtime.jsp
         代码:normal
                  
   2.servlet异步推送新闻——Ajax长轮询
       JSP页面:webapp/WEB-INF/views/pushNews.jsp, 
       代码包servlet3, Spring带来的DeferedResult  案例:servlet异步——推送实时新闻
      
   3.基于长轮询的服务器堆模型:Server——sent——events(SSE)    案例:SSE——贵金属期货价格实时查询
          JSP页面:webapp/WEB-INF/view/nobleMetal.jsp
          代码: SSE
   4. Spring带来的sseEmitters 案例:自动饮料售货机微信支付   emitter——SseController
          JSP页面:webapp/WEB-INF/view/wechatpay.jsp
          代码:emitter

3.WebSocket通信 (第八节课的2 第九节课的1)

WebSocket通信
  什么是webSocket: 
      1.HTML5中的协议,实现与客户端与服务器双向,基于消息的文本或二进制数据通信
      2.适合于对数据的实时性要求比较强的场景,如通信、直播、共享桌面,特别适合于客户与服务频繁交互的情况下,如实时共享、多人协作等平台。
      3.采用新的协议,后端需要单独实现
      4.客户端并不是所有浏览器都支持 (sse也是这样)
      
      知识点1:websocket借用了http的协议完成握手           
      知识点2:webSocket是个规范,在实际的实现中有html5规范中的websocket API和websocket的子协议stomp 
      知识点:长连接包括:长轮询,SSE(server sent events)WebSocket
——————————————————————————————————————————      
  实现方式一:stomp Simple Text Origented Messaging Protocol(简单流文本定向消息协议)       
  一。SpringBootStomp进行集成(代码包stomp)
  基于STOPM的聊天室: 代码包Stomp 
     条件:
      服务端:1.WebSocketConfig配置类上加注解@Configuration@EnableWebSocketMessageBroker
            2.实现WebSocketMessageBrokerConfigurer,重写两个方法
                a.registerStompEndpoints注册EndPoints(registry.addEndpoint("/endpointMark"))
                b.configureMessageBroker配置消息代理
            3..浏览器发出webSocket请求的地址打上注解
                 @MessageMapping("/massRequest"), —————客户端发送群发消息的地址
                 @SendTo("/mass/getResponse")      —————客户端接收群发消息的地址
				 
				 @MessageMapping("/aloneRequest") ————客户端发送单聊消息的地址
				 this.template.convertAndSendToUser(chatRoomRequest.getUserId() +"","/alone",response);   
                       ————客户端接收单聊消息的地址 相当于 @SendToUser(/queue/userId/along)
            
                 
      客户端端:1.浏览器须有三个js:socket.min.js/stopm.min.js/jquery.js,还有自己定义的一个wechat_room.js
              2.weChat_roo.js做的事情
                 a.打开通道建立连接 var socket = new SockJS('/endpointMark');
                 
                 b.发起订阅(获取消息),stompClient.subscribe('/mass/getResponse',            一对多发起订阅,接收消息
                                     stompClient.subscribe('/queue/' + userId + '/alone',  一对一发起订阅,接收消息
                                     
                 c.发送消息,  stompClient.send("/massRequest",{},JSON.stringify(postValue));  群发消息
                             stompClient.send("/aloneRequest",{},JSON.stringify(postValue));  私发消息
                              
                                     
          
          
    
     代码:要使用webstomp必须有一个配置类WebSocketConfig,该类实现WebSocketMessageBrokerConfigurer,该类加注解@Configuration,
     页面:wechat_room.html  
      三个方法 (在wechatroom.JS里面)1.建立连接  2.订阅    3.发出请求
      建立连接:wechat_room.js里面的JS的连接方法connect的var socket = new SockJS('/endpointMark')与配置文件WebSocketConfig的registry.addEndpoint("/endpointMark")对应
      订阅:JS的订阅方法stompTopic的stompClient.subscribe('/mass/getResponse',function(response)与控制层的StompController方法上的注解@SendTo("/mass/getResponse")对应
      发送:JS的群发消息方法sendMassMessagestompClient.send("/massRequest",{},JSON.stringify(postValue));与控制层的StompController的类上注解@MessageMapping("/massRequest")对应

测试:启动服务StompApplication  浏览器输入:http://localhost:8080/chatroom 跳转到wechat_room.html 打开多个页面进行聊天测试
                              是因为WbeMvcConfig.java文件里面的    registry.addViewController("/chatroom").setViewName("/wechat_room"); (前面路径,后面页面)
————————————————————————————————————————————————————————————————————————————————————    

二。SpringBoot和原生WebSocket集成  (代码包socket  第九节课的1)
  页面ws.html里面的socket = new WebSocket("ws://localhost:8080/ws/asset"); 
  与webServer.java里面的类注解值@ServerEndpoint(value = "/ws/asset")对应
  测试:运行WebsocketApplication,浏览器输入:http://localhost:8080/ws

————————————————————————————————————————————————————————————————————————————————————    

三。websocket和netty集成(代码包websocket-netty)(第九节课的1 )IETF 发布的WebSocket RFC,定义了6 种帧,Netty 为它们每种都提供了一个POJO 实现
WebSocketFrame类型:Binary/text/Continuation/Close/Ping/Pong
1.开启WebSocket支持:类WebSocketConfig
模式:websocket——netty
测试:运行WebScocketServer
  

在这里插入图片描述

四.实现自己的通信框架

代码包:advantage  (第九节课的1 2)
功能描述:1.基于NteetyNio通信框架,提供高性能的的异步通信能力     NettyServer NettyClient
         2.提供消息的编解码框架,可以实现POJO的序列化和反序列化     kryo文件夹
         3.提供基于IP地址的白名单接入认证机制                     LoginAuthRespHandler登录检查
         4.链路的有效性校验机制                                 即客户端定时发送心跳包
         5.链路的断连从连机制                                   即服务端挂掉重启后,客户端主动连接

1.连接:
 服务端提供端口,客户端连接服务端用ip+端口

2.消息的编解码框架,可以实现POJO的序列化和反序列化 ————————————————————————

  消息     =     消息头    +   消息体
MyMessage  =    MyHeader  +   body

序列化/反序列化器:KryoSerializer    
        ByteBuf sendBuf = Unpooled.buffer();	
	序列化:	            KryoSerializer.serialize(message, sendBuf)
	反序列号:	   MyMessage decodeMsg = (MyMessage)KryoSerializer.deserialize(sendBuf);
编解码器:KryoEncoder/KryoDecoder
	编码器KryoEncoder把消息编为字节所以继承MessageToByteEncoder                      KryoSerializer.serialize(message, out);
	解码器KryoDecoder把接收到的反序列化为我们要的实体ByteToMessageDecoder            Object obj = KryoSerializer.deserialize(in);

3.IP地址的白名单接入认证机制思路
   当客户端请求连接,获取客户端ip;如果ip存在白名单map中,就允许登录,否则不允许

4.超时检测: 使用netty内置的handler,  
  如下表示50s没有收到报文,那么就会抛出异常
   ch.pipeline().addLast("readTimeoutHandler", new ReadTimeoutHandler(50));

5.发送心跳报文 是在接收到的消息是登录成功
————————————————————————————————————————————————————————————————————————————————
3.Handler汇总

       服务端                                                   客户端
1.剥离接收到的消息长度                                 1.剥离接收到的消息长度(netty)   
2.给发送出去的消息增加长度                             2.给发送出去的消息增加长度(netty)
3.反序列化                                           3.反序列化
4.序列化                                             4.序列化
5.超时检测                                           5.超时检测(netty)
6.登录应答                                           6.发出登录请求
7.心跳应答                                           7.发出心跳请求
8.服务端业务处理
       
       
发送心跳包的方法:在 HeartBeatReqHandlerHeartBeatTask 的 run方法
     
客户端每隔5秒钟发送一个心跳:在 HeartBeatReqHandler 的 channelRead方法里,
   heartBeat = ctx.executor().scheduleAtFixedRate(new HeartBeatReqHandler.HeartBeatTask(ctx), 0,5000,TimeUnit.MILLISECONDS);  
  
重连机制:
  NettyClient类里面的connect的finally 



Netty提供的可以打印报文的Handler:
 (SocketChannel) ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));

测试:先启动NettyServer

在这里插入图片描述

四。总结汇总

1、服务端和客户端的启动:

	//服务端
	@PostConstruct
    public static void startNettyServer() throws InterruptedException {
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup work = new NioEventLoopGroup();
        //服务端
        ServerBootstrap bootstrap = new ServerBootstrap();
        //服务端信息设置
        bootstrap.group(boss, work)
                .channel(NioServerSocketChannel.class)
                //保持长连接
                .childOption(ChannelOption.SO_KEEPALIVE, true)
                .childHandler(new ServerInitializer());
         //定义端口等待连接
        ChannelFuture future = bootstrap.bind(8001).sync();
        if (future.isSuccess()) {
            System.out.println("netty服务启动成功");
        }
    }
    
//daotong的服务端
@Component
@ChannelHandler.Sharable
@Slf4j
public class ChannelIdleServerHandler extends ChannelInboundHandlerAdapter {
    @Resource
    private CommonHandler commonHandler;
    /** 流量埋点 */
    @Resource
    private TrafficMonitoringService trafficMonitoringService;

    @Resource
    private ThreadUtils threadUtils;

    @Trace
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        // 当前 channel 绑定的 channelUserId
        String channelUserId = ChannelCache.getChannelUserId(ctx.channel());

        // 判断是否要关闭空闲channel
        if (commonHandler.closeIdleChannel(ctx, evt, channelUserId)) {
            return;
        }

        // channel 已关闭
        if (!ctx.channel().isActive() || StringUtils.isEmpty(channelUserId)) {
            return;
        }

        // 用时间戳生成消息id
        String uniqueId = System.currentTimeMillis() + "";

        // 分别发消息 判断channel对应哪个协议
        StringBuilder msgBuilder = new StringBuilder();
        String subProtocols = ChannelCache.getChannelSubProto(ctx.channel());
        boolean needSaveRedis = false;

        // 按协议发送
        if (subProtocols.startsWith(Constant.ACMP_PREFIX)) {
            msgBuilder.append(AcmpTestUtil.triggerMessage(uniqueId));
        } else if (ProtocolsEnum.OCPP_1POINT6.getValue().equals(subProtocols)) {
            msgBuilder.append(Ocpp1Point6Util.triggerMessage(uniqueId,
                    TriggerMessagePayload.builder()
                            .connectorId(1)
                            .requestedMessage(OcppAction.HEART_BEAT.getValue())
                            .build().toString()));
            needSaveRedis = true;
        }else if (ProtocolsEnum.OCPP_2POINT0.getValue().equals(subProtocols)) {
            TriggerMessageRequest triggerMessageRequest = TriggerMessageRequest.builder().requestedMessage(OcppAction.HEART_BEAT.getValue()).evse(EVSEType.builder().id(1).connectorId(1).build()).build();
            msgBuilder.append(Ocpp1Point6Util.triggerMessage(uniqueId,JSONObject.toJSONString(triggerMessageRequest)));
            needSaveRedis = true;
        } else if (ProtocolsEnum.BRCP_1POINT0.getValue().equals(subProtocols)) {
            // 心跳
            msgBuilder.append(BrcpRequestMessageDTO.builder()
                    .seq(uniqueId)
                    .type(BrcpRequestMessageDTO.ActionEnum.HEART_BEAT.getValue())
                    .data("")
                    .build().toString());
        } else if (ProtocolsEnum.APMP_1POINT0.getValue().equals(subProtocols)) {
            // apmp心跳
            msgBuilder.append(MessageDTO.builder()
                    .seq(uniqueId).cmd(Constant.HEART_BEAT_CMD).data(new Object()).build().toString());
        } else {
            // app 心跳
            msgBuilder.append(MessageDTO.builder()
                    .seq(uniqueId).cmd(MessageDTO.CmdEnum.HEART_BEAT.getValue()).data(new Object()).build().toString());
        }

        String msgContent = msgBuilder.toString();

        // 消息写 redis
        boolean finalNeedSaveRedis = needSaveRedis;
        threadUtils.pool.execute(RunnableWrapper.of(() -> commonHandler.saveOcppMessageToRedis(channelUserId,
                ProtocolsEnum.OCPP_1POINT6.getValue().equals(subProtocols) || ProtocolsEnum.OCPP_2POINT0.getValue().equals(subProtocols), msgContent, finalNeedSaveRedis)));

        // 发送
        ctx.channel().writeAndFlush(new TextWebSocketFrame(msgContent)).addListener(future -> {
            // 云到桩 发MQ及redis
            threadUtils.pool.execute(RunnableWrapper.of(() -> commonHandler.saveOcppCachePileMsg(channelUserId,
                    ProtocolsEnum.OCPP_1POINT6.getValue().equals(subProtocols) ||  ProtocolsEnum.OCPP_2POINT0.getValue().equals(subProtocols), msgContent, Constant.CLOUD_TO_PILE)));

            if (!future.isSuccess()) {
                log.error("向用户 {} 发送消息: {} 失败,失败原因: {}", channelUserId, msgContent, future.cause());
                return;
            }
            log.info("向用户 {} 发送消息成功: {}", channelUserId, msgContent);

            // ---- 发送完心跳消息,收集Ocpp协议和运维协议的心跳
            if (Constant.checkSn(channelUserId)) {
                if (subProtocols.startsWith(Constant.ACMP_PREFIX)) {
                    trafficMonitoringService.trafficMonitoring(Constant.getSn(channelUserId).toUpperCase(),
                            TrafficMonitoringConstant.PROTOCOL_ACMP, TrafficMonitoringConstant.REQUEST, TrafficMonitoringConstant.HEARTBEAT, null, (long) msgContent.getBytes().length);
                } else if (ProtocolsEnum.OCPP_1POINT6.getValue().equals(subProtocols) || ProtocolsEnum.OCPP_2POINT0.getValue().equals(subProtocols)) {
                    trafficMonitoringService.trafficMonitoring(Constant.getSn(channelUserId).toUpperCase(),
                            TrafficMonitoringConstant.PROTOCOL_OCPP, TrafficMonitoringConstant.REQUEST, TrafficMonitoringConstant.HEARTBEAT, null, (long) msgContent.getBytes().length);
                }
            }
        });
    }
}
	
	//客户端
	@PostConstruct
    public void startNettyClient() throws Exception {
        EventLoopGroup group = new NioEventLoopGroup();
        //客户端
        Bootstrap bootstrap = new Bootstrap();
        //客户端设置
        bootstrap.group(group)
                .channel(NioSocketChannel.class)
                .option(ChannelOption.TCP_NODELAY, true)
                .handler(new ClientInitializer());
        //定义服务端ip和端口进行连接
        ChannelFuture future = bootstrap.connect(new InetSocketAddress("127.0.0.1", 8001)).sync();
		 if (future.isSuccess()) {
            System.out.println("连接netty服务服务器成功");
        }
    }

2、发送消息的地方

//两个地方可以发送(一个客户端/服务器启动里面, 一个handler里面的channelRead)
地方a.客户端服务器启动里面:channel.writeAndFlush(Unpooled.copiedBuffer(arr));
	 //1.连接绑定里面获取channelFuture
	 ChannelFuture future = bootstrap.bind(8001).sync();   // ChannelFuture future = bootstrap.connect(new InetSocketAddress("127.0.0.1", 8001)).sync();
	 //2.channelFuture获取chanel
	 Channel channel = future.sync().channel();  
	 //3.发送消息
    ChannelFuture future = channel.writeAndFlush(Unpooled.copiedBuffer(arr)).sync();
    
  
地方b.handler里面的channelRead   channelRead0(ChannelHandlerContext ctx, Object msg)
  	ctx.writeAndFlush(Unpooled.copiedBuffer(resp.getBytes()));
	

注意:发送消息的数据类型为netty的ByteBuf,如果要发送JAVA对象需要指定编码器解码器的handler

	ByteBuf的数据转换:
		 1.ByteBuf转为字符串:  
	  		ByteBuf byteBuf = (ByteBuf) msg;
            String strMsg = byteBuf .toString(CharsetUtil.UTF_8);
            
	      2.字符串/对象转为ByteBufbyte[] byteArray = requestDto.toString().getBytes();
            ByteBuf byteBuf = Unpooled.copiedBuffer(byteArray);   	  

3、发送消息的类型为java对象 (指定编码器解码器的handler)

1.先写一个序列化器
2.写编码器和解码器
3.初始化器加上编码器和解码器
3.1 序列化器
package com.test.netty.protocol;


import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.Schema;
import com.dyuproject.protostuff.runtime.RuntimeSchema;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ProtostuffUtil {

    private static Map<Class<?>, Schema<?>> cachedSchema = new ConcurrentHashMap<Class<?>, Schema<?>>();

    private static <T> Schema<T> getSchema(Class<T> clazz) {
        @SuppressWarnings("unchecked")
        Schema<T> schema = (Schema<T>) cachedSchema.get(clazz);
        if (schema == null) {
            schema = RuntimeSchema.getSchema(clazz);
            if (schema != null) {
                cachedSchema.put(clazz, schema);
            }
        }
        return schema;
    }

    public static <T> byte[] serialize(T obj) {
        @SuppressWarnings("unchecked")
        Class<T> clazz = (Class<T>) obj.getClass();
        LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
        try {
            Schema<T> schema = getSchema(clazz);
            return ProtostuffIOUtil.toByteArray(obj, schema, buffer);
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        } finally {
            buffer.clear();
        }
    }

    public static <T> T deserialize(byte[] data, Class<T> clazz) {
        try {
            T obj = clazz.newInstance();
            Schema<T> schema = getSchema(clazz);
            ProtostuffIOUtil.mergeFrom(data, obj, schema);
            return obj;
        } catch (Exception e) {
            throw new IllegalStateException(e.getMessage(), e);
        }
    }

    public static void main(String[] args) {
        //1.构建对象
        TIMReqMsg timReqMsg = new TIMReqMsg();
        timReqMsg.setRequestUserName("张三");
        timReqMsg.setReceiveUserName("李四");
        timReqMsg.setMsg("你好李四,我是张三");
        timReqMsg.setMsgType("登录");
        System.out.println(timReqMsg);

        //2.对象转成字节数组
        byte[] userBytes = ProtostuffUtil.serialize(timReqMsg);

        //3.字节数组转成对象
        TIMReqMsg reqProtocol = ProtostuffUtil.deserialize(userBytes, TIMReqMsg.class);
        System.out.println(reqProtocol);
    }
}

3.2 编码器和解码器`
package com.test.netty.protocol;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
//netty编码器
public class ObjEncoder extends MessageToByteEncoder {

    private Class<?> genericClass;

    public ObjEncoder(Class<?> genericClass) {
        this.genericClass = genericClass;
    }

    @Override
    protected void encode(ChannelHandlerContext ctx, Object in, ByteBuf out) {
        if (genericClass.isInstance(in)) {
            byte[] data = ProtostuffUtil.serialize(in);
            out.writeInt(data.length);
            out.writeBytes(data);
        }
    }
}


package com.test.netty.protocol;

import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;

import java.util.List;
//netty解码器
public class ObjDecoder extends ByteToMessageDecoder {

    private Class<?> genericClass;

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
        if (in.readableBytes() < 4) {
            return;
        }
        in.markReaderIndex();
        int dataLength = in.readInt();
        if (in.readableBytes() < dataLength) {
            in.resetReaderIndex();
            return;
        }
        byte[] data = new byte[dataLength];
        in.readBytes(data);
        out.add(ProtostuffUtil.deserialize(data, genericClass));
    }

    public ObjDecoder(Class<?> genericClass) {
        this.genericClass = genericClass;
    }
}
//编码器   将对象转为字节
public class KryoEncoder extends MessageToByteEncoder<MyMessage> {
    @Override
    protected void encode(ChannelHandlerContext ctx, MyMessage message, byteBuf out) throws Exception {
        KryoSerializer.serialize(message, out);
        ctx.flush();
    }
}

//解码器   将字节转为对象
public class KryoDecoder extends ByteToMessageDecoder {
    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        Object obj = KryoSerializer.deserialize(in);
        out.add(obj);
    }
}

3.3 初始化器加上编码器和解码器
public class ClientInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) {
        //编码器
        ch.pipeline().addLast("ObjectEncoder", new ObjEncoder(TIMReqMsg.class));
        //解码器
        ch.pipeline().addLast("ObjectDecoder", new ObjDecoder(TIMReqMsg.class));


        //普通handler
        ch.pipeline().addLast("ClientNormalHandler", new ClientNormalHandler());
    }
}

4、指定要处理的handler

a.先创建多个handler类
 	public class ServerNormalHandler extends SimpleChannelInboundHandler<Object> {
		   @Override
		   protected void channelRead0(ChannelHandlerContext ctx, Object msg) {
		   }
		
		   @Override
		   public void channelRead(ChannelHandlerContext ctx, Object msg) {
		   }
		
		   @Override
		   public void channelActive(ChannelHandlerContext ctx) {
		   }
		
		   @Override
		   public void channelInactive(ChannelHandlerContext ctx) {
		   }
		
		   @Override
		   public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
		   }
	}

b.创建Initializer初始化器,把a步骤里面的handler里面加进去
	public class ServerInit extends ChannelInitializer<SocketChannel> {
	    @Override
	    protected void initChannel(SocketChannel ch) throws Exception {
	        /*Netty提供的日志打印Handler,可以展示发送接收出去的字节*/
	        //ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
	        /*剥离接收到的消息的长度字段,拿到实际的消息报文的字节数组*/
	        ch.pipeline().addLast("frameDecoder",
	                new LengthFieldBasedFrameDecoder(65535,
	                        0,2,0,
	                        2));
	        /*给发送出去的消息增加长度字段*/
	        ch.pipeline().addLast("frameEncoder",
	                new LengthFieldPrepender(2));
	        /*反序列化,将字节数组转换为消息实体*/
	        ch.pipeline().addLast(new KryoDecoder());
	        /*序列化,将消息实体转换为字节数组准备进行网络传输*/
	        ch.pipeline().addLast("MessageEncoder",
	                new KryoEncoder());
	        /*超时检测*/
	        ch.pipeline().addLast("readTimeoutHandler",
	                new ReadTimeoutHandler(50));
	        /*登录应答*/
	        ch.pipeline().addLast(new LoginAuthRespHandler());
	
	        /*心跳应答*/
	        ch.pipeline().addLast("HeartBeatHandler",
	                new HeartBeatRespHandler());
	
	        /*服务端业务处理*/
	        ch.pipeline().addLast("ServerBusiHandler",
	                new ServerBusiHandler());
	    }
	}

c.在客户端服务端指定不同的ClientInitializer
	serverBootstrap.group(boss, work)
                .childHandler(new ServerInitializer());

	 bootstrap.group(group)
                .handler(new ClientInitializer());



或者另外一种方法:直接在客户端服务端
		 .handler(new ChannelInitializer<SocketChannel>() {
             @Override
              public void initChannel(SocketChannel ch) throws Exception {
                  ch.pipeline().addLast(
                  	 new FixedLengthFrameDecoder(35),
                     new ClientHandler());
              }
         });

4、handler的各种继承

ChannelInboundHandlerAdapter   
SimpleChannelInboundHandler<Object>  

   继承ChannelInboundHandlerAdapter
      channelRead
      channelReadComplete
      exceptionCaught
      
   继承SimpleChannelInboundHandler   
      channelInactive(建立连接后触发的方法)
   	  handlerAdded(ChannelHandlerContext channelHandlerContext) 
      channelRead0
      channelRead(ChannelHandlerContext channelHandlerContext, Object msg)
      messageReceived(ChannelHandlerContext channelHandlerContext, MyMessage myMessage) {      
      channelReadComplete
      exceptionCaught

如果要把多个Handler放在一起,继承ChannelInitializer<SocketChannel>,重写initChannel()方法
  public class ServerInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        /*Netty提供的日志打印Handler,可以展示发送接收出去的字节*/
        socketChannel.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
        /*核心业务处理*/
        socketChannel.pipeline().addLast("serverNormalHandler", new BusinessHandler());


    }
}

5、handler处理消息

1.往下传:c         channelHandlerContext.fireChannelRead(msg);
2.销毁消息不往下传: ReferenceCountUtil.release(msg);

6、获取通道 SocketChannel channel

    服务端:ctx.channel()
   
     //客户端连接上的时候向服务端发送消息,然后服务端read0获取channel维护用户和channel
     protected void channelRead0(ChannelHandlerContext ctx, TIMReqMsg msg) throws Exception {
        LOGGER.info("received msg=[{}]", msg.toString());

        if (msg.getType() == Constants.CommandType.LOGIN) {
            //保存客户端与 Channel 之间的关系  requestId用户id   reqMsg用户名
            SessionSocketHolder.put(msg.getRequestId(), (NioSocketChannel) ctx.channel());;
        }
      }
   		


  	客户端:
  		//先连接获取channelFuture
  		 ChannelFuture future = bootstrap.connect(timServer.getIp(), timServer.getTimServerPort()).sync();
  		 //获取channel
  		 SocketChannel  channel = (SocketChannel) future.channel();
  		 //发送消息
  		 ChannelFuture future = channel.writeAndFlush(login);





                    

6.写发送消息:在服务端或客户端用Channel,在handler用ChannelHandlerContext,方法都是writeAndFlush
     服务端、/客户端
     ChannelFuture future = channel.writeAndFlush(Unpooled.copiedBuffer(arr)).sync();
     if (!future.isSuccess()) {
         log.error("发送数据出错:{}", future.cause());
      }
    
    handler里面:ctx.writeAndFlush(Unpooled.copiedBuffer(resp.getBytes()));
	
	消息类型为netty框架的 ByteBuf
	   字符串转为ByteBufByteBuf byteBuf = Unpooled.copiedBuffer(msg, CharsetUtil.UTF_8);
	   ByteBuf转为字符串:  ByteBuf buf = (ByteBuf) msg;
                          String strMsg = buf.toString(CharsetUtil.UTF_8);

7.服务端获取连接的客户端地址和id: 
   channelHandlerContext对象
      ctx.channel().remoteAddress().toString()
      ctx.channel().remoteAddress(), 
      ctx.channel().id()   
	
     InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress().
     String ip = address.getAddress().getHostAddress();

   SocketChannel对象
     ip和端口:ch.remoteAddress().getAddress().getHostAddress(), 
             ch.remoteAddress().getPort()) 

8.netty内置handler
   1.打印报文的Handler:
      (SocketChannel) ch.pipeline().addLast(new LoggingHandler(LogLevel.INFO));
      
   2. 超时检测handler: 如下表示50s没有收到报文,那么就会抛出异常
       ch.pipeline().addLast("readTimeoutHandler", new ReadTimeoutHandler(50));


9.定时心跳
  1.心跳方法
      /*心跳请求任务*/
    private class HeartBeatTask implements Runnable {
        private final ChannelHandlerContext ctx;
        //心跳计数,可用可不用,已经有超时处理机制
        private final AtomicInteger heartBeatCount;

        public HeartBeatTask(final ChannelHandlerContext ctx) {
            this.ctx = ctx;
            heartBeatCount = new AtomicInteger(0);
        }

        @Override
        public void run() {
            MyMessage heatBeat = buildHeatBeat();
//            LOG.info("Client send heart beat messsage to server : ---> "
//                            + heatBeat);
            ctx.writeAndFlush(heatBeat);
        }

        private MyMessage buildHeatBeat() {
            MyMessage message = new MyMessage();
            MyHeader myHeader = new MyHeader();
            myHeader.setType(MessageTypeEnum.HEARTBEAT_REQ.value());
            message.setMyHeader(myHeader);
            return message;
        }
    }
 2.定时
 heartBeat = ctx.executor().scheduleAtFixedRate(
                    new HeartBeatReqHandler.HeartBeatTask(ctx), 0,
                    5000,
                    TimeUnit.MILLISECONDS);
                    
10.spring初始化的时候就启动netty或者说运行某个方法
   1.写一个类,类上加注解@Component
   2.实现InitializingBean 接口,重写
   
@Component
public class BusiClient implements InitializingBean {
	@Override
    public void afterPropertiesSet() throws Exception {
       
    }
}



7、netty提供的hander

//心跳
ch.pipeline().addLast("idleStateHandler",
new IdleStateHandler(transportSettings.getReaderIdleTimeSeconds(), 0, 0));
                                       读的超时时间        写的超时时间  读写的超时时间        

8、daotong的心跳检测处理

ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel ch) {
        // 空闲心跳检测 与自定义的 userEventTriggered 配合一起作为心跳检测的处理
        ch.pipeline().addLast("idleStateHandler", new IdleStateHandler(readerIdleTimeOut, 0, 0, TimeUnit.SECONDS));

        // 空闲心跳处理handler
        ch.pipeline().addLast("channelIdleServerHandler", channelIdleServerHandler);
    }
});
@Component
@ChannelHandler.Sharable
@Slf4j
public class ChannelIdleServerHandler extends ChannelInboundHandlerAdapter {
    @Resource
    private CommonHandler commonHandler;
    /** 流量埋点 */
    @Resource
    private TrafficMonitoringService trafficMonitoringService;

    @Resource
    private ThreadUtils threadUtils;

    @Trace
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        // 当前 channel 绑定的 channelUserId
        String channelUserId = ChannelCache.getChannelUserId(ctx.channel());

        // 判断是否要关闭空闲channel
        if (commonHandler.closeIdleChannel(ctx, evt, channelUserId)) {
            return;
        }

        // channel 已关闭
        if (!ctx.channel().isActive() || StringUtils.isEmpty(channelUserId)) {
            return;
        }

        // 用时间戳生成消息id
        String uniqueId = System.currentTimeMillis() + "";

        // 分别发消息 判断channel对应哪个协议
        StringBuilder msgBuilder = new StringBuilder();
        String subProtocols = ChannelCache.getChannelSubProto(ctx.channel());
        boolean needSaveRedis = false;

        // 按协议发送
        if (subProtocols.startsWith(Constant.ACMP_PREFIX)) {
            msgBuilder.append(AcmpTestUtil.triggerMessage(uniqueId));
        } else if (ProtocolsEnum.OCPP_1POINT6.getValue().equals(subProtocols)) {
            msgBuilder.append(Ocpp1Point6Util.triggerMessage(uniqueId,
                    TriggerMessagePayload.builder()
                            .connectorId(1)
                            .requestedMessage(OcppAction.HEART_BEAT.getValue())
                            .build().toString()));
            needSaveRedis = true;
        }else if (ProtocolsEnum.OCPP_2POINT0.getValue().equals(subProtocols)) {
            TriggerMessageRequest triggerMessageRequest = TriggerMessageRequest.builder().requestedMessage(OcppAction.HEART_BEAT.getValue()).evse(EVSEType.builder().id(1).connectorId(1).build()).build();
            msgBuilder.append(Ocpp1Point6Util.triggerMessage(uniqueId,JSONObject.toJSONString(triggerMessageRequest)));
            needSaveRedis = true;
        } else if (ProtocolsEnum.BRCP_1POINT0.getValue().equals(subProtocols)) {
            // 心跳
            msgBuilder.append(BrcpRequestMessageDTO.builder()
                    .seq(uniqueId)
                    .type(BrcpRequestMessageDTO.ActionEnum.HEART_BEAT.getValue())
                    .data("")
                    .build().toString());
        } else if (ProtocolsEnum.APMP_1POINT0.getValue().equals(subProtocols)) {
            // apmp心跳
            msgBuilder.append(MessageDTO.builder()
                    .seq(uniqueId).cmd(Constant.HEART_BEAT_CMD).data(new Object()).build().toString());
        } else {
            // app 心跳
            msgBuilder.append(MessageDTO.builder()
                    .seq(uniqueId).cmd(MessageDTO.CmdEnum.HEART_BEAT.getValue()).data(new Object()).build().toString());
        }

        String msgContent = msgBuilder.toString();

        // 消息写 redis
        boolean finalNeedSaveRedis = needSaveRedis;
        threadUtils.pool.execute(RunnableWrapper.of(() -> commonHandler.saveOcppMessageToRedis(channelUserId,
                ProtocolsEnum.OCPP_1POINT6.getValue().equals(subProtocols) || ProtocolsEnum.OCPP_2POINT0.getValue().equals(subProtocols), msgContent, finalNeedSaveRedis)));

        // 发送
        ctx.channel().writeAndFlush(new TextWebSocketFrame(msgContent)).addListener(future -> {
            // 云到桩 发MQ及redis
            threadUtils.pool.execute(RunnableWrapper.of(() -> commonHandler.saveOcppCachePileMsg(channelUserId,
                    ProtocolsEnum.OCPP_1POINT6.getValue().equals(subProtocols) ||  ProtocolsEnum.OCPP_2POINT0.getValue().equals(subProtocols), msgContent, Constant.CLOUD_TO_PILE)));

            if (!future.isSuccess()) {
                log.error("向用户 {} 发送消息: {} 失败,失败原因: {}", channelUserId, msgContent, future.cause());
                return;
            }
            log.info("向用户 {} 发送消息成功: {}", channelUserId, msgContent);

            // ---- 发送完心跳消息,收集Ocpp协议和运维协议的心跳
            if (Constant.checkSn(channelUserId)) {
                if (subProtocols.startsWith(Constant.ACMP_PREFIX)) {
                    trafficMonitoringService.trafficMonitoring(Constant.getSn(channelUserId).toUpperCase(),
                            TrafficMonitoringConstant.PROTOCOL_ACMP, TrafficMonitoringConstant.REQUEST, TrafficMonitoringConstant.HEARTBEAT, null, (long) msgContent.getBytes().length);
                } else if (ProtocolsEnum.OCPP_1POINT6.getValue().equals(subProtocols) || ProtocolsEnum.OCPP_2POINT0.getValue().equals(subProtocols)) {
                    trafficMonitoringService.trafficMonitoring(Constant.getSn(channelUserId).toUpperCase(),
                            TrafficMonitoringConstant.PROTOCOL_OCPP, TrafficMonitoringConstant.REQUEST, TrafficMonitoringConstant.HEARTBEAT, null, (long) msgContent.getBytes().length);
                }
            }
        });
    }
}

SpringBoot整合netty客户端

 <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.28.Final</version>
        </dependency>

1.BusiClient 由main方法启动改成随着spring启动而启动(实现InitializingBean 接口)并且类上加@Component接口

@Component
@Data
public class BusiClient implements InitializingBean {
    private NettyClient nettyClient;

    @Override
    public void afterPropertiesSet() throws Exception {
        nettyClient = new NettyClient();
        new Thread(nettyClient).start();
        while(!nettyClient.isConnected()){
            synchronized (nettyClient){
                nettyClient.wait();
            }
        }
        System.out.println("网络通信已准备好,可以进行业务操作了........");
    }
}

2.控制器或serivce层注入BusiClient ,调用send方法发送消息给业务端

  @Autowired private BusiClient busiClient;

   @GetMapping("/test2")
   public void test2(){
       System.out.println("进入控制器");
       NettyClient nettyClient = busiClient.getNettyClient();
       nettyClient.send("你好,服务端!");
   }
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

飘然生

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值