Hello Netty World
Posted Qiyuanc
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Hello Netty World相关的知识,希望对你有一定的参考价值。
Hello Netty World
Netty 介绍
(官网)Netty is an asynchronous event-driven network application framework,
for rapid development of maintainable high performance protocol servers & clients.
(翻译)Netty是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。
先说结论,Netty 可以干什么:通常,我们使用 SpringBoot 搭建服务器,可以在网页上通过 URL 发出请求,服务器处理请求后返回响应,此时客户端和服务器是通过 HTTP 协议交换数据的;而使用 Netty,我们可以基于客户端和服务器之间的字节流,自定义编码解码规则(网络协议),从而实现自定义网络协议的应用程序。
再来说说 Netty 是什么:官网的介绍是,Netty 是一个网络应用程序框架(类似 SpringBoot 是 Web 应用程序的框架),即用于网络编程的一个框架。涉及到网络编程,就少不了网络编程底层的套接字(Socket),原生的 Socket 编程就不说了,Java 中已经提供了 NIO(Non-Blocking IO,非阻塞 IO)用于高性能的网络编程,那 NIO 和 Netty 有什么关系呢?再说一下结论:Netty 也是一种 NIO 框架,但对比 Java 原生的 NIO 和其他 NIO 框架,它更好使。
接着对比一下 NIO 和 Netty 的区别:
先看 NIO 的缺点:
-
API 复杂,作为 Java 直接提供的 API,NIO 的 API 封装度较低,使用起来比较繁杂,学习成本高,需要熟练掌握 Selector、ServerSocketChannel、SocketChannel、ByteBuffer 等对象;
-
使用 NIO 需要对多线程和网络编程较为了解,因为 NIO 涉及到 Reactor 模式(也叫 Dispatcher 模式,即 I/O 多路复用监听事件,收到事件后,根据事件类型分配(Dispatch)给某个进程 / 线程),没有良好的基础很难使用 NIO 写出健康的程序。
-
NIO 还会有一些自带的 BUG,如 epoll bug,会导致 Selector 空轮询造成 CPU 占用 100%(最终是修复了,但影响力太大,臭名昭著)。
与之相比,Netty 就具有相对的优点:
-
底层封装了 NIO,API 简单,非常容易上手;
-
内置多种编码器解码器,支持多种协议,功能强大;
-
对比原生 NIO 和其他 NIO 框架,Netty 的性能是最好的;
-
社区活跃,使用者多,出现 BUG 的修复速度也非常快,质量有保证。
总之,对于构建应用来说,即使没有任何 NIO 相关知识,直接使用 Netty 也不会有任何负担。当然,最好的还是能了解底层的原理。下面通过构建一个 Netty 的 Hello World 程序,就可以感受到 Netty 的强大。
Hello Netty
服务端
从0开始,创建一个 Maven 的 Java 项目,引入 Netty 的依赖:
<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.36.Final</version>
</dependency>
</dependencies>
然后,就可以直接开始构建服务器了,创建 MyServer 类,代码如下:
public class MyServer
public static void main(String[] args) throws Exception
// 创建两个线程组 boosGroup、workerGroup(步骤1)
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try
// 创建服务端的启动对象 ServerBootstrap,设置启动参数(步骤2)
ServerBootstrap bootstrap = new ServerBootstrap();
// 设置上面创建的线程组 boosGroup 和 workerGroup
bootstrap.group(bossGroup, workerGroup)
// 设置服务端的通道实现类型
.channel(NioServerSocketChannel.class)
// 设置线程队列的连接个数
.option(ChannelOption.SO_BACKLOG, 128)
// 设置保持活动连接状态
.childOption(ChannelOption.SO_KEEPALIVE, true)
// 使用匿名内部类的形式初始化通道对象 并设置处理器(步骤3,处理器当然是自己写,自定义业务流程)
.childHandler(new ChannelInitializer<SocketChannel>()
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception
// 为服务端 添加一个处理器
socketChannel.pipeline().addLast(new MyServerHandler());
);
System.out.println("Netty 服务端启动-Hello Server!");
// 绑定端口号 服务端启动(步骤4)
ChannelFuture channelFuture = bootstrap.bind(7777).sync();
// 监听关闭通道
channelFuture.channel().closeFuture().sync();
finally
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
简单归纳一下创建服务器的步骤:
-
创建两个线程组对象 bossGroup 和 workerGroup;
-
创建服务端的启动对象 ServerBootstrap,设置线程组、启动参数;
-
为服务端的通道设置处理器(顾名思义,此处就是自定义处理数据的地方了);
-
为服务端绑定端口,启动服务。
可见服务端的启动非常简单,步骤1、2、4可以说都是固定步骤,只有在步骤3中,需要添加处理数据的通道处理器,此处的 MyServerHandler 就是自定义的,代码如下:
public class MyServerHandler extends ChannelInboundHandlerAdapter
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
// Read时触发
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("收到客户端" + ctx.channel().remoteAddress() + "发送的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception
// ReadComplete后触发,发送消息给客户端
ctx.writeAndFlush(Unpooled.copiedBuffer("服务端收到,你是" + ctx.channel().remoteAddress(), CharsetUtil.UTF_8));
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception
// 发生异常关闭通道
ctx.close();
MyServerHandler 类继承了 ChannelInboundHandlerAdapter,它就是一个处理器了。其中,重写了 channelRead
等方法,这些方法都会在对应的场合被调用,如 channelRead
,对于接收到的数据 MyServerHandler 会调用 channelRead
方法处理数据,处理完成后又会调用 channelReadComplete
方法。
此处可以看到,channelRead
中读取了数据,在服务端打印了收到的数据(将数据转化为字节流后再转 Sting 读取),读取完后,又由 channelReadComplete
方法给客户端发送回复。
其中用到了 ChannelHandlerContext 对象,GPT 给出的介绍如下:
ChannelHandlerContext(通道处理上下文)是 Netty 中处理通道事件的核心接口,它封装了 Channel 和 ChannelHandler 之间的关联关系,以及 ChannelHandler 之间的交互操作。每当 Netty 中的通道(Channel)被激活时,会生成一个 ChannelHandlerContext 对象,并通过 ChannelPipeline 向通道处理器 ChannelHandler 传递事件和数据,让它们能够对通道进行处理。
简单来说,ChannelHandlerContext 提供了一种 Channel 和 ChannelHandler 之间进行通信和交互的方式,可以通过它来访问 Channel、调用 ChannelPipeline 中的其他处理器等。
客户端
完成了服务端,就到客户端了,与服务端类似,代码如下:
public class MyClient
public static void main(String[] args) throws Exception
// 创建 NIO 线程组(步骤1)
NioEventLoopGroup eventExecutors = new NioEventLoopGroup();
try
// 创建客户端的启动对象 Bootstrap,设置启动参数(步骤2)
Bootstrap bootstrap = new Bootstrap();
// 设置线程组
bootstrap.group(eventExecutors)
// 设置客户端的通道实现类型
.channel(NioSocketChannel.class)
// 使用匿名内部类初始化通道,并设置处理器(步骤3,处理器也是自定义的)
.handler(new ChannelInitializer<SocketChannel>()
@Override
protected void initChannel(SocketChannel ch) throws Exception
// 为客户端 添加一个处理器
ch.pipeline().addLast(new MyClientHandler());
);
System.out.println("Netty 客户端到位-Hello Client!");
// 设置 IP 和 端口 连接服务端,操作系统会给客户端分配一个空闲端口号
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 7777).sync();
// 对通道关闭进行监听
channelFuture.channel().closeFuture().sync();
finally
//关闭线程组
eventExecutors.shutdownGracefully();
简单归纳一下创建客户端的步骤:
-
创建 NIO 线程组对象 eventExecutors;
-
创建客户端的启动对象 Bootstrap,设置线程组、启动参数;
-
为客户端的通道设置处理器(与客户端系统,自定义的数据处理);
-
为客户端设置连接 IP 和端口,启动连接。
可以看到,创建客户端的流程与服务端差不多,只是参数不太一样。自定义的 MyClientHandler 代码如下:
public class MyClientHandler extends ChannelInboundHandlerAdapter
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception
// Active时触发(连接时),发送消息到服务端
ctx.writeAndFlush(Unpooled.copiedBuffer("こんにちは、世界", CharsetUtil.UTF_8));
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception
// Read时触发
ByteBuf byteBuf = (ByteBuf) msg;
System.out.println("收到服务端" + ctx.channel().remoteAddress() + "的消息:" + byteBuf.toString(CharsetUtil.UTF_8));
其中,channelActive
方法在连接时触发,向服务器发送消息;收到数据时调用 channelRead
处理数据,在客户端打印数据内容(字节流数据转 String)。
启动
完成了服务端和客户端后,就可以启动它们尝试一下了。首先启动服务端,直接运行 MyServer:
再运行 MyClient:
可以看到 MyClient 直接有输出内容了,这是因为启动连接成功后 MyClient 调用 channelActive
向 MyServer 发送了数据,MyServer 调用 channelRead
读取了数据,完成后又调用 channelReadComplete
向 MyClient 发送了数据,最后由 MyClient 的 channelRead
打印到了控制台上。
回到 MyServer 的控制台查看输出:
Hello Netty 程序完成,就是这么简单了。
Netty 中文教程 Hello World !详解
1.HelloServer 详解
HelloServer首先定义了一个静态终态的变量---服务端绑定端口7878。至于为什么是这个7878端口,纯粹是笔者个人喜好。大家可以按照自己的习惯选择端口。当然了。常用的几个端口(例如:80,8080,843(Flash及Silverlight策略文件请求端口等等),3306(Mysql数据库占用端口))最好就不要占用了,避免一些奇怪的问题。
HelloServer类里面的代码并不多。只有一个main函数,加上内部短短的几行代码。
Main函数开始的位置定义了两个工作线程,一个命名为WorkerGroup,另一个命名为BossGroup。都是实例化NioEventLoopGroup。这一点和3.x版本中基本思路是一致的。Worker线程用于管理线程为Boss线程服务。
讲到这里需要解释一下EventLoopGroup,它是4.x版本提出来的一个新概念。类似于3.x版本中的线程。用于管理Channel连接的。在main函数的结尾就用到了EventLoopGroup提供的便捷的方法,shutdownGraceFully(),翻译为中文就是优雅的全部关闭。感觉是不是很有意思。作者居然会如此可爱的命名了这样一个函数。查看相应的源代码。我们可以在DefaultEventExecutorGroup的父类MultithreadEventExecutorGroup中看到它的实现代码。关闭了全部EventExecutor数组child里面子元素。相比于3.x版本这是一个比较重大的改动。开发者可以很轻松的全部关闭,而不需要担心出现内存泄露。
在try里面实例化一个ServerBootstrap b。设置group。设置channel为NioServerSocketChannel。
设置childHandler,在这里使用实例化一个HelloServerInitializer类来实现,继承ChannelInitializer<SocketChannel>。内部的代码我们可以在前文的注视中大致了解一下,主要作用是设置相关的字节解码编码器,和代码处理逻辑。Handler是Netty包里面占很大一个比例。可见其的作用和用途。Handler涉及很多领域。HTTP,UDP,Socket,WebSocket等等。详细的部分在本章的第三节解释。
设置好Handler绑定端口7878,并调用函数sync(),监听端口(等待客户端连接和发送消息)。并监听端口关闭(为了防止线程停止)。
最后finally我们要优雅的全部关闭服务端。^_^
2.HelloClient详解
相比于服务端的代码。客户端要精简一些。
客户端仅仅只需要一个worker的EventLoopGroup。其次是类似于ServerBootstrap的HandlerInitializer。
唯一不同的可能就是客户端的connect方法。服务端的绑定并监听端口,客户端是连接指定的地址。Sync().channel()是为了返回这个连接服务端的channel,并用于后面代码的调用。
BufferedReader 这个是用于控制台输入的。不做详细的解释了就。大家都懂的。
当用户输入一行内容并回车之后。循环的读取每一行内容。然后使用writeAndFlush向服务端发送消息。
3.HandlerInitializer详解
Handler在Netty中是一个比较重要的概念。有着相当重要的作用。相比于Netty的底层。我们接触更多的应该是他的Handler。在这里我将它剥离出来单独解释。
ServerHandlerInitializer继承与ChannelInitializer<SocketChannel>需要我们实现一个initChannel()方法。我们定义的handler就是写在这里面。
在最开始的地方定义了一个DelimiterBasedFrameDecoder。按直接翻译就是基于分隔符的帧解码器。再一次感觉框架的作者的命名,好直接好简单。详细的内容我们在后面的文章中在为大家详细的解释。目前大家知道他是以分隔符为分割标准的解码器就好了。
也许有人会问分隔符是什么?我只能 !*_* :“纳尼 !!”。分隔符其实就是“\n”我们在学习C语言的时候最常用的的也许就是这个分隔符了吧。
下面的则是StringDecoder 和StringEncoder。字符串解码器和字符串编码器。
最后面则是我们自己的逻辑。服务/客户端逻辑是在消息解码之后处理的。然后服务/客户端返回相关消息则是需要对消息进行相对应的编码。最终才是以二进制数据流的形势发送给服务/客户端的。
以上是关于Hello Netty World的主要内容,如果未能解决你的问题,请参考以下文章