阅读 181

高性能网络通信框架Netty-Java NIO基础 | Spring For All

原文链接: www.spring4all.com

三、使用 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