美文网首页
高性能网络通信框架Netty-Java NIO基础

高性能网络通信框架Netty-Java NIO基础

作者: 阿里加多 | 来源:发表于2018-06-06 08:38 被阅读213次

    三、使用 Java NIO 搭建简单的客户端与服务端实现网络通讯

    本节我们使用JDK中原生 NIO API来创建一个简单的TCP客户端与服务器交互的网络程序。

    3.1 客户端程序

    这个客户端功能是当客户端连接到服务端后,给服务器发送一个Hello,然后从套接字里面读取服务器端返回的内容并打印,具体代码如下:

    public class NioClient {
    
        // (1)创建发送和接受缓冲区
        private static ByteBuffer sendbuffer = ByteBuffer.allocate(1024);
        private static ByteBuffer receivebuffer = ByteBuffer.allocate(1024);
    
        public static void main(String[] args) throws IOException {
            // (2) 获取一个客户端socket通道
            SocketChannel socketChannel = SocketChannel.open();
            // (3)设置socket为非阻塞方式
            socketChannel.configureBlocking(false);
            // (4)获取一个选择器
            Selector selector = Selector.open();
            // (5)注册客户端socket到选择器
            SelectionKey selectionKey = socketChannel.register(selector, 0);
            // (6)发起连接
            boolean isConnected = socketChannel.connect(new InetSocketAddress("127.0.0.1", 7001));
    
            // (7)如果连接没有马上建立成功,则设置对链接完成事件感兴趣
            if (!isConnected) {
                selectionKey.interestOps(SelectionKey.OP_CONNECT);
    
            }
    
            int num = 0;
            while (true) {
    
                // (8) 选择已经就绪的网络IO操作,阻塞方法
                int selectCount = selector.select();
                System.out.println(num + "selectCount:" + selectCount);
                // (9)返回已经就绪的通道的事件
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                
                //(10)处理所有就绪事件
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                SocketChannel client;
                while (iterator.hasNext()) {
                    //(10.1)获取一个事件,并从集合移除
                    selectionKey = iterator.next();
                    iterator.remove();
                    //(10.2)获取事件类型
                    int readyOps = selectionKey.readyOps();
                    //(10.3)判断是否是OP_CONNECT事件
                    if ((readyOps & SelectionKey.OP_CONNECT) != 0) {
                        //(10.3.1)等待客户端socket完成与服务器端的链接
                        client = (SocketChannel) selectionKey.channel();
                        if (!client.finishConnect()) {
                            throw new Error();
    
                        }
    
                        System.out.println("--- client already connected----");
                        
                        //(10.3.2)设置要发送给服务端的数据
                        sendbuffer.clear();
                        sendbuffer.put("hello server,im a client".getBytes());
                        sendbuffer.flip();
                        //(10.3.3)写入输入。
                        client.write(sendbuffer);
                        //(10.3.4)设置感兴趣事件,读事件
                        selectionKey.interestOps(SelectionKey.OP_READ);
    
                    //(10.4)判断是否是OP_READ事件
                    } else if ((readyOps & SelectionKey.OP_READ) != 0) {
                        client = (SocketChannel) selectionKey.channel();
                        //(10.4.1)读取数据并打印
                        receivebuffer.clear();
                        int count = client.read(receivebuffer);
                        if (count > 0) {
                            String temp = new String(receivebuffer.array(), 0, count);
                            System.out.println(num++ + "receive from server:" + temp);
                        }
    
                    }
                }
            }
        }
    
    • 代码(1)分别创建了一个发送和接受buffer,用来发送数据时候byte化内容和接受数据。

    • 代码(2)获取一个客户端套接字通道。

    • 代码(3)设置socket通道为非阻塞模式,默认是阻塞模式。

    • 代码(4)(5)获取一个选择器,然后注册客户端套接字通道到该选择器,并且设置感兴趣的事情为0,就是不对任何事件感兴趣。

    • 代码(6)(7)调用套接字通道的connect方法,连接服务器(服务器套接字地址为127.0.0.1:7001),由于步骤(3)设置了为非阻塞,所以步骤(6)马上会返回。代码(7)判断连接是否已经完成,如果没有,则设置选择器去监听OP_CONNECT事件,也就是指明对该事件感兴趣。

    • 然后进入while循环进行事件处理,其中代码(8)选择已经就绪的网络IO事件,如果当前没有就绪的则阻塞当前线程。当有就绪事件后,会返回获取的事件个数,会执行代码(9)具体取出来具体事件列表。

    • 代码(10)循环处理所有就绪事件,代码(10.1)迭代出一个事件key,然后从集合中删除,代码(10.2)获取事件key感兴趣的标志,代码(10.3)则看兴趣集合里面是否有OP_CONNECT,如果有则说明有OP_CONNECT事件已经就绪了,那么执行步骤(10.3.1)等待客户端与服务端完成三次握手,然后步骤(10.3.2)(10.3.3)写入hello server,im a client到服务器端。然后代码(10.3.4)设置对OP_READ事件感兴趣。

    • 代码(10.4)则看如果当前事件key是OP_READ事件,说明服务器发来的数据已经在接受buffer就绪了,客户端可以去具体拿出来了,然后代码10.4.1从客户端套接字里面读取数据并打印。

    注:设置套接字为非阻塞后,connect方法会马上返回的,所以需要根据结果判断是否为链接建立OK了,如果没有成功,则需要设置对该套接字的op_connect事件感兴趣,在这个事件到来的时候还需要调用finishConnect方法来具体完成与服务器的链接,在finishConnect返回true后说明链接已经建立完成了,则这时候可以使用套接字通道发送数据到服务器,并且设置堆该套接字的op_read事件感兴趣,从而可以监听到服务端发来的数据,并进行处理。

    3.2 服务端程序

    服务端程序代码如下:

    public class NioServer {
    
        // (1) 缓冲区
        private ByteBuffer sendbuffer = ByteBuffer.allocate(1024);
        private ByteBuffer receivebuffer = ByteBuffer.allocate(1024);
        private Selector selector;
    
        public NioServer(int port) throws IOException {
            // (2)获取一个服务器套接字通道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            // (3)socket为非阻塞
            serverSocketChannel.configureBlocking(false);
            // (4)获取与该通道关联的服务端套接字
            ServerSocket serverSocket = serverSocketChannel.socket();
            // (5)绑定服务端地址
            serverSocket.bind(new InetSocketAddress(port));
            // (6)获取一个选择器
            selector = Selector.open();
            // (7)注册通道到选择器,选择对OP_ACCEPT事件感兴趣
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            System.out.println("----Server Started----");
    
            // (8)处理事件
            int num = 0;
            while (true) {
                // (8.1)获取就绪的事件集合
                int selectKeyCount = selector.select();
                System.out.println(num++ + "selectCount:" + selectKeyCount);
    
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                // (8.2)处理就绪事件
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey selectionKey = iterator.next();
                    iterator.remove();
                    processSelectedKey(selectionKey);
                }
            }
        }
    
        private void processSelectedKey(SelectionKey selectionKey) throws IOException {
    
            SocketChannel client = null;
            // (8.2.1)客户端完成与服务器三次握手
            if (selectionKey.isAcceptable()) {
                // (8.2.1.1)获取完成三次握手的链接套接字
                ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
                client = server.accept();
                if (null == client) {
                    return;
                }
                System.out.println("--- accepted client---");
    
                // (8.2.1.2)该套接字为非阻塞模式
                client.configureBlocking(false);
                // (8.2.1.3)注册该套接字到选择器,对OP_READ事件感兴趣
                client.register(selector, SelectionKey.OP_READ);
    
                // (8.2.2)为读取事件
            } else if (selectionKey.isReadable()) {
                // (8.2.2.1) 读取数据
                client = (SocketChannel) selectionKey.channel();
                receivebuffer.clear();
                int count = client.read(receivebuffer);
                if (count > 0) {
                    String receiveContext = new String(receivebuffer.array(), 0, count);
                    System.out.println("receive client info:" + receiveContext);
                }
                // (8.2.2.2)发送数据到client
                sendbuffer.clear();
                client = (SocketChannel) selectionKey.channel();
                String sendContent = "hello client ,im server";
                sendbuffer.put(sendContent.getBytes());
                sendbuffer.flip();
                client.write(sendbuffer);
                System.out.println("send info to client:" + sendContent);
    
            }
    
        }
    
        
        public static void main(String[] args) throws IOException {
            int port = 7001;
            NioServer server = new NioServer(port);
        }
    }
    
    • 代码(1)分别创建了一个发送和接受buffer,用来发送数据时候byte化内容,和接受数据。
    • 代码(2)获取一个服务端监听套接字通道。
    • 代码(3)设置socket通道为非阻塞模式,默认是阻塞模式。
    • 代码(4)获取与该通道关联的服务端套接字
    • 代码(5)绑定服务端套接字监听端口为7001
    • 代码(6)(7) 获取一个选择器,并注册通道到选择器,选择对OP_ACCEPT事件感兴趣,到这里服务端已经开始监听客户端链接了。
    • 代码(8) 具体处理事件,8.1选择当前就绪的事件,8.2遍历所有就绪事件,顺序调用processSelectedKey进行处理。
    • 代码(8.2.1) 当前事件key对应的OP_ACCEPT事件,则执行代码8.2.1.1获取已经完成三次握手的链接套接字,并通过代码8.2.1.2设置该链接套接字为非阻塞模式,通过代码8.2.1.3注册该链接套接字到选择器,并设置对对OP_READ事件感兴趣。
    • 代码(8.2.2) 判断如果当前事件key为OP_READ则通过代码8.2.2.1链接套接字里面获取客户端发来的数据,通过代码8.2.2.2发送数据到客户端。

    注:在这个例子里面监听套接字serverSocket和serverSocket接受到的所有链接套接字都注册到了同一个选择器上,其中processSelectedKey里面8.2.1是用来处理serverSocket接受的新链接的,8.2.2是用来处理链接套接字的读写的。

    到这里服务端和客户端就搭建好了,首先启动服务器,然后运行客户端,会输入如下:

    0selectCount:1    
    --- client already connected----  
    1selectCount:1
    2receive from server:hello client ,im server
    

    这时候服务器的输出结果为:

    ----Server Started----
    0selectCount:1
    --- accepted client---
    1selectCount:1
    receive client info:hello server,im a client
    send info to client:hello client ,im server
    

    简单分析下结果:

    • 服务器端启动后,会先输出----Server Started----

    • 客户端启动后去链接服务器端,三次握手完毕后,服务器会获取op_accept事件,会通过accept获取链接套接字,所以输出了:
      0selectCount:1
      --- accepted client---

    • 然后客户端接受到三次握手信息后,获取到了op_connect事件,所以输出:
      0selectCount:1
      --- client already connected----
      然后发送数据到服务器端

    • 服务端收到数据后,选择器会选择出op_read事件,读取客户端发来的内容,并发送回执到客户端:
      1selectCount:1
      receive client info:hello server,im a client
      send info to client:hello client ,im server

    • 客户端收到服务器端回执后,选择器会选择出op_read事件,所以客户端会读取服务器端发来的内容,所以输出:
      1selectCount:1
      2receive from server:hello client ,im server

    最后

    想了解JDK NIO和更多Netty基础的可以单击我
    更多关于分布式系统中服务降级策略的知识可以单击 单击我
    想系统学dubbo的单击我
    想学并发的童鞋可以 单击我

    image.png

    相关文章

      网友评论

          本文标题:高性能网络通信框架Netty-Java NIO基础

          本文链接:https://www.haomeiwen.com/subject/gjbvsftx.html