Java进阶(2)——NIO之网络IO、Netty、RPC

1.网络IO

1.1 概述和核心 API

前面在进行文件 IO 时用到的 FileChannel 并不支持非阻塞操作,学习 NIO 主要就是进行网络 IO操作,Java NIO 中的网络通道是非阻塞 IO 的实现,基于事件驱动,非常适用于服务器需要维持大量连接,但是数据交换量不大的情况,例如一些即时通信的服务等等…
在 Java 中编写 Socket 服务器,通常有以下几种模式:
 一个客户端连接用一个线程,优点:程序编写简单;缺点:如果连接非常多,分配的线程也会非常多,服务器可能会因为资源耗尽而崩溃。
 把每一个客户端连接交给一个拥有固定数量线程的连接池,优点:程序编写相对简单,可以处理大量的连接。确定:线程的开销非常大,连接如果非常多,排队现象会比较严重。
 使用 Java 的 NIO,用非阻塞的 IO 方式处理。这种模式可以用一个线程,处理大量的客户端连接。

1. Selector(选择器)

能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。
在这里插入图片描述
该类的常用方法如下所示:
 public static Selector open(),得到一个选择器对象
 public int select(long timeout),监控所有注册的通道,当其中有 IO 操作可以进行时,将对应的 SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
 public Set selectedKeys(),从内部集合中得到所有的 SelectionKey

2. SelectionKey

代表了 Selector 和网络通道的注册关系,一共四种:
 int OP_ACCEPT:有新的网络连接可以 accept,值为 16
 int OP_CONNECT:代表连接已经建立,值为 8
 int OP_READ 和 int OP_WRITE:代表了读、写操作,值为 1 和 4
该类的常用方法如下所示:
 public abstract Selector selector(),得到与之关联的 Selector 对象
 public abstract SelectableChannel channel(),得到与之关联的通道
 public final Object attachment(),得到与之关联的共享数据
 public abstract SelectionKey interestOps(int ops),设置或改变监听事件
 public final boolean isAcceptable(),是否可以 accept
 public final boolean isReadable(),是否可以读
 public final boolean isWritable(),是否可以写

3. ServerSocketChannel

用来在服务器端监听新的客户端 Socket 连接,常用方法如下所示:
 public static ServerSocketChannel open(),得到一个 ServerSocketChannel 通道
 public final ServerSocketChannel bind(SocketAddress local),设置服务器端端口号
 public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,
取值 false 表示采用非阻塞模式
 public SocketChannel accept(),接受一个连接,返回代表这个连接的通道对象
 public final SelectionKey register(Selector sel, int ops),注册一个选择器并设置监听事件

4. SocketChannel

网络 IO 通道,具体负责进行读写操作。NIO 总是把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。常用方法如下所示:
 public static SocketChannel open(),得到一个 SocketChannel 通道
 public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
 public boolean connect(SocketAddress remote),连接服务器
 public boolean finishConnect(),如果上面的方法连接失败,接下来就要通过该方法完成连接操作
 public int write(ByteBuffer src),往通道里写数据
 public int read(ByteBuffer dst),从通道里读数据
 public final SelectionKey register(Selector sel, int ops, Object att),注册一个选择器并设置监听事件,最后一个参数可以设置共享数据
 public final void close(),关闭通道
在这里插入图片描述

1.2 入门案例

API 学习后,接下来使用 NIO 开发一个入门案例,实现服务器端和客户端之间的数据通信(非阻塞)。
在这里插入图片描述

