Netty入门

一、概念

    Netty是Jboss提供的一个Java开源框架,它是基于NIO的网络框架,封装了NIO底层复杂的实现细节,给我们提供了简单好用的概念来实现编程。有了Netty,我们可以实现自己的HTTP服务器,FTP服务器,UDP服务器,RPC服务器,WebSocket服务器等等。HTTP服务器之所以称为HTTP服务器,是因为编码解码协议是HTTP协议,如果协议是Redis协议,那它就成了Redis服务器,如果协议是WebSocket,那它就成了WebSocket服务器,等等。 使用Netty我们就可以定制编解码协议,实现自己的特定协议的服务器。

二、实现

    本文不过多介绍关于Netty的概念和和细节,我提供了一个客户端,服务端的例子,每段代码后面有解释。

    首先去下载好Netty所需要的jar包,建立一个项目,放好jar包。

1. 服务端实现

package com.server;

import io.netty.bootstrap.ServerBootstrap;

import io.netty.channel.ChannelFuture;

import io.netty.channel.ChannelInitializer;

import io.netty.channel.ChannelOption;

import io.netty.channel.EventLoopGroup;

import io.netty.channel.nio.NioEventLoopGroup;

import io.netty.channel.socket.SocketChannel;

import io.netty.channel.socket.nio.NioServerSocketChannel;

public class TimeServer {

public static void main(String[] args) {

int port = 9898;

new TimeServer().bind(port);

}

public void bind(int port) {

/**

* interface EventLoopGroup extends EventExecutorGroup extends

* ScheduledExecutorService extends ExecutorService 配置服务端的 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());

/** 服务器启动辅助类配置完成后,调用 bind 方法绑定监听端口,调用 sync 方法同步等待绑定操作完成 */

ChannelFuture f = b.bind(port).sync();

System.out.println(Thread.currentThread().getName() + ",服务器开始监听端口,等待客户端连接.........");

/**

* 下面会进行阻塞,等待服务器连接关闭之后 main 方法退出,程序结束

*

*/

f.channel().closeFuture().sync();

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

/** 优雅退出,释放线程池资源 */

bossGroup.shutdownGracefully();

workerGroup.shutdownGracefully();

}

}

private class ChildChannelHandler extends ChannelInitializer {

@Override

protected void initChannel(SocketChannel arg0) throws Exception {

arg0.pipeline().addLast(new TimeServerHandler());

}

}

}

    在bind方法中创建了两个NioEventLoopGroup实例。NioEventLoopGroup是个线程组,它包含一组NIO线程,专门用于网络事件的处理,实际上它们就是Reactor线程组。这里创建两个的原因是一个用于服务端接收客户端的连接,另一个用于SocketChannel的网络读写。接下来我们再创建了ServerBootstrap对象,它是Netty用于启动NIO的辅助启动类,目的是降低服务端的开发复杂度。下一行调用了ServerBootstrap的group方法,将两个NIO线程当作入参传递到ServerBootstrap中。接着设置Channel为NioServerSocketChannel,它的功能对应于JDK NIO类库中的ServerSocketChannel类。然后配置NioServerSocketChannel的TCP参数,此处将它的backlog设置为1024,最后绑定I/O事件的处理类ChildChannelHandler,它的作用类似于Reactor模式中的Handler类,主要用于处理网络I/O事件,例如记录日志,对消息进行编解码等。

    服务端启动辅助类配置完成后,调用它的bind方法绑定监听端口,随后,调用它的同步阻塞方法sync等待绑定操作完成。完成之后Netty会返回一个ChannelFuture,它的功能类似于JDK的java.util.concurrent.Future,主要用于异步操作的通知回调。

    还用了f.channel().closeFuture().sync()方法进行阻塞,等待服务器端链路关闭之后main函数才退出。最后用了shutdownGracefully()方法进行优雅地退出,它会释放跟shutdownGracefully相关联的资源。

package com.server;

import io.netty.buffer.ByteBuf;

import io.netty.buffer.Unpooled;

