什么是Netty
netty是一个异步的,基于事件驱动的网络通讯框架(基于NIO开发的,所以之后会对NIO多做说明)
异步和同步的区别
同步:客户端发送请求给服务器端,在服务端给出响应之前不能做任何事;
异步:和同步相反,客户端发送请求给服务器端,在服务端给出响应之前可以做任何事(ajax就是异步);
事件驱动
简单理解就是有一个页面,点击一个按钮,调用响应的函数,点击然后调用的这个过程就是一个事件驱动;
网络通讯框架
客户端与服务器之间的通信方式
Nio的介绍
由于Netty是基于Nio开发的,所以最好先对Nio有一定了解。 如不了解,先移步Nio介绍:
https://blog.csdn.net/z_z_k/article/details/122177503?spm=1001.2014.3001.5501
Netty的线程模型
Netty的线程模型是reactor模型
Reactor模型介绍
Reactor模型的别称:分发模型,反应器模型,通知模型
Reactor模式是基于事件驱动的,客户端不直接和服务端进行连接,先连接到Reactor,Reactor根据监听到时事件进行分配,处理事件是由线程池负责的
根据Reactor的数量和线程池的数量,Reactor分为三种模型:
单Reactor单线程
单Reactor多线程
主从Reactor多线程
单Reactor单线程
如图所示:
客户端不直接和服务器端连接,而是和中间的Reactor进行连接;
Reactor负责监听客户端的事件,分发到不同的事件处理器,处理不同的事件
事件处理使用的是线程池,分发时根据事件分发给空闲的线程池
优点:
模型简单,单线程进行通讯,不存在资源竞争
缺点:
单线程无法发挥多核CPU的优势,不适合高并发场景,分发事件后进行处理时,不能进行其他连接事件,容易出现性能瓶颈。
使用场景:
客户端数量少的场景,业务处理响应快,比如 Redis
单Reactor多线程
如图所示:
和单Reactor单线程相比,区别在于处理业务请求的时候不在是堵塞的了,
Handle不负责处理事件,只负责响应,业务处理交给线程池处理后,可以去关注其他的请求事件
线程池处理完后,结果返回给Handle,Handle和client端是有连接的,client端也能接收到结果
优点:
能发挥多核CPU的优势
缺点:
多线程场景下,复杂度增加了。单Reactor容易出现性能瓶颈
主从Reactor多线程
如图所示:
红色线部分是和单Reactor多线程的区别
主Reactor判断监听事件是否连接请求的事件,是就直接处理,不是就交给从Reactor
从Reactor负责处理非连接请求的事件,处理方式同 单Reactor多线程
图例画了一主一从,实际可以多主多从
优点:
主从线程分工明确,交互简单
缺点:
编程复杂度高
使用场景:
Nginx,Netty
Netty模型
Netty抽象出两组线程池 BossGroup 专门负责接收客户端的连接, WorkerGroup 专门负责网络的读写
BossGroup 和 WorkerGroup 类型都是 NioEventLoopGroup
NioEventLoopGroup 相当于一个事件循环组, 这个组中含有多个事件循环 ,每一个事件循环是 NioEventLoop
NioEventLoop 表示一个不断循环的执行处理任务的线程, 每个NioEventLoop 都有一个selector , 用于监听绑定在其上的socket的网络通讯
NioEventLoopGroup 可以有多个线程, 即可以含有多个NioEventLoop
每个Boss中NioEventLoop 循环执行的步骤有3步
轮询accept 事件
处理accept 事件 , 与client建立连接 , 生成NioScocketChannel , 并将其注册到某个worker中NIOEventLoop上的selector
处理任务队列的任务 ,即 runAllTasks
每个 Worker中NIOEventLoop 循环执行的步骤
轮询read, write 事件
处理i/o事件, 即read , write 事件,在对应NioScocketChannel 处理
c. 处理任务队列的任务 , 即 runAllTasks
每个Worker NIOEventLoop 处理业务时,会使用pipeline(管道), pipeline 中包含了 channel , 即通过pipeline 可以获取到对应通道, 管道中维护了很多的 处理器
简单案例
实现客户端和服务端进行互通,客户端连接服务端,发送一个消息,服务器端收到消息,在返回一个消息
服务器端代码
package com.netty.test;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class NettyServerTest {
public static void main(String[] args) {
//创建 bossLoopGroup 和 workLoopGroup 线程组
//bossLoopGroup 负责处理连接请求,workLoopGroup 负责真正的业务处理
EventLoopGroup bossLoopGroup = new NioEventLoopGroup();
EventLoopGroup workLoopGroup = new NioEventLoopGroup();
//创建服务器端
ServerBootstrap serverBootstrap = new ServerBootstrap();
try {
//设置服务器端启动参数
serverBootstrap.group(bossLoopGroup,workLoopGroup)
.channel(NioServerSocketChannel.class)//使用 NioServerSocketChannel 作为服务器端的通道实现
.option(ChannelOption.SO_BACKLOG,128)//设置线程队列的连接个数
.childOption(ChannelOption.SO_KEEPALIVE,true)//设置保持活动连接状态
.childHandler(new ChannelInitializer<SocketChannel>() {
//设置 pipeline 的处理器
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new NettyServerHandler());
}
});
//绑定端口,同步执行
ChannelFuture cf = serverBootstrap.bind(8888).sync();
//异步关闭通道(不是直接关闭,只是监听有关闭行为后在关闭)
cf.channel().closeFuture().sync();
} catch (InterruptedException e) {
System.out.println("有异常");
}finally {
//优雅关闭
bossLoopGroup.shutdownGracefully();
}
}
}
服务器端业务处理代码
package com.netty.test;
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;
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 读取数据
* @param ctx 上下文对象,包含很多
* @param msg 消息
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("客户端发送的消息是: " + byteBuf.toString(CharsetUtil.UTF_8));
}
/**
* 读取数据结束
* @param ctx
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.copiedBuffer("服务端收到,客户端请说", CharsetUtil.UTF_8));
}
/**
* 出现异常
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
客户端代码
package com.netty.test;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
public class NettyClientTest {
public static void main(String[] args) {
//创建客户端
Bootstrap client = new Bootstrap();
//创建线程组,处理读写事件
EventLoopGroup eventExecutors = new NioEventLoopGroup();
try {
//绑定参数
client.group(eventExecutors)
.channel(NioSocketChannel.class) //使用 NioSocketChannel 作为客户端的通道实现
.handler(new ChannelInitializer<SocketChannel>() {
//设置 pipeline 的处理器
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new NettyClientHandler());
}
});
//连接服务器
ChannelFuture future = client.connect("localhost", 8888).sync();
//异步关闭通道(不是直接关闭,只是监听有关闭行为后在关闭)
future.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
客户端业务代码
package com.netty.test;
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;
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 读取数据
* @param ctx 上下文对象,包含很多
* @param msg 消息
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("服务端返回的消息是: " + byteBuf.toString(CharsetUtil.UTF_8));
}
/**
* 通道就绪触发此功能
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//发送数据
ctx.writeAndFlush(Unpooled.copiedBuffer("呼叫服务端,收到请回答", CharsetUtil.UTF_8));
}
/**
* 读取结束
* @param ctx
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.close();
}
/**
* 出现异常
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
代码分析
上面的代码分为4块:
服务器端代码,服务器端处理业务代码,客户端代码,客户端处理业务代码
服务器端代码:
创建 bossLoopGroup 和 workLoopGroup 线程组
创建 ServerBootstrap 负责初始化netty服务器,并监听8888端口
pipeline:
和Channel是互相包含的关系,用谁都能得到另外一个。
每个Channel都有一个与之对应的pipeline
维护了一个ChannelHandlerContext 的双向链表
ChannelHandlerContext :就是上面自定义的业务处理类,一个pipeline可以绑定多个Handler
ChannelFuture :
是一个用于保存Channel异步操作结果的对象
shutdownGracefully
优雅退出,结束线程,关闭连接
服务器端业务代码:
继承ChannelInboundHandlerAdapter类,有很多方法可以重写:
比如读取数据时,读完数据时,写入数据时,写完数据时调用的方法等
ChannelHandlerContext:
上下文对象,可以用它取出很多属性
byteBuf:
Netty的字节Buffer,和NIO的ByteBuffer功能类似,他的底层比ByteBuffer多了两个参数,分别记录了 读取数据和写入数据时的下标位置,所有不需要filp进行转换
客户端的代码和服务端类似,就不在赘述了
Netty实现群聊系统
功能说明
实现功能要求:
客户端 上线,断开 要通知其他客户端
客户端发送消息,要让自己和其他的客户端都接收到这条消息,并区分是否为自己
代码实现
服务端代码
package com.netty.test.groupchat;
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.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
public class NettyGroupChatServer {
public static void main(String[] args) throws InterruptedException {
//创建两个 EventLoopGroup
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
//创建Netty启动器
ServerBootstrap serverBootstrap = new ServerBootstrap();
try {
//绑定Netty启动参数
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childHandler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//Netty自带的编解码Handler
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast("decoder", new StringDecoder());
//心跳检测
//第一个参数代表 3秒 没有读操作,则告诉我们的Handler
//第二个参数代表 5秒 没有写操作,则告诉我们的Handler
//第三个参数代表 7秒 没有读写操作,则告诉我们的Handler
pipeline.addLast("idle", new IdleStateHandler(3,5,7, TimeUnit.SECONDS));
//自定义handler,收发消息使用
pipeline.addLast("myServerHandler", new NettyGroupChatServerHandler());
}
});
//绑定端口 8888
ChannelFuture channelFuture = serverBootstrap.bind(8888).sync();
//异步监听关闭事件
channelFuture.channel().closeFuture().sync();
}finally {
//优雅关闭
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
服务端业务处理代码
package com.netty.test.groupchat;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.CharsetUtil;
import io.netty.util.concurrent.GlobalEventExecutor;
public class NettyGroupChatServerHandler extends ChannelInboundHandlerAdapter {
//Netty自带的,用于保存Channel集合的数据
//此处用jdk的集合来保存也是没有毛病的,就是处理起来没有自带的便捷
private static ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
//客户端注册连接的处理
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
//给其他客户端发送消息
channels.writeAndFlush(Unpooled.copiedBuffer("客户端:" + ctx.channel().remoteAddress() + " 上线了\n" +
"\n", CharsetUtil.UTF_8));
//将当前channel加入到集合中
channels.add(ctx.channel());
}
//客户端断开连接的处理
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
//进入到这个方法中,集合中已经将 channel 删除了
//给其他客户端发送消息
channels.writeAndFlush(Unpooled.copiedBuffer("客户端:" + ctx.channel().remoteAddress() + " 下线了\n" +
"\n", CharsetUtil.UTF_8));
}
//转发客户端发送的消息
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
channels.forEach(channel -> {
if(channel == ctx.channel()){
channel.writeAndFlush(Unpooled.copiedBuffer("自己:" + msg + "\n\r", CharsetUtil.UTF_8));
}else{
channel.writeAndFlush(Unpooled.copiedBuffer("客户端 " + channel.remoteAddress() + ": " + msg + "\n\r", CharsetUtil.UTF_8));
}
});
}
/**
* 心跳检测
* @param ctx
* @param evt
* @throws Exception
*/
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
if(evt instanceof IdleStateEvent){
IdleStateEvent event = (IdleStateEvent) evt;
switch (event.state()){
case READER_IDLE:
System.out.println("读空闲的处理逻辑");
break;
case WRITER_IDLE:
System.out.println("写空闲的处理逻辑");
// 不处理
break;
case ALL_IDLE:
System.out.println("读写空闲的处理逻辑");
// 不处理
break;
}
}
}
//出现异常
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
客户端代码
package com.netty.test.groupchat;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import java.util.Scanner;
public class NettyGroupChatClient {
public static void main(String[] args) throws InterruptedException {
//创建 EventLoopGroup
EventLoopGroup group = new NioEventLoopGroup();
//创建Netty启动器
Bootstrap bootstrap = new Bootstrap();
try {
//绑定Netty启动参数
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.handler(new ChannelInitializer<SocketChannel>() {
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//Netty自带的编解码Handler
pipeline.addLast("encoder", new StringEncoder());
pipeline.addLast("decoder", new StringDecoder());
//自定义handler,收发消息使用
pipeline.addLast("myClientHandler", new NettyGroupChatClientHandler());
}
});
//绑定端口 8888
ChannelFuture channelFuture = bootstrap.connect("localhost",8888).sync();
//客户端输入聊天的消息内容
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()){
String msg = scanner.nextLine();
channelFuture.channel().writeAndFlush("客户端" + channelFuture.channel().remoteAddress() + "说:" + msg + "\n\r");
}
//异步监听关闭事件
channelFuture.channel().closeFuture().sync();
}finally {
//优雅关闭
group.shutdownGracefully();
}
}
}
客户端处理业务代码
package com.netty.test.groupchat;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
public class NettyGroupChatClientHandler extends ChannelInboundHandlerAdapter {
//读取数据
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println(msg + "\n\r");
}
//出现异常
public v
oid exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
代码分析
代码和上一个Netty模型时的demo代码高度重合
大致过程:
创建线程组,创建netty服务器,绑定服务器参数,绑定对应的Handler,用于处理逻辑
心跳检测:
服务器端的 pipeline 添加一个 IdleStateHandler,这是netty提供的心跳检测处理器,参数为 没读、没写、没读写的 事件调用时间
添加 IdleStateHandler 后,在它后面的 Handler 中重写 userEventTriggered 方法,pipeline 底层双向链表,后面的就是后添加的那个 Handler
当规定时间,有 没读、没写、没读写的 事件发生是,会回调我们重写的 userEventTriggered 方法
底层就是一个延时定时任务,通过比较时间,触发事件
心跳检测作用:
异常断开时,没有调用我们的 正常退出的 方法,但是服务器端不知道,这时候可以通过心跳检测
可以在客户端写个伪代码,隔一会和服务端通讯一次
如果N长时间后 没有读写操作,则认为异常断开,手动删除这个客户端
这个demo是一个简单的群聊系统,如果要改为一个一对一聊天,要如何呢?
改为一对一聊天主要需要改动的是serverHandler
将channel的集合用map存起来,key是用户对象,value是channel对象
a用户和b用户私私聊,那就将他们俩个的channel从Map中取出来,就能实现私聊了
websocket长连接
浏览器和服务器之间发送请求都是基于Http协议的,Http协议是基于tcp协议的,建立一次连接要经历3次握手四次挥手
Http协议都是一次性的连接,发送求情->请求响应->请求连接销毁
如果用tcp协议开发一个聊天功能,那么耗费的资源是巨大的,需要频繁的建立断开连接
websocket
是一种相对于Http请求的长连接,并且这种连接是双向的, 建立连接后,可以进行双向通讯,通讯后也不会立刻销毁当前连接
Netty实现和浏览器长连接代码
代码主要分为服务端代码和浏览器端代码
代码基本和上面案例的区别不大, 所以就不全粘出代码了,只将区别的部分粘贴出来
服务器端代码
服务端代码和以上案例基本一致,只有添加的handler不同
//打印日志
pipeline.addLast("logging",new LoggingHandler("DEBUG"));//设置log监听器,并且日志级别为debug,方便观察运行流程
//websocket协议本身是基于http协议的,所以这边也要使用http解编码器
pipeline.addLast("http-codec",new HttpServerCodec());
//netty是基于分段请求的,HttpObjectAggregator的作用是将请求分段再聚合,参数是聚合字节的最大长度
pipeline.addLast("aggregator",new HttpObjectAggregator(65536));
//用于大数据的分区传输
pipeline.addLast("http-chunked",new ChunkedWriteHandler());
//参数是访问路径 服务客户端访问服务器的时候指定的url是:ws://localhost:8888/sayHello
pipeline.addLast(new WebSocketServerProtocolHandler("/sayHello"));
//自定义的业务handler
pipeline.addLast("handler",new WebSocketHandler());
Handler的业务代码
集成 SimpleChannelInboundHandler 上面的案例也可以继承这个类,父子关系,添加泛型 TextWebSocketFrame
发送消息时 ctx.writeAndFlush(new TextWebSocketFrame("发送数据内容是" + textWebSocketFrame.text() ));
浏览器代码
浏览器端的代码大多都是以一种回调的形式实现的
重要的过程包括,建立连接,发送消息,接受回调消息,断开连接等
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<script type="text/javascript">
var socket;
//如果浏览器支持WebSocket
if(window.WebSocket){
//参数就是与服务器连接的地址
socket = new WebSocket("ws://localhost:8888/sayHello");
//客户端收到服务器消息的时候就会执行这个回调方法
socket.onmessage = function (event) {
var ta = document.getElementById("responseText");
ta.value = ta.value + "\n"+event.data;
}
//连接建立的回调函数
socket.onopen = function(event){
var ta = document.getElementById("responseText");
ta.value = "连接开启";
}
//连接断掉的回调函数
socket.onclose = function (event) {
var ta = document.getElementById("responseText");
ta.value = ta.value +"\n"+"连接关闭";
}
}else{
alert("浏览器不支持WebSocket!");
}
//发送数据
function send(message){
if(!window.WebSocket){
return;
}
//当websocket状态打开
if(socket.readyState == WebSocket.OPEN){
socket.send(message);
}else{
alert("连接没有开启");
}
}
</script>
<form onsubmit="return false">
<textarea name = "message" style="width: 400px;height: 200px"></textarea>
<input type ="button" value="给服务器发送数据" onclick="send(this.form.message.value);">
<textarea id ="responseText" style="width: 400px;height: 200px;"></textarea>
<input type="button" onclick="javascript:document.getElementById('responseText').value=''" value="清空接收数据">
</form>
</body>
</html>
ProtoBuf
ProtoBuf 简介
平时进行开发接口,最多的数据传输形式就是 Http+json 的形式,但是这种网络传输效率比较差,而且不能跨语言
ProtoBuf是google公司的开源出来的一种效率高,可以跨平台,跨语言,高扩展的结构化数据存储结构
网络传输数据需要是二进制的形式
如果发送的数据是一个对象,传输前将这个对象转换成二进制的形式就是序列化,接受这个二进制数据转换为需要的对象就是反序列化
ProtoBuf 的使用
导入对应的maven依赖
编写.proto文件,定义各种类和属性
使用 proto.exe 将.proto文件编译城java文件
Netty也提供了对ProtoBuf的支持
.proto文件编写
syntax="proto3";//ProtoBuf的依赖版本是3
option optimize_for=SPEED;//加快解析
option java_package="com.netty.test"; //指定生成到那个包下
option java_outer_classname="MyProtoBufInfo"; //指定生成的外部类名称
//protobuf 可以使用message 管理自定义的类
message MyMessage{
//定义一个枚举
enum DataType{
StudentType = 0; //在proto3 要求enum编号从0开始
Student = 1;
}
DataType data_type=1; //用data_type标识传的是哪一个枚举类型
//oneof表示每次枚举类型最多只能出现定义的message(Student、Teacher)的其中一个
oneof dataBody{
Student student = 2;
Teacher teacher = 3;
}
}
//定义传输使用的Student类
//int32 是protoBuf提供的类型,官网有它的类型对应关系,下面也会贴出来
message Student{
int32 id=1; //1代表的不是id为1,而是这个类的第一个属性
string name =2;//2代表的不是name为2,而是这个类的第二个属性
}
message Teacher{
int32 id=1; //1代表的不是id为1,而是这个类的第一个属性
string name =2;//2代表的不是name为2,而是这个类的第二个属性
}
上面文件中的1,2,3,4不是具体的属性值,而是第几个的意思
因为传输时可能有很多种对象,所以创建了一个枚举,里面是传输时可能用到的对象。
如果传输只会出现一个对象,那么message 的这个代码块就不需要了
Protobuf 数据类型对应
使用protoc.exe生成.java文件
在网上下载 proto.exe ,使用对应的命令生成java文件,并放在项目中
服务端代码
bootstrap.group(bossGroup, workerGroup) //设置两个线程组
.channel(NioServerSocketChannel.class) //使用 NioServerSocketChannel 作为服务器的通道实现
.option(ChannelOption.SO_BACKLOG, 128) //设置线程队列得到连接个数
.childOption(ChannelOption.SO_KEEPALIVE, true) //设置保持活动连接状态
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//加入Netty提供的Protobuf编码处理器,参数是我们上面自定义的.proto文件内容
//如果编写时只有一个类,没有message 代码快,此处不需要.MyMessage
pipeline .addLast("decoder", new ProtobufDecoder(MyProtoBufInfo.MyMessage.getDefaultInstance()));
ch.pipeline().addLast(new NettyServerTestHandler());
}
});
服务端Handler代码
/**
* 直接用泛型为 MyProtoBufInfo.MyMessage,上面自定义的
*/
public class NettyServerTestHandler extends SimpleChannelInboundHandler<MyProtoBufInfo.MyMessage> {
/**
* 读取客户端发送过来的消息
*/
@Override
public void channelRead0(ChannelHandlerContext ctx, MyProtoBufInfo.MyMessage msg) throws Exception {
//根据dataType显示不同信息
MyProtoBufInfo.MyMessage.DataType dataType = msg.getDataType();
if (dataType == MyProtoBufInfo.MyMessage.DataType.StudentType) {
MyProtoBufInfo.Student student = msg.getStudent();
System.out.println("学生id="+student.getId()+"学生姓名="+student.getName());
} else if (dataType == MyProtoBufInfo.MyMessage.DataType.TeacherType) {
MyProtoBufInfo.Teacher teacher = msg.getTeacher();
System.out.println("老师id="+teacher.getId()+"老师姓名="+teacher.getName());
} else {
System.out.println("传输的类型不正确");
}
}
}
客户端代码
bootstrap.group(group) //设置线程组
.channel(NioSocketChannel.class) //设置客户端通道的实现类(使用反射)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//Netty提供的解码器
ch.pipeline().addLast("encoder", new ProtobufEncoder());
//加入自己的处理器
ch.pipeline().addLast(new NettyClientTestHandler());
}
});
客户端Handler代码
public class NettyClientTestHandler extends ChannelInboundHandlerAdapter {
/**
* 当通道就绪就会触发
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//随机发送student或者teacher对象
int random = new Random().nextInt(2);
MyProtoBufInfo.MyMessage myMessage = null;
if (random == 0) {
myMessage = MyProtoBufInfo.MyMessage.newBuilder().setDataType(MyProtoBufInfo.MyMessage.DataType.StudentType)
.setStudent(MyProtoBufInfo.Student.newBuilder().setId(100).setName("我是学生").build()).build();
}else{
myMessage = MyProtoBufInfo.MyMessage.newBuilder().setDataType(MyProtoBufInfo.MyMessage.DataType.TeacherType)
.setTeacher(MyProtoBufInfo.Teacher.newBuilder().setId(666).setName("我是老师").build()).build();
}
ctx.writeAndFlush(myMessage);
}
}
Netty编解码器
java编解码
java的序列化也是一种编解码,序列化也可以称之为编码,反序列化可以称之为解码
序列化(编码):将浏览器传递的参数进行序列化,成为二进制字节数组在网络中进行传输
反序列化(解码):从网络接受的二进制的字节数组,转化为对象
java序列化的缺点:
效率低,二进制后流变大了,无法跨语言
Netty编解码器
用处
编码器:将消息对象转成字节,int,long或其他序列形式在网络上传输。
解码器:负责将消息从字节,int,long或其他序列形式转成指定的消息对象。
不管是编码器还是解码器,其实都是pipeline中handler链中的一员
所以假设第一个Handler是转成int的,那么转为int后会将这个int传参到第二个handler中
出站时调用编码器:ChannelOutboundHandler,入站时调用解码器:ChannelInboundHandler
出站:就是需要调用编码器的时候
从客户端发送消息给服务端
从服务端发送消息给客户端
入站:就是需要调用解码器的时候
服务端接受到从客户端发送的消息
客户端接收到从服务端发送的消息
Netty解码器
Netty主要提供了 ByteToMessageDecoder 和 MessageToMessageDecoder 两个抽象类
这两个类的父级都是 ChannelInboundHandler,证实了他们就是handler链中的一员
ByteToMessageDecoder :主要用于字节转换为消息体
MessageToMessageDecoder :从一种消息转换为另一种消息(对象)
继承这两个类,都需要重写 decode 方法,写自己的解码逻辑,需要判断字节数,可能出现粘包拆包
public class ToIntegerDecoder extends ByteToMessageDecoder {
/**
* 解码
* @param ctx 上下文对象
* @param in 需要解码的数据
* @param out 解码后的有效数据列表,我们需要将解码后的数据添加到这个List中
* @throws Exception
*/
@Override
public void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
//4是因为int类型是4个字节,这个方法要将编码数据解码为int类型
//如果上面集成的是 ReplayingDecoder,则不需要判断字节数,ReplayingDecoder是ByteToMessageDecoder 的子类
if (in.readableBytes() >= 4) {
out.add(in.readInt());
} }
}
如果编码和解码的数据不一致的情况,读取数据时,就会直接将编码的数据输出
下图中蓝色的方法,就是判断数据一致的方法
Netty编码器
Netty主要提供了 ByteToMessageCodec 和 MessageToMessageCodec 两个抽象类
这个编码器的两个类和上面的解码器类的对应关系一目了然了
继承这两个类,都需要重写 encode 方法,写自己的解码逻辑
//可以直接指定泛型进行后续处理
public class IntegerToByteEncoder extends MessageToByteEncoder<Integer> {
/**
* 解码
* @param ctx 上下文对象
* @param in 需要解码的数据
* @param out 输出流
* @throws Exception
*/
@Override
protected void encode(ChannelHandlerContext ctx, Integer msg, ByteBuf out) throws Exception {
//将Integer转成二进制字节流写入ByteBuf中
out.writeInt(msg);
}
}
注意事项以及总结
解码时要判断传输的数据字节长度,因为可能出现粘包拆包的情况
在读取数据的时候,如果编解码类型不一致,则直接输出编码的数据,就不是解码的数据了
假如解码时是按4个字节(int)进行处理的,而编码时传的是 一个长度为 16 的UTF8字符串,那么,会循环调用四次解码方法
还有很多其他的编解码器就不一一介绍了
粘包拆包
tcp协议发送数据是以流的形式,它并不知道业务数据是什么样子的,它会对发送的数据进行拆分或者合并
如果发送的流数据小,并且在很小的间隔区间有很多次,则会将这些流数据合并进行发送,如果很大,就拆分
以上图为例:
要发送的数据为 a和b
流数据可能会把a分为4份.第一次发送的是a的第一份数据,然后之后,会把a的剩余数据和b一起传输
也可能会把a和b一起传输
这个会根据实际情况就行拆分数据,并封装在一起
粘包拆包代码演示
客户端处理器循环5次发送消息
//客户端注册连接的处理
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
for(int i = 1; i < 6;i++){
ctx.writeAndFlush(Unpooled.copiedBuffer("我是第"+i+"次发送",CharsetUtil.UTF_8));
}
}
相同代码我启动了4次,每次服务端接受到的消息都是不一样的
粘包拆包解决
解决方案:
使用自定义的编解码方式,创建一个对象,假设对象有两个值一个content(内容)和一个length(内容的长度)
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
/**
* 自定义编码器
*/
public class MyEncoder extends MessageToByteEncoder<TcpInfo> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, TcpInfo tcpInfo, ByteBuf byteBuf) throws Exception {
//输出内容和内容长度
byteBuf.writeInt(tcpInfo.getLength());
byteBuf.writeBytes(tcpInfo.getContent());
}
}
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;
import java.util.List;
/**
* 自定义解码器,继承ReplayingDecoder不用判断字节长度
*/
public class MyDecoder extends ReplayingDecoder {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
//创建TcpInfo对象
TcpInfo info = new TcpInfo();
//内容的长度
info.setLength(byteBuf.readInt());
//内容
byte[] content = new byte[info.getLength()];
byteBuf.readBytes(content);
info.setContent(content);
list.add(info);
}
}
/**
* 客户端连接后发送消息
* @param ctx
* @throws Exception
*/
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for(int i = 1; i < 6;i++){
//每次发送的消息用TcpInfo 对象封装起来
TcpInfo info = new TcpInfo();
byte[] bytes = ("我是第"+i+"次发送").getBytes();
info.setContent(bytes);
info.setLength(bytes.length);
ctx.writeAndFlush(info);
}
}
/**
* 服务端接收客户端发送的消息
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
TcpInfo tcpInfo = (TcpInfo) msg;
System.out.println(new String(tcpInfo.getContent(),CharsetUtil.UTF_8));
}
编解码器的Handler要加入到对应的pipeline中
在次执行后发现,打印数据的顺序是我们想要的顺序了,不会像一开始一样打印的结果每次都不一样