//服务器端
public class NIOServer {
    public static void main(String[] args) throws Exception{
        //1.创建一个服务器端  老大
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        //2.创建一个Selector对象 间谍
        Selector selector = Selector.open();
        //3.给服务器端绑定一个端口号
        serverChannel.bind(new InetSocketAddress(9999));
        //4.设置服务器端为非阻塞的
        serverChannel.configureBlocking(false);
        //5.给服务器对象注册一个selector,用以监听客户端连接事件
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
        //6.监听事件,开始干活
        while (true){
            //6.1监控客户端
            if(selector.select(2000) == 0){//select里是连接的超时时间,如果客户端没有响应连接,那么在这里可以干别的,这就是NIO的优势
                System.out.println("server:连接不上客户端,先干点别的事情");
                continue;//客户端连不上还需要继续连接才行
            }
            //6.2得到SelectionKey,判断通道里的事件
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();//有多少个客户端就有多少个SelectionKey
            while (iterator.hasNext()) {
                //拿到所有的连接信息,对应不同的连接状态或者信息的时候干不同的活
                SelectionKey selectionKey = iterator.next();
                if(selectionKey.isAcceptable()){//处理连接事件,就好像传统网络编程中的服务器端需要拿到一个客户端来进行处理
                    System.out.println("OP_ACCEPT");
                    SocketChannel acceptSocket = serverChannel.accept();
                    acceptSocket.configureBlocking(false);//设置拿到的socket为非阻塞式的
                    acceptSocket.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));//服务器端的socket就关心读取状态
                }
                if (selectionKey.isReadable()) {//处理读取信息事件
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    ByteBuffer buffer = (ByteBuffer) selectionKey.attachment();
                    channel.read(buffer);//将通道里的信息读取出来,放到缓冲区中
                    System.out.println("客户端发来的消息:" + new String(buffer.array()));
                }

                //6.3手动从集合中删除当前的key,防止重复处理
                iterator.remove();
            }
        }
    }
}

上面代码用 NIO 实现了一个服务器端程序,能不断接受客户端连接并读取客户端发过来的数据。

public class NIOClient {
    //NIO客户端程序
    public static void main(String[] args) throws Exception{
        //1.开启一个客户端通道
        SocketChannel socketChannel = SocketChannel.open();
        //2.设置通道为非阻塞式的
        socketChannel.configureBlocking(false);
        //3.客户端连接服务器
        InetSocketAddress address = new InetSocketAddress("127.0.0.1",9999);

        if(!socketChannel.connect(address)){
            while (!socketChannel.finishConnect()){//这是NIO的优势,在线程阻塞的时候可以干别的事情
                System.out.println("我是客户端,我暂时连接不上服务器,我不等了,我干点别的");
            }
        }
        //4.如果客户端连接上了服务器就准备发送数据
        //4.准备一个缓冲区
        String msg = "hello server!";
        ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
        //5.将缓冲区的数据写入到通道中去
        socketChannel.write(buffer);
        //6.关闭流
        System.in.read();
    }
}

上面代码通过 NIO 实现了一个客户端程序,连接上服务器端后发送了一条数据,运行效果如下图所示:
在这里插入图片描述

1.3 网络聊天案例

刚才通过 NIO 实现了一个入门案例,基本了解了 NIO 的工作方式和运行流程,接下来用 NIO 实现一个多人聊天案例,具体代码如下所示:

扫描二维码关注公众号,回复: 10283160 查看本文章
public class ChatServer {

    private Selector selector;
    private ServerSocketChannel serverChannel;
    private static final int PORT = 9999; //服务器端口

