Netty快速开始

前言

Github:https://github.com/yihonglei/thinking-in-netty

一 Netty概述

Netty 提供异步的、事件驱动的网络应用程序框架和工具,用以快速简单开发高性能、高可靠性的网络服务器和客户端程序。

它大大简化了网络编程,如TCP和UDP套接字服务器。“快速简单”并不意味着生成的应用程序将受到可维护性或

性能问题的影响。Netty经过精心设计,并积累了许多协议(如ftp、smtp、http)的实施经验,以及各种二进制和

基于文本的遗留协议。因此,Netty成功地找到了一种方法,可以在不妥协的情况下实现轻松的开发、性能、稳定性和灵活性。

二 Netty特性

Netty官方架构图。

设计

  • 统一的API,适用于同的协议(阻塞和非阻塞)
  • 基于灵活、可扩展的事件驱动模型SEDA
  • 高度可定制的线程模型
  • 可靠的无连接数据Socket支持(UDP)

性能

  • 更好的吞吐量,低延迟
  • 更省资源
  • 尽量减少必要的内存拷贝

安全

  • 完整的SSL/ TLS和STARTTLS的支持

易用

  • 完善的Java doc,用户指南和样例
  • 仅依赖于JDK1.6(netty 4.x)

三 Netty核心组件

1、Tansport Channel

对应NIO中的Channel,是NIO的基本构造,可以把Channel看成是传入(入站)或者传出(出站)数据的载体。

因此,它可以被打开或者关闭,连接或者断开连接。

2、EventLoop、ChannelHandler、EventLoopGroup

EventLoop是NIO中selector程序的抽象,消除了在NIO中手动的编写派发代码。在Netty内部,将会为每个Channel

分配一个EventLoop,用以处理所有I/O事件:

  • 注册感兴趣的事件;
  • 将事件派发给ChannelHandler;
  • 安排进一步的处理;

每一个事件触发后将直接交由ChannelHandler接口实现处理。

EventLoop本身只由一个线程驱动,处理一个Channel的所有I/O事件,并且在该EventLoop的整个生命周期内部不会改变。

一个EventLoopGroup可以包含多个EventLoop。

3、ChannelFutue

Netty提供了另外一种在操作完成时通知应用程序的方式。在未来的某个时刻完成时提供对其结果的访问。

JDK内置包里面java.util.concurrent.Future接口提供了实现,但是操作比较麻烦,Netty提供了自己的实现,

ChannelFuture用于在执行异步操作的时候使用。ChannelFutue不会阻塞,完全是异步和事件驱动的。

4、ByteBuf

对应NIO中的ByteBuffer。

5、Bootstrap 和 ServerBootstrap 

对应NIO中的Selector、ServerSocketChannel等的创建、配置、启动等。

Netty里面有很多的概念,概念说太多就迷糊了,先来个实例,体验下Netty入门编程。

四 Netty入门实战

jar包

<dependency>
   <groupId>io.netty</groupId>
   <artifactId>netty-all</artifactId>
   <version>4.1.44.Final</version>
</dependency>

1、服务端

1.1 EchoServer

启动服务端,等待客户端连接,将连接事件交给EchoServerHandler处理。

package com.jpeony.netty.echo;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

import java.net.InetSocketAddress;

/**
 * 一 Netty Server启动流程
 * 1、设置服务端ServerBootStrap启动参数
 * <p>
 * group()方法分配一个NioEventLoopGroup实例以进行事件处理,如接受新连接以及读/写数据;
 * <p>
 * channel()设置通道类型;
 * <p>
 * localAddress()指定服务器绑定的本地的InetSocketAddress;
 * <p>
 * childHandler()添加一个EchoServerHandler到Channel的ChannelPipeline;
 * 2、调用ServerBootStrap.bind()方法启动服务端
 * 调用ServerBootStrap.bind()方法启动服务端,bind方法会在group中注册NioServerScoketChannel,
 * 监听客户端的连接请求会创建一个NioServerSocketChannel实例,并将其在group中进行注册;
 *
 * @author yihonglei
 */
public class EchoServer {
    private final int port;

    public EchoServer(int port) {
        this.port = port;
    }

    /**
     * 服务器端启动方法
     *
     * @author yihonglei
     */
    private void start() throws Exception {
        // 创建EventLoopGroup
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            // 创建ServerBootstrap
            ServerBootstrap b = new ServerBootstrap();

            // 指定EventLoopGroup以处理服务端事件,需要适用于NIO的实现。
            b.group(bossGroup, workerGroup)
                    // 指定所使用的NIO传输Channel
                    .channel(NioServerSocketChannel.class)
                    // 使用指定的端口设置套接字地址
                    .localAddress(new InetSocketAddress(port))
                    // 添加一个EchoServerHandler到Channel的ChannelPipeline
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) {
                            // EchoServerHandler被标注为@Shareable,所以我们可以总是使用同样的实例
                            ch.pipeline().addLast(new EchoServerHandler());
                        }
                    });

            // 异步地绑定服务器,调用sync()方法阻塞直到绑定完成
            ChannelFuture f = b.bind().sync();

            // 绑定Channel的CloseFuture,并且阻塞当前线程直到它完成
            f.channel().closeFuture().sync();
        } finally {
            // 关闭EventLoopGroup并且释放所有的资源
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        // 设置端口值
        new EchoServer(9999).start();
    }
}

