JAVA【Netty】第三讲(持续更新)
文章目录
Google Protobuf
编码和解码的基本介绍
- 编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码[示意图]
codec
(编解码器)的组成部分有两个:decoder
(解码器)和encoder
(编码器)。encoder
负责把业务数据转换成字节码数据,decoder
负责把字节码数据转换成业务数据
Netty 本身的编码解码的机制和问题分析
Netty
自身提供了一些codec
(编解码器)Netty
提供的编码器
StringEncoder
:对字符串数据进行编码。ObjectEncoder
:对Java对象进行编码。
Netty
提供的解码器
StringDecoder
,对字符串数据进行解码ObjectDecoder
,对 Java 对象进行解码
Netty
本身自带的ObjectDecoder
和ObjectEncoder
可以用来实现POJO
对象或各种业务对象的编码和解码,底层使用的仍是Java序列化技术,而Java序列化技术本身效率就不高,存在如下问题
- 无法跨语言
- 序列化后的体积太大,是二进制编码的5倍多。
- 序列化性能太低
- 引出新的解决方案[
Google
的Protobuf
]
Protobuf
Protobuf
基本介绍和使用示意图Protobuf
是Google
发布的开源项目,全称Google Protocol Buffers
,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或RPC
[远程过程调用remote procedure call
]数据交换格式。目前很多公司 从http + json 转向tcp + protobuf
,效率会更高。- 参考文档:https://developers.google.com/protocol-buffers/docs/proto 语言指南
Protobuf
是以message
的方式来管理数据的.- 支持跨平台、跨语言,即[客户端和服务器端可以是不同的语言编写的](支持目前绝大多数语言,例如
C++
、C#
、Java
、python
等) - 高性能,高可靠性
- 使用
protobuf
编译器能自动生成代码,Protobuf
是将类的定义使用.proto
文件进行描述。说明,在idea
中编写.proto
文件时,会自动提示是否下载.ptoto
编写插件.可以让语法高亮。 - 然后通过
protoc.exe
编译器根据.proto
自动生成.java
文件 protobuf
使用示意图
Protobuf 快速入门实例
编写程序,使用 Protobuf
完成如下功能
- 客户端可以发送一个
StudentPoJo
对象到服务器(通过Protobuf
编码) - 服务端能接收
StudentPoJo
对象,并显示信息(通过Protobuf
解码)
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.6.1</version>
</dependency>
Student.proto
syntax = "proto3"; //版本
option java_outer_classname = "StudentPOJO";//生成的外部类名,同时也是文件名
//protobuf 使用message 管理数据
message Student { //会在 StudentPOJO 外部类生成一个内部类 Student, 他是真正发送的POJO对象
int32 id = 1; // Student 类中有 一个属性 名字为 id 类型为int32(protobuf类型) 1表示属性序号,不是值
string name = 2;
}
编译
protoc.exe --java_out=.Student.proto
将生成的 StudentPOJO 放入到项目使用
生成的StudentPOJO代码太长就不贴在这里了
NettyServer
package com.qf.netty.protobuf;
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.protobuf.ProtobufDecoder;
public class NettyServer {
public static void main(String[] args) throws Exception {
//创建boss group 和worker group
//DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
//bossGroup指定的是一条线程
//workerGroup默认情况下是核心数*2
EventLoopGroup bossGroup=new NioEventLoopGroup(1);
EventLoopGroup workerGroup=new NioEventLoopGroup();
//创建服务端的启动对象,设置配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
//使用连式编程来进行设置
try {
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 sc) throws Exception {
System.out.println("服务器的值:"+sc.hashCode());
sc.pipeline().addLast("decoder",new ProtobufDecoder(StudentPOJO.Student.getDefaultInstance()));
sc.pipeline().addLast(new NettyServerHandler());
}
});
System.out.println("服务器已经准备好....");
//绑定端口
ChannelFuture sync = bootstrap.bind(6668).sync();
//对我们的future对象进行监听,异步关注响应是否成功
sync.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (sync.isSuccess()){
System.out.println("端口 6668 成功");
}else if (sync.isCancelled()){
System.out.println("端口 6668 失败");
}
}
});
//对关闭通道进行监听
sync.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
NettyServerHandler
package com.qf.netty.protobuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
import java.util.concurrent.TimeUnit;
public class NettyServerHandler extends SimpleChannelInboundHandler<StudentPOJO.Student> {
//读取数据实际(读取来自客户端的数据)
/* @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
*//* StudentPOJO.Student student= (StudentPOJO.Student) msg;
System.out.println("客户端发送的数据:"+student.getId()+"名字="+student.getName());*//*
}
*/
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, StudentPOJO.Student student) throws Exception {
System.out.println(student);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//服务器写出数据
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端 ~~~喵喵喵2",CharsetUtil.UTF_8));
}
/**
* 异常情况下的处理
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
NettyClient
package com.qf.netty.protobuf;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
//客户端需要第一个事件循环组
NioEventLoopGroup group = new NioEventLoopGroup();
try{
//创建客户端启动
Bootstrap bootstrap = new Bootstrap();
//设置相关的参数
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast("encoder",new ProtobufEncoder());
socketChannel.pipeline().addLast(new NettyClientHandler());
}
});
//连接端口
ChannelFuture sync = bootstrap.connect("127.0.0.1", 6668).sync();
//对关闭通道进行监听
sync.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
NettyClientHandler
package com.qf.netty.protobuf;
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
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client:"+ctx);
StudentPOJO.Student student = StudentPOJO.Student.newBuilder().setId(4).setName("xiaohdfhsf").build();
ctx.writeAndFlush(student);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf= (ByteBuf) msg;
System.out.println("服务器回复的消息:"+byteBuf.toString(CharsetUtil.UTF_8));
System.out.println("服务器的地址:"+ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.channel().close();
}
}
Protobuf 快速入门实例 2
- 编写程序,使用
Protobuf
完成如下功能 - 客户端可以随机发送
StudentPoJo
/WorkerPoJo
对象到服务器(通过Protobuf
编码) - 服务端能接收
StudentPoJo
/WorkerPoJo
对象(需要判断是哪种类型),并显示信息(通过Protobuf
解码)
proto
syntax = "proto3";
option optimize_for = SPEED; // 加快解析
option java_package="com.atguigu.netty.codec2"; //指定生成到哪个包下
option java_outer_classname="MyDataInfo"; // 外部类名, 文件名
/*
1.protobuf 可以使用message 管理其他的message。最终决定使用哪一个message作为传输对象
2.假设你某个项目需要传输20个对象,你不可能新建20个proto文件吧。此时你就可以
在一个文件里定义20个message,最后再用一个总的message(比方说这里的MyMessage)
来决定在实际传输时真正需要传输哪一个对象
3.因为你实际传输的时候大部分情况传输的都是一个对象,所以下面用oneof进行了限制
4.是否可以传多个对象呢?我个人认为是可以的,比如可以通过map(目前我也不太了解proto的语法)
*/
message MyMessage {
//定义一个枚举类型,DataType如果是0则表示一个Student对象实例,DataType这个名称自定义
enum DataType {
StudentType = 0; //在proto3 要求enum的编号从0开始
WorkerType = 1;
}
//用data_type 来标识传的是哪一个枚举类型,这里才真正开始定义MyMessage的数据类型
DataType data_type = 1; //所有后面的数字都只是编号而已
/*
1.oneof关键字 表示每次枚举类型进行传输时,限制最多只能传输一个对象。
dataBody名称也是自定义的
2.为什么这里的序号是2呢?因为上面DataType data_type = 1 占了第一个序号了
3.MyMessage里真正出现的类型只有两个
①DataType类型
②Student类型或者Worker类型(这两个在真正传输的时候只会有一个出现)
*/
oneof dataBody {
Student student = 2; //注意这后面的数字也都只是编号而已
Worker worker = 3;
}
}
message Student {
int32 id = 1;//Student类的属性
string name = 2; //
}
message Worker {
string name=1;
int32 age=2;
}
NettyServer
package com.qf.netty.code2;
import com.qf.netty.protobuf.StudentPOJO;
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.protobuf.ProtobufDecoder;
public class NettyServer {
public static void main(String[] args) throws Exception {
//创建boss group 和worker group
//DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
//bossGroup指定的是一条线程
//workerGroup默认情况下是核心数*2
EventLoopGroup bossGroup=new NioEventLoopGroup(1);
EventLoopGroup workerGroup=new NioEventLoopGroup();
//创建服务端的启动对象,设置配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
//使用连式编程来进行设置
try {
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 sc) throws Exception {
System.out.println("服务器的值:"+sc.hashCode());
sc.pipeline().addLast("decoder",new ProtobufDecoder( MyDataInfo.MyMessage.getDefaultInstance()));
sc.pipeline().addLast(new NettyServerHandler());
}
});
System.out.println("服务器已经准备好....");
//绑定端口
ChannelFuture sync = bootstrap.bind(6668).sync();
//对我们的future对象进行监听,异步关注响应是否成功
sync.addListener(new ChannelFutureListener() {
@Override
public void operationComplete(ChannelFuture channelFuture) throws Exception {
if (sync.isSuccess()){
System.out.println("端口 6668 成功");
}else if (sync.isCancelled()){
System.out.println("端口 6668 失败");
}
}
});
//对关闭通道进行监听
sync.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
NettyServerHandler
package com.qf.netty.code2;
import com.qf.netty.protobuf.StudentPOJO;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
public class NettyServerHandler extends SimpleChannelInboundHandler< MyDataInfo.MyMessage> {
//读取数据实际(读取来自客户端的数据)
/* @Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
*//* StudentPOJO.Student student= (StudentPOJO.Student) msg;
System.out.println("客户端发送的数据:"+student.getId()+"名字="+student.getName());*//*
}
*/
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, MyDataInfo.MyMessage msg) throws Exception {
MyDataInfo.MyMessage.DataType dataType = msg.getDataType();
if (dataType==MyDataInfo.MyMessage.DataType.StudentType){
MyDataInfo.Student student = msg.getStudent();
System.out.println("学生的名字:"+student.getName()+"学生的学号:"+student.getId());
}else if (dataType==MyDataInfo.MyMessage.DataType.WorkerType){
MyDataInfo.Worker worker = msg.getWorker();
System.out.println("工人的年龄:"+worker.getAge()+"工人的名字:"+worker.getName());
}else {
System.out.println("输入有误");
}
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//服务器写出数据
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端 ~~~喵喵喵2",CharsetUtil.UTF_8));
}
/**
* 异常情况下的处理
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
NettyClient
package com.qf.netty.code2;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
//客户端需要第一个事件循环组
NioEventLoopGroup group = new NioEventLoopGroup();
try{
//创建客户端启动
Bootstrap bootstrap = new Bootstrap();
//设置相关的参数
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast("encoder",new ProtobufEncoder());
socketChannel.pipeline().addLast(new NettyClientHandler());
}
});
//连接端口
ChannelFuture sync = bootstrap.connect("127.0.0.1", 6668).sync();
//对关闭通道进行监听
sync.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
NettyClientHandler
package com.qf.netty.code2;
import com.qf.netty.protobuf.StudentPOJO;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
import java.util.Random;
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 通道就绪就会触发次方法
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
int random=new Random().nextInt();
MyDataInfo.MyMessage message=null;
if (random==0){
message=MyDataInfo.MyMessage.newBuilder().setDataType(MyDataInfo.MyMessage.DataType.StudentType).setStudent(MyDataInfo.Student.newBuilder().setId(5).setName("小荔枝的").build()).build();
}else{
message=MyDataInfo.MyMessage.newBuilder().setDataType(MyDataInfo.MyMessage.DataType.WorkerType).setWorker(MyDataInfo.Worker.newBuilder().setAge(22).setName("工人").build()).build();
}
ctx.writeAndFlush(message);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf byteBuf= (ByteBuf) msg;
System.out.println("服务器回复的消息:"+byteBuf.toString(CharsetUtil.UTF_8));
System.out.println("服务器的地址:"+ctx.channel().remoteAddress());
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.channel().close();
}
}
Netty编解码器和 Handler 调用机制
基本说明
Netty
的组件设计:Netty
的主要组件有Channel
、EventLoop
、ChannelFuture
、ChannelHandler
、ChannelPipe
等ChannelHandler
充当了处理入站和出站数据的应用程序逻辑的容器。例如,实现ChannelInboundHandler
接口(或ChannelInboundHandlerAdapter
),你就可以接收入站事件和数据,这些数据会被业务逻辑处理。当要给客户端发送响应时,也可以从ChannelInboundHandler
冲刷数据。业务逻辑通常写在一个或者多个ChannelInboundHandler
中。ChannelOutboundHandler
原理一样,只不过它是用来处理出站数据的ChannelPipeline
提供了ChannelHandler
链的容器。以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过pipeline
中的一系列ChannelOutboundHandler
,并被这些Handler
处理,反之则称为入站的
出站,入站如果搞不清楚,看下面的Netty的handler链的调用机制,通过一个例子和图讲清楚
编码解码器
- 当
Netty
发送或者接受一个消息的时候,就将会发生一次数据转换。入站消息会被解码:从字节转换为另一种格式(比如java
对象);如果是出站消息,它会被编码成字节。 Netty
提供一系列实用的编解码器,他们都实现了ChannelInboundHadnler
或者ChannelOutboundHandler
接口。在这些类中,channelRead
方法已经被重写了。以入站为例,对于每个从入站Channel
读取的消息,这个方法会被调用。随后,它将调用由解码器所提供的decode()
方法进行解码,并将已经解码的字节转发给ChannelPipeline
中的下一个ChannelInboundHandler
。
解码器 - ByteToMessageDecoder
- 关系继承图
- 由于不可能知道远程节点是否会一次性发送一个完整的信息,
tcp
有可能出现粘包拆包的问题,这个类会对入站数据进行缓冲,直到它准备好被处理.【后面有说TCP的粘包和拆包问题】 - 一个关于
ByteToMessageDecoder
实例分析
Netty的handler链的调用机制
实例要求:
- 使用自定义的编码器和解码器来说明
Netty
的handler
调用机制
客户端发送long
-> 服务器
服务端发送long
-> 客户端
读者可以看下这个图,带着这个图去看下面的例子。
MyServer
package com.qf.netty.Inboudandoutbound;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class MyServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup=new NioEventLoopGroup(1);
EventLoopGroup workerGroup=new NioEventLoopGroup();
try {
//创建服务端的启动对象,设置配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new MyServerInitializer());
//绑定端口
ChannelFuture sync = bootstrap.bind(6668).sync();
//对关闭通道进行监听
sync.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
MyServerInitializer
package com.qf.netty.Inboudandoutbound;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//先编码
pipeline.addLast(new MyByteToLongEncoder());
pipeline.addLast(new MyByteToLongDecoder2());
pipeline.addLast(new MyServerHandler());
}
}
MyServerHandler
package com.qf.netty.Inboudandoutbound;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class MyServerHandler extends SimpleChannelInboundHandler<Long> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, Long aLong) throws Exception {
System.out.println("服务器接收到消息:"+channelHandlerContext.channel().remoteAddress()+"数据:"+aLong);
channelHandlerContext.writeAndFlush(6888L);
}
}
MyClient
package com.qf.netty.Inboudandoutbound;
import com.qf.netty.code2.NettyClientHandler;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
public class MyClient {
public static void main(String[] args) throws InterruptedException {
//客户端需要第一个事件循环组
NioEventLoopGroup group = new NioEventLoopGroup();
try{
//创建客户端启动
Bootstrap bootstrap = new Bootstrap();
//设置相关的参数
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new MyClientInitializer());
//连接端口
ChannelFuture sync = bootstrap.connect("127.0.0.1", 6668).sync();
//对关闭通道进行监听
sync.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
MyClientInitializer
package com.qf.netty.Inboudandoutbound;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new MyByteToLongDecoder());
pipeline.addLast(new MyByteToLongEncoder());
pipeline.addLast(new MyClientHandler());
}
}
MyClientHandler
package com.qf.netty.Inboudandoutbound;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class MyClientHandler extends SimpleChannelInboundHandler<Long> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, Long aLong) throws Exception {
System.out.println("服务器的ip=" + channelHandlerContext.channel().remoteAddress());
System.out.println("收到服务器消息=" + aLong);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("MyClientHandler 被调用");
ctx.writeAndFlush(88888L);
super.channelActive(ctx);
}
}
MyByteToLongEncoder
package com.qf.netty.Inboudandoutbound;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.MessageToByteEncoder;
public class MyByteToLongEncoder extends MessageToByteEncoder<Long> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, Long o, ByteBuf byteBuf) throws Exception {
System.out.println("MyByteToLongEncoder 被调用");
System.out.println("msg:"+o);
byteBuf.writeLong(o);
}
}
MyByteToLongDecoder
package com.qf.netty.Inboudandoutbound;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.MessageToMessageDecoder;
import java.util.List;
public class MyByteToLongDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> list) throws Exception {
System.out.println("MyByteToLongDecoder 被调用");
if (in.readableBytes()>=8){
list.add(in.readLong());
}
}
}
效果
出站入站
关于出站入站,很多人可能有点迷糊
1)客户端有出站入站,服务端也有出站入站
2)以客户端为例,如果有服务端传送的数据到达客户端,那么对于客户端来说就是入站;
如果客户端传送数据到服务端,那么对于客户端来说就是出站;
同理,对于服务端来说,也是一样的,有数据来就是入站,有数据输出就是出站
3)为什么服务端和客户端的Serverhandler都是继承SimpleChannelInboundHandler
,而没有ChannelOutboundHandler
出站类?
实际上当我们在handler中调用ctx.writeAndFlush()方法后,就会将数据交给ChannelOutboundHandler进行出站处理,只是我们没有去定义出站类而已,若有需求可以自己去实现ChannelOutboundHandler出站类
4)总结就是客户端和服务端都有出站和入站的操作
**服务端发数据给客户端:**服务端—>出站—>Socket通道—>入站—>客户端
**客户端发数据给服务端:**客户端—>出站—>Socket通道—>入站—>服务端
下面是Netty官方源码给的图,我个人觉的不是太好理解,上面的图好理解一些
ByteToMessageDecoder的小细节
package com.qf.netty.Inboudandoutbound;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class MyClientHandler extends SimpleChannelInboundHandler<Long> {
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, Long aLong) throws Exception {
System.out.println("服务器的ip=" + channelHandlerContext.channel().remoteAddress());
System.out.println("收到服务器消息=" + aLong);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("MyClientHandler 被调用");
ctx.writeAndFlush(88888L);
super.channelActive(ctx);
}
}
package com.qf.netty.Inboudandoutbound;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.codec.MessageToMessageDecoder;
import java.util.List;
public class MyByteToLongDecoder extends ByteToMessageDecoder {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf in, List<Object> list) throws Exception {
System.out.println("MyByteToLongDecoder 被调用");
if (in.readableBytes()>=8){
list.add(in.readLong());
}
}
}
- 由于发送的字符串是16字节,根据上面注释说的内容,decode会被调用两次
如下图验证结果:
- 同时又引出了一个小问题
当我们MyClientHandler
传一个Long时,会调用我们的MyLongToByteEncoder
的编码器。那么控制台就会打印这样一句话:MyLongToByteEncoder encode 被调用。但是这里并没有调用编码器,这是为什么呢?
MyClientHandler
这个处理器的后一个处理器是MyLongToByteEncoder
MyLongToByteEncoder
的父类是MessageToByteEncoder
,在MessageToByteEncoder
中有下面的一个方法
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ByteBuf buf = null;
try {
//这里会判断当前msg 是不是应该处理的类型,如果是就处理,不是就跳过encode
if (acceptOutboundMessage(msg)) {
@SuppressWarnings("unchecked")
I cast = (I) msg;
buf = allocateBuffer(ctx, cast, preferDirect);
try {
encode(ctx, cast, buf);
} finally {
ReferenceCountUtil.release(cast);
}
if (buf.isReadable()) {
ctx.write(buf, promise);
} else {
buf.release();
ctx.write(Unpooled.EMPTY_BUFFER, promise);
}
buf = null;
} else {
ctx.write(msg, promise);
}
} catch (EncoderException e) {
throw e;
} catch (Throwable e) {
throw new EncoderException(e);
} finally {
if (buf != null) {
buf.release();
}
}
}
3.当我们以这样的形式发送数据
ctx.writeAndFlush(Unpooled.copiedBuffer("abcdabcdabcdabcd",CharsetUtil.UTF_8));
这两个类型并不匹配,也就不会走编码器。因此我们编写 Encoder 是要注意传入的数据类型和处理的数据类型一致
结论:
- 不论解码器
handler
还是编码器handler
即接收的消息类型必须与待处理的消息类型一致,否则该handler
不会被执行 - 在解码器进行数据解码时,需要判断缓存区(
ByteBuf
)的数据是否足够,否则接收到的结果会期望结果可能不一致。
解码器 - ReplayingDecoder
public abstract class ReplayingDecoder<S> extends ByteToMessageDecoder
ReplayingDecoder
扩展了ByteToMessageDecoder
类,使用这个类,我们不必调用readableBytes()
方法,也就不用判断还有没有足够的数据来读取。参数S
指定了用户状态管理的类型,其中Void
代表不需要状态管理- 应用实例:使用
ReplayingDecoder
编写解码器,对前面的案例进行简化[案例演示]
package com.qf.netty.Inboudandoutbound;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;
import java.util.List;
public class MyByteToLongDecoder2 extends ReplayingDecoder<Void> {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
list.add(byteBuf.readLong());
}
}
ReplayingDecoder
使用方便,但它也有一些局限性:
- 并不是所有的
ByteBuf
操作都被支持,如果调用了一个不被支持的方法,将会抛出一个UnsupportedOperationException
。 ReplayingDecoder
在某些情况下可能稍慢于ByteToMessageDecoder
,例如网络缓慢并且消息格式复杂时,消息会被拆成了多个碎片,速度变慢
其它编解码器
LineBasedFrameDecoder
:这个类在Netty
内部也有使用,它使用行尾控制字符(\n或者\r\n)作为分隔符来解析数据。DelimiterBasedFrameDecoder
:使用自定义的特殊字符作为消息的分隔符。HttpObjectDecoder
:一个HTTP
数据的解码器LengthFieldBasedFrameDecoder
:通过指定长度来标识整包消息,这样就可以自动的处理黏包和半包消息。
Log4j 整合到 Netty
- 在
Maven
中添加对Log4j
的依赖在pom.xml
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.25</version>
<scope>test</scope>
</dependency>
- 配置
Log4j
,在resources/log4j.properties
log4j.rootLogger=DEBUG,stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=[%p]%C{1}-%m%n
- 演示整合
TCP 粘包和拆包及解决方案
TCP 粘包和拆包基本介绍
TCP
是面向连接的,面向流的,提供高可靠性服务。收发两端(客户端和服务器端)都要有一一成对的socket
,因此,发送端为了将多个发给接收端的包,更有效的发给对方,使用了优化方法(Nagle
算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端就难于分辨出完整的数据包了,因为面向流的通信是无消息保护边界的- 由于
TCP
无消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题,看一张图 TCP
粘包、拆包图解
假设客户端分别发送了两个数据包 D1
和 D2
给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:
- 服务端分两次读取到了两个独立的数据包,分别是
D1
和D2
,没有粘包和拆包 - 服务端一次接受到了两个数据包,
D1
和D2
粘合在一起,称之为TCP
粘包 - 服务端分两次读取到了数据包,第一次读取到了完整的
D1
包和D2
包的部分内容,第二次读取到了D2
包的剩余内容,这称之为TCP
拆包 - 服务端分两次读取到了数据包,第一次读取到了
D1
包的部分内容D1_1
,第二次读取到了D1
包的剩余部分内容D1_2
和完整的D2
包。
TCP 粘包和拆包现象实例
在编写 Netty
程序时,如果没有做处理,就会发生粘包和拆包的问题
看一个具体的实例:
MyServer
package com.qf.netty.Inboudandoutbound;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class MyServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup=new NioEventLoopGroup(1);
EventLoopGroup workerGroup=new NioEventLoopGroup();
try {
//创建服务端的启动对象,设置配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new MyServerInitializer());
//绑定端口
ChannelFuture sync = bootstrap.bind(6668).sync();
//对关闭通道进行监听
sync.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
MyServerInitializer
package com.qf.netty.tcp;
import com.qf.netty.Inboudandoutbound.MyByteToLongDecoder2;
import com.qf.netty.Inboudandoutbound.MyByteToLongEncoder;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//先编码
pipeline.addLast(new MyServerHandler());
}
}
MyServerHandler
package com.qf.netty.tcp;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.nio.charset.Charset;
import java.util.UUID;
public class MyServerHandler extends SimpleChannelInboundHandler<ByteBuf> {
public int count;
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf msg) throws Exception {
int i = msg.readableBytes();
byte[] bytes=new byte[i];
msg.readBytes(bytes);
String message=new String(bytes, Charset.forName("utf-8"));
System.out.println("来自客户端的数据:"+message);
System.out.println("来自客户端的消息次数:"+(++count));
System.out.println();
String uuid= UUID.randomUUID().toString();
ByteBuf byteBuf = Unpooled.copiedBuffer((uuid).getBytes("utf-8"));
channelHandlerContext.writeAndFlush(byteBuf);
}
}
MyClient
package com.qf.netty.tcp;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
public class MyClient {
public static void main(String[] args) throws InterruptedException {
//客户端需要第一个事件循环组
NioEventLoopGroup group = new NioEventLoopGroup();
try{
//创建客户端启动
Bootstrap bootstrap = new Bootstrap();
//设置相关的参数
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new MyClientInitializer());
//连接端口
ChannelFuture sync = bootstrap.connect("127.0.0.1", 6668).sync();
//对关闭通道进行监听
sync.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
MyClientInitializer
package com.qf.netty.tcp;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new MyClientHandler());
}
}
MyClientHandler
package com.qf.netty.tcp;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.nio.charset.Charset;
public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
public int count;
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf msg) throws Exception {
int i = msg.readableBytes();
byte[] bytes=new byte[i];
msg.readBytes(bytes);
String message=new String(bytes, Charset.forName("utf-8"));
System.out.println("接收到来自服务器的数据"+message+" "+(++count));
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 10; i++) {
ByteBuf byteBuf = Unpooled.copiedBuffer(("hello server" + i).getBytes("utf-8"));
ctx.writeAndFlush(byteBuf);
}
}
}
效果
第一次运行:
Client
Server
第二次运行:
Client
Server
可以看到第一次运行时,服务器一次性将10个数据都接收了,第二次运行时分六次接收的,这就很形象的看出了TCP的粘包现象。
TCP 粘包和拆包解决方案
- 常用方案:使用自定义协议+编解码器来解决
- 关键就是要解决服务器端每次读取数据长度的问题,这个问题解决,就不会出现服务器多读或少读数据的问题,从而避免的
TCP
粘包、拆包。
看一个具体的实例
- 要求客户端发送
5
个Message
对象,客户端每次发送一个Message
对象 - 服务器端每次接收一个
Message
,分5
次进行解码,每读取到一个Message
,会回复一个Message
对象给客户端。
MessageProtocol
package com.qf.netty.tcpprotoco;
public class MessageProtocol {
private int len;
private byte[] content;
public int getLen() {
return len;
}
public void setLen(int len) {
this.len = len;
}
public byte[] getContent() {
return content;
}
public void setContent(byte[] content) {
this.content = content;
}
}
MyServer
package com.qf.netty.tcpprotoco;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
public class MyServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup=new NioEventLoopGroup(1);
EventLoopGroup workerGroup=new NioEventLoopGroup();
try {
//创建服务端的启动对象,设置配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new MyServerInitializer());
//绑定端口
ChannelFuture sync = bootstrap.bind(6668).sync();
//对关闭通道进行监听
sync.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
MyServerInitializer
package com.qf.netty.tcpprotoco;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
//先编码
pipeline.addLast(new MyByteToLongDecoder2());
pipeline.addLast(new MyMessageEncoder());
pipeline.addLast(new MyServerHandler());
}
}
MyServerHandler
package com.qf.netty.tcpprotoco;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.nio.charset.Charset;
import java.util.UUID;
public class MyServerHandler extends SimpleChannelInboundHandler<MessageProtocol> {
public int count;
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, MessageProtocol msg) throws Exception {
int i = msg.getLen();
byte[] content = msg.getContent();
String message=new String(content, Charset.forName("utf-8"));
System.out.println("来自客户端的数据:"+message);
System.out.println("来自客户端的消息次数:"+(++count));
System.out.println();
String uuid= UUID.randomUUID().toString();
int len=uuid.getBytes(Charset.forName("utf-8")).length;
byte[] bytes = uuid.getBytes(Charset.forName("utf-8"));
MessageProtocol messageProtocol=new MessageProtocol();
messageProtocol.setLen(len);
messageProtocol.setContent(bytes);
channelHandlerContext.writeAndFlush(messageProtocol);
}
}
MyClient
package com.qf.netty.tcpprotoco;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioSocketChannel;
public class MyClient {
public static void main(String[] args) throws InterruptedException {
//客户端需要第一个事件循环组
NioEventLoopGroup group = new NioEventLoopGroup();
try{
//创建客户端启动
Bootstrap bootstrap = new Bootstrap();
//设置相关的参数
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new MyClientInitializer());
//连接端口
ChannelFuture sync = bootstrap.connect("127.0.0.1", 6668).sync();
//对关闭通道进行监听
sync.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
MyClientInitializer
package com.qf.netty.tcpprotoco;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new MyMessageEncoder());
pipeline.addLast(new MyByteToLongDecoder2());
pipeline.addLast(new MyClientHandler());
}
}
MyClientHandler
package com.qf.netty.tcpprotoco;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.nio.charset.Charset;
public class MyClientHandler extends SimpleChannelInboundHandler<MessageProtocol> {
public int count;
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, MessageProtocol msg) throws Exception {
int len = msg.getLen();
byte[] content = msg.getContent();
String message=new String(content, Charset.forName("utf-8"));
System.out.println("接收到来自服务器的数据"+message+" "+(++count));
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
for (int i = 0; i < 5; i++) {
String msg="今天天气热,适合吃火锅";
byte[] content = msg.getBytes(Charset.forName("utf-8"));
int length=content.length;
MessageProtocol messageProtocol=new MessageProtocol();
messageProtocol.setLen(length);
messageProtocol.setContent(content);
ctx.writeAndFlush(messageProtocol);
}
}
}
MyMessageEncoder
package com.qf.netty.tcpprotoco;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.MessageToByteEncoder;
public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocol> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, MessageProtocol msg, ByteBuf byteBuf) throws Exception {
int len=msg.getLen();
byte[] content = msg.getContent();
byteBuf.writeInt(len);
byteBuf.writeBytes(content);
}
}
MyByteToLongDecoder2
package com.qf.netty.tcpprotoco;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;
import java.util.List;
public class MyByteToLongDecoder2 extends ReplayingDecoder<Void> {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
int i = byteBuf.readInt();
byte[] content=new byte[i];
byteBuf.readBytes(content);
MessageProtocol messageProtocol=new MessageProtocol();
messageProtocol.setLen(i);
messageProtocol.setContent(content);
list.add(messageProtocol);
}
}
效果
Client输出
MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
MyMessageEncoder encode 方法被调用
//下面是客户端开始一个一个的收到服务端的回复
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=1b5286dd-0fc2-4f62-9bf7-d5fad84179b5
客户端接收消息数量=1
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=653d18cb-ab72-4163-8b95-09c94ecac873
客户端接收消息数量=2
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=3be6e403-91bb-4437-ada8-6cdb9eb7ef00
客户端接收消息数量=3
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=94c8f306-fd9c-455a-956c-16698ce4150b
客户端接收消息数量=4
MyMessageDecoder decode 被调用
客户端接收到消息如下
长度=36
内容=7890de9c-0fa2-4317-8de1-1d464315fa1b
客户端接收消息数量=5
Server输出
MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=27
内容=今天天气冷,吃火锅
服务器接收到消息包数量=1
服务端开始回复消息------
MyMessageEncoder encode 方法被调用
MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=27
内容=今天天气冷,吃火锅
服务器接收到消息包数量=2
服务端开始回复消息------
MyMessageEncoder encode 方法被调用
MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=27
内容=今天天气冷,吃火锅
服务器接收到消息包数量=3
服务端开始回复消息------
MyMessageEncoder encode 方法被调用
MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=27
内容=今天天气冷,吃火锅
服务器接收到消息包数量=4
服务端开始回复消息------
MyMessageEncoder encode 方法被调用
MyMessageDecoder decode 被调用
服务器接收到信息如下
长度=27
内容=今天天气冷,吃火锅
服务器接收到消息包数量=5
服务端开始回复消息------
MyMessageEncoder encode 方法被调用
无论运行几次,Server都是分5次接收的,这样就解决了TCP粘包问题。
Netty 心跳(heartbeat)服务源码剖析
源码剖析目的
Netty
作为一个网络框架,提供了诸多功能,比如编码解码等,Netty
还提供了非常重 要的一个服务-----心跳机制heartbeat
。通过心跳检查对方是否有效,这是 RPC
框架 中是必不可少的功能。下面我们分析一下Netty内部 心跳服务源码实现。
源码剖析
说明
- Netty 提供了 IdleStateHandler ,ReadTimeoutHandler,WriteTimeoutHandler 三 个Handler 检测连接的有效性,重点分析
IdleStateHandler
. - 如图
3)ReadTimeout
事件和WriteTimeout
事件都会自动关闭连接,而且,属于异常处理,所以,这里只是介绍以下,我们重点看IdleStateHandler。
IdleStateHandler分析
private final boolean observeOutput; //是否考虑出站慢的问题,默认是false
private final long readerIdleTimeNanos; //读空闲的时间,单位纳秒,默认为0,禁用
private final long writerIdleTimeNanos; //写空闲的时间,单位纳秒,默认为0,禁用
private final long allIdleTimeNanos; //读或者写空闲的时间,单位纳秒,默认为0,禁用
handlerAdded
方法
当该handler
被添加到pipeline
中时,则调用initialize
方法
private void initialize(ChannelHandlerContext ctx) {
// Avoid the case where destroy() is called before scheduling timeouts.
// See: https://github.com/netty/netty/issues/143
switch (state) {
case 1:
case 2:
return;
}
state = 1;
initOutputChanged(ctx);
lastReadTime = lastWriteTime = ticksInNanos();
if (readerIdleTimeNanos > 0) {
//这里的schedule方法会调用eventLoop的schedule方法,将定时任务添加进队列中
readerIdleTimeout = schedule(ctx, new ReaderIdleTimeoutTask(ctx),
readerIdleTimeNanos, TimeUnit.NANOSECONDS);
}
if (writerIdleTimeNanos > 0) {
writerIdleTimeout = schedule(ctx, new WriterIdleTimeoutTask(ctx),
writerIdleTimeNanos, TimeUnit.NANOSECONDS);
}
if (allIdleTimeNanos > 0) {
allIdleTimeout = schedule(ctx, new AllIdleTimeoutTask(ctx),
allIdleTimeNanos, TimeUnit.NANOSECONDS);
}
}
只要给定的参数大于0,就创建一个定时任务,每个事件都创建。同时,将state状态设置为1,防止重复初始化调用initOutputChanged方法,初始化“监控出站数据属性”。
三个任务类
这3个定时任务分别对应读,写,读或者写事件。共有一个父类(AbstractIdleTask
)。这个父类提供了一个模板方法
private final class ReaderIdleTimeoutTask extends AbstractIdleTask {
ReaderIdleTimeoutTask(ChannelHandlerContext ctx) {
super(ctx);
}
@Override
protected void run(ChannelHandlerContext ctx) {
long nextDelay = readerIdleTimeNanos;
if (!reading) {
nextDelay -= ticksInNanos() - lastReadTime;
}
if (nextDelay <= 0) {
// Reader is idle - set a new timeout and notify the callback.
readerIdleTimeout = schedule(ctx, this, readerIdleTimeNanos, TimeUnit.NANOSECONDS);
boolean first = firstReaderIdleEvent;
firstReaderIdleEvent = false;
try {
IdleStateEvent event = newIdleStateEvent(IdleState.READER_IDLE, first);
channelIdle(ctx, event);
} catch (Throwable t) {
ctx.fireExceptionCaught(t);
}
} else {
// Read occurred before the timeout - set a new timeout with shorter delay.
readerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
}
}
}
说明:
1)得到用户设置的超时时间。
2)如果读取操作结束了(执行了channelReadComplete方法设置),就用当前时间减去给定时间和最后一次读(执操作的时间行了channelReadComplete方法设置),如果小于O,就触发事件。反之,继续放入队
列。间隔时间是新的计算时间。
3)触发的逻辑是:首先将任务再次放到队列,时间是刚开始设置的时间,返回一个promise对象,用于做
取消操作。然后,设置first属性为false,表示,下一次读取不再是第一次了,这个属性在channelRead方
法会被改成rue。
4)创建一个IdleStateEvent类型的写事件对象,将此对象传递给用户的UserEventTriggered方法。完成触
发事件的操作。
5)总的来说,每次读取操作都会记录一个时间,定时任务时间到了,会计算当前时间和最后一次读的时间的间隔,如果间隔超过了设置的时间,就触发UserEventTriggered方法。∥前面介绍IdleStateHandler说过,可以看一下
写事件的run方法(即VriterIdleTimeoutTask的run方法)分析
@Override
protected void run(ChannelHandlerContext ctx) {
long lastWriteTime = IdleStateHandler.this.lastWriteTime;
long nextDelay = writerIdleTimeNanos - (ticksInNanos() - lastWriteTime);
if (nextDelay <= 0) {
// Writer is idle - set a new timeout and notify the callback.
writerIdleTimeout = schedule(ctx, this, writerIdleTimeNanos, TimeUnit.NANOSECONDS);
boolean first = firstWriterIdleEvent;
firstWriterIdleEvent = false;
try {
if (hasOutputChanged(ctx, first)) {
return;
}
IdleStateEvent event = newIdleStateEvent(IdleState.WRITER_IDLE, first);
channelIdle(ctx, event);
} catch (Throwable t) {
ctx.fireExceptionCaught(t);
}
} else {
// Write occurred before the timeout - set a new timeout with shorter delay.
writerIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
}
}
说明:
写任务的代码逻辑基本和读任务的逻辑一样,唯一不同的就是有一个针对出站较慢数据的判断
hasOutputChanged
所有事件的run方法(即AllldleTimeoutTask的rum方法)分析
@Override
protected void run(ChannelHandlerContext ctx) {
long nextDelay = allIdleTimeNanos;
if (!reading) {
nextDelay -= ticksInNanos() - Math.max(lastReadTime, lastWriteTime);
}
if (nextDelay <= 0) {
// Both reader and writer are idle - set a new timeout and
// notify the callback.
allIdleTimeout = schedule(ctx, this, allIdleTimeNanos, TimeUnit.NANOSECONDS);
boolean first = firstAllIdleEvent;
firstAllIdleEvent = false;
try {
if (hasOutputChanged(ctx, first)) {
return;
}
IdleStateEvent event = newIdleStateEvent(IdleState.ALL_IDLE, first);
channelIdle(ctx, event);
} catch (Throwable t) {
ctx.fireExceptionCaught(t);
}
} else {
// Either read or write occurred before the timeout - set a new
// timeout with shorter delay.
allIdleTimeout = schedule(ctx, this, nextDelay, TimeUnit.NANOSECONDS);
}
}
说明:
1)表示这个监控着所有的事件。当读写事件发生时,都会记录。代码逻辑和写事件的的基本一致:
2)需要大家注意的地方是
long nextDelay allldleTimeNanos;
if (!reading){
∥当前时间减去最后一次写或读的时间,若大于0,说明超时了
nextDelay -ticksInNanos()-Math.max(lastReadTime,last Write Time);
3)这里的时间计算是取读写事件中的最大值来的。然后像写事件一样,判断是否发生了写的慢的情况。
10.小结Nety的心跳机制
l)IdleStateHandler可以实现心跳功能,当服务器和客户端没有任何读写交互时,并超过了给定的时间,则会
触发用户handler的userEventTriggered方法。用户可以在这个方法中尝试向对方发送信息,如果发送失败,则关
闭连接。
2)IdleStateHandler的实现基于EventLoop的定时任务,每次读写都会记录一个值,在定时任务运行的时候,
通过计算当前时间和设置时间和上次事件发生时间的结果,来判断是否空闲。
3)内部有3个定时任务,分别对应读事件,写事件,读写事件。通常用户监听读写事件就足够了。
4)同时,IdleStateHandler内部也考虑了一些极端情况:客户端接收缓慢,一次接收数据的速度超过了设置的
空闲时间。Netty通过构造方法中的observeOutput属性来决定是否对出站缓冲区的情况进行判断。
5)如果出站缓慢,Ny不认为这是空闲,也就不触发空闲事件。但第一次无论如何也是要触发的。因为第一
次无法判断是出站缓慢还是空闲。当然,出站缓慢的话,可能造成OOM,OOM比空闲的问题更大。
6)所以,当你的应用出现了内存溢出,OOM之类,并且写空闲极少发生(使用了observeOutput为true),
那么就需要注意是不是数据出站速度过慢。
7)还有一个注意的地方:就是ReadTimeoutHandler,它继承自IdleStateHandler,当触发读空闲事件的时候,
就触发ctx.fireExceptionCaught方法,并传入一个ReadTimeoutException,然后关闭Socket。
8)而WriteTimeoutHandler的实现不是基于IdleStateHandler的,他的原理是,当调用write方法的时候,会
创建一个定时任务,任务内容是根据传入的promise的完成情况来判断是否超出了写的时间。当定时任务根据指
定时间开始运行,发现promise的isDone方法返回false,表明还没有写完,说明超时了,则抛出异常。当write
方法完成后,会打断定时任务。
用 Netty 自己实现简单的RPC
RPC 基本介绍
RPC(Remote Procedure Call)
—远程过程调用,是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程- 两个或多个应用程序都分布在不同的服务器上,它们之间的调用都像是本地方法调用一样(如图)
过程:
-
调用者(
Caller
),调用远程API(Remote API
) -
调用远程API会通过一个RPC代理(
RpcProxy
) -
RPC代理再去调用
RpcInvoker
(这个是PRC的调用者) -
RpcInvoker
通过RPC连接器(RpcConnector
) -
RPC连接器用两台机器规定好的PRC协议(
RpcProtocol
)把数据进行编码 -
接着RPC连接器通过RpcChannel通道发送到对方的PRC接收器(RpcAcceptor)
-
PRC接收器通过PRC协议进行解码拿到数据
-
然后将数据传给
RpcProcessor
-
RpcProcessor
再传给RpcInvoker
-
RpcInvoker
调用Remote API
-
最后推给被调用者(Callee)
-
常见的
RPC
框架有:比较知名的如阿里的Dubbo
、Google
的gRPC
、Go
语言的rpcx
、Apache
的thrift
,Spring
旗下的SpringCloud
。
我们的RPC 调用流程图
RPC 调用流程说明
- 服务消费方(
client
)以本地调用方式调用服务 client stub
接收到调用后负责将方法、参数等封装成能够进行网络传输的消息体client stub
将消息进行编码并发送到服务端server stub
收到消息后进行解码server stub
根据解码结果调用本地的服务- 本地服务执行并将结果返回给
server stub
server stub
将返回导入结果进行编码并发送至消费方client stub
接收到消息并进行解码- 服务消费方(
client
)得到结果
小结:RPC
的目标就是将 2 - 8
这些步骤都封装起来,用户无需关心这些细节,可以像调用本地方法一样即可完成远程服务调用
己实现 Dubbo RPC(基于 Netty)
需求说明
Dubbo
底层使用了Netty
作为网络通讯框架,要求用Netty
实现一个简单的RPC
框架- 模仿
Dubbo
,消费者和提供者约定接口和协议,消费者远程调用提供者的服务,提供者返回一个字符串,消费者打印提供者返回的数据。底层网络通信使用Netty 4.1.20
设计说明
- 创建一个接口,定义抽象方法。用于消费者和提供者之间的约定。
- 创建一个提供者,该类需要监听消费者的请求,并按照约定返回数据。
- 创建一个消费者,该类需要透明的调用自己不存在的方法,内部需要使用
Netty
请求提供者返回数据 - 开发的分析图
代码
封装的RPC
可以把这块代码理解成封装的dubbo
NettyServer
package com.qf.netty.dubbo.netty;
import com.qf.netty.group.NettyGroupChatInitializer;
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 NettyServer {
//端口
private int port;
//ip
private String hostname;
public static void startServer(String hostname,int port) throws InterruptedException {
startServer0(hostname,port);
}
public static void startServer0(String hostname, int port) throws InterruptedException {
//bossGroup指定的是一条线程
//workerGroup默认情况下是核心数*2
EventLoopGroup bossGroup=new NioEventLoopGroup(1);
EventLoopGroup workerGroup=new NioEventLoopGroup();
//创建服务端的启动对象,设置配置参数
ServerBootstrap bootstrap = new ServerBootstrap();
try {
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 sc) throws Exception {
ChannelPipeline pipeline = sc.pipeline();
pipeline.addLast("decoder",new StringDecoder());
pipeline.addLast("encoder",new StringEncoder());
pipeline.addLast(new NettyServerHandler());
}
});
//绑定端口
ChannelFuture sync = bootstrap.bind(hostname,port);
//对关闭通道进行监听
sync.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
NettyServerHandler
package com.qf.netty.dubbo.netty;
import com.qf.netty.dubbo.provider.HelloServiceImpl;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
public class NettyServerHandler extends SimpleChannelInboundHandler<String> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
System.out.println("---服务端开始收到来自客户单的消息---");
System.out.println("接收到来自客户端的信息"+msg);
String hello = new HelloServiceImpl().hello(msg);
ctx.writeAndFlush(hello);
}
/**
* 异常处理
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
NettyClientHandler
package com.qf.netty.dubbo.netty;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import java.util.concurrent.Callable;
public class NettyClientHandler extends SimpleChannelInboundHandler<String> implements Callable {
private ChannelHandlerContext context; //上下文
private String result; //最后返回的结果
private String para; //客户端传入的参数
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("channelActive 被调用");
context=ctx;
}
@Override
protected synchronized void channelRead0(ChannelHandlerContext ctx, String msg) throws Exception {
result=msg;
notify();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
//被代理对象调用,发送数据到服务器-》wait-》等待被唤醒-》返回结果
@Override
public synchronized Object call() throws Exception {
System.out.println("call1被调用");
context.writeAndFlush(para);
wait();
//等待被唤醒之后,返回结果
System.out.println("call2被调用");
return result;
}
void setPara(String para){
System.out.println("set para");
this.para=para;
}
}
NettyClient
package com.qf.netty.dubbo.netty;
import com.qf.netty.group.NettyGroupChatClientHandler;
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.lang.reflect.Proxy;
import java.util.Scanner;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class NettyClient {
//创建线程池
private static ExecutorService executor= Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
private static NettyClientHandler client;
private int count;
public Object getBean(final Class<?> serivceClass, final String providerName) {
return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class<?>[]{serivceClass}, (proxy, method, args) -> {
System.out.println("(proxy, method, args) 进入...." + (++count) + " 次");
//{} 部分的代码,客户端每调用一次 hello, 就会进入到该代码
if (client == null) {
initClient();
}
//设置要发给服务器端的信息
//providerName:协议头,args[0]:就是客户端要发送给服务端的数据
client.setPara(providerName + args[0]);
//
return executor.submit(client).get();
});
}
//初始化客户端
private static void initClient() {
client = new NettyClientHandler();
//创建EventLoopGroup
NioEventLoopGroup group = new NioEventLoopGroup();
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(
new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new StringDecoder());
pipeline.addLast(new StringEncoder());
pipeline.addLast(client);
}
}
);
try {
bootstrap.connect("127.0.0.1", 7000).sync();
} catch (Exception e) {
e.printStackTrace();
}
}
}
接口HelloService
package com.qf.netty.dubbo.publicinterface;
public interface HelloService {
String hello(String name);
}
HelloServiceImpl
package com.qf.netty.dubbo.provider;
import com.qf.netty.dubbo.publicinterface.HelloService;
public class HelloServiceImpl implements HelloService {
private static int count = 0;
@Override
public String hello(String name) {
System.out.println("收到来自客户端的信息"+name);
if (name!=null){
return "你好,客户端我已接收到你的消息"+"客户端的消息:"+name+"第" + (++count) + " 次 \n";
}else{
return "你好,客户端我已接收到你的消息";
}
}
}
ServerBootStrap
package com.qf.netty.dubbo.provider;
import com.qf.netty.dubbo.netty.NettyServer;
public class ServerBootStrap {
public static void main(String[] args) throws InterruptedException {
NettyServer.startServer("127.0.0.1",7000);
}
}
ClientBootStrap
package com.qf.netty.dubbo.consumer;
import com.qf.netty.dubbo.netty.NettyClient;
import com.qf.netty.dubbo.publicinterface.HelloService;
public class ClientBootStrap {
//这里定义协议头
public static final String providerName = "HelloService#hello#";
public static void main(String[] args) throws InterruptedException {
//创建一个消费者
NettyClient customer = new NettyClient();
//创建代理对象
HelloService service = (HelloService) customer.getBean(HelloService.class, providerName);
for (;;){
String hello = service.hello("你好,dubbo~");
System.out.println("调用的结果"+hello);
Thread.sleep(2000);
}
}
}
调用过程
ClientBootstrap#main
发起调用- 走到下面这一行代码后
HelloService service = (HelloService) customer.getBean(HelloService.class, providerName);
-
调用
NettyClient#getBean
,在此方法里与服务端建立链接。 -
于是就执行
NettyClientHandler#channelActive
-
接着回到
NettyClient#getBean
调用NettyClientHandler#setPara
,调用完之后再回到NettyClient#getBean
,用线程池提交任务 -
因为用线程池提交了任务,就准备执行
NettyClientHandler#call
线程任务 -
在
NettyClientHandler#call
中发送数据给服务提供者
context.writeAndFlush(para);
由于还没收到服务提供者的数据结果,所以wait住
- 来到了服务提供者这边,从Socket通道中收到了数据,所以执行
NettyServerHandler#channelRead
,然后因为此方法中执行了
String result = new HelloServiceImpl().hello(msg.toString().substring(msg.toString().lastIndexOf("#") + 1));
-
就去
HelloServiceImpl#hello
中执行业务逻辑,返回数据给NettyServerHandler#channelRead
,NettyServerHandler#channelRead
再把数据发给客户端 -
NettyClientHandler#channelRead
收到服务提供者发来的数据,唤醒之前wait的线程 -
所以之前wait的线程从
NettyClientHandler#call
苏醒,返回result给NettyClient#getBean
-
NettyClient#getBean
get()到数据,ClientBootstrap#main
中的此函数调用返回,得到服务端提供的数据。
String res = service.hello("你好 dubbo~");
13.至此,一次RPC调用结束。
效果
ClientBootstrap打印
(proxy, method, args) 进入....1 次
setPara
channelActive 被调用
call1 被调用
channelRead 被调用
call2 被调用
调用的结果 res= 你好客户端, 我已经收到你的消息。消息为:[你好 dubbo~] ,第1 次
(proxy, method, args) 进入....2 次
setPara
call1 被调用
channelRead 被调用
call2 被调用
调用的结果 res= 你好客户端, 我已经收到你的消息。消息为:[你好 dubbo~] ,第2 次
(proxy, method, args) 进入....3 次
setPara
call1 被调用
channelRead 被调用
call2 被调用
调用的结果 res= 你好客户端, 我已经收到你的消息。消息为:[你好 dubbo~] ,第3 次
(proxy, method, args) 进入....4 次
setPara
call1 被调用
channelRead 被调用
call2 被调用
调用的结果 res= 你好客户端, 我已经收到你的消息。消息为:[你好 dubbo~] ,第4 次
(proxy, method, args) 进入....5 次
setPara
call1 被调用
channelRead 被调用
call2 被调用
调用的结果 res= 你好客户端, 我已经收到你的消息。消息为:[你好 dubbo~] ,第5 次
ServerBootstrap打印
服务提供方开始提供服务~~
---服务端开始收到来自客户单的消息---
原始消息:HelloService#hello#你好 dubbo~
收到客户端消息=你好 dubbo~
---服务端开始收到来自客户单的消息---
原始消息:HelloService#hello#你好 dubbo~
收到客户端消息=你好 dubbo~
---服务端开始收到来自客户单的消息---
原始消息:HelloService#hello#你好 dubbo~
收到客户端消息=你好 dubbo~
---服务端开始收到来自客户单的消息---
原始消息:HelloService#hello#你好 dubbo~
收到客户端消息=你好 dubbo~
---服务端开始收到来自客户单的消息---
原始消息:HelloService#hello#你好 dubbo~
收到客户端消息=你好 dubbo~