文章较长,是自己在学习了尚硅谷Netty视频后,整理的笔记。大家可根据目录查找自己想了解的知识部分。文章中不合理之处,还请指正😃
原生NIO存在的问题
- NIO的类库和API繁杂,使用麻烦,需要熟练掌握Selector,ServerSocketChannel,SocketChannel,ByteBuffer等
- 需要具备其他的额外技能,要熟悉Java多线程编程,因为NIO编程涉及到Reactor模式(反应器模式),你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序
- 开发工作量和难度都非常大:例如客户端面临断连重连,网络闪断,半包读写,失败缓存,网络拥塞和异常流的处理等
- JDK NIO 的bug:例如Epoll bug,它会导致Selector空轮询,最终导致cpu 100%。直到JDK1.7版本该问题依旧存在,没有被根本解决
Netty的介绍
Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.
- Netty是由JBOSS提供的一个Java开源框架,目前在Github上维护。官网:https://netty.io
- Netty可以帮助你快速,简单的开发一个网络应用,相当于简化和流程化了NIO的开发过程
- Netty是目前最流行的NIO框架,Netty在互联网领域,大数据分布式计算领域,游戏行业,通信行业获得了广泛的应用。知名的Elasticsearch、Dubbo框架内部都采用了Netty
- Netty是一个异步的,基于事件驱动的网络应用框架,可以用于快速开发高性能,高可靠性的网络IO程序
- Netty主要针对在TCP协议下,面向Clients端的高并发应用,或者Peer-to-Peer场景下的大量数据持续(实时)传输的应用
- Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景
- 要透彻理解Netty,必须先学习NIO,方便阅读Netty源码
总结:Netty是一个异步的,基于事件驱动的高性能NIO网络通信框架,可以实现服务之间高效,实时的网络通信
Netty的优点
Netty对JDK自带的NIO的API进行了封装,解决了上述问题
- 设计优雅:适用于各种传输类型的统一,API阻塞和非阻塞Socket,基于灵活且可扩展的事件模型,可以清晰地分离关注点,高度可定制的线程模型 -单线程,一个或多个线程池
- 使用方便:详细记录的Javadoc,用户指南和示例;没有其他依赖项,JDK1.5(Netty 3.x)或JDK1.6(Netty 4.x)就足够了
- 高性能,高吞吐量,更低延迟,减少资源消耗,最小化不必要的内存复制
- 安全:完整的SSL/TLS和StartTLS支持
- 社区活跃,不断更新:社区活跃,版本迭代周期短,发现的bug可以被及时修复,同时,更多的新功能会被加入
- 目前推荐使用的稳定版本是 Netty 4.x
Netty的应用场景
- 互联网行业
- 在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,Netty作为异步高性能的通信框架,往往作为基础通信组件被这些RPC框架使用
- 典型的应用有:阿里的分布式服务框架Dubbo,它的RPC框架使用Dubbo协议进行节点之间的网络通信,Dubbo协议默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信
- 游戏行业
- Netty作为高性能的基础通信组件,提供了TCP/UDP 和 HTTP 协议栈,方便定制开发私有协议栈,账号登陆服务器
- 地图服务器之间可以方便的通过Netty进行高性能通信
- 大数据领域
- 经典的Hadoop的高性能通信和序列化组件(Avro实现数据文件共享)的RPC框架,默认采用Netty进行跨节点通信
- 它的Netty Service 基于Netty框架二次封装实现
Netty线程模型
线程模型
目前存在的线程模型有:
- 传统阻塞I/O服务模型
- Reactor模式
根据Reactor的数量和处理资源池线程的数量的不同,主要有3种典型的实现:
- 单Reactor,单线程
- 单Reactor,多线程
- 主从Reactor,多线程
Netty线程模型(主要基于主从Reactor多线程模型做了一定的改进,其中主从Reactor多线程模型中有多个Reactor)
传统阻塞I/O服务模型
工作原理图
模型特点
- 采用阻塞IO模式获取输入的数据
- 每个连接都需要独立的线程完成完成数据的输入,业务处理,数据返回
问题分析
- 当并发数很大,就会创建大量的线程,占用很大系统资源
- 连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在read操作,造成线程资源浪费
Reactor模式
针对传统阻塞I/O服务模型的2个缺点,解决方案:
- 基于I/O复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有的连接。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理
- 基于线程池复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务
整体设计思想图
说明:
- Reactor模式,通过一个或多个输入同时传递给服务处理器的模式(基于事件驱动)
- 服务端程序处理传入的多个请求,并将它们同步分派到相应的处理线程,因此Reactor模式也叫Dispatcher模式
- Reactor模式使用IP复用监听事件,收到事件后,分发给某个线程,这点就是网络服务器高并发处理的关键
Reactor模式中核心组成:
- Reactor:Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件作出反应。就像公司的电话接线员,他接听来自客户的电话并将线路转移到适当的联系人
- Handlers:处理程序执行I/O事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor通过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操作
单Reactor单线程
原理图
优缺点
- 优点:模型简单,没有多线程,进程通信,竞争的问题,全部都在一个线程中完成
- 缺点:
- 性能问题:只有一个线程,无法有效发挥多核cpu的性能。handler在处理某个连接上的业务时,整个线程无法处理其他的连接事件,很容易导致性能瓶颈。
- 可靠性问题:线程意外终止,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障
单Reactor多线程
原理图
优缺点
- 优点:可以充分的利用多核cpu的处理能力
- 缺点:多线程数据共享和访问比较复杂,reactor处理所有的事件的监听和响应,是采用单线程处理的,在高并发场景容易出现性能瓶颈
主从Reactor多线程
原理图
优缺点:
- 优点:父线程和子线程的数据交互简单,职责明确,父线程只需要接收新连接,子线程完成后续的业务处理。Reactor主线程只需要把新连接传给子线程,子线程无需返回数据。
- 缺点:编程复杂度高
结合实例:这种模型在许多项目中广泛应用。包括Nginx主从Reactor多进程模型,Memcached主从多线程,Netty主从多线程模型
Reactor模式小结
简单化理解:
- 单Reactor单线程:前台接待员和服务员是同一个人,全称为客户服务
- 单Reactor多线程:1个前台接待,多个服务员,接待员只负责接待客户,具体的服务由服务员提供
- 主从Reactor多线程:多个前台接待,多个服务员
Reactor模式具有如下优点:
- 响应快,不必为单个同步事件所阻塞,虽然Reactor本身依然是同步的
- 可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销
- 扩展性好,可以方便的通过增加Reactor实例个数来充分利用cpu资源
- 复用性好,Reactor模型本身与具体事件的处理逻辑无关,具有很高的复用性
Netty模型
工作原理 - 简单版
- BossGroup线程维护selector,只关注accept
- 当接收到accept事件后,获取到对应的SocketChannel,封装成NIOSocketChannel,并注册到WorkerGroup(事件循环)的selector中,进行维护
- 当WorkerGroup线程监听到selector上注册的通道中发生了读写事件后,就进行处理(交给handler处理)。handler已经添加到通道中了
工作原理 - 进阶版
Netty工作原理图 - 详细版
Netty代码实现
/**
* @author lixing
* @date 2022-04-29 15:03
* @description Netty服务端
*/
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
// 创建BossGroup和WorkerGroup
// bossGroup只处理连接请求,真正与客户端进行的业务处理,交给workerGroup完成。这两个都是无限循环
// bossGroup和workerGroup默认含有的子线程(NioEventLoop)个数为 cpu核数*2
// 可以自定义设置bossGroup和workerGroup的NioEventLoop线程数,如果workerGroup设置子线程数为8,则如果有超过8个客户端连接,会按照1,2,3...,8,1,2...的方式循环分配
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
// 创建服务器端启动对象,配置参数
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 使用链式编程来进行设置
serverBootstrap.group(bossGroup, workerGroup) // 设置两个线程组
.channel(NioServerSocketChannel.class) // 使用NioServerSocketChannel作为服务器端的通道实现类(反射)
.option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列,得到连接个数
.childOption(ChannelOption.SO_KEEPALIVE, true) // 设置保持活动连接状态
.childHandler(new ChannelInitializer<SocketChannel>() { // 创建一个通道初始化对象(匿名对象)
// 给pipeline设置处理器
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new NettyServerHandler()); // 向管道最后追加一个处理器
}
}); // 给workerGroup的EventLoop对应的管道设置处理器
System.out.println("服务器 is ok...");
ChannelFuture cf = serverBootstrap.bind(6668).sync(); // 绑定端口,并且同步,生成了一个ChannelFuture对象
cf.channel().closeFuture().sync(); // 对关闭通道进行见监听
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
/**
* @author lixing
* @date 2022-04-29 15:38
* @description 自定义一个处理器,需要继承netty规定好的某个HandlerAdapter
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
// 读事件(读取客户端发送的数据)
// ctx 上下文对象,含有 管道pipeline,一个管道里会有很多个业务处理的handler,通道channel,地址
// msg 客户端发送的数据,是Object格式的
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("server ctx="+ctx);
System.out.println("看看channel与pipeline的关系"); // channel与pipeline是相互包含的关系,ctx中包含channel和pipeline,以及其他的信息(大部分的信息都囊括在ctx中)
Channel channel = ctx.channel();
ChannelPipeline pipeline = ctx.pipeline(); // 本质是一个双向链表
// 将msg转成一个ByteBuf
ByteBuf buf = (ByteBuf) msg;
System.out.println("接收到客户端 "+ctx.channel().remoteAddress()+" 发送的消息:"+buf.toString(CharsetUtil.UTF_8));
}
// 读事件完毕,发送数据回复给客户端
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// write + flush 将数据写入到缓存,并刷新
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端 😵 喵", CharsetUtil.UTF_8));
}
// 发生异常,则关闭通道
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
/**
* @author lixing
* @date 2022-04-29 16:19
* @description Netty客户端
*/
public class NettyClient {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new NettyClientHandler());
}
});
System.out.println("客户端 is ok...");
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
/**
* @author lixing
* @date 2022-04-29 17:24
* @description
*/
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
// 当通道就绪时,会触发
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("client ctx="+ctx);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,Server 😵 喵", CharsetUtil.UTF_8));
}
// 当通道有读取事件时,会触发
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
System.out.println("收到服务器 "+ctx.channel().remoteAddress()+" 回复的消息;"+buf.toString(CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
任务队列中task有三种典型的应用场景
- 用户程序自定义的普通任务
// NettyServerHandler.java中
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 如果这里有一个非常耗时的业务处理,就会一直阻塞在这里 (服务端阻塞)
// 解决方案1:用户程序自定义的普通任务
// 将耗时任务 -> 异步执行 -> 提交到channel对应的NIOEventLoop的taskQueue中
// 注意:eventLoop是一个线程,taskQueue是线程的任务队列
ctx.channel().eventLoop().execute(new Runnable(){
@Override
public void run(){
try{
Thread.sleep(10 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端 😵 喵", CharsetUtil.UTF_8));
}catch(Exception e){
System.out.println("发生异常:"+e.getMessage());
}
}
});
ctx.channel().eventLoop().execute(new Runnable(){
@Override
public void run(){
try{
Thread.sleep(20 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端 😵 喵2", CharsetUtil.UTF_8)); // 打印会等待30秒,因为两个任务是先后进入队列的。任务队列中多个任务先后执行,是在同一个线程中执行的
}catch(Exception e){
System.out.println("发生异常:"+e.getMessage());
}
}
});
System.out.println("go on ...");
}
- 用户自定义定时任务
// NettyServerHandler.java中
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 如果这里有一个非常耗时的业务处理,就会一直阻塞在这里 (服务端阻塞)
// 解决方案1:用户程序自定义的定时任务
// 将任务提交到channel对应的NIOEventLoop的scheduleTaskQueue中
ctx.channel().eventLoop().schedule(new Runnable(){
@Override
public void run(){
try{
Thread.sleep(5 * 1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("hello,客户端 😵 喵3", CharsetUtil.UTF_8));
}catch(Exception e){
System.out.println("发生异常:"+e.getMessage());
}
}
}, 5, TimeUnit.SECONDS);
System.out.println("go on ...");
}
- 非当前Reactor线程调用Channel的各种方法
// NettyServer.java
serverBootstrap.group(bossGroup, workerGroup) // 设置两个线程组
.channel(NioServerSocketChannel.class) // 使用NioServerSocketChannel作为服务器端的通道实现类(反射)
.option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列,得到连接个数
.childOption(ChannelOption.SO_KEEPALIVE, true) // 设置保持活动连接状态
.childHandler(new ChannelInitializer<SocketChannel>() { // 创建一个通道初始化对象(匿名对象)
// 给pipeline设置处理器
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 在此处可以获取客户端的channel,将其保存到一个集合中管理,可以在推送消息时,将业务加入到到不同channel对应的NIOEventLoop的taskQueue或scheduleTaskQueue中执行
System.out.println("客户socketChannel的hashcode="+socketChannel.hashCode());
socketChannel.pipeline().addLast(new NettyServerHandler()); // 向管道最后追加一个处理器
}
}); // 给workerGroup的EventLoop对应的管道设置处理器
Netty模型方案再说明
- Netty抽象出两组线程池,BossGroup专门负责接收客户端连接,WorkerGroup专门负责网络读写操作
- NioEventLoop表示一个不断循环执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上的socket网络通道
- NioEventLoop内部采用串行化设计,从消息的读取->解码->处理->编码->发送,始终由IO线程NioEventLoop负责
- NioEventLoopGroup下包含多个NioEventLoop
- 每个NioEventLoop中包含一个Selector,一个taskQueue
- 每个NioEventLoop的Selector上可以注册监听多个NioChannel
- 每个NioChannel只会绑定在唯一的NioEventLoop上
- 每个NioChannel都绑定有一个自己的ChannelPipeline
Netty异步模型
- 异步的概念与同步相对,当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的组件在完成后,通过状态,通知和回调来通知调用者。
- Netty中的I/O操作是异步的,包括Bind,Write,Connect等操作会简单的返回一个ChannelFuture
- 调用者并不能立刻获得结果,而是通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果
- Netty的异步模型是建立在future和callback之上的。callback就是回调,重点说future,它的核心思想是:假设一个方法run,计算过程可能非常耗时,等待run返回显然不合适,那么可以在调用run的时候,立马返回一个future,后续可以通过future去监控方法run的处理过程(即:Future-Listener机制)
Netty实现http请求与响应(实例)
/**
* @author lixing
* @date 2022-05-05 16:26
* @description netty模拟http请求
*/
public class TestServer {
public static void main(String[] args) {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try{
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new TestServerInitializer());
ChannelFuture channelFuture = serverBootstrap.bind(7777).sync();
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
/**
* @author lixing
* @date 2022-05-05 16:27
* @description
*/
public class TestServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 向管道中加入处理器
// 得到管道
ChannelPipeline pipeline = socketChannel.pipeline();
// 加入一个netty提供的httpServerCodec codec => [coder - decoder] http编解码器
// 1. httpServerCodec 是netty提供的处理http的编解码器
pipeline.addLast("MyHttpServerCodec", new HttpServerCodec());
// 2. 增加一个自定义的handler
pipeline.addLast("MyTestHttpServerHandler", new TestHttpServerHandler());
}
}
/**
* @author lixing
* @date 2022-05-05 16:26
* @description
*/
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
// 读取客户端数据
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
// 判断msg是不是httpRequest请求
if(msg instanceof HttpRequest){
HttpRequest httpRequest = (HttpRequest) msg;
// 获取uri
URI uri = new URI(httpRequest.uri());
if("/favicon.ico".equals(uri.getPath())){
System.out.println("请求了 favicon.ico,不做响应");
return;
}
System.out.println("msg类型="+msg.getClass());
System.out.println("客户端地址="+ctx.channel().remoteAddress());
// 回复信息给浏览器 [http协议]
ByteBuf content = Unpooled.copiedBuffer("hello,我是服务器", CharsetUtil.UTF_8);
// 构造一个http响应,即httpResponse
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, content);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain;charset=utf-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
// 将构建好的response返回
ctx.writeAndFlush(response);
}
}
}
启动服务端后,再浏览器地址栏输入:http://localhost:7777
,访问,可以看到页面显示文本 hello,我是服务器
。客户端实际发起了两次http请求,第一次 http://localhost:7777/
请求数据,第二次http://localhost:7777/favicon.ico
请求网站图标文件
服务端打印:
msg类型=class io.netty.handler.codec.http.DefaultHttpRequest
客户端地址=/0:0:0:0:0:0:0:1:52059
请求了 favicon.ico,不做响应
Netty核心组件
Bootstrap,ServerBootstrap
- Bootstrap是引导的意思,一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类
- 常用方法
public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup) //用于服务端,用于设置两个EventLoopGroup
public B group(EventLoopGroup group) //用于客户端,用来设置一个EventLoopGroup
public B channel(Class<? extends C> channelClass) //用来设置一个服务端的通道实现
public <T> B option(ChannelOption<T> option, T value) //用来给ServerChannel添加配置
public <T> ServerBootstrap childOption(ChannelOption<T> childOption, T value) //用来给接收到的通道添加配置
public B handler(ChannelHandler handler) //【给BossGroup设置handler】
public ServerBootstrap childHandler(ChannelHandler childHandler) //用来设置业务处理类(自定义handler)【给workerGroup设置handler】
public ChannelFuture bind(int inetPort) //用于服务端,用来设置占用的端口号
Future,ChannelFuture
- Netty中所有的IO操作都是异步的,不能立刻得知消息是否被正确处理,但是可以过一会儿等它执行完成或者直接注册一个监听,具体的实现就是通过Future和ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时,监听会自动触发注册的监听事件
- 常见的方法有:
Channel channel() //返回当前正在进行IO操作的通道
ChannelFuture sync() //等待异步操作执行完毕
Channel
- Netty网络通信的组件,能够用于执行网络I/O操作
- 通过Channel可以获得当前网络连接的通道状态
- 通过Channel可以获得网络连接的配置参数(例如接收缓冲区大小)
- Channel提供异步的网络I/O操作(如:建立连接,读写,绑定端口),异步调用意味着任何I/O调用都将立即返回,并且不保证在调用结束时所请求的I/O操作已完成
- 调用立即返回一个ChannelFuture实例,通过注册监听器到ChannelFuture上,可以在I/O操作成功,失败或取消时回调通知调用方
- 支持关联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内部机制就可以自动不断地查询这些注册的Channel是否有已准备就绪地I/O事(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个Channel
ChannelHandler
- ChannelHandler是一个接口,处理I/O事件或拦截I/O操作,并将其转发到其ChannelPipeline(业务处理链)中的下一个处理程序
- ChannelHandler本身没有提供很多方法,因为这个接口有很多方法需要实现,方便使用期间可以继承它的子类
- Channel及其实现类一览图:
Pipeline和ChannelPipeline
ChannelPipeline是一个重点:
- ChannelPipeline是一个handler的集合,它负责处理和拦截inbound或者outbound的事件和操作,相当于一个贯穿Netty的链(也可以这样理解:ChannelPipeline是保存ChannelHandler的list,用于处理或拦截Channel的入站事件和出站事件)
- ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及Channel中各个ChannelHandler如何相互交互
- 在Netty中每个channel都有且仅有一个ChannelPipeline与之对应,他们的组成关系如下:
- 常用方法:
ChannelPipeline addFirst(ChannelHandler... handlers) //把一个业务处理类(handler)添加到链中的第一个位置ChannelPipeline
addLast(ChannelHandler... handlers) //把一个业务处理类(handler)添加到链中的最后一个位置
ChannelHandlerContext
- 保存Channel相关的所有上下文信息,同时关联一个ChannelHandler对象
- 即ChannelHandlerContext中包含一个具体的事件处理器ChannelHandler,同时ChannelHandlerContext中也绑定了对应的pipeline和Channel的信息,方便对ChannelHandler进行调用
- 常用方法:
ChannelFuture close() //关闭通道
ChannelOutboundInvoker flush() //刷新
ChannelFuture writeAndFlush(Object msg) //将数据写到ChannelPipeline中当前ChannelHandler的下一个ChannelHandler开始处理(出战)
EventLoopGroup与其实现类NioEventLoopGroup
- EventLoopGroup是一组EventLoop的抽象,Netty为了更好的利用多核cpu资源,一般会有多个
Netty编解码器机制
编解码器基本介绍
编解码器实现的其实就是序列化的过程(编码器=>序列化 解码器=>反序列化)
序列化的使用场景
- 对数据进行持久化(将对象数据保存到文件系统,缓存,或db中。需要对数据进行序列化)
- 在网络中传输对象数据(发送方需要先通过编码器将对象数据转换成字节流,通过socketChannel进行传输,接收端需要通过解码器将字节流还原成原始的对象数据,进行下一步的业务逻辑处理)
业务数据网络传输的过程
Netty本身的编解码器及存在的问题
- Netty自身提供了一些codec(编解码器)
- 有:StringEncoder,StringDecoder,ObjectEncoder,ObjectDecoder
- Netty本身自带的ObjectEncoder,ObjectDecoder可以用来实现POJO对象或各种业务对象的编解码,底层使用的仍是java序列化技术。而java序列化技术本身效率不高,存在如下问题:
- 无法跨语言
- 序列化后体积太大,是二进制编码的5倍多
- 序列化的性能太低
google提出的Protobuf
- Protobuf 是 Google 发布的开源项目,全称 Google Protocol Buffers,是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储或 RPC [远程过程调用 remote procedure call ]数据交换格式。目前很多公司 从http + json 转向tcp + protobuf,效率会更高
- Protobuf 是以 message 的方式来管理数据的
- 支持跨平台、跨语言,即[客户端和服务器端可以是不同的语言编写的](支持目前绝大多数语言,例如 C++、C#、Java、python 等)
- 高性能,高可靠性
- 使用 protobuf 编译器能自动生成代码,Protobuf 是将类的定义使用 .proto 文件进行描述。说明,在 idea 中编写 .proto 文件时,会自动提示是否下载 .ptoto 编写插件.可以让语法高亮。
然后通过 protoc.exe 编译器根据 .proto 自动生成 .java 文件 - 参考文档:https://developers.google.com/protocol-buffers/docs/proto
Netty使用protobuf进行网络数据传输的流程图
Netty出站和入站机制
客户端和服务端都会有编解码器
客户端(先编码)发送数据 ——> socket通道 ——> 服务端(先解码)接收数据
其中:
- 解码decoder在InBoundHandler(入站handler)中完成的,因为服务端接收从socket通道中流过来的数据,相对于服务端来说是入站操作。而且数据传输是被编码过的字节流,所以接收后首先需要进行解码,才能进行后续的业务处理
- 编码encoder是在OutBoundHandler(出站handler)中完成的,因为客户端需要将数据发送到socket通道中,所以相对于客户端来说是出站操作。而且网络传输数据需要序列化,所以需要进行编码处理
- 而且,客户端传输数据给服务端,服务端收到后,也需要回复消息给客户端。所以,客户端,服务端的channelPipeline管道中同时包含编码encoder是在解码decoder在InBoundHandler和OutBoundHandler
- 在配置服务端serverBootStrap的childHandler,即初始化channel时,需要先向channelPipeline中添加对应的编解码器,再添加自定义的业务处理handler
TCP粘包拆包问题及解决方案
TCP粘包拆包基本介绍
- TCP是面向连接的,面向流的传输,提供高可靠性服务。收发两端都要有一一成对的socket。因此,发送端为了将多个包更有效的发送给对方,使用了优化算法(Nagle算法):即将多次间隔较小且数据量小的数据包合并成一个大的数据块,然后进行封包。这样做虽然提高了效率,但是接收端难于分辨出完整的数据包了
- 由于TCP无消息保护边界,需要在接收端吃力消息边界问题,也就是我们所说的粘包,拆包问题
- 假设客户端分别发送了两个数据包 D1 和 D2 给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况:
- 服务端分两次读取到了两个独立的数据包,分别是 D1 和 D2,没有粘包和拆包
- 服务端一次接受到了两个数据包,D1 和 D2 粘合在一起,称之为 TCP 粘包
- 服务端分两次读取到了数据包,第一次读取到了完整的 D1 包和 D2 包的部分内容,第二次读取到了 D2 包的剩余内容,这称之为 TCP 拆包
- 服务端分两次读取到了数据包,第一次读取到了 D1 包的部分内容 D1_1,第二次读取到了 D1 包的剩余部分内容 D1_2 和完整的 D2 包。
TCP粘包和拆包现象示例
// 服务端 MyServer.java
public class MyServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new MyServerInitializer());
ChannelFuture channelFuture = serverBootstrap.bind(7777).sync();
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
// 服务端 MyServerInitializer.java
public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyServerHandler());
}
}
// 服务端 自定义handler MyServerHandler.java
public class MyServerHandler extends SimpleChannelInboundHandler<ByteBuf> {
private int count;
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf byteBuf) throws Exception {
byte[] buffer = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(buffer);
// 将buffer转成字符串
String msg = new String(buffer, StandardCharsets.UTF_8);
System.out.println("服务端接收数据:" + msg);
System.out.println("服务器接收到的消息量:" + (++this.count));
// 服务器回送数据给客户端,回送一个随机id
ByteBuf sendMsg = Unpooled.copiedBuffer(UUID.randomUUID().toString()+" ", StandardCharsets.UTF_8);
ctx.writeAndFlush(sendMsg);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
// 客户端 MyClient.java
public class MyClient {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
try{
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class).handler(new MyClientInitializer());
ChannelFuture channelFuture = bootstrap.connect("localhost", 7777).sync();
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
// 客户端 MyClientInitializer.java
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyClientHandler());
}
}
// 客户端自定义handler MyClientHandler.java
public class MyClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
private int count;
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 使用客户端发送1o条数据
for (int i = 0; i < 10; i++) {
ByteBuf byteBuf = Unpooled.copiedBuffer("hello,server" + i, StandardCharsets.UTF_8);
ctx.writeAndFlush(byteBuf);
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf byteBuf) throws Exception {
byte[] buffer = new byte[byteBuf.readableBytes()];
byteBuf.readBytes(buffer);
// 将buffer转成字符串
String msg = new String(buffer, StandardCharsets.UTF_8);
System.out.println("客户端接收到消息:" + msg);
System.out.println("客户端接收到的消息量:" + (++this.count)+'\n');
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
# 分别启动三个客户端后,服务端的打印
服务端接收数据:hello,server0hello,server1hello,server2hello,server3hello,server4hello,server5hello,server6hello,server7hello,server8hello,server9
服务器接收到的消息量:1
服务端接收数据:hello,server0
服务器接收到的消息量:1
服务端接收数据:hello,server1hello,server2
服务器接收到的消息量:2
服务端接收数据:hello,server3hello,server4hello,server5
服务器接收到的消息量:3
服务端接收数据:hello,server6
服务器接收到的消息量:4
服务端接收数据:hello,server7
服务器接收到的消息量:5
服务端接收数据:hello,server8hello,server9
服务器接收到的消息量:6
服务端接收数据:hello,server0
服务器接收到的消息量:1
服务端接收数据:hello,server1
服务器接收到的消息量:2
服务端接收数据:hello,server2hello,server3hello,server4
服务器接收到的消息量:3
服务端接收数据:hello,server5hello,server6hello,server7
服务器接收到的消息量:4
服务端接收数据:hello,server8hello,server9
服务器接收到的消息量:5
分析打印可看出:由于传输的数据流是连续的,长度不固定。服务端无法区分不同的业务数据包。故对于客户端发送的数据包,会以拆包或粘包的形式输出
TCP粘包和拆包的解决方案
- 使用 自定义协议 + 编解码器,来解决
- 关键就是要解决服务端每次读取数据长度的问题。这个问题解决了,就不会出现服务器多读或少读数据的问题,从而避免TCP的粘包,拆包。
解决方案实例:
// 自定义协议包:包含属性(数据字节码内容,内容的字节长度)
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;
}
}
// 自定义编码器
public class MyMessageEncoder extends MessageToByteEncoder<MessageProtocol> {
@Override
protected void encode(ChannelHandlerContext channelHandlerContext, MessageProtocol messageProtocol, ByteBuf byteBuf) throws Exception {
System.out.println("MyMessageEncoder encode方法被调用");
byteBuf.writeInt(messageProtocol.getLen());
byteBuf.writeBytes(messageProtocol.getContent());
}
}
// 自定义解码器
// ReplayingDecoder 是 byte-to-message 解码的一种特殊的抽象基类,读取缓冲区的数据之前需要检查缓冲区是否有足够的字节。
// 使用ReplayingDecoder就无需自己检查,若ByteBuf中有足够的字节,则会正常读取;若没有足够的字节则会停止解码。
public class MyMessageDecoder extends ReplayingDecoder<Void> {
@Override
protected void decode(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf, List<Object> list) throws Exception {
System.out.println("MyMessageDecoder decode方法被调用");
// 需要将得到的二进制字节码 => MessageProtocol数据包(对象)
int len = byteBuf.readInt();
byte[] content = new byte[len];
byteBuf.readBytes(content);
// 封装成MessageProtocol数据包,放入list中,传递给下一个handler进行业务处理
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(len);
messageProtocol.setContent(content);
list.add(messageProtocol);
}
}
// 服务端
public class MyServer {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new MyServerInitializer());
ChannelFuture channelFuture = serverBootstrap.bind(7777).sync();
channelFuture.channel().closeFuture().sync();
}finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
public class MyServerInitializer extends ChannelInitializer<SocketChannel> {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyMessageDecoder()); // 解码器:处理接收的数据
pipeline.addLast(new MyMessageEncoder()); // 编码器:处理发送的数据
pipeline.addLast(new MyServerHandler());
}
}
public class MyServerHandler extends SimpleChannelInboundHandler<MessageProtocol> {
private int count;
// 通过MessageProtocol协议包来接受通道中的数据,通过获取数据的length来精准接收单个的业务数据包,并进行消息回复等处理
@Override
protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {
// 接收到数据,并处理
int len = msg.getLen();
byte[] content = msg.getContent();
System.out.println("服务端接收到信息如下:");
System.out.println("长度="+len);
System.out.println("内容="+new String(content, StandardCharsets.UTF_8));
System.out.println("服务器接收到消息包数量="+(++this.count));
// 服务端回复消息给客户端(为什么客户端会收到10个回复包,因为服务器这边每收到1个数据包,就会回复一个。对于一个完整数据包的判定,就是依赖自定义的MessageProtocol)
String responseContent = UUID.randomUUID().toString();
int responseLen = responseContent.getBytes(StandardCharsets.UTF_8).length;
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setLen(responseLen);
messageProtocol.setContent(responseContent.getBytes(StandardCharsets.UTF_8));
ctx.writeAndFlush(messageProtocol);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println(cause.getMessage());
ctx.close();
}
}
// 客户端
public class MyClient {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
try{
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group).channel(NioSocketChannel.class).handler(new MyClientInitializer());
ChannelFuture channelFuture = bootstrap.connect("localhost", 7777).sync();
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
public class MyClientInitializer extends ChannelInitializer<SocketChannel> {
// 由于客户端和服务端都会涉及发送和接收数据,所以需要往pipeline中同时加入编码器和解码器
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast(new MyMessageDecoder()); // 加入解码器
pipeline.addLast(new MyMessageEncoder()); // 加入编码器
pipeline.addLast(new MyClientHandler());
}
}
public class MyClientHandler extends SimpleChannelInboundHandler<MessageProtocol> {
private int count;
// 每次发送一次数据之前,都先用自定义的协议进行包装(业务数据长度+数据内容)
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 使用客户端发送5条数据
for (int i = 0; i < 5; i++) {
String msgTo = "今天天气冷,吃火锅";
byte[] content = msgTo.getBytes(StandardCharsets.UTF_8);
int length = msgTo.getBytes(StandardCharsets.UTF_8).length; // 获取待发送数据的长度
// 创建协议包对象
MessageProtocol messageProtocol = new MessageProtocol();
messageProtocol.setContent(content);
messageProtocol.setLen(length);
ctx.writeAndFlush(messageProtocol);
}
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, MessageProtocol msg) throws Exception {
int len = msg.getLen();
byte[] content = msg.getContent();
System.out.println("客户端接收到信息如下:");
System.out.println("长度="+len);
System.out.println("内容="+new String(content, StandardCharsets.UTF_8));
System.out.println("客户端接收到消息包数量="+(++this.count));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("异常消息:" + cause.getMessage());
ctx.close();
}
}
# 服务端打印
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方法被调用
# 客户端打印
MyMessageEncoder encode方法被调用
MyMessageEncoder encode方法被调用
MyMessageEncoder encode方法被调用
MyMessageEncoder encode方法被调用
MyMessageEncoder encode方法被调用
MyMessageDecoder decode方法被调用
客户端接收到信息如下:
长度=36
内容=4be633f3-fb74-49bc-8ffa-828fe9078706
客户端接收到消息包数量=1
MyMessageDecoder decode方法被调用
客户端接收到信息如下:
长度=36
内容=4242aac2-7279-400d-9704-fb5727dd9eb8
客户端接收到消息包数量=2
MyMessageDecoder decode方法被调用
客户端接收到信息如下:
长度=36
内容=30691ff2-3d2d-486d-8b92-51eb70fda47d
客户端接收到消息包数量=3
MyMessageDecoder decode方法被调用
客户端接收到信息如下:
长度=36
内容=721e6e6f-6422-4c29-955f-3a13d44352ca
客户端接收到消息包数量=4
MyMessageDecoder decode方法被调用
客户端接收到信息如下:
长度=36
内容=6bf94563-b2d4-4e22-bbb0-47a5031bab90
客户端接收到消息包数量=5
Netty心跳机制
Netty心跳机制介绍
- Netty提供了IdleStateHandler,ReadTimeoutHandler,WriteTimeoutHandler三个handler检测连接的有效性
- IdleStateHandler的作用:当连接空闲时间(读或者写)太长时,会触发一个IdleStateEvent事件,然后,你可以通过ChannelInboundHandler中重写userEventTriggered方法来处理该事件。
- }
- ReadTimeoutHandler的作用:如果在指定时间内没有发生读事件,就会抛出异常,并自动关闭这个连接。我们可以在exceptionCaught方法中处理这个异常。
- WriteTimeoutHandler的作用:如果在指定时间内没有发生写事件,就会抛出异常,并自动关闭这个连接。我们可以在exceptionCaught方法中处理这个异常。
将任务添加到异步线程池
问题:
在ServerHandler的channelRead方法中执行耗时任务时,即使使用execute()单独执行多个任务,实质上还是将任务添加到了同一个线程中。
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("NettyServerHandler 线程是 = "+Thread.currentThread().getName());
// 执行耗时任务
ctx.channel().eventLoop().execute(()->{
try {
Thread.sleep(5*1000);
System.out.println("NettyServerHandler execute1 线程是 = "+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
ctx.channel().eventLoop().execute(()->{
try {
Thread.sleep(5*1000);
System.out.println("NettyServerHandler execute2 线程是 = "+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
}
# 打印 = 可以看出,三个线程中的线程名称是相同的,也就是说任务都添加到同一个线程中了
NettyServerHandler 线程是 = nioEventLoopGroup-3-1
NettyServerHandler execute1 线程是 = nioEventLoopGroup-3-1
NettyServerHandler execute2 线程是 = nioEventLoopGroup-3-1
在Netty中做耗时的,不可预料的操作,比如数据库,网络请求,会严重影响Netty对socket的处理速度。
而解决方法就是将耗时任务添加到异步线程池中,防止阻塞Netty的IO线程,可以有两种方式:
- handler中将耗时任务加入线程池
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
// group就充当了业务线程池,可以将任务提交到该线程池中
static final EventExecutorGroup group = new DefaultEventExecutorGroup(16);
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("NettyServerHandler 线程是 = "+Thread.currentThread().getName());
// 执行耗时任务
group.submit(()->{
try {
Thread.sleep(5*1000);
System.out.println("NettyServerHandler execute1 线程是 = "+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 执行耗时任务
group.submit(()->{
try {
Thread.sleep(5*1000);
System.out.println("NettyServerHandler execute2 线程是 = "+Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("go on");
}
}
# 打印
NettyServerHandler 线程是 = nioEventLoopGroup-3-1
go on
NettyServerHandler execute2 线程是 = defaultEventExecutorGroup-4-2
NettyServerHandler execute1 线程是 = defaultEventExecutorGroup-4-1
- context中将耗时任务加入线程池
public class NettyServer {
public static void main(String[] args) throws InterruptedException {
// 创建业务线程池
EventExecutorGroup group = new DefaultEventExecutorGroup(2);
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class) // 使用NioServerSocketChannel作为服务器端的通道实现类(反射)
.option(ChannelOption.SO_BACKLOG, 128) // 设置线程队列,得到连接个数
.childOption(ChannelOption.SO_KEEPALIVE, true) // 设置保持活动连接状态
.childHandler(new ChannelInitializer<SocketChannel>() { // 创建一个通道初始化对象(匿名对象)
// 给pipeline设置处理器
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 在此处可以获取客户端的channel,将其保存到一个集合中管理,可以在推送消息时,将业务加入到到不同channel对应的NIOEventLoop的taskQueue或scheduleTaskQueue中执行
System.out.println("客户socketChannel的hashcode="+socketChannel.hashCode());
// 说明:如果我们在使用addLast()向pipeline中添加handler时,前面有指定EventExecutorGroup,那么该handler会优先加入到线程池中
socketChannel.pipeline().addLast(group, new NettyServerHandler());
}
}); // 给workerGroup的EventLoop对应的管道设置处理器
}
}
// 主要修改在这里
// socketChannel.pipeline().addLast(group, new NettyServerHandler());
两种方式的对比
- 第一种方式在handler中添加异步,可能更自由,比如如果需要访问数据库,那么我就异步,如果不需要,就不异步,异步会拖长接口响应时间。因为需要将任务放进mpscTask中。如果IO时间很短,task很多,可能一个循环下来,都没时间执行整个task,导致响应时间达不到要求。
- 第二种方式是Netty的标准方式(即加入到队列),但是,这么做会将整个handler都交给业务线程池。不论耗时不耗时,都加入到队列里,不够灵活。
- 各有优劣,从灵活性考虑,第一种较好
Netty实现RPC
RPC基本介绍
- RPC(Remote Procedure Call)远程过程调用,是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外的为这个交互作用编程
- 两个或多个应用程序都分布在不同的服务器上,他们之间的调用都像是本地方法调用一样
- 常见的RPC框架有:阿里的Dubbo,Google的gRPC,go语言的rpcx,Apache的thrift,SpringCloud
RPC调用流程图
RPC 调用流程说明
- 服务消费方(client)以本地调用方式调用服务
- client stub 接收到调用后负责将方法、参数等封装成能够进行网络传输的消息体
- client stub 将消息进行编码并发送到服务端
- server stub 收到消息后进行解码
- server stub 根据解码结果调用本地的服务
- 本地服务执行并将结果返回给 server stub
- server stub 将返回导入结果进行编码并发送至消费方
- client stub 接收到消息并进行解码
- 服务消费方(client)得到结果
小结:RPC 的目标就是将 2 - 8 这些步骤都封装起来,用户无需关心这些细节,可以像调用本地方法一样即可完成远程服务调用
通过Netty自己实现一个RPC
需求说明
- Dubbo 底层使用了 Netty 作为网络通讯框架,要求用 Netty 实现一个简单的 RPC 框架
- 模仿 Dubbo,消费者和提供者约定接口和协议,消费者远程调用提供者的服务,提供者返回一个字符串,消费者打印提供者返回的数据。底层网络通信使用 Netty 4.1.20
设计说明
- 创建一个接口,定义抽象方法。用于消费者和提供者之间的约定。
- 创建一个提供者,该类需要监听消费者的请求,并按照约定返回数据。
- 创建一个消费者,该类需要透明的调用自己不存在的方法,内部需要使用 Netty 请求提供者返回数据
- 开发的分析图
程序代码
publicinterface 公共接口部分
// HelloService.java
/**
* @author lixing
* @date 2022-05-16 10:42
* @description 接口,服务提供方和服务消费方公用的部分
*/
public interface HelloService {
String hello(String msg);
}
netty部分
// NettyServer.java
public class NettyServer {
public static void startServer(String hostname, int port){
startServer0(hostname, port);
}
// 编写一个方法,完成对NettyServer的初始化和启动
private static void startServer0(String hostname, int port) {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try{
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(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(new NettyServerHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(hostname, port).sync();
System.out.println("服务提供方开始提供服务~~");
channelFuture.channel().closeFuture().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
// NettyServerHandler.java
/**
* @author lixing
* @date 2022-05-16 10:55
* @description server端的自定义业务处理器
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 获取客户端发送的消息,并调用服务
System.out.println("msg="+msg);
// 客户端在调用服务器的服务时,我们需要定义一个协议。满足协议才能调用服务
// 比如规定协议:每次发送消息都必须以某个字符串开头 “HelloService#hello”,即消息头部必须带“HelloService#”才能调用服务
if(msg.toString().startsWith("HelloService#")){
// 调用服务
String res = new HelloServiceImpl().hello(msg.toString().substring(msg.toString().lastIndexOf("#") + 1));
ctx.writeAndFlush(res);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
}
// NettyClient.java
public class NettyClient {
// 创建线程池
private static ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
// 客户端处理器
private static NettyClientHandler clientHandler;
// 编写方法使用代理模式,获取一个代理对象
public Object getBean(final Class<?> serviceClass, final String providerName){
return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class<?>[] {serviceClass}, (proxy, method, args)->{ // 每调用一次远程服务,此块代码就会重复执行一次
if(clientHandler == null){
initClient();
}
// 设置要发给服务器端的信息 (privoderName是协议头,args[0] 就是调用远程服务,传递的参数)
clientHandler.setParams(providerName+args[0]);
return executor.submit(clientHandler).get();
});
}
// 初始化客户端
private static void initClient() throws InterruptedException {
clientHandler = new NettyClientHandler();
EventLoopGroup 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(clientHandler);
}
});
bootstrap.connect("127.0.0.1", 7000).sync();
}
}
// NettyClientHandler.java
public class NettyClientHandler extends ChannelInboundHandlerAdapter implements Callable {
private ChannelHandlerContext context; // 上下文
private String result; // 返回的结果
private String params; // 客户端调用方法时,传入的参数
// 与服务器的连接创建成功后,就会被调用
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
context = ctx;
}
// 收到数据后,调用的方法
@Override
public synchronized void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
result = msg.toString();
// 唤醒等待的线程
notify();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.close();
}
// 被代理对象调用,发送数据给服务器 -> wait -> 等待被唤醒(被channelRead唤醒) -> 返回结果
@Override
public synchronized Object call() throws Exception {
// 客户端发送给服务端的消息
context.writeAndFlush(params);
// 进入等待,等待channelRead方法获取到服务器返回结果后,唤醒
// 客户端发送调用远程服务的参数,等待调用到远程服务,并获取到返回结果,才进行后续操作,所以需要等待
wait();
return result;
}
void setParams(String params){
this.params = params;
}
}
provider,服务提供方部分
// HelloServiceImpl.java
// 服务方提供的服务,实现类
public class HelloServiceImpl implements HelloService {
// 当有消费方调用该方法时,就返回一个结果
@Override
public String hello(String msg){
System.out.println("收到客户端消息="+msg);
if(msg != null){
return "你好客户端,我已经收到你的消息["+msg+"]";
}else{
return "你好客户端,我已经收到你的消息";
}
}
}
// ServerBootStrap.java
/**
* @author lixing
* @date 2022-05-16 10:47
* @description ServerBootStrap会启动一个服务提供者,NettyServer
*/
public class ServerBootStrap {
public static void main(String[] args) {
NettyServer.startServer("127.0.0.1", 7000);
}
}
consumer,服务消费方部分
// ClientBootStrap.java
public class ClientBootStrap {
// 定义协议头
public static final String providerName = "HelloService#hello#";
public static void main(String[] args) {
// 创建一个消费者
NettyClient consumer = new NettyClient();
// 创建代理对象
HelloService service = (HelloService) consumer.getBean(HelloService.class, providerName);
// 通过代理对象调用远程服务
String res = service.hello("你好 dubbo~~");
System.out.println("客户端调用远程服务的结果="+res);
}
}
依次启动ServerBootStrap.java,ClientBootStrap.java。可以看到程序打印:
# 服务端
服务提供方开始提供服务~~
msg=HelloService#hello#你好 dubbo~~
收到客户端消息=你好 dubbo~~
# 客户端
客户端调用远程服务的结果=你好客户端,我已经收到你的消息[你好 dubbo~~]