    public ChatServer(){
        try {
            //1.设置服务器端通道
            serverChannel = ServerSocketChannel.open();
            //2.设置选择器
            selector = Selector.open();
            //3.绑定服务器端口号
            serverChannel.bind(new InetSocketAddress(PORT));
            //4.设置服务器通道为非阻塞式的
            serverChannel.configureBlocking(false);
            //5.给服务器端注册选择器监听
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            printInfo("chat server is ready");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start(){
        try {
            while (true){
                if(selector.select(2000) == 0){//体现非阻塞式io的优势,连接阻塞可以干别的事情
                    printInfo("服务器:现在没有客户端链接我,我干点别的事");
                    continue;
                }
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()){
                    SelectionKey selectionKey = iterator.next();
                    if(selectionKey.isAcceptable()){//连接请求事件
                        SocketChannel channel = serverChannel.accept();
                        channel.configureBlocking(false);
                        channel.register(selector,SelectionKey.OP_READ);
                        System.out.println(channel.getRemoteAddress().toString().substring(1) + "上线了。。。");
                    }
                    if(selectionKey.isReadable()){//发生读取事件
                        //读取通道里的消息,并广播给所有的通道(模拟的是一个群聊)
                        readMsg(selectionKey);
                    }
                    //每次循环都把当前的key删除
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //读取客户端发来的消息,并广播出去
    public void readMsg(SelectionKey key){
        try {
            SocketChannel channel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            channel.read(buffer);
            String msg = new String(buffer.array());
            printInfo(msg);
            //把消息广播出去
            broadCast(channel,msg);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    //广播数据给所有的客户端
    public void broadCast(SocketChannel except,String msg){//不给发消息的自己发送广播
        System.out.println("服务器发送广播了。。。");
        Set<SelectionKey> keys = selector.keys();
        for(SelectionKey nowKey :keys){
            Channel targetChannel = nowKey.channel();
            if(targetChannel instanceof SocketChannel && targetChannel != except){
                SocketChannel destChannel = (SocketChannel)targetChannel;
                ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                try {
                    destChannel.write(buffer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    public void printInfo(String str){
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        System.out.println("[" + sdf.format(new Date()) + "] -> " + str);
    }

    public static void main(String[] args) {
        new ChatServer().start();
    }
}

上述代码使用 NIO 编写了一个聊天程序的服务器端,可以接受客户端发来的数据,并能把数据广播给所有客户端。

//聊天程序客户端
public class ChatClient {
    private String HOST = "127.0.0.1";
    private int PORT = 9999;
    private SocketChannel channel;
    private String userName;

    public ChatClient(){
        try {
            //1.开启一个客户端通道
            channel = SocketChannel.open();
            //2.配置客户端通道为非阻塞式的
            channel.configureBlocking(false);
            //3,配置客户端的端口号
            InetSocketAddress address = new InetSocketAddress(HOST,PORT);
            //4.连接服务器
            if (!channel.connect(address)){
                while (!channel.finishConnect()){
                    System.out.println("Client:" + "连接不上服务器,我干点别的事...");
                }
            }
            userName = channel.getLocalAddress().toString().substring(1);
        } catch (IOException e) {
            e.printStackTrace();
        }
        System.out.println("---------------Client(" + userName + ") is ready---------------");
    }

    //发送消息给服务器端
    public void sendMsg(String msg){
        if(msg.equalsIgnoreCase("bye")){
            try {
                channel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return;
        }
        msg = userName + " say: " + msg;
        ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
        try {
            channel.write(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //从服务器端接受消息
    public void receiveMsg(){
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int res = 0;
        try {
            res = channel.read(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //读取到消息,就打印出来
        if(res > 0){
            String msg = new String(buffer.array());
            System.out.println(msg.trim());
        }
    }
}

上述代码通过 NIO 编写了一个聊天程序的客户端,可以向服务器端发送数据,并能接收服务器广播的数据。

//启动聊天程序客户端
public class TestChat {
    public static void main(String[] args) {
        ChatClient chatClient = new ChatClient();

        //开启一个线程,死循环地读取服务器发过来的消息
        new Thread(){
            @Override
            public void run() {
                while (true) {
                    chatClient.receiveMsg();
                    try {
                        Thread.sleep(2000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        //客户端发送数据
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String msg = scanner.nextLine();
            chatClient.sendMsg(msg);
        }
    }
}

上述代码运行了聊天程序的客户端,并在主线程中发送数据,在另一个线程中不断接收服务器端的广播数据,该代码运行一次就是一个聊天客户端,可以同时运行多个聊天客户端,聊天效果如下图所示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

1.4 AIO编程

JDK 7 引入了 Asynchronous I/O,即 AIO。在进行 I/O 编程中,常用到两种模式:Reactor和 Proactor。Java 的 NIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的处理。
AIO 即 NIO2.0,叫做异步不阻塞的 IO。AIO 引入异步通道的概念,采用了Proactor 模式,简化了程序编写,一个有效的请求才启动一个线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。目前 AIO 还没有广泛应用.

1.5 IO 对比总结

IO 的方式通常分为几种:同步阻塞的 BIO、同步非阻塞的 NIO、异步非阻塞的 AIO。
 BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,但程序直观简单易理解。
 NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4 开始支持。
 AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂,JDK7 开始支持。

举个例子再理解一下:
 同步阻塞:你到饭馆点餐,然后在那等着,啥都干不了,饭馆没做好,你就必须等着!
 同步非阻塞:你在饭馆点完餐,就去玩儿了。不过玩一会儿,就回饭馆问一声:好了没啊!
 异步非阻塞:饭馆打电话说,我们知道您的位置,一会给你送过来,安心玩儿就可以了,类似于现在的外卖。
在这里插入图片描述

2.Netty

2.1 概述

Netty 是由 JBOSS 提供的一个 Java 开源框架。Netty 提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络 IO 程序。
Netty 是一个基于 NIO 的网络编程框架,使用 Netty 可以帮助你快速、简单的开发出一个网络应用,相当于简化和流程化了 NIO 的开发过程。
作为当前最流行的 NIO 框架,Netty 在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,知名的 Elasticsearch 、Dubbo 框架内部都采用了 Netty。

在这里插入图片描述

2.2 Netty 整体设计

2.2.1 线程模型

1.单线程模型

在这里插入图片描述
服务器端用一个线程通过多路复用搞定所有的 IO 操作(包括连接,读、写等),编码简单,清晰明了,但是如果客户端连接数量较多,将无法支撑,咱们前面的 NIO 案例就属于这种模型。

2. 线程池模型

在这里插入图片描述
服务器端采用一个线程专门处理客户端连接请求,采用一个线程池负责 IO 操作。在绝大多数场景下,该模型都能满足使用。

3. Netty 模型

在这里插入图片描述
比较类似于上面的线程池模型,Netty 抽象出两组线程池,BossGroup 专门负责接收客户端连接,WorkerGroup 专门负责网络读写操作。NioEventLoop 表示一个不断循环执行处理任务的线程,每个 NioEventLoop 都有一个 selector,用于监听绑定在其上的 socket 网络通道。NioEventLoop 内部采用串行化设计,从消息的读取->解码->处理->编码->发送,始终由 IO 线程 NioEventLoop 负责。
 一个 NioEventLoopGroup 下包含多个 NioEventLoop
 每个 NioEventLoop 中包含有一个 Selector,一个 taskQueue
 每个 NioEventLoop 的 Selector 上可以注册监听多个 NioChannel
 每个 NioChannel 只会绑定在唯一的 NioEventLoop 上
 每个 NioChannel 都绑定有一个自己的 ChannelPipeline

2.2.2 异步模型

 FUTURE, CALLBACK 和 HANDLER
Netty 的异步模型是建立在 future 和 callback 的之上的。callback 大家都比较熟悉了,这里重点说说 Future,它的核心思想是:假设一个方法 fun,计算过程可能非常耗时,等待 fun返回显然不合适。那么可以在调用 fun 的时候,立马返回一个 Future,后续可以通过 Future去监控方法 fun 的处理过程。
在使用 Netty 进行编程时,拦截操作和转换出入站数据只需要您提供 callback 或利用future 即可。这使得链式操作简单、高效, 并有利于编写可重用的、通用的代码。Netty 框架的目标就是让你的业务逻辑从网络基础应用编码中分离出来、解脱出来。
在这里插入图片描述

2.3 核心 API

1. ChannelHandler 及其实现类

ChannelHandler 接口定义了许多事件处理的方法,我们可以通过重写这些方法去实现具体的业务逻辑。API 关系如下图所示:
在这里插入图片描述
我们经常需要自定义一个 Handler 类去继承 ChannelInboundHandlerAdapter,然后通过
重写相应方法实现业务逻辑,接下来看看一般都需要重写哪些方法:
 public void channelActive(ChannelHandlerContext ctx),通道就绪事件
 public void channelRead(ChannelHandlerContext ctx, Object msg),通道读取数据事件
 public void channelReadComplete(ChannelHandlerContext ctx) ,数据读取完毕事件
 public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause),通道发生异常事件

2. Pipeline 和 ChannelPipeline

ChannelPipeline 是一个 Handler 的集合,它负责处理和拦截 inbound 或者 outbound 的事件和操作,相当于一个贯穿 Netty 的链。
在这里插入图片描述
 ChannelPipeline addFirst(ChannelHandler… handlers),把一个业务处理类(handler)添加到链中的第一个位置
 ChannelPipeline addLast(ChannelHandler… handlers),把一个业务处理类(handler)添加到链中的最后一个位置

3. ChannelHandlerContext

这 是 事 件 处 理 器 上 下 文 对 象 , Pipeline 链 中 的 实 际 处 理 节 点 。 每 个 处 理 节 点ChannelHandlerContext 中 包 含 一 个 具 体 的 事 件 处 理 器ChannelHandler , 同 时ChannelHandlerContext 中也绑定了对应的 pipeline 和 Channel 的信息,方便对 ChannelHandler进行调用。常用方法如下所示
 ChannelFuture close(),关闭通道
 ChannelOutboundInvoker flush(),刷新
 ChannelFuture writeAndFlush(Object msg) , 将 数 据 写 到 ChannelPipeline 中 当前ChannelHandler 的下一个 ChannelHandler 开始处理(出站)

4. ChannelOption

Netty 在创建 Channel 实例后,一般都需要设置 ChannelOption 参数。ChannelOption 是Socket 的标准参数,而非 Netty 独创的。常用的参数配置有:

  1. ChannelOption.SO_BACKLOG对应 TCP/IP 协议 listen 函数中的 backlog 参数,用来初始化服务器可连接队列大小。服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接。多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog 参数指定了队列的大小。
  2. ChannelOption.SO_KEEPALIVE ,一直保持连接活动状态。

5.ChannelFuture

表示 Channel 中异步 I/O 操作的结果,在 Netty 中所有的 I/O 操作都是异步的,I/O 的调用会直接返回,调用者并不能立刻获得结果,但是可以通过 ChannelFuture 来获取 I/O 操作的处理状态。
 Channel channel(),返回当前正在进行 IO 操作的通道
 ChannelFuture sync(),等待异步操作执行完毕

6.EventLoopGroup 和其实现类 NioEventLoopGroup

EventLoopGroup 是一组 EventLoop 的抽象,Netty 为了更好的利用多核 CPU 资源,一般会有多个 EventLoop 同时工作,每个 EventLoop 维护着一个 Selector 实例。
EventLoopGroup 提供 next 接口,可以从组里面按照一定规则获取其中一个 EventLoop来处理任务。在 Netty 服务器端编程中,我们一般都需要提供两个 EventLoopGroup,例如:BossEventLoopGroup 和 WorkerEventLoopGroup。
通常一个服务端口即一个 ServerSocketChannel对应一个Selector 和一个EventLoop线程。BossEventLoop 负责接收客户端的连接并将 SocketChannel 交给 WorkerEventLoopGroup 来进行 IO 处理,如下图所示:
在这里插入图片描述
BossEventLoopGroup 通常是一个单线程的 EventLoop,EventLoop 维护着一个注册了
ServerSocketChannel 的 Selector 实例,BossEventLoop 不断轮询 Selector 将连接事件分离出来,
通常是 OP_ACCEPT 事件,然后将接收到的 SocketChannel 交给 WorkerEventLoopGroup,
WorkerEventLoopGroup 会由 next 选择其中一个 EventLoopGroup 来将这个 SocketChannel 注
册到其维护的 Selector 并对其后续的 IO 事件进行处理。
常用方法如下所示:
 public NioEventLoopGroup(),构造方法
 public Future<?> shutdownGracefully(),断开连接,关闭线程

7.ServerBootstrap 和 Bootstrap

ServerBootstrap 是 Netty 中的服务器端启动助手,通过它可以完成服务器端的各种配置;Bootstrap 是 Netty 中的客户端启动助手,通过它可以完成客户端的各种配置。常用方法如下所示:
 public ServerBootstrap group(EventLoopGroup parentGroup, EventLoopGroup childGroup),该方法用于服务器端,用来设置两个 EventLoop
 public B group(EventLoopGroup group) ,该方法用于客户端,用来设置一个 EventLoop
 public B channel(Class<? extends C> channelClass),该方法用来设置一个服务器端的通道实现
 public B option(ChannelOption option, T value),用来给 ServerChannel 添加配置
public ServerBootstrap childOption(ChannelOption childOption, T value),用来给接收到的通道添加配置
 public ServerBootstrap childHandler(ChannelHandler childHandler),该方法用来设置业务处理类(自定义的 handler)
 public ChannelFuture bind(int inetPort) ,该方法用于服务器端,用来设置占用的端口号
 public ChannelFuture connect(String inetHost, int inetPort) ,该方法用于客户端,用来连接服务器端

8. Unpooled 类

这是 Netty 提供的一个专门用来操作缓冲区的工具类,常用方法如下所示: public static ByteBuf copiedBuffer(CharSequence string, Charset charset),通过给定的数据和字符编码返回一个 ByteBuf 对象(类似于 NIO 中的 ByteBuffer 对象)

2.4 入门案例

很关键的两个点就是,服务器端需要创建两个线程池对象,一个用来监听客户端的连接,一个处理网络操作;而客户端需要一个线程池对象,用来处理网络操作。这就是Netty方式的网络编程与线程池或者NIO编程的不同。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.x</groupId>
    <artifactId>NettyDemo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.25.Final</version>
        </dependency>
    </dependencies>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.2</version>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                        <encoding>UTF-8</encoding>
                        <showWarnings>true</showWarnings>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>
</project>

上述代码在 pom 文件中引入了 netty 的坐标

//服务器端的业务处理类
public class NettyServerHandler extends ChannelInboundHandlerAdapter {//idea中可以通过alt+7键查看一个类的所有的继承类方法

    //读取数据事件
    @Override
    public void channelRead(ChannelHandlerContext ctx,Object msg){
        System.out.println("Server:" + ctx);
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("客户端发来的消息:" + buf.toString(CharsetUtil.UTF_8));
    }

    //读取事件完毕事件
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx){
        ctx.writeAndFlush(Unpooled.copiedBuffer("就是没钱",CharsetUtil.UTF_8));
    }

    //异常处理事件
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx,Throwable t){
        ctx.close();
    }

}

上述代码定义了一个服务器端业务处理类,继承 ChannelInboundHandlerAdapter,并分别重写了三个方法。

public class NettyServer {

    public static void main(String[] args) throws Exception{

        //1.组建一个线程组:接受客户端连接
        NioEventLoopGroup bossGroup = new NioEventLoopGroup();
        //2.创建一个线程组:处理网络操作
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();
        //3.创建服务器端启动助手来配置参数
        ServerBootstrap serverBootstrap = new ServerBootstrap();
        serverBootstrap.group(bossGroup,workerGroup) //4.设置两个线程组
        .channel(NioServerSocketChannel.class)//5.使用NioServerChannel作为服务器端的通道
        .option(ChannelOption.SO_BACKLOG,128)//6.设置线程队列中等待连接的个数
        .childOption(ChannelOption.SO_KEEPALIVE,true)//7.保持活动连接状态
        .childHandler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {//8.创建一个通道初始化对象
                socketChannel.pipeline().addLast(new NettyServerHandler());//9.往pipeline链中添加自定义的handler类
            }
        });
        System.out.println("...Server is ready...");
        ChannelFuture cf = serverBootstrap.bind(9999).sync();//10.绑定端口   非阻塞式的
        System.out.println("...Server is started...");
        //11.关闭通道,关闭线程组
        cf.channel().closeFuture().sync();//异步
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }
}

上述代码编写了一个服务器端程序,配置了线程组,配置了自定义业务处理类,并绑定端口号进行了启动

//客户端业务处理类
public class NettyClientHandler extends ChannelInboundHandlerAdapter {

    //通道就绪事件
    @Override
    public void channelActive(ChannelHandlerContext ctx){
        System.out.println("Client:"+ctx);
        ctx.writeAndFlush(Unpooled.copiedBuffer("老板,还钱吧", CharsetUtil.UTF_8));
    }

    //读取数据事件
    @Override
    public void channelRead(ChannelHandlerContext ctx,Object msg) {
        ByteBuf buf = (ByteBuf) msg;
        System.out.println("服务器端发来的消息:" + buf.toString(CharsetUtil.UTF_8));
    }
}

上述代码自定义了一个客户端业务处理类,继承 ChannelInboundHandlerAdapter ,并分别重写了四个方法。

//网络客户端
public class NettyClient {

    public static void main(String[] args) throws Exception{

        //1. 创建一个线程组
        EventLoopGroup group=new NioEventLoopGroup();
        //2. 创建客户端的启动助手,完成相关配置
        Bootstrap b=new Bootstrap();
        b.group(group)  //3. 设置线程组
                .channel(NioSocketChannel.class)  //4. 设置客户端通道的实现类
                .handler(new ChannelInitializer<SocketChannel>() {  //5. 创建一个通道初始化对象
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline().addLast(new NettyClientHandler()); //6.往Pipeline链中添加自定义的handler
                    }
                });
        System.out.println("......Client is  ready......");

        //7.启动客户端去连接服务器端  connect方法是异步的   sync方法是同步阻塞的  这里的业务事项等待得到了连接的结果才继续后边的操作
        ChannelFuture cf=b.connect("127.0.0.1",9999).sync();

        //8.关闭连接(异步非阻塞)
        cf.channel().closeFuture().sync();
    }
}

上述代码编写了一个客户端程序,配置了线程组,配置了自定义的业务处理类,并启动连接了服务器端。最终运行效果如下图所示:
在这里插入图片描述
在这里插入图片描述

2.5 网络聊天案例

上边通过 Netty 实现了一个入门案例,基本了解了 Netty 的 API 和运行流程,接下来在入门案例的基础上再实现一个多人聊天案例,具体代码如下所示:

//自定义一个服务器端业务处理类
public class ChatServerHandler extends SimpleChannelInboundHandler<String> {

    public static List<Channel> channels = new ArrayList<>();

    @Override  //通道就绪
    public void channelActive(ChannelHandlerContext ctx)  {
        Channel inChannel=ctx.channel();
        channels.add(inChannel);
        System.out.println("[Server]:"+inChannel.remoteAddress().toString().substring(1)+"上线");
    }
    @Override  //通道未就绪
    public void channelInactive(ChannelHandlerContext ctx)  {
        Channel inChannel=ctx.channel();
        channels.remove(inChannel);
        System.out.println("[Server]:"+inChannel.remoteAddress().toString().substring(1)+"离线");
    }
    @Override  //读取数据
    protected void channelRead0(ChannelHandlerContext ctx, String s)  {
        Channel inChannel=ctx.channel();
        for(Channel channel:channels){
            if(channel!=inChannel){
                channel.writeAndFlush("["+inChannel.remoteAddress().toString().substring(1)+"]"+"说:"+s+"\n");
            }
        }
    }
	@Override //发生异常
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
		Channel incoming = ctx.channel();
		System.out.println("[Server]:"+incoming.remoteAddress().toString().substring(1)+"异常"); ctx.close();
	}
}

上述代码通过继承 SimpleChannelInboundHandler 类自定义了一个服务器端业务处理类,并在该类中重写了四个方法,当通道就绪时,输出在线;当通道未就绪时,输出下线;当通 道发来数据时,读取数据;当通道出现异常时,关闭通道。

//聊天程序服务器端
public class ChatServer {

    private int port; //服务器端端口号

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

    public void run() throws Exception {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap b = new ServerBootstrap();
            b.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .option(ChannelOption.SO_BACKLOG, 128)
                    .childOption(ChannelOption.SO_KEEPALIVE, true)
                    .childHandler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch) {
                            ChannelPipeline pipeline=ch.pipeline();
                            //往pipeline链中添加一个解码器
                            pipeline.addLast("decoder",new StringDecoder());
                            //往pipeline链中添加一个编码器
                            pipeline.addLast("encoder",new StringEncoder());
                            //往pipeline链中添加自定义的handler(业务处理类)
                            pipeline.addLast(new ChatServerHandler());
        }
    });
            System.out.println("Netty Chat Server启动......");
            ChannelFuture f = b.bind(port).sync();
            f.channel().closeFuture().sync();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
            System.out.println("Netty Chat Server关闭......");
        }
    }

    public static void main(String[] args) throws Exception {
        new ChatServer(9999).run();
    }
}

上述代码通过 Netty 编写了一个服务器端程序,里面要特别注意的是:我们往 Pipeline链中添加了处理字符串的编码器和解码器,它们加入到 Pipeline 链中后会自动工作,使得我们在服务器端读写字符串数据时更加方便(不用人工处理 ByteBuf)。

//自定义一个客户端业务处理类
public class ChatClientHandler extends SimpleChannelInboundHandler<String> {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, String s) throws Exception {
        System.out.println(s.trim());
    }
}

上述代码通过继承 SimpleChannelInboundHandler 自定义了一个客户端业务处理类,重写了一个方法用来读取服务器端发过来的数据。

//聊天程序客户端
public class ChatClient {
    private final String host; //服务器端IP地址
    private final int port;  //服务器端端口号

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

    public void run(){
        EventLoopGroup group = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = new Bootstrap()
                    .group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new ChannelInitializer<SocketChannel>() {
                        @Override
                        public void initChannel(SocketChannel ch){
                            ChannelPipeline pipeline=ch.pipeline();
                            //往pipeline链中添加一个解码器
                            pipeline.addLast("decoder",new StringDecoder());
                            //往pipeline链中添加一个编码器
                            pipeline.addLast("encoder",new StringEncoder());
                            //往pipeline链中添加自定义的handler(业务处理类)
                            pipeline.addLast(new ChatClientHandler());
                        }
                    });

            ChannelFuture cf=bootstrap.connect(host,port).sync();
            Channel channel=cf.channel();
            System.out.println("------"+channel.localAddress().toString().substring(1)+"------");
            Scanner scanner=new Scanner(System.in);
            while (scanner.hasNextLine()){
                String msg=scanner.nextLine();
                channel.writeAndFlush(msg+"\r\n");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            group.shutdownGracefully();
        }
    }

    public static void main(String[] args) throws Exception {
        new ChatClient("127.0.0.1",9999).run();
    }
}

可以同时运行多个聊天客户端,运行效果如下图所示:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.6 编码和解码

1.概述

我们在编写网络应用程序的时候需要注意 codec (编解码器),因为数据在网络中传输的都是二进制字节码数据,而我们拿到的目标数据往往不是字节码数据。因此在发送数据时就需要编码,接收数据时就需要解码。
codec 的组成部分有两个:decoder(解码器)和 encoder(编码器)。encoder 负责把业务数据转换成字节码数据,decoder 负责把字节码数据转换成业务数据。
其实 Java 的序列化技术就可以作为 codec 去使用,但是它的硬伤太多:

  1. 无法跨语言,这应该是 Java 序列化最致命的问题了。
  2. 序列化后的体积太大,是二进制编码的 5 倍多。
  3. 序列化性能太低。
    由于 Java 序列化技术硬伤太多,因此 Netty 自身提供了一些 codec,如下所示:
    Netty 提供的解码器:
  4. StringDecoder, 对字符串数据进行解码
  5. ObjectDecoder,对 Java 对象进行解码。。。
    Netty 本身自带的 ObjectDecoder 和 ObjectEncoder 可以用来实现 POJO 对象或各种业务对象的编码和解码,但其内部使用的仍是 Java 序列化技术,所以我们不建议使用。因此对于 POJO 对象或各种业务对象要实现编码和解码,我们需要更高效更强的技术。

2.Google 的 Protobuf

Protobuf 是 Google 发布的开源项目,全称 Google Protocol Buffers,特点如下:
 支持跨平台、多语言(支持目前绝大多数语言,例如 C++、C#、Java、python 等)
 高性能,高可靠性
 使用 protobuf 编译器能自动生成代码,Protobuf 是将类的定义使用.proto 文件进行描述,然后通过 protoc.exe 编译器根据.proto 自动生成.java 文件

目前在使用 Netty 开发时,经常会结合 Protobuf 作为 codec (编解码器)去使用,具体用法暂不介绍。

3.RPC

3.1 概述

RPC(Remote Procedure Call),即远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络实现的技术。常见的 RPC 框架有: 源自阿里的 Dubbo,Spring 旗下的 Spring Cloud,Google 出品的 grpc 等等。
在这里插入图片描述

  1. 服务消费方(client)以本地调用方式调用服务
  2. client stub 接收到调用后负责将方法、参数等封装成能够进行网络传输的消息体
  3. client stub 将消息进行编码并发送到服务端
  4. server stub 收到消息后进行解码
  5. server stub 根据解码结果调用本地的服务
  6. 本地服务执行并将结果返回给 server stub
  7. server stub 将返回导入结果进行编码并发送至消费方
  8. client stub 接收到消息并进行解码
  9. 服务消费方(client)得到结果
    RPC 的目标就是将 2-8 这些步骤都封装起来,用户无需关心这些细节,可以像调用本地方法一样即可完成远程服务调用。接下来我们基于 Netty 自己动手搞定一个 RPC。

3.2 设计和实现

3.2.1 结构设计

在这里插入图片描述
 Client(服务的调用方): 两个接口 + 一个包含 main 方法的测试类
 Client Stub: 一个客户端代理类 + 一个客户端业务处理类
 Server(服务的提供方): 两个接口 + 两个实现类
 Server Stub: 一个网络处理服务器 + 一个服务器业务处理类
注意:服务调用方的接口必须跟服务提供方的接口保持一致(包路径可以不一致)
最终要实现的目标是:在 TestNettyRPC 中远程调用 HelloRPCImpl 或 HelloNettyImpl。

3.2.2 代码实现

先略过。。。

发布了122 篇原创文章 · 获赞 1 · 访问量 7333

猜你喜欢

转载自blog.csdn.net/qq_36079912/article/details/104446634
今日推荐