Netty学习之Server和Client端的构建过程

Netty简介

Netty封装了JAVA NIO的底层网络通信库,并依照网络编程Reactor的设计模式,实现了一套易于开发者使用的高性能并发网络开发构架,现已广泛应用于许多的大于项目,如Spark、Kafka等,开发者可以很容易很通过Netty官方文档github官网找到诸多示例代码,这篇文章仅仅是依官方示例简单拆解,为求更加通俗。

文章中的描述有时会涉及一些Netty构架的设计及实现,如果有些地方不易理解,还请读者浏览本人其它相关博文。

Server端的构建流程

1. 创建EventLoopGroup

EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workerGroup)
  .channel(NioServerSocketChannel.class)
  .option(ChannelOption.SO_BACKLOG, 100);

如上的代码,通过创建一个ServerBootstrap类,引导用户创建一个能够以多线程的方式,来处理Socket通信的服务器实例。
EventLoop:可以认为就是一个无限循环的线程,可以处理Netty中的SocketChannel的连接、读取和写入。当一个新的连接打开时,Netty会从workerGroup中按顺序或是按规则挑选一个EventLoop并返回,然后将这个新的Netty Channel注册到这个EventLoop实例上,实际上就是更新Channel的EventLoop引用为挑选出来的实例上;然后调用自己的register(…)方法将自己注册到这个EventLoop绑定的Selector上,同时也会发送fireChannelRegistered(…)消息给所有注册在这个Channel上的handler。

EventLoopGroup:内部保存了多个EventLoop的实例,可以认为就是一个线程池。

ServerBootstap:可以通过group(…)方法,指定两个EventLoopGroup的实例,分别用来为Selector的处理和Channel的处理指定工作空间。至于什么是Selector处理和Channel处理,请见后面消息处理流程的分析

NioServerSocketChannel:Netty中定义的类,专门用于ServerBootstrap的构建,它内部封装了JAVA NIO ServerSocketChannel,负责接收客户端新连接,创建相应的NioSocketChannel实例,然后在发生读操作时将新生成的NioSocketChannel注册到WorkerGroup上处理。一个NioServerSocketChannel实例,监听一个端口,并注册在BossGroup的某个EventLoop上,当底层JAVA NIO SocketChannel上的发生读写操作时,利用类继承的特性,实现SocketChannel读写从BossGroup线程池到WorkerGroup线程池的迁移。

2. 创建自定义消息处理器

用户可以通过继承ChannelInboundHandlerAdapter类来定义消息的处理流程,如下的代码仅仅是将收到的消息对象打印,然后再通过ChannelHandlerContext实例返回给Client端。
ChannelInboundHandlerAdapter:消息接收端的处理器,这个是Netty的默认实现类,可以响应底层SocketChannel产生的各种事件,如读消息事件、读消息完成事件、yfSocketChannel异常处理等。Nettry中所有带有InboundHandler关键字的类,表示处理消息的流入,相反的带有OutboundHandler关键字的类处理消息的流出。
ChannelHandlerContext:内部保存了底层SocketChannel的引用,因此可以通过调用此实例的write(…)方法将消息写出到底层的Socket通道。

@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
    	System.print(msg);
        ctx.write(msg);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}

3. 注册消息处理器

用户可以定义许多消息处理器类,并将这些处理器对象绑定到底层的SocketChannel实例上,这种绑定关系需要用户在创建ServerBootstrap实例时,通过handler(…)和childHandler(…)方法指明。
例如下面的代码首先调用handler(…)方法,为bossGroup绑定了一个LoggingHandler类的实例,表示bossGroup中的线程在处理收到的事件时,事件会流经这个对象,然后根据事件的不同执行LoggingHandler实现的不同方法,这里仅仅是打印日志信息,就像EchoServerHandler的行为。
然后调用了ServerBootstrap.childHandler(…)方法,为workerGroup绑定了个消息处理器实例,分别是LoggingHandlerEchoServerHandler,表示workerGroup中的线程在处理收到的事件时,事件会流经这两个类,然后根据事件的不同执行它们的不同方法。其中EchoServerHandler实例在处理Channel的读取事件时,打印接收到的消息,然后再返回给客户端,而LoggingHandler则打印所有事件的日志。

NioEventLoop类前面已经提到过,其内部会保存一个JAVA NIO中的Selector的实例,用于Socket Channel各种状态的检测。boosGroup中的线程就是用来循环调用绑定的Selector的方法,获取注册在这个Selector上的JAVA SocketChannel的状态,比如某个JAVA SocketChannel的状态是OP_WRITE,表示底层的SocketChannel可以写出数据,因此就会将在这个通道上所有等待被发送的数据,通过调用JAVA的SocketChannel.write(…)方法,写入到Socket缓存区。