import io.netty.channel.ChannelHandlerContext;

import io.netty.channel.ChannelInboundHandlerAdapter;

public class TimeServerHandler extends ChannelInboundHandlerAdapter {

/**

* 收到客户端消息,自动触发

*/

@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[] reg = new byte[buf.readableBytes()];

/**

* readBytes:将缓冲区字节数组复制到新建的 byte 数组中 然后将字节数组转为字符串

*/

buf.readBytes(reg);

String body = new String(reg, "UTF-8");

System.out.println(Thread.currentThread().getName() + ",The server receive  order : " + body);

/**

* 回复消息 copiedBuffer:创建一个新的缓冲区,内容为里面的参数 通过 ChannelHandlerContext 的 write

* 方法将消息异步发送给客户端

*/

String respMsg = "I am Server,消息接收 success!";

ByteBuf respByteBuf = Unpooled.copiedBuffer(respMsg.getBytes());

ctx.write(respByteBuf);

}

@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) throws Exception {

        /**当发生异常时,关闭 ChannelHandlerContext,释放和它相关联的句柄等资源 */

        ctx.close();

    }

}

    TimeServerHandler类继承自ChannelInboundHandlerAdapter,它用于对网络事件进行读写操作,通常我们只需关注channelRead和exceptionCaught方法。下面对这两个方法进行说明。

    ByteBuf buf = (ByteBuf) msg做了类型转换,将msg转换为Netty的ByteBuf对象。ByteBuf类似于JDK中java.nio.ByteBuffer对象,不过它提供了更加强大和灵活的功能。通过ByteBuf的readableBytes方法可以获取缓冲区可读的字节数,根据可读字节数创建byte数组,通过ByteBuf的readBytes方法将缓冲区中的字节数组复制到新建的byte数组中,最后通过new String构造函数获取请求消息。这是对消息进行判断,如果是"QUERY TIME ORDER"则创建应答消息,通过ChanelHandlerContext的write方法异步发送应答消息给客户端。

    在channelReadComplete()方法中调用了ChanelHandlerContext的flush方法,它的作用是将消息发送队列中的消息写入到SocketChannel中发送给对方。从性能角度考虑,为了防止频繁地唤醒Selector进行消息发送,Netty的write方法并不直接将消息写入SocketChannel中,调用write方法只是把待发送的消息放到缓冲数组中,再通过调用flush方法,将发送缓冲区中的消息全部写入到SocketChannel中。

    在exceptionCaught方法中调用了ctx.close()方法,它的作用是当发生异常时关闭ChannelHandlerContext,释放ChannelHandlerContext相关联的句柄等资源。

2. 客户端实现

package com.client;

import io.netty.bootstrap.Bootstrap;

import io.netty.channel.ChannelFuture;

import io.netty.channel.ChannelInitializer;

import io.netty.channel.ChannelOption;

import io.netty.channel.EventLoopGroup;

import io.netty.channel.nio.NioEventLoopGroup;

import io.netty.channel.socket.SocketChannel;

import io.netty.channel.socket.nio.NioSocketChannel;

