沉淀再出发:关于netty的一些理解和使用
一、前言
netty在底层的数据通信和封装之中有着重要的作用,下面我们就来看看netty的简单使用过程,以及背后的原理。
二、netty的简单使用
2.1、netty的环境部署和使用
在这里我们使用myeclipse平台,maven管理工具进行开发,其实使用eclipse或者其他软件也可以。首先我们新建一个maven项目,项目名和包名自定:
之后我们修改pom.xml文件,增加netty依赖:
保存之后,系统就会自动为我们下载和安装了,非常的方便,这样,我们的环境就部署完毕了。
2.2、一个简单的案例
下面我们看一个简单地案例:
我们新建一个包,然后写入两个文件:
首先我们编写一个处理连接的类 HelloServerHandler :
1 package com.coder.server; 2 3 import io.netty.buffer.ByteBuf; 4 import io.netty.channel.ChannelHandlerContext; 5 import io.netty.channel.ChannelInboundHandlerAdapter; 6 import io.netty.util.CharsetUtil; 7 import io.netty.util.ReferenceCountUtil; 8 9 10 public class HelloServerHandler extends ChannelInboundHandlerAdapter { 11 /** 12 * 收到数据时调用 13 */ 14 @Override 15 public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 16 try { 17 ByteBuf in = (ByteBuf)msg; 18 System.out.print(in.toString(CharsetUtil.UTF_8)); 19 } finally { 20 // 抛弃收到的数据 21 ReferenceCountUtil.release(msg); 22 } 23 } 24 25 /** 26 * 当Netty由于IO错误或者处理器在处理事件时抛出异常时调用 27 */ 28 @Override 29 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 30 // 当出现异常就关闭连接 31 cause.printStackTrace(); 32 ctx.close(); 33 } 34 }
其次,我们编写接收连接,并且派发和处理的类 HelloServer :
1 package com.coder.server; 2 3 import io.netty.bootstrap.ServerBootstrap; 4 import io.netty.channel.ChannelFuture; 5 import io.netty.channel.ChannelInitializer; 6 import io.netty.channel.ChannelOption; 7 import io.netty.channel.EventLoopGroup; 8 import io.netty.channel.nio.NioEventLoopGroup; 9 import io.netty.channel.socket.SocketChannel; 10 import io.netty.channel.socket.nio.NioServerSocketChannel; 11 12 public class HelloServer { 13 private int port; 14 15 public HelloServer(int port) { 16 this.port = port; 17 } 18 19 public void run() throws Exception { 20 EventLoopGroup bossGroup = new NioEventLoopGroup(); // 用来接收进来的连接 21 EventLoopGroup workerGroup = new NioEventLoopGroup(); // 用来处理已经被接收的连接 22 System.out.println("准备运行端口:" + port); 23 24 try { 25 ServerBootstrap b = new ServerBootstrap(); 26 b.group(bossGroup, workerGroup) 27 .channel(NioServerSocketChannel.class) // 这里告诉Channel如何接收新的连接 28 .childHandler( new ChannelInitializer<SocketChannel>() { 29 @Override 30 protected void initChannel(SocketChannel ch) throws Exception { 31 // 自定义处理类 32 ch.pipeline().addLast(new HelloServerHandler()); 33 } 34 }) 35 .option(ChannelOption.SO_BACKLOG, 128) 36 .childOption(ChannelOption.SO_KEEPALIVE, true); 37 38 // 绑定端口,开始接收进来的连接 39 ChannelFuture f = b.bind(port).sync(); 40 41 // 等待服务器socket关闭 42 f.channel().closeFuture().sync(); 43 } catch (Exception e) { 44 workerGroup.shutdownGracefully(); 45 bossGroup.shutdownGracefully(); 46 } 47 } 48 49 public static void main(String[] args) throws Exception { 50 int port = 12345; 51 new HelloServer(port).run(); 52 } 53 }
然后运行,等待连接就好了,那么问题来了,使用什么进行连接呢?在windows中,我们可以使用Telnet,这个比较方便和简单,但是我们需要打开控制面板的程序和功能模块,并且启动服务,之后最好重启一下电脑:
下面我们运行程序,并使用Telnet客户端测试一下:
在telnet中‘ctrl+]’可以显示输入的文字,否则将看不到输入。
三、使用netty自定义时间服务器
本例中我们试图在服务器和客户端连接被创立时发送一个消息,然后在客户端解析收到的消息并输出。并且,在这个项目中使用 POJO 代替 ByteBuf 来作为传输对象。
3.1、pojo对象创建
Time 类:
1 package com.coder.pojo; 2 3 import java.util.Date; 4 5 /** 6 * 自定义时间数据类 7 * 8 */ 9 public class Time { 10 private final long value; 11 12 public Time() { 13 // 除以1000是为了使时间精确到秒
//注意这里的this,其实就是调用了 public Time(long value) ,并且更加的方便和快捷。 14 this(System.currentTimeMillis() / 1000L); 15 } 16 17 public Time(long value) { 18 this.value = value; 19 } 20 21 public long value() { 22 return value; 23 } 24 25 @Override 26 public String toString() { 27 return new Date((value()) * 1000L).toString(); 28 } 29 }
3.2、服务器程序
TimeEncoderPOJO类:
1 package com.coder.server; 2 3 import com.coder.pojo.Time; 4 5 import io.netty.buffer.ByteBuf; 6 import io.netty.channel.ChannelHandlerContext; 7 import io.netty.handler.codec.MessageToByteEncoder; 8 9 /** 10 * 服务器数据编码类 11 * 12 */ 13 public class TimeEncoderPOJO extends MessageToByteEncoder<Time> { 14 15 // 发送数据时调用 16 @Override 17 protected void encode(ChannelHandlerContext ctx, Time msg, ByteBuf out) throws Exception { 18 // 只传输当前时间,精确到秒 19 out.writeInt((int)msg.value()); 20 } 21 22 }
TimeServerHandlerPOJO类:连接建立并且准备通信的时候进行处理,发送当前时间,并增加监听。
1 package com.coder.server; 2 3 import com.coder.pojo.Time; 4 5 import io.netty.channel.ChannelFuture; 6 import io.netty.channel.ChannelFutureListener; 7 import io.netty.channel.ChannelHandlerContext; 8 import io.netty.channel.ChannelInboundHandlerAdapter; 9 10 /** 11 * 服务器解码器 12 * 连接建立时发送当前时间 13 * 14 */ 15 public class TimeServerHandlerPOJO extends ChannelInboundHandlerAdapter { 16 /** 17 * 连接建立的时候并且准备进行通信时被调用 18 */ 19 @Override 20 public void channelActive(final ChannelHandlerContext ctx) throws Exception { 21 // 发送当前时间信息 22 ChannelFuture f = ctx.writeAndFlush(new Time()); 23 // 发送完毕之后关闭 Channel 24 f.addListener(ChannelFutureListener.CLOSE); 25 } 26 27 @Override 28 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 29 cause.printStackTrace(); 30 ctx.close(); 31 } 32 }
TimeServerPOJO类:服务器的主程序
1 package com.coder.server; 2 3 import io.netty.bootstrap.ServerBootstrap; 4 import io.netty.channel.ChannelFuture; 5 import io.netty.channel.ChannelInitializer; 6 import io.netty.channel.ChannelOption; 7 import io.netty.channel.EventLoopGroup; 8 import io.netty.channel.nio.NioEventLoopGroup; 9 import io.netty.channel.socket.SocketChannel; 10 import io.netty.channel.socket.nio.NioServerSocketChannel; 11 12 public class TimeServerPOJO { 13 private int port; 14 15 public TimeServerPOJO(int port) { 16 this.port = port; 17 } 18 19 public void run() throws Exception { 20 EventLoopGroup bossGroup = new NioEventLoopGroup(); // 用来接收进来的连接 21 EventLoopGroup workerGroup = new NioEventLoopGroup(); // 用来处理已经被接收的连接 22 System.out.println("准备运行端口:" + port); 23 24 try { 25 ServerBootstrap b = new ServerBootstrap(); // 启动NIO服务的辅助启动类 26 b.group(bossGroup, workerGroup) 27 .channel(NioServerSocketChannel.class) // 这里告诉Channel如何接收新的连接 28 .childHandler( new ChannelInitializer<SocketChannel>() { 29 @Override 30 protected void initChannel(SocketChannel ch) throws Exception { 31 // 自定义处理类 32 // 注意添加顺序 33 ch.pipeline().addLast(new TimeEncoderPOJO(),new TimeServerHandlerPOJO()); 34 } 35 }) 36 .option(ChannelOption.SO_BACKLOG, 128) 37 .childOption(ChannelOption.SO_KEEPALIVE, true); 38 39 // 绑定端口,开始接收进来的连接 40 ChannelFuture f = b.bind(port).sync(); 41 42 // 等待服务器socket关闭 43 f.channel().closeFuture().sync(); 44 } catch (Exception e) { 45 workerGroup.shutdownGracefully(); 46 bossGroup.shutdownGracefully(); 47 } 48 } 49 50 public static void main(String[] args) throws Exception { 51 int port = 12345; 52 new TimeServerPOJO(port).run(); 53 } 54 }
其中ch.pipeline().addLast(new TimeEncoderPOJO(),new TimeServerHandlerPOJO());方法的含义为:Handles an I/O event or intercepts an I/O operation, and forwards it to its next handler in its ChannelPipeline
.也就是说当我们添加一些处理的时候会按照管道的方式,一步步的处理,因此先后顺序非常重要。
3.3、客户端程序
先来看看解码器(服务器端发送了编码后的时间信息,因此,这里客户端收到之后需要解码):
TimeDecoderPOJO 类:
1 package com.coder.client; 2 3 import java.util.List; 4 5 import com.coder.pojo.Time; 6 7 import io.netty.buffer.ByteBuf; 8 import io.netty.channel.ChannelHandlerContext; 9 import io.netty.handler.codec.ByteToMessageDecoder; 10 11 public class TimeDecoderPOJO extends ByteToMessageDecoder { 12 /** 13 * 有新数据接收时调用 14 * 为防止分包现象,先将数据存入内部缓存,到达满足条件之后再进行解码 15 */ 16 @Override 17 protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception { 18 if(in.readableBytes() < 4) { 19 return; 20 } 21 22 // out添加对象则表示解码成功 23 out.add(new Time(in.readUnsignedInt())); 24 } 25 }
再看看客户端数据处理类:
TimeClientHandlerPOJO类:
1 package com.coder.client; 2 3 import com.coder.pojo.Time; 4 5 import io.netty.channel.ChannelHandlerContext; 6 import io.netty.channel.ChannelInboundHandlerAdapter; 7 8 /** 9 * 客户端数据处理类 10 * 11 */ 12 public class TimeClientHandlerPOJO extends ChannelInboundHandlerAdapter { 13 @Override 14 public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { 15 // 直接将信息转换成Time类型输出即可 16 Time time = (Time)msg; 17 System.out.println(time); 18 ctx.close(); 19 } 20 21 @Override 22 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { 23 cause.printStackTrace(); 24 ctx.close(); 25 } 26 }
最后是客户端的主程序:
TimeClientPOJO类:
1 package com.coder.client; 2 3 import io.netty.bootstrap.Bootstrap; 4 import io.netty.channel.ChannelFuture; 5 import io.netty.channel.ChannelInitializer; 6 import io.netty.channel.ChannelOption; 7 import io.netty.channel.EventLoopGroup; 8 import io.netty.channel.nio.NioEventLoopGroup; 9 import io.netty.channel.socket.SocketChannel; 10 import io.netty.channel.socket.nio.NioSocketChannel; 11 12 public class TimeClientPOJO { 13 public static void main(String[] args) throws Exception{ 14 String host = "127.0.0.1"; // ip 15 int port = 12345; // 端口 16 EventLoopGroup workerGroup = new NioEventLoopGroup(); 17 18 try { 19 Bootstrap b = new Bootstrap(); // 与ServerBootstrap类似 20 b.group(workerGroup); // 客户端不需要boss worker 21 b.channel(NioSocketChannel.class); 22 b.option(ChannelOption.SO_KEEPALIVE, true); // 客户端的socketChannel没有父亲 23 b.handler(new ChannelInitializer<SocketChannel>() { 24 @Override 25 protected void initChannel(SocketChannel ch) throws Exception { 26 // POJO 27 ch.pipeline().addLast(new TimeDecoderPOJO() ,new TimeClientHandlerPOJO()); 28 } 29 }); 30 31 // 启动客户端,客户端用connect连接 32 ChannelFuture f = b.connect(host, port).sync(); 33 34 // 等待连接关闭 35 f.channel().closeFuture().sync(); 36 } finally { 37 workerGroup.shutdownGracefully(); 38 } 39 } 40 }
至此程序编写完毕,先运行服务器,再运行客户端程序,然后测试即可,我们会发现服务器一直等待着请求,当客户端连接上之后,服务器就会发出带着格式的时间,客户端接收到之后进行解码,然后显示出来并且退出。在同一个myeclipse之中可以运行多个程序,使用下图中的按钮可以进行切换。
四、netty的基本组成部分
4.1、Channel
Channel 是 Java NIO 的一个基本构造。它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执行一个或者多个不同的I/O操作的程序组件)的开放连接,如读操作和写操作。目前,可以把 Channel 看作是传入(入站)或者传出(出站)数据的载体。因此,它可以被打开或者被关闭,连接或者断开连接。
4.2、Callback(回调)
Netty 在内部使用了回调来处理事件;当一个回调被触发时,相关的事件可以被一个 interfaceChannelHandler 的实现处理。
4.3、Future
Future 提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。JDK 预置了 interface java.util.concurrent.Future,但是其所提供的实现,只允许手动检查对应的操作是否已经完成,或者一直阻塞直到它完成。这是非常繁琐的,所以 Netty 提供了它自己的实现ChannelFuture,用于在执行异步操作的时候使用。
4.4、Event 和 Handler
Netty 使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经发生的事件来触发适当的动作。这些动作可能是:记录日志、数据转换、流控制、应用程序逻辑。Netty 是一个网络编程框架,所以事件是按照它们与入站或出站数据流的相关性进行分类的。可能由入站数据或者相关的状态更改而触发的事件包括:连接已被激活或者连接失活、数据读取、用户事件、错误事件。出站事件是未来将会触发的某个动作的操作结果,这些动作包括:打开或者关闭到远程节点的连接、将数据写到或者冲刷到套接字。
Netty 的 ChannelHandler 为处理器提供了基本的抽象,目前可以认为每个 ChannelHandler 的实例都类似于一种为了响应特定事件而被执行的回调。Netty 提供了大量预定义的可以开箱即用的 ChannelHandler 实现,包括用于各种协议(如 HTTP 和 SSL/TLS)的 ChannelHandler。在内部 ChannelHandler 自己也使用了事件和 Future。