最新更新,报文发送,机器终端(车)与服务端
先学习一下基本内容,以下是基于基本内容
互相转换:byte(字节,字节是数字单位,他的数组是十进制内容),bit(二进制内容,不用操心这部分),十六进制0x(0-9-a-f),String(字符的数组,引用类型)
框架与语言:socket(tcp),java,netty
java代码
终端发给服务端,16进制字符串 转换 10进制的字节数组(数字数组)。通过outStream流发送。
服务端下发给终端,与上句原理一样。
String sourceStrs="AB AA";//AA是170,AB是171//有多个用空格隔开
//因为bytes[]不能追加,所以用字节流写入,写入后在转换
ByteArrayOutputStream output = new ByteArrayOutputStream();
String[] sourceStrArr = sourceStrs.split(" ");
for (int i = 0; i < sourceStrArr.length; i++) {
String sourceStr = sourceStrArr[i];
String byteNum = Tools.trans16t10(sourceStr);//将16进制字符串转换 得到字节
output.write(Integer.parseInt(byteNum));//将单个字节追加
}
byte[] sendBytes = output.toByteArray();//将字节流转换为字节数组
//写一个字节数组过去
outputStream.write(sendBytes);
服务端解析终端,二进制转换为byte[]数组,数组转为16进制。就能解析出16进制内容
我用的netty,他的事件部分,decode执行的部分
//创建一个字节数组
byte[] bufs2=new byte[in.readableBytes()];
//将接收的字节存放到字节数组中
in.readBytes(bufs2);//in是ByteBuf in重写的内容,终端传来的数据
//将字节数组,他转成16进制的内容,这样就能和协议匹配了
String jiqiStr = TuLiTcpTools.bytes2hex(bufs2);
方法补充:
trans16t10
public static String trans16t10(String str){ String myStr[] = { "a", "b", "c", "d", "e", "f" }; int result = 0; int n = 1; for (int i = str.length() - 1; i >= 0; i--) { String param = str.substring(i, i + 1); for (int j = 0; j < myStr.length; j++) { if (param.equalsIgnoreCase(myStr[j])) { param = "1" + String.valueOf(j); } } result += Integer.parseInt(param) * n; n *= 16; } // System.out.println(result); // System.out.println(Integer.parseInt(str, 16)); return String.valueOf(result); }
bytes2hex,10进制转16进制
public static String bytes2hex(byte[] bytes) { StringBuilder sb = new StringBuilder(); String tmp; sb.append("["); for (byte b : bytes) { // 将每个字节与0xFF进行与运算,然后转化为10进制,然后借助于Integer再转化为16进制 tmp = Integer.toHexString(0xFF & b); if (tmp.length() == 1) { tmp = "0" + tmp;//只有一位的前面补个0 } sb.append(tmp).append(" ");//每个字节用空格断开 } sb.delete(sb.length() - 1, sb.length());//删除最后一个字节后面对于的空格 sb.append("]"); return sb.toString(); }
netty程序
一个netty程序,分3个内容,前2个必须要,第3个依据业务可选
1.netty服务端的server,包含netty的配置和启动
2.handler处理端,对客户端(终端)发来的数据进行处理
3.编解码器,它其实是两部分,编码和解码,一般把他定义在handler之前
一个完整的netty服务端(JAVA版)
导入netty依赖
io.netty
netty-all
4.1.36.Final
编写server
package nettyServer;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class nServer {
public static void main(String[] args) throws InterruptedException {
//创建bossGroup,接受连接请求,用evnetLoopGroup接受
//创建workGroup,执行工作,业务处理
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
//上面2个是线程组,loop循环,这2个都是无限循环
//创建配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
//使用链式编程,设置
bootstrap.group(bossGroup,workGroup)//设置2个线程组
.channel(NioServerSocketChannel.class)//指定服务通道为nio模型
.option(ChannelOption.SO_BACKLOG,128)//设置线程得到的连接个数
.childOption(ChannelOption.SO_KEEPALIVE,true)//设置保持长连接状态
.childHandler(new ChannelInitializer() {
@Override//给pipline设置处理器
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast("decoder",new nDecode());
pipeline.addLast(new nHandler());//增加处理器,handler
}
});//设置work的EventLoop对应管道设置处理器
System.out.println("服务器准备完成");
//绑定端口,并且同步处理,future对象
ChannelFuture channelFuture = bootstrap.bind(12306).sync();
//当监听到后,处理完,在关闭,没有监听到则不会调用
channelFuture.channel().closeFuture().sync();
}
}
编写handler
注意:尽量用try catch,如果像我这样Exception,在springBoot中,异常状况下,客户端可能收不到数据,并且不会出现打印异常,让你错以为是卡死的情况。
package nettyServer;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
//自定义一个handler,需要继承netty定义好的handler适配器,否则无效
public class nHandler extends ChannelInboundHandlerAdapter{
@Override
//ChannelHandlerContext管道,通道,地址,他都能拿到
//msg是客户端发送来的数据
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("server ctx="+ctx);
//将msg 转成一个ByteBuf
//ByteBuf 是 Netty 提供的,注意不要使用nio的byteBuffer
System.out.println(msg);
ByteBuf buf = (ByteBuf) msg;
System.out.println("客户端发送的消息是"+buf.toString(CharsetUtil.UTF_8));
}
@Override
//读完了客户端的消息后,执行的内容
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//要写完,之后在flush,flush是发送(刷新,到通道)
//对发送的数据进行编码
ctx.channel().writeAndFlush(Unpooled.copiedBuffer("我收到了\r\n",CharsetUtil.UTF_8));
}
//处理异常,一般是关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
编写解码器(业务不需要可以删除)
注:可以复制重写的方法,但是不能直接复制,我的代码内容,我引入了自己业务中的工具类。
package com.dt.tuli.springBoot_netty;
import com.dt.tuli.tools.TuLiTcpTools;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.util.CharsetUtil;
import nettyServer.nServerAccept;
import java.util.List;
public class nDecodeSpringBoot extends ByteToMessageDecoder {
//ctx上下文,比如地址ip,端口等,从这拿
//in,客户端发来的数据
//rs经过解码器,最后保留的数据,输出的数据
@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List
测试客户端编写
注:删除了其中的业务敏感数据,所以与打印结果有所不同
import java.io.*;
import java.net.Socket;
public class SocketClient {
public static void main(String[] args) throws InterruptedException {
try {
// 和服务器创建连接
Socket socket = new Socket("localhost",12306);
// 要发送给服务器的信息
OutputStream os = socket.getOutputStream();
PrintWriter pw = new PrintWriter(os);
// byte[] bytes = new byte[97 98 99 10 01 01 10 21 03 49];
// pw.write(0xAA);
//
//
//
// pw.flush();
os.write("xxxxx".getBytes());
// 找到原因了,我代码里面写了个循环
// 从服务器接收的信息
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String info = null;
while((info = br.readLine())!=null){
System.out.println("我是客户端,服务器返回信息:"+info);
}
socket.shutdownOutput();
br.close();
is.close();
os.close();
pw.close();
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
结果
客户端
服务端
netty普通server转springBoot+netty
在springBoot中集成netty的目的是,可以通过springBoot的接口,向netty下发指令
上一个案例,是利用netty本身的server,而集成到springBoot中,需要新开一个线程运行netty服务,并且在启动springBoot的同时,启动netty。
新开一个线程,需要修改netty的server服务
启动springBoot的同时,启动netty,需要新写一个springBoot的config类
修改netty的server (nServerSpringBoot)
package com.dt.tuli.springBoot_netty; import io.netty.bootstrap.ServerBootstrap; import io.netty.channel.*; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioServerSocketChannel; import nettyServer.nDecode; public class nServer4SpringBoot { public static void bind(int port) throws InterruptedException { Thread thread = new Thread(new Runnable() { @Override public void run() { //创建bossGroup,接受连接请求,用evnetLoopGroup接受 //创建workGroup,执行工作,业务处理 EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workGroup = new NioEventLoopGroup(); //上面2个是线程组,loop循环,这2个都是无限循环 try { //创建配置参数 ServerBootstrap bootstrap = new ServerBootstrap(); //使用链式编程,设置 bootstrap.group(bossGroup, workGroup)//设置2个线程组 .channel(NioServerSocketChannel.class)//指定服务通道为nio模型 .option(ChannelOption.SO_BACKLOG, 128)//设置线程得到的连接个数 .childOption(ChannelOption.SO_KEEPALIVE, true)//设置保持长连接状态 .childHandler(new ChannelInitializer() { @Override//给pipline设置处理器 protected void initChannel(SocketChannel socketChannel) throws Exception { ChannelPipeline pipeline = socketChannel.pipeline(); pipeline.addLast(new nDecode()); pipeline.addLast(new nHandler4SpringBoot());//增加处理器,handler } });//设置work的EventLoop对应管道设置处理器 System.out.println("服务器准备完成"); //绑定端口,并且同步处理,future对象 ChannelFuture channelFuture = bootstrap.bind(port).sync(); //当监听到后,处理完,在关闭,没有监听到则不会调用 channelFuture.channel().closeFuture().sync(); } catch (InterruptedException e) { e.printStackTrace(); } finally { bossGroup.shutdownGracefully(); } } }); thread.start(); } }
新建springBoot配置类
package com.dt.tuli.springBoot_netty;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
public class nConfig4SpringBoot implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
nServer4SpringBoot.bind(12306);
}
}
此时启动springBoot,则会自动启动netty
springBoot,服务端如何主动下发指令,给机器
1.对每台机器,建立映射,存储套接字
2.服务端通过套接字发送给机器
我们业务,车端会通过tcp协议,不断的往服务端的某个端口发送数据-登录数据,此时服务端可以根据厂家协议,解析登录数据,获取到登录数据的信息,比如,车端发送的手机卡号,设备编号。
此时就可以存储设备编号,和socket连接,在netty中存储的是channel通道。
下次要主动下发的时候,从map中取出对应的编号的socket连接,进行发送。
建立存储套接字map
package com.dt.tuli.springBoot_netty;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
//用来存储客户端和服务端建立的管道
public enum TCPCache {
INSTANCE;
private Map clientInfoContext = new ConcurrentHashMap<>();
private Map serverInfoContext = new ConcurrentHashMap<>();
public Map getClientInfoContext() {
return clientInfoContext;
}
public Map getServerInfoContext() {
return serverInfoContext;
}
}
将socket套接字存入,套接字Map中(在hadnler的某个事件中,为了测试我放在了
channelReadComplete方法部分。实际业务是放在了channelRead部分,解码成功后校验完毕,进行存储。
其中test是唯一编号
TCPCache.INSTANCE.getClientInfoContext().computeIfAbsent("test",no -> ctx);//no是别名
创建springBoot类
@PostMapping("/send")
@ResponseBody
//服务端主动推送测试
public R TestSendMessage() {
Map clientInfoContext = TCPCache.INSTANCE.getClientInfoContext();
System.out.println("进来接口了");
System.out.println(clientInfoContext.keySet());
if(clientInfoContext.get("test")==null){
return failed("终端,还没有和服务端建立tcp连接");
}
// Unpooled.copiedBuffer()
try {
ChannelHandlerContext ctx = clientInfoContext.get("test");
ctx.channel().writeAndFlush(Unpooled.copiedBuffer("123\n".getBytes())).sync();
// ctx.writeAndFlush("123\n");
} catch (Exception e) {
e.printStackTrace();
}
return ok("推送完成");
}
测试中: 我的客户端测试是用\n区分是不是下一段的。所以通过请求写数据要\n。(实际开发,我是以包长度进行获取的)另外启动服务端后,要启动客户端才可以主动发送数据,因为客户端在启动时候,我的代码中向服务端发送了消息,建立了连接。收到数据后,服务端会存储这个连接,之后springBoot才能主动推送数据。
结果:
客户端:
杂谈,如果消息不想要的话,或者不符合你的规则就丢掉,比如,我发送的开头必须有aa aa,结果他发了个a1 aa,这就不符合规则,你想要丢掉,就在decode解码的时候,进行return;这样消息就不会发送给处理者,而是直接到结束者阶段。
修改decode代码
jiqiStr是我发过来的数据,而我截取了他的开头,看看符不符合规则
String substring = jiqiStr.substring(0, 8);//随便取的前面的数,看看开头有没有aa,没有则说明不是这个协议的报头,直接丢弃 if (!substring.contains("aa aa")){ //如果没有,则直接丢弃,不会走对数据的处理方法,而直接走处理完数据后的方法 return; }//下面的语句都不会被执行
........
rs.add(nServerAccept);//List
测试,含aa aa的正确规则,返回结果
含a1 aa的错误规则,返回结果
服务端的handler代码
由于业务需要,一个服务,要同时与多辆车连接,并且能够通过接口向车下发指令。车上报数据,给服务端,服务端入时序数据库。
由于一个车辆需要和服务连接,多个车辆将消耗多个线程,而线程又是由cpu产生,线程之间的传递是通过网络,这里的硬件要求就是,高cpu核数线程数,以及宽带网速高和稳定。
netty比较复杂,他是封装了nio,然后在封装了netty,然后在改进了roactor。所以框架比较复杂。学起来需要时间。建议不要速成,速成后,你没法改成你自己想要的业务。
044_尚硅谷_Netty入门-服务端1_哔哩哔哩_bilibili
解码器,解协议:
Netty自定义编-解码器解决TCP通讯粘包拆包的问题 - william_zhao - 博客园
netty是什么?
netty是一个nio框架,解决了socket的单线程效率低,采用了nio的优势,多线程,但是又屏蔽了nio的复杂性。
但是nio肯定是略低于bio的速度的。bio会一直阻塞,来了就收发,nio会缓冲,如果没有任务,可以先去做其他的事,然后再切回来。
ps:科普,bio是传统框架,同步进行会阻塞,效率不高,nio不会阻塞。
NIO全称 java non-blocking IO。
NIO三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)
深入了解:BIO和NIO的区别_你喜欢炸酱面么的博客-CSDN博客_bio nio
netty的架构原理
采用nio的模式,原来bio一个socket对应一个线程,变成了多个socket对应一个线程
select
原理是,用户提交读写后,会交给1个线程进行注册,所有的用户都交给这1个线程注册,注册后,这个线程就会对注册的事件,进行监控。一旦有消息发送来了,他就监控到了,然后发到缓冲区,这时候,会回调出一个新线程来处理,处理完,新线程又回去做其他的事情。
buffer缓冲区
数据只能从channel中读到buffer中,或者把数据从buffer写入channel中
线程模型
选择让哪个线程进行解码,这将很影响性能
线程模型1: 事件驱动,轮询查询
轮询就是不断的判断是否存在,存在则处理
事件驱动模型,就是把任务发到队列,另1个线程拉取队列内容,分发到不同的子线程中去执行任务,如下:
reactor模型
netty模型
模块组件
这个比较重要,设计到代码
【Bootstrap、ServerBootstrap】
Bootstrap 意思是引导,一个 Netty 应用通常由一个 Bootstrap 开始,主要作用是配置整个 Netty 程序,串联各个组件,Netty 中 Bootstrap 类是客户端程序的启动引导类,ServerBootstrap 是服务端启动引导类。
【Future、ChannelFuture】
正如前面介绍,在 Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过 Future 和 ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。
【Channel】
Netty 网络通信的组件,能够用于执行网络 I/O 操作。Channel 为用户提供:
1)当前网络连接的通道的状态(例如是否打开?是否已连接?)
2)网络连接的配置参数 (例如接收缓冲区大小)
3)提供异步的网络 I/O 操作(如建立连接,读写,绑定端口),异步调用意味着任何 I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成。
4)调用立即返回一个 ChannelFuture 实例,通过注册监听器到 ChannelFuture 上,可以 I/O 操作成功、失败或取消时回调通知调用方。
5)支持关联 I/O 操作与对应的处理程序。不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应。
下面是一些常用的 Channel 类型:
NioSocketChannel,异步的客户端 TCP Socket 连接。
NioServerSocketChannel,异步的服务器端 TCP Socket 连接。
NioDatagramChannel,异步的 UDP 连接。
NioSctpChannel,异步的客户端 Sctp 连接。
NioSctpServerChannel,异步的 Sctp 服务器端连接,这些通道涵盖了 UDP 和 TCP 网络 IO 以及文件 IO。
【Selector】
Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件。当向一个 Selector 中注册 Channel 后,Selector 内部的机制就可以自动不断地查询(Select) 这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel 。
【NioEventLoop】
NioEventLoop 中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务:I/O 任务,即 selectionKey 中 ready 的事件,如 accept、connect、read、write 等,由 processSelectedKeys 方法触发。非 IO 任务,添加到 taskQueue 中的任务,如 register0、bind0 等任务,由 runAllTasks 方法触发。两种任务的执行时间比由变量 ioRatio 控制,默认为 50,则表示允许非 IO 任务执行的时间与 IO 任务的执行时间相等。
【NioEventLoopGroup】
NioEventLoopGroup,主要管理 eventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个 Channel 上的事件,而一个 Channel 只对应于一个线程。
【ChannelHandler】
ChannelHandler 是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序。ChannelHandler 本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类:
ChannelInboundHandler 用于处理入站 I/O 事件。
ChannelOutboundHandler 用于处理出站 I/O 操作。
或者使用以下适配器类:
ChannelInboundHandlerAdapter 用于处理入站 I/O 事件。
ChannelOutboundHandlerAdapter 用于处理出站 I/O 操作。
ChannelDuplexHandler 用于处理入站和出站事件。
【ChannelHandlerContext】
保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象。
【ChannelPipline】
保存 ChannelHandler 的 List,用于处理或拦截 Channel 的入站事件和出站操作。ChannelPipeline 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel 中各个的 ChannelHandler 如何相互交互。
下图引用 Netty 的 Javadoc 4.1 中 ChannelPipeline 的说明,描述了 ChannelPipeline 中 ChannelHandler 通常如何处理 I/O 事件。I/O 事件由 ChannelInboundHandler 或 ChannelOutboundHandler 处理,并通过调用 ChannelHandlerContext 中定义的事件传播方法。
例如:ChannelHandlerContext.fireChannelRead(Object)和 ChannelOutboundInvoker.write(Object)转发到其最近的处理程序。
入站事件由自下而上方向的入站处理程序处理,如图左侧所示。入站 Handler 处理程序通常处理由图底部的 I/O 线程生成的入站数据。通常通过实际输入操作(例如 SocketChannel.read(ByteBuffer))从远程读取入站数据。出站事件由上下方向处理,如图右侧所示。出站 Handler 处理程序通常会生成或转换出站传输,例如 write 请求。I/O 线程通常执行实际的输出操作,例如 SocketChannel.write(ByteBuffer)。在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应,它们的组成关系如下:
一个 Channel 包含了一个 ChannelPipeline,而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。
入站事件和出站事件在一个双向链表中,入站事件会从链表 head 往后传递到最后一个入站的 handler,出站事件会从链表 tail 往前传递到最前一个出站的 handler,两种类型的 handler 互不干扰。
没有时间了,没想到netty学习还是比较复杂的,由于工作期限的要求,我现在开始使用步骤,大家感兴趣,可以看下方的阅读
原理参考:
Netty:原理架构解析_区块链之美的博客-CSDN博客_netty原理详解
Netty核心原理_myself study log的博客-CSDN博客_netty原理
netty的使用
我采用的是java编写
依赖导入
io.netty
netty-all
4.1.36.Final
netty服务端创建
服务端要创建的有
监听类,负责读取消息
连接类,负责处理消息
启动类,负责启动
公共代码,负责保存,客户端于服务端的连接关系
监听类:
链接类:
启动类:
公共代码类:
参考:
一个简单的Netty demo_颜翎的博客-CSDN博客_netty的demo
下面这个文章,可以完成,服务端,向客户端发送命令。
Netty--TCP--实例_IT利刃出鞘的博客-CSDN博客_netty tcp实例
springBoot和netty案例:
Spring Boot + Netty + WebSocket 实现消息推送 (qq.com)
踩坑:
netty客户端能收到netty服务端的,socket客户端能收到socket服务端的,netty服务端能收到socket客户端的,socket客户端【收不到】netty的,一直卡着
原因是因为,netty不知道你消息发送完了,所以会一直卡着,这句话的意思是告诉他,我发送完了。
socket(客户端)与netty(服务端)交互。在客户端发送完消息后加上
socket.shutdownOutput();
但是这不是最终解法,最终的问题是netty身上,如果是这样,那每次客户端都需要重新和netty建立连接。开销大,而且会丢包。
最终的解法是设定netty,让netty回复客户端。底层tcp不知道,你到底有没有发完,你关闭后,可能他以为是发完了。这种情况需要使用消息解析器去判断
消息解析器,每次发完一段话,给他一个结束标志,他就知道,是不是这段消息发送完了。
最佳方案,居然是 在服务端的回复后面加"\n"