public class TimeClient {

/**

* 使用 3 个线程模拟三个客户端

*

* @param args

*/

public static void main(String[] args) {

for (int i = 0; i < 3; i++) {

new Thread(new MyThread()).start();

}

}

static class MyThread implements Runnable {

@Override

public void run() {

connect("localhost", 9898);

}

public void connect(String host, int port) {

/** 配置客户端 NIO 线程组/池 */

EventLoopGroup group = new NioEventLoopGroup();

try {

/**

* Bootstrap 与 ServerBootstrap 都继承(extends)于 AbstractBootstrap

* 创建客户端辅助启动类,并对其配置,与服务器稍微不同,这里的 Channel 设置为 NioSocketChannel 然后为其添加

* Handler,这里直接使用匿名内部类,实现 initChannel 方法 作用是当创建 NioSocketChannel

* 成功后,在进行初始化时,将它的ChannelHandler设置到ChannelPipeline中,用于处理网络I/O事件

*/

Bootstrap b = new Bootstrap();

b.group(group).channel(NioSocketChannel.class).option(ChannelOption.TCP_NODELAY, true)

.handler(new ChannelInitializer() {

@Override

public void initChannel(SocketChannel ch) throws Exception {

ch.pipeline().addLast(new TimeClientHandler());

}

});

/** connect:发起异步连接操作,调用同步方法 sync 等待连接成功 */

ChannelFuture channelFuture = b.connect(host, port).sync();

System.out.println(Thread.currentThread().getName() + ",客户端发起异步连接..........");

/** 等待客户端链路关闭 */

channelFuture.channel().closeFuture().sync();

} catch (InterruptedException e) {

e.printStackTrace();

} finally {

/** 优雅退出,释放NIO线程组 */

group.shutdownGracefully();

}

}

}

}

    connect()方法中代码EventLoopGroup group = new NioEventLoopGroup();首先创建客户端处理I/O读写的NioEventLoopGroup线程组,然后继续创建客户端辅助类Bootstrap,随后需要对其进行配置。与服务端不同的是,它的Channel需要设置为NioSocketChannel,然后为其添加handler,此处为了简单直接创建匿名内部类,实现initChannel方法,其作用是当创建NioSocketChannel成功之后,在初始化它的时候将它的ChannelHandler设置到ChannelPipeline中,用于处理网络I/O事件。

    客户端启动辅助类设置完成之后,调用connect方法发起异步连接,然后调用同步方法等待连接成功。最后,当客户端连接关闭之后,客户端主函数退出,在退出之前,释放NIO线程组的资源。

package com.client;

import java.util.logging.Logger;

import io.netty.buffer.ByteBuf;

import io.netty.buffer.Unpooled;

import io.netty.channel.ChannelHandlerContext;

import io.netty.channel.ChannelInboundHandlerAdapter;

public class TimeClientHandler extends ChannelInboundHandlerAdapter {

/**

* 用于对网络事件进行读写操作

*/

private static final Logger logger = Logger.getLogger(TimeClientHandler.class.getName());

/**

* 当客户端和服务端 TCP 链路建立成功之后,Netty 的 NIO 线程会调用 channelActive 方法

*/

@Override

public void channelActive(ChannelHandlerContext ctx) throws Exception {

String reqMsg = "我是客户端 " + Thread.currentThread().getName();

byte[] reqMsgByte = reqMsg.getBytes("UTF-8");

ByteBuf reqByteBuf = Unpooled.buffer(reqMsgByte.length);

/**

* writeBytes:将指定的源数组的数据传输到缓冲区 调用 ChannelHandlerContext 的 writeAndFlush

* 方法将消息发送给服务器

*/

reqByteBuf.writeBytes(reqMsgByte);

ctx.writeAndFlush(reqByteBuf);

}

/**

* 当服务端返回应答消息时,channelRead 方法被调用,从 Netty 的 ByteBuf 中读取并打印应答消息

*/

@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(Thread.currentThread().getName() + ",Server return Message:" + body);

ctx.close();

}

/**

    * 当发生异常时,打印异常 日志,释放客户端资源

    */

    @Override

    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {

        /**释放资源*/

        logger.warning("Unexpected exception from downstream : " + cause.getMessage());

        ctx.close();

    }

}

    这里重点关注三个方法:channelActive, channelRead, 和channelCaught。当客户端和服务端TCP链路建立成功之后,Netty的NIO线程会调用channelActive方法,发送查询指令给服务端,调用channelHandlerContext的writeAndFlush方法将请求消息发送给服务端。

    当服务端返回应答消息时,channelRead方法被调用,会读取并打印消息。当发生异常时,会在exceptionCaught方法中打印出错信息并关系相关资源。

    下图是客户端、服务端交互信息。


Netty入门_第1张图片

学习自:《Netty权威指南》

                                                                                    2019-09-08

你可能感兴趣的:(Netty入门)