用户指南
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 看成线程,控制 多线程处理 并发 Channel和EventLoop是一对多的关系
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(简单流文本定向消息协议)
一。SpringBoot和Stomp进行集成(代码包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.基于Nteety的Nio通信框架,提供高性能的的异步通信能力 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.服务端业务处理
发送心跳包的方法:在 HeartBeatReqHandler 的 HeartBeatTask 的 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.字符串/对象转为ByteBuf:
byte[] 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
字符串转为ByteBuf:ByteBuf 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("你好,服务端!");
}