1.2 EchoServerHandler

EchoServerHandler负责服务端具体逻辑处理。

package com.jpeony.netty.echo;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;


/**
 * 服务端业务逻辑处理。
 *
 * @author yihonglei
 */
@ChannelHandler.Sharable //注解@ChannelHandler.Sharable表示一个ChannelHandler可以被多个Channel安全地共享。
public class EchoServerHandler extends ChannelInboundHandlerAdapter {

    /**
     * 每个传入的消息都要调用该方法。
     *
     * @author yihonglei
     */
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // 将客户端发送过来的消息打印到控制台
        ByteBuf in = (ByteBuf) msg;
        System.out.println("server received msg from client:" + in.toString(CharsetUtil.UTF_8));

        // 写一条消息响应给客户端
        ByteBuf responseMsg = Unpooled.wrappedBuffer(new String("Hello Client!").getBytes());
        ctx.write(responseMsg);
    }

    /**
     * 通知ChannelInboundHandler最后一次对channelRead()的调用时当前批量读取中的最后一条消息。
     *
     * @author yihonglei
     */
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        // 将未决消息冲刷到远程节点,并且关闭该Channel
        ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
                .addListener(ChannelFutureListener.CLOSE);
    }

    /**
     * 在读取操作期间,有异常抛出时会调用。
     *
     * @author yihonglei
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // 打印异常栈跟踪信息
        cause.printStackTrace();

        // 关闭该Channel
        ctx.close();
    }
}

2、客户端

2.1 EchoClient

1)连接到服务器;

2)发送一个或者多个消息;

3)对于每个消息,等待并接收从服务器发回的消息;

4)关闭连接;

客户端逻辑交由EchoClientHandler处理。

package com.jpeony.netty.echo;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;

import java.net.InetSocketAddress;

/**
 * 1、初始化客户端,将创建一个Bootstrap实例;
 * 2、为事件处理分配一个NioEventLoopGroup实例,其中事件处理包括创建新的连接以及处理入站和出站数据;
 * 3、为服务器连接创建一个InetSocketAddress实例;
 * 4、当连接被建立时,一个EchoClientHandler实例会被安装到ChannelPipeline中;
 * 5、在一切都设置完成后,调用Bootstrap.connect()方法连接到远程节点;
 *
 * @author yihonglei
 */
public class EchoClient {
    private final String host;
    private final int port;

    public EchoClient(String host, int port) {
        this.host = host;
        this.port = port;
    }

    /**
     * 客户端启动方法
     *
     * @author yihonglei
     */
    private void start() throws InterruptedException {
        // 创建EventLoopGroup
        EventLoopGroup group = new NioEventLoopGroup();

        try {
            // 创建Bootstrap
            Bootstrap b = new Bootstrap();

            // 指定EventLoopGroup以处理客户端事件,需要适用于NIO的实现。
            b.group(group)
                    // 适用于NIO传输的Channel类型
                    .channel(NioSocketChannel.class)
                    // 设置服务器的InetSocketAddress
                    .remoteAddress(new InetSocketAddress(host, port))
                    // 在创建Channel时,向ChannelPipeline中添加一个EchoClientHandler实例
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        protected void initChannel(SocketChannel ch) throws Exception {
                            ch.pipeline().addLast(new EchoClientHandler());
                        }
                    });

            // 连接到远程节点,阻塞等待知道连接完成
            ChannelFuture f = b.connect().sync();

            // 阻塞,直到Channel关闭
            f.channel().closeFuture().sync();
        } finally {
            // 关闭EventLoopGroup并且释放所有的资源
            group.shutdownGracefully().sync();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new EchoClient("127.0.0.1", 9999).start();
    }
}

2.2 EchoClientHandler

负责客户端逻辑处理。

package com.jpeony.netty.echo;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;

/**
 * 客户端业务逻辑处理
 *
 * @author yihonglei
 */
@ChannelHandler.Sharable // 标记该类的实例可以被多个Channel共享
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

    /**
     * 连接服务器成功之后被调用
     *
     * @author yihonglei
     */
    @Override
    public void channelActive(ChannelHandlerContext ctx) {
        // 当被通知Channel是活跃的时候,发送一条消息(向服务端发送一条消息)
        String data = "Hello Server!";
        ctx.writeAndFlush(Unpooled.copiedBuffer(data, CharsetUtil.UTF_8));
    }

    /**
     * 从服务器接收到消息时被调用
     *
     * @author yihonglei
     * @date 2019/4/20 16:27
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, ByteBuf in) {
        // 将服务端响应的消息打印出来
        System.out.println("client received msg from client:" + in.toString(CharsetUtil.UTF_8));
    }

    /**
     * 在处理过程中抛异常时调用
     *
     * @author yihonglei
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // 打印异常栈跟踪信息
        cause.printStackTrace();

        // 关闭该Channel
        ctx.close();
    }
}

3、程序启动

先启动服务端EchoServer,然后再启动客户端EchoClient。

服务端接收到客户端发送过来的消息Hello Server!

客户端收到服务端响应的消息Hello Client!

发布了502 篇原创文章 · 获赞 358 · 访问量 118万+

猜你喜欢

转载自blog.csdn.net/yhl_jxy/article/details/89424593