一、Netty简介
官方定义为:”Netty 是一款异步的事件驱动的网络应用程序框架,支持快速地开发可维护的高性能的面向协议的服务器 和客户端”。
使用Netty不适用jdk原生NIO原因
- 使用JDK自带的NIO需要了解太多的概念,编程复杂,一不小心bug横飞
- Netty底层IO模型随意切换,只需要做微小的改动,Netty可以直接从NIO模型变身为IO模型
- Netty自带的拆包解包,异常检测等机制让你从NIO的繁重细节中脱离出来,只需要关心业务逻辑
- 解决许多空轮询查询bug
- Netty底层对线程,selector做了很多细小的优化,精心设计的reactor线程模型做到非常高效的并发处理
- 自带各种协议栈可以处理任何一种通用协议
- Netty社区活跃
- Netty已经历各大rpc框架,消息中间件,分布式通信中间件线上的广泛验证,健壮性无比强大
二、开发包获取
maven依赖
- 如果使用 Maven 进行项目开发管理,则 Netty 也提供了 Maven 依赖。
- 可以直接从Maven仓库拿到依赖
<!-- https://mvnrepository.com/artifact/io.netty/netty-all -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.36.Final</version>
</dependency>
- 也可以从Netty官网拿依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty</artifactId> <!-- 4.0以上版本用netty-all -->
<version>X.Y.Z.Q</version>
<scope>compile</scope>
</dependency>
三、简单例子
以一个简单的例子来对 Netty 网络编程有一个初步的了解,其中的细节可以以后慢慢消化:先开启服务器等待客户端连接,然后开启客户端,同时给服务器发送一条消息,服务器接收到消息后,回发一条消息。
服务端
- TimeServer
public class TimeServer {
public void bind(int port) {
/* *配置服务端的 NIO 线程池,用于网络事件处理,实质上他们就是 Reactor 线程组
bossGroup 用于服务端接受客户端连接,workerGroup 用于进行 SocketChannel 网络读写*/
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
/**
* ServerBootstrap 是 Netty 用于启动 NIO 服务端的辅助启动类,用于降低开发难度
*/
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup,workerGroup)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG,1024)
.childHandler(new ChildChannelHandler());
ChannelFuture f = b.bind(port).sync();
f.channel().closeFuture().sync();
} catch (Exception e) {
e.printStackTrace();
} finally {
/**优雅退出,释放线程池资源*/
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
new TimeServer().bind(port);
}
}
- ChildChannelHandler
public class ChildChannelHandler extends ChannelInitializer<SocketChannel> {
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new TimeServerHandler());
}
}
- TimeServerHandler
/**
* @author shuliangzhao
* @Title: TimeServerHandler
* @ProjectName design-parent
* @Description: TODO
* @date 2019/8/13 21:04
*/
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
/**
* 收到客户端消息,自动触发
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
//将 msg 转为 Netty 的 ByteBuf 对象,类似 JDK 中的 java.nio.ByteBuffer,不过 ButeBuf 功能更强,更灵活
ByteBuf buf = (ByteBuf) msg;
//readableBytes:获取缓冲区可读字节数,然后创建字节数组
//从而避免了像 java.nio.ByteBuffer 时,只能盲目的创建特定大小的字节数组,比如 1024
byte[] req = new byte[buf.readableBytes()];
//readBytes:将缓冲区字节数组复制到新建的 byte 数组中
//然后将字节数组转为字符串
buf.readBytes(req);
String body = new String(req, "UTF-8");
System.out.println("The time server receive order : " + body);
//回复消息
//copiedBuffer:创建一个新的缓冲区,内容为里面的参数
//通过 ChannelHandlerContext 的 write 方法将消息异步发送给客户端*/
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new java.util.Date(
System.currentTimeMillis()).toString() : "BAD ORDER";
ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.write(resp);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
//flush:将消息发送队列中的消息写入到 SocketChannel 中发送给对方,为了频繁的唤醒 Selector 进行消息发送
//Netty 的 write 方法并不直接将消息写如 SocketChannel 中,调用 write 只是把待发送的消息放到发送缓存数组中,再通过调用 flush
//方法,将发送缓冲区的消息全部写入到 SocketChannel 中
ctx.flush();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
/**当发生异常时,关闭 ChannelHandlerContext,释放和它相关联的句柄等资源 */
ctx.close();
}
}
客户端
- TimeClient
/**
* @author shuliangzhao
* @Title: TimeClient
* @ProjectName design-parent
* @Description: TODO
* @date 2019/8/13 21:13
*/
public class TimeClient {
public void connect(int port, String host) throws Exception {
// 配置客户端NIO线程组
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group).channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY, true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(new TimeClientHandler());
}
});
// 发起异步连接操作
ChannelFuture f = b.connect(host, port).sync();
// 当代客户端链路关闭
f.channel().closeFuture().sync();
} finally {
// 优雅退出,释放NIO线程组
group.shutdownGracefully();
}
}
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
int port = 8080;
if (args != null && args.length > 0) {
try {
port = Integer.valueOf(args[0]);
} catch (NumberFormatException e) {
// 采用默认值
}
}
new TimeClient().connect(port, "127.0.0.1");
}
}
- TimeClientHandler
/**
* @author shuliangzhao
* @Title: TimeClientHandler
* @ProjectName design-parent
* @Description: TODO
* @date 2019/8/13 21:14
*/
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
private final ByteBuf firstMessage;
/**
* Creates a client-side handler.
*/
public TimeClientHandler() {
byte[] req = "QUERY TIME ORDER".getBytes();
firstMessage = Unpooled.buffer(req.length);
firstMessage.writeBytes(req);
}
/**
* 当客户端和服务端 TCP 链路建立成功之后,Netty 的 NIO 线程会调用 channelActive 方法
* @param ctx
*/
@Override
public void channelActive(ChannelHandlerContext ctx) {
String reqMsg = "我是客户端 " + Thread.currentThread().getName();
System.out.println(reqMsg);
//writeBytes:将指定的源数组的数据传输到缓冲区
//调用 ChannelHandlerContext 的 writeAndFlush 方法将消息发送给服务器
ctx.writeAndFlush(firstMessage);
}
/**
* 当服务端返回应答消息时,channelRead 方法被调用,从 Netty 的 ByteBuf 中读取并打印应答消息
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg)
throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "UTF-8");
System.out.println("Now is : " + body);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
// 释放资源
System.out.println("Unexpected exception from downstream : "
+ cause.getMessage());
ctx.close();
}
}
基于 Netty 的应用开发不但 API 使用简单,开发模式固定,而且扩展性和定制性非常好。需要注意的是,本示例没有考虑读半包的处理,对于简单的功能,或者不苛责的环境下是没有的问题的,但是如果进行性能测试或者压力测试,就不敢保证正常运行了,所以后面会介绍半包处理情况。