NioServerSocketChannel:这个类是在Server端创建Bootstrap时指定的用于包装JAVA SocketChannel实例的包装类,由于Netty使用的是主从线程模型,因此可以通过继承同一个Netty中的Channel基类,很方便地将bossGroup线程中要进行读写处理的Channel,转换成一个新NioSocketChannel类型,然后重新注册到workerGroup上,交由workerGroup中的某个EventLoop实例执行,从而实现主从分离。

b.handler(new LoggingHandler(LogLevel.INFO))
  .childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    public void initChannel(SocketChannel ch) throws Exception {
      ChannelPipeline p = ch.pipeline();
      p.addLast(new LoggingHandler(LogLevel.INFO));
      p.addLast(new EchoServerHandler());
    }
  });

4. 启动监听

// Start the server.
ChannelFuture f = b.bind(PORT).sync();
// Wait until the server socket is closed.
f.channel().closeFuture().sync();

5. 关闭Group

// Shut down all event loops to terminate all threads.
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();

完整的代码示例

/**
 * Echoes back any received data from a client.
 */
public final class EchoServer {
    static final boolean SSL = System.getProperty("ssl") != null;
    static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));

    public static void main(String[] args) throws Exception {
        // Configure SSL.
        final SslContext sslCtx;
        if (SSL) {
            SelfSignedCertificate ssc = new SelfSignedCertificate();
            sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()).build();
        } else {
            sslCtx = null;
        }

        // Configure the server.
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        final EchoServerHandler serverHandler = new EchoServerHandler();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
             .channel(NioServerSocketChannel.class)
             .option(ChannelOption.SO_BACKLOG, 100)
             .handler(new LoggingHandler(LogLevel.INFO))
             .childHandler(new ChannelInitializer<SocketChannel>() {
                 @Override
                 public void initChannel(SocketChannel ch) throws Exception {
                     ChannelPipeline p = ch.pipeline();
                     if (sslCtx != null) {
                         p.addLast(sslCtx.newHandler(ch.alloc()));
                     }
                     //p.addLast(new LoggingHandler(LogLevel.INFO));
                     p.addLast(serverHandler);
                 }
             });

            // Start the server.
            ChannelFuture f = b.bind(PORT).sync();

            // Wait until the server socket is closed.
            f.channel().closeFuture().sync();
        } finally {
            // Shut down all event loops to terminate all threads.
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

Client端的构建流程

基本过程与Server端相同,主要的不同是使用Bootstrap引导构建,并指定工作的Socket通道类型为NioSocketChannel,完整的客户端代码示例如下:

public class EchoClientHandler extends ChannelInboundHandlerAdapter {

    private final ByteBuf firstMessage;

    /**
     * Creates a client-side handler.
     */
    public EchoClientHandler() {
        firstMessage = Unpooled.buffer(EchoClient.SIZE);
        for (int i = 0; i < firstMessage.capacity(); i ++) {
            firstMessage.writeByte((byte) i);
        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        ctx.writeAndFlush(firstMessage);
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        ctx.write(msg);
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
       ctx.flush();
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}

public final class EchoClient {

    static final boolean SSL = System.getProperty("ssl") != null;
    static final String HOST = System.getProperty("host", "127.0.0.1");
    static final int PORT = Integer.parseInt(System.getProperty("port", "8007"));
    static final int SIZE = Integer.parseInt(System.getProperty("size", "256"));

    public static void main(String[] args) throws Exception {
        // Configure SSL.git
        final SslContext sslCtx;
        if (SSL) {
            sslCtx = SslContextBuilder.forClient()
                .trustManager(InsecureTrustManagerFactory.INSTANCE).build();
        } else {
            sslCtx = null;
        }

        // Configure the client.
        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 {
                     ChannelPipeline p = ch.pipeline();
                     if (sslCtx != null) {
                         p.addLast(sslCtx.newHandler(ch.alloc(), HOST, PORT));
                     }
                     //p.addLast(new LoggingHandler(LogLevel.INFO));
                     p.addLast(new EchoClientHandler());
                 }
             });

            // Start the client.
            ChannelFuture f = b.connect(HOST, PORT).sync();

            // Wait until the connection is closed.
            f.channel().closeFuture().sync();
        } finally {
            // Shut down the event loop to terminate all threads.
            group.shutdownGracefully();
        }
    }
}

NioSocketChannel:与NioServerSocketChannel有相同的父类,Netty中通过继承的方式最大化地复用代码,并利用多态特性为这两个Channel实现类定义不同的行为,即NioSocketChannel的read(…)/write(…)方法是真正地读写操作系统Socket缓存区,而NioSocketServerChannel只负责read(…),其实现实际上是将读的工作委托给NioSocketChannel实例,并在WorkerGroup上完成。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值