Netty学习之旅------第2篇---NIO和Netty实现客户端与服务端通信

Netty学习之旅------第2篇---NIO和Netty实现客户端与服务端通信

  • 1 上篇回顾
  • 2 非阻塞I/O(NIO)
  • 3 基于netty的客户端与服务器通信
  • 4 总结

温馨提示:内容局限于本人的理解,如果有错误,请指正,谢谢!

学习目标:
(1)熟悉Netty的客户端和服务器通信
(2)通过阻塞I/O、伪异步I/O的demo 了解它们是通信的基本流程。

1 上篇回顾

上一篇文档,介绍了阻塞和伪异步I/O的案例,虽然伪异步I/O有一定的提升,但还是阻塞的。并未达到真正的非阻塞和高性能,本篇文章将接着学习下非阻塞I/O是如何做的,如何实现高性能?带着疑问,我们开始来学习。

2 非阻塞I/O(NIO)

Server.java 服务端启动类

import java.io.IOException;
/**
 * @Auther: chen
 * @Date:
 * @Description:
 */
public class Server {
    public static void main(String[] args){
        ServerHandle serverHandle = null;
        try {
            serverHandle = new ServerHandle(9092);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //启动多路复用器ServerHandle来处理请求
        new Thread(serverHandle,"SERVER-CHEN-THREAD").start();
    }
}

ServerHandle.java 多路复用器

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.Set;

/**
 * @Auther: chen
 * @Date:
 * @Description:
 */
public class ServerHandle implements Runnable {
    private int port;

    private volatile boolean stop;

    private Selector selector;
    private ServerSocketChannel serverSocketChannel;

    /**
     * 初始化多路复用器
     *
     * @param port
     * @throws IOException
     */
    public ServerHandle(int port) throws IOException {
        this.port = port;
        selector = Selector.open();
        serverSocketChannel = ServerSocketChannel.open();
        //设置非阻塞模式
        serverSocketChannel.configureBlocking(false);
        //监听端口
        serverSocketChannel.bind(new InetSocketAddress(port));
        //注册ACCEPT 事件,表示关注客户端的注册事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        System.out.println("服务器初始化完毕");
    }

    public void stop() {
        this.stop = true;
    }

    @Override
    public void run() {
        while (!stop) {
            try {
                //每隔1s检查一次
                selector.select(1000);
                //获取到已经准备的keys,就是已经准备好的事件
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    //获取单个事件
                    SelectionKey key = iterator.next();
                    //处理key
                    try {
                        handleKey(key);
                    } catch (Exception ex) {
                        //出异常了,取消key
                        if (null != key) {
                            key.cancel();
                            if (key.channel() != null) {
                                try {
                                    key.channel().close();
                                } catch (IOException e) {
                                    e.printStackTrace();
                                }
                            }
                        }
                    }finally {
                        //处理后,把key移除掉
                        iterator.remove();
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (null != selector) {
            try {
                //多路复用器关闭后,所有注册在复用器上面的channel和pipe都会自动关闭
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    /**
     * 处理key
     * @param key
     * @throws IOException
     */
    private void handleKey(SelectionKey key) throws IOException {
        //判断key是否有效
        if (key.isValid()) {
            //key 连接状态
            if (key.isAcceptable()) {
                //客户端的连接请求
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                SocketChannel sc = ssc.accept();
                sc.configureBlocking(false);
                //连接上了,注册OP_READ 事件
                sc.register(selector, SelectionKey.OP_READ);
                System.out.println("有客户端来连接了,帮他注册读事件");
            }
            //key 是否可读状态
            if (key.isReadable()) {
                SocketChannel sc = (SocketChannel) key.channel();
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //读取数据
                int readData = sc.read(buffer);
                // >0 读取到了数据
                if (readData > 0) {
                    buffer.flip();
                    byte[] bytes = new byte[buffer.remaining()];
                    buffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("服务器: 接收到客户端的数据,内容:" + body);
                    //发送数据给客户端
                    doWrite(sc, "现在是北京时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
                    //将key标记为可读
                    key.interestOps(SelectionKey.OP_READ);
                } else if (readData < 0) {
                    //链路关闭了,则取消key,关闭sc
                    key.cancel();
                    sc.close();
                } else {
                   // readData==0
                    // 没有读取到数据
                }
            }
        }
    }

    /**
     * 发送数据
     * @param socketChannel
     * @param hello
     * @throws IOException
     */
    private void doWrite(SocketChannel socketChannel, String hello) throws IOException {
        if (hello != null) {
            byte[] bytes = hello.getBytes();
            ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
            buffer.put(bytes);
            //当前缓冲区是读状态,通过flip改变成写状态
            buffer.flip();
            socketChannel.write(buffer);
        }

    }
}

Client.java 客户端启动类

import java.io.IOException;

/**
 * @Auther: chen
 * @Date:
 * @Description:
 */
public class Client {
    public static void main(String[] args) {
        ClientHandle clientHandle = null;
        try {
            clientHandle = new ClientHandle("127.0.0.1",9092);
            new Thread(clientHandle,"CLIENT-CHEN-THREAD").start();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}

ClientHandle.java 客户端handle

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * @Auther: chen
 * @Date:
 * @Description:
 */
public class ClientHandle implements Runnable {
    private int port;

    private volatile boolean stop;

    private Selector selector;
    private String ip;
    private SocketChannel socketChannel;

    public ClientHandle(String ip, int port) throws IOException {
        this.ip = ip;
        this.port = port;
        selector = Selector.open();
        socketChannel = SocketChannel.open();
        socketChannel.configureBlocking(false);
    }

    @Override
    public void run() {
        try {
            //建立连接
            doConnect();
        } catch (IOException e) {
            e.printStackTrace();
        }
        while (!stop) {
            try {
                selector.select(1000);
            } catch (IOException e) {
                e.printStackTrace();
            }
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                try {
                    handleKey(key);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        // 多路复用器关闭后,所有注册在上面的Channel和Pipe等资源都会被自动去注册并关闭,所以不需要重复释放资源
        if (selector != null) {
            try {
                selector.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 处理key
     * @param key
     * @throws IOException
     */
    private void handleKey(SelectionKey key) throws IOException {
        /**
         * 验证key是否有效
         */
        if (key.isValid()) {
            //检查连接是否成功
            SocketChannel sc = (SocketChannel) key.channel();
            //key 连接状态
            if (key.isConnectable()) {
                //检查连接完成
                if (sc.finishConnect()) {
                    //完成,注册读事件
                    sc.register(selector, SelectionKey.OP_READ);
                    doWrite(sc);
                } else {
                    System.exit(1);
                }
            }
            //key 读取状态
            if (key.isReadable()) {
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                int read = sc.read(buffer);
                if (read > 0) {
                    //buffer长度是1024 ,假如但是实际存储的位置10,那我要把0-10的数据读取出来,上线limit设置为10
                    buffer.flip();
                    byte[] bytes = new byte[buffer.remaining()];
                    buffer.get(bytes);
                    String body = new String(bytes, "UTF-8");
                    System.out.println("客户端:接收到服务数据,内容 " + body);
                    this.stop = true;
                } else if (read < 0) {
                    key.cancel();
                    sc.close();
                } else {

                }
            }
        }
    }

    /**
     * 建立连接
     * @throws IOException
     */
    private void doConnect() throws IOException {
        //建立连接成功,则注册读取事件
        if (socketChannel.connect(new InetSocketAddress(ip, port))) {
            socketChannel.register(selector, SelectionKey.OP_READ);
            //成功了,发送数据给服务器
            doWrite(socketChannel);
        } else {
            //注册失败,继续注册连接事件
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
        }
    }

    /**
     * 发送数据
     * @param socketChannel
     * @throws IOException
     */
    private void doWrite(SocketChannel socketChannel) throws IOException {
        byte[] bytes = "我是客户端".getBytes();
        ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
        buffer.put(bytes);
        buffer.flip();
        socketChannel.write(buffer);
        if (!buffer.hasRemaining()) {
            System.out.println("客户端发送完毕!");
        }

    }
}

运行效果:
服务端:
Netty学习之旅------第2篇---NIO和Netty实现客户端与服务端通信_第1张图片
客户端:
Netty学习之旅------第2篇---NIO和Netty实现客户端与服务端通信_第2张图片
上面实现了客户端发送数据到服务端,服务端返回当前的时间给客户端的功能。客户端是channel,在selector上注册所关注的事件,然后selector的轮询来处理请求,都是通过selector来处理,一个selector就可以同时处理多个客户的请求。客户端发送请求后,并未阻塞,等自己关注的数据准备好了,再进行相应的操作。通过简单的案例,对nio有个大概的了解。
调用的NIO时序图如下:
Netty学习之旅------第2篇---NIO和Netty实现客户端与服务端通信_第3张图片

综合几种方式的源码来看,可以看出nio的编程的复杂度是最高的,为什么这么复杂的编码,应用却越来越广泛,主要有以下几个优点:
1、客户端发送请求都是异步的,通过多路复用器注册事件、等待内核准备好读写,再进行处理。
2、ScoketChannel 的读写操作也都是异步的,如果没有可读写的数据不会同步等待,直接返回。
3、epoll 没有句柄的限制,意味着selector可以同时处理成千上万个客户端的请求,并且性能不会随着客户端的增加而线性下降,适合做高性能、高负载的网络服务器。

虽然优点很多,但如果要用原生的NIO来编码,那相当的难受,并且效率很低,于是,有了netty,百度百科对于netty的介绍来看下:

Netty提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
也就是说,Netty 是一个基于NIO的客户、服务器端编程框架,使用Netty 可以确保你快速和简单的开发出一个网络应用,例如实现了某种协议的客户、服务端应用。Netty相当于简化和流线化了网络应用的编程开发过程,例如:基于TCP和UDP的socket服务开发。

netty 虽然简化了,但性能并没有降低。如果用netty来实现我们之前的客户端与服务器通信,那如何实现呢?

3 基于netty的客户端与服务器通信

Server.java 服务器的main函数

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

/**
 * @Auther: chen
 * @Date:
 * @Description:
 */
public class Server {
    public static void main(String[] args){
        int port = 9092;
              new Server().bind(port) ;
    }

    private void bind(int port) {
        EventLoopGroup bossGroup = new NioEventLoopGroup();//@1
        EventLoopGroup workGroup = new NioEventLoopGroup();//@2
        ServerBootstrap bootstrap = new ServerBootstrap();
        try {
            ChannelFuture channelFuture = bootstrap.group(bossGroup, workGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 1024)
                .childHandler(new ServerChannelHandle())
                .bind(port).sync();//@3

            channelFuture.channel().closeFuture().sync();@4
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            bossGroup.shutdownGracefully();
            workGroup.shutdownGracefully();
            bootstrap.clone();
        }

    }
}

@1 是新建一个组来接收客户端请求,@2 是新建一个组来处理I/O请求
@3 就是绑定组合、监听端口channel、设置处理的handle和设置参数
@4 同步等待关闭
这里可以看出处理请求和I/O的组是不同的,这2个组就是不同的线程池。这里接收请求和实际处理的I/O分开,有什么好处呢?其实就是如果我一个线程池,那就和伪异步I/O一样,并没有实现真正的异步。

ServerChannelHandle.java 服务器端绑定处理请求的handle,在initChannel中可以自己实现编码器和解码器一些自动义操作

import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;

/**
 * @Auther: chen
 * @Date:
 * @Description:
 */
public class ServerChannelHandle extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline().addLast(new ServerHandle());//@1
    }
}

@1 这里通过handle的initChannel,设置了处理自己业务逻辑的类,这里其实还可以增加消息的编码和解码等操作、自定义协议等。

ServerHandle.java 处理具体业务的类


import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @Auther: chen
 * @Date:
 * @Description:
 */
public class ServerHandle extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buffer = (ByteBuf) msg;
        byte [] bytes = new byte[buffer.readableBytes()];
        buffer.readBytes(bytes);
        String body = new String(bytes,"UTF-8");
        System.out.println("客户端发送的数据->"+ body);
        String res = "现在是北京时间:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        ByteBuf byteBuf = Unpooled.copiedBuffer(res.getBytes());
        ctx.writeAndFlush(byteBuf);
    }


    //读取完成后处理方法
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        System.out.println("EchoServerHandler.channelReadComplete");
        //ctx.flush();
    }

    //异常捕获处理方法
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        // Close the connection when an exception is raised.
        cause.printStackTrace();
        ctx.close();
    }
}

这里处理的逻辑很简单,就是把发送来的数据,读取打印出来。

接下来,编写客户端代码
Client.java


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

/**
 * @Auther: chen
 * @Date:
 * @Description:
 */
public class Client {
    public static void main(String[] args){
        int port = 9092;
        String host = "127.0.0.1";
        new Client().connection(host, port) ;
    }

    private void connection(String host, int port) {
        EventLoopGroup group = new NioEventLoopGroup();//@1
        Bootstrap bootstrap = new Bootstrap();
        try {
            Bootstrap b = bootstrap.group(group).channel(NioSocketChannel.class)
                    .option(ChannelOption.TCP_NODELAY, true)
                    .handler(new ChildChannelHandle());//@2

            ChannelFuture f = b.connect(host, port).sync();//@3
            System.out.println("客户端启动完成");
            f.channel().closeFuture().sync(); //@4
        } catch (InterruptedException e) {
            e.printStackTrace();
        }finally {
            group.shutdownGracefully();
        }
    }
}

@1 定义了个组来处理客户端的逻辑
@2 绑定组、设置参数、绑定hanele和channel
@3 同步连接服务器
@4 同步等关闭
这里和服务器很相似,只是这里只需要指定一个组,然后指定ip、port建立连接。

ChildChannelHandle 客户端处理请求的handle

import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.SocketChannel;

/**
 * @Auther: chen
 * @Date:
 * @Description:
 */
public class ChildChannelHandle extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel socketChannel) throws Exception {
        socketChannel.pipeline().addLast(new ClientHandle());
    }
}

处理具体业务类
ClientHandle.java

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;

/**
 * @Auther: chen
 * @Date:
 * @Description:
 */
public class ClientHandle extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buffer = (ByteBuf) msg;
        byte [] bytes = new byte[buffer.readableBytes()];
        buffer.readBytes(bytes);
        String body = new String(bytes,"UTF-8");
        System.out.println("服务器返回->"+ body);
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ByteBuf buffer = Unpooled.copiedBuffer("我是客户端".getBytes());
        ctx.writeAndFlush(buffer);
    }
}

分析:
这比上面的NIO方便太多了吧,很简洁

4 总结

通过上面的案例,了解了NIO和netty 客户端与服务器连接的大致实现,发现代码简洁太多了。这也是运用netty的优势。
代码的地址:https://gitlab.com/157538651/netty-example

你可能感兴趣的:(Java,Netty)