JavaNIO--2. Implement ECHO server

JavaNIO technology to realize ECHO server

The so-called ECHO server is a kind of server that returns what content the client sends to the server, which is almost the simplest network server (of course, there are simpler discard servers)

Reading requires basics: JavaNIO Basics

1. The use of NIO core components

The core components of NIO mainly include Selectorand Channel, and Bufferare mainly used Channelfor data interaction with and, so a detailed introduction is not given here.

1.1 Initialize NIO components

public class NioServer {

    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    private int port;

    public NioServer(int port) throws IOException {
        // 打开一个ServerSocketChannel
        serverSocketChannel = ServerSocketChannel.open();
        // 设置为非阻塞模式才能注册到Selector
        serverSocketChannel.configureBlocking(false);
        // 打开一个选择器
        selector = Selector.open();
        this.port = port;   
    }

    // 启动服务器的方法
    private void startServer() {
        try {
            serverSocketChannel.bind(new InetSocketAddress(port));
            // 注册该通道到选择器,注意兴趣操作是SelectionKey.OP_ACCEPT
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            selectLoop();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }
}

write picture description here

  1. Need to understand that it Channelneeds to be set to non-blocking mode in order to register with the selector

  2. ChannelWhen calling a register()method, you need to specify an interest operation, which means that the selector will monitor whether the channel is ready to perform operations. Interest operations are: SelectionKey.OP_ACCEPT, SelectionKey.OP_READ, SelectionKey.OP_WRITE, SelectionKey.OP_CONNECT, corresponding to ServerSocketChannelthe accept()method that can be executed (without blocking), SocketChannelthe read()/write()method of Can be executed (without blocking), and the SocketChannelcontained methods can be called (without blocking).Socketconnect()

If you don't know much about the operation model corresponding to NIO, you can refer to my last blog: IO Multiplexing and NIO

1.2 Accept components

    private void acceptClient(SelectionKey selectionKey) throws IOException {
        // 与对端Socket建立连接
        SocketChannel socketChannel = serverSocketChannel.accept();

        if (socketChannel != null) {
            System.err.println("接收到一个连接,对端IP为:"+socketChannel.socket().getInetAddress());
        }
        // 将接收到的SocketChannel注册到Selector,注意此时通道要设置为非阻塞模式,且兴趣操作为OP_READ
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
    }

ServerSocketThe process of use is the same as that of the traditional accept()method. It should be noted that the traditional accept()call will block until a TCP connection is established, and the use of Selectorselectors can avoid blocking and ensure that one (or more) Socketconnections must be waiting when the method is called. Establish.

1.3SelectLoop (core component)

You can see that one java.nio.channels.Selectorcan register multiple channels, and Selectorcan monitor the status of the channel registered to itself.

    private void selectLoop() throws IOException {
        while(true) {
            // select()方法会阻塞,直到有注册到该选择器的通道有兴趣事件准备完毕
            selector.select();
            // selectedKeys()方法会获得有兴趣事件发生的通道,注册得到的SelectionKey的集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while(iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                // 循环判断其中的key
                if (selectionKey.isAcceptable()) {
                    // 如果key处于可接受状态,就进入接收函数
                    acceptClient(selectionKey);
                }else if(selectionKey.isReadable()) {
                    // 如果key处于可读状态,就进入读函数
                    readDate(selectionKey);
                }
            }
            // 每次处理完通道事件以后,要进行一次清空
            selectionKeys.clear();
        }
    }

It can be seen that by calling the selector, the select()event channel will be continuously obtained, as long as the channel registered to the selector will be polled once, and we whilecan achieve single-threaded non-blocking I/O through the loop.

2. NIO channel read and write (Buffer)

2.1 Read channel content

    private void readDate(SelectionKey selectionKey) throws IOException {

        // 每一次都先获取之前绑定在这个key上的buffer
        ByteBuffer oldBuffer = (ByteBuffer)selectionKey.attachment();

        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        ByteBuffer newBuffer = ByteBuffer.allocate(64);

        int read;
        while((read = socketChannel.read(newBuffer))<=0) {
            return;
        }

        newBuffer.flip();
        // 读取Buffer,看是否有换行符
        String line = readLine(newBuffer);
        if (line != null) {

            // 如果这次读到了行结束符,就将原来不含有行结束符的buffer合并位一行
            String sendData = readLine(mergeBuffer(oldBuffer, newBuffer));
            if (readLineContent(sendData).equalsIgnoreCase("exit")) { // 如果这一行的内容是exit就断开连接
                socketChannel.close();
                return;
            }
            // 然后直接发送回到客户端
            ByteBuffer sendBuffer = ByteBuffer.wrap(sendData.getBytes("utf-8"));
            while (sendBuffer.hasRemaining()) {
                socketChannel.write(sendBuffer);
            }
            selectionKey.attach(null);
        }else {
            // 如果这次没读到行结束付,就将这次读的内容和原来的内容合并,并刷新绑定到key对象上
            selectionKey.attach(mergeBuffer(oldBuffer, newBuffer));
        }

    }

write picture description here

2.2 Buffer processing helper methods

/**
     * 读取ByteBuffer直到一行的末尾
     * 返回这一行的内容,包括换行符
     * 
     * @param buffer
     * @return String 读取到行末的内容,包括换行符 ; null 如果没有换行符
     * @throws UnsupportedEncodingException
     */
    private String readLine(ByteBuffer buffer) throws UnsupportedEncodingException {
        // windows中的换行符表示手段 "\r\n"
        // 基于windows的软件发送的换行符是会是CR和LF
        char CR = '\r';
        char LF = '\n';

        boolean crFound = false;
        int index = 0;
        int len = buffer.limit();
        buffer.rewind();
        while(index < len) {
            byte temp = buffer.get();
            if (temp == CR) {
                crFound = true;
            }
            if (crFound && temp == LF) {
                // Arrays.copyOf(srcArr,length)方法会返回一个 源数组中的长度到length位 的新数组
                return new String(Arrays.copyOf(buffer.array(), index+1),"utf-8");
            }
            index ++;
        }
        return null;
    }

    /**
     * 获取一行的内容,不包括换行符
     * @param buffer
     * @return String 行的内容
     * @throws UnsupportedEncodingException
     */
    private String readLineContent(String line) throws UnsupportedEncodingException {
        return line.substring(0, line.length() - 2);
    }

    /**
     * 对传入的Buffer进行拼接
     * @param oldBuffer
     * @param newBuffer
     * @return ByteBuffer 拼接后的Buffer
     */
    public static ByteBuffer mergeBuffer(ByteBuffer oldBuffer,ByteBuffer newBuffer) {
        // 如果原来的Buffer是null就直接返回
        if (oldBuffer == null) {
            return newBuffer;
        }
        // 如果原来的Buffer的剩余长度可容纳新的buffer则直接拼接
        newBuffer.rewind();
        if (oldBuffer.remaining() > (newBuffer.limit()-newBuffer.position())) {
            return oldBuffer.put(newBuffer);
        }

        // 如果不是以上两种情况就构建新的Buffer进行拼接
        int oldSize = oldBuffer != null?oldBuffer.limit():0;
        int newSize = newBuffer != null?newBuffer.limit():0;
        ByteBuffer result = ByteBuffer.allocate(oldSize+newSize);

        result.put(Arrays.copyOfRange(oldBuffer.array(), 0, oldSize));
        result.put(Arrays.copyOfRange(newBuffer.array(), 0, newSize));

        return result;
    }

These codes are auxiliary methods implemented to achieve ECHO return, mainly for Buffer processing.

3. Test results

write code snippet here
write picture description here
write picture description here

Use telnetto test the connection, realize the function of the ECHO server, and the input exitwill close the connection.

4. Complete code

import java.io.IOException;
import java.io.UnsupportedEncodingException;
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.util.Arrays;
import java.util.Iterator;
import java.util.Set;

public class NioServer {

    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    private int port;

    public NioServer(int port) throws IOException {
        serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        selector = Selector.open();
        this.port = port;   
    }

    private void selectLoop() throws IOException {
        while(true) {
            // select()方法会阻塞,直到有注册到该选择器的通道有兴趣事件发生
            selector.select();
            // selectedKeys()方法会获得有兴趣事件发生的通道,注册得到的SelectionKey的集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while(iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                // 循环判断其中的key
                if (selectionKey.isAcceptable()) {
                    // 如果key处于可接受状态,就进入接收函数
                    acceptClient(selectionKey);
                }else if(selectionKey.isReadable()) {
                    // 如果key处于可读状态,就进入读函数
                    readDate(selectionKey);
                }
            }
            selectionKeys.clear();
        }
    }

    /**
     * 接收连接并将建立的通道注册到选择器
     * 
     * @param selectionKey
     * @throws IOException
     */
    private void acceptClient(SelectionKey selectionKey) throws IOException {
        // 与对端Socket建立连接
        SocketChannel socketChannel = serverSocketChannel.accept();

        if (socketChannel != null) {
            System.err.println("接收到一个连接,对端IP为:"+socketChannel.socket().getInetAddress());
        }
        // 将接收到的SocketChannel注册到Selector,注意此时通道要设置为非阻塞模式,且兴趣操作为OP_READ
        socketChannel.configureBlocking(false);
        socketChannel.register(selector, SelectionKey.OP_READ);
    }

    private void readDate(SelectionKey selectionKey) throws IOException {

        // 每一次都先获取之前绑定在这个key上的buffer
        ByteBuffer oldBuffer = (ByteBuffer)selectionKey.attachment();

        SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
        ByteBuffer newBuffer = ByteBuffer.allocate(64);

        int read;
        while((read = socketChannel.read(newBuffer))<=0) {
            return;
        }

        newBuffer.flip();
        String line = readLine(newBuffer);
        if (line != null) {

            // 如果这次读到了行结束符,就将原来不含有行结束符的buffer合并位一行
            String sendData = readLine(mergeBuffer(oldBuffer, newBuffer));
            if (readLineContent(sendData).equalsIgnoreCase("exit")) { // 如果这一行的内容是exit就断开连接
                socketChannel.close();
                return;
            }
            // 然后直接发送回到客户端
            ByteBuffer sendBuffer = ByteBuffer.wrap(sendData.getBytes("utf-8"));
            while (sendBuffer.hasRemaining()) {
                socketChannel.write(sendBuffer);
            }
            selectionKey.attach(null);
        }else {
            // 如果这次没读到行结束付,就将这次读的内容和原来的内容合并,并刷新绑定到key对象上
            selectionKey.attach(mergeBuffer(oldBuffer, newBuffer));
        }

    }

    /**
     * 读取ByteBuffer直到一行的末尾
     * 返回这一行的内容,包括换行符
     * 
     * @param buffer
     * @return String 读取到行末的内容,包括换行符 ; null 如果没有换行符
     * @throws UnsupportedEncodingException
     */
    private String readLine(ByteBuffer buffer) throws UnsupportedEncodingException {
        // windows中的换行符表示手段 "\r\n"
        // 基于windows的软件发送的换行符是会是CR和LF
        char CR = '\r';
        char LF = '\n';

        boolean crFound = false;
        int index = 0;
        int len = buffer.limit();
        buffer.rewind();
        while(index < len) {
            byte temp = buffer.get();
            if (temp == CR) {
                crFound = true;
            }
            if (crFound && temp == LF) {
                // Arrays.copyOf(srcArr,length)方法会返回一个 源数组中的长度到length位 的新数组
                return new String(Arrays.copyOf(buffer.array(), index+1),"utf-8");
            }
            index ++;
        }
        return null;
    }

    /**
     * 获取一行的内容,不包括换行符
     * @param buffer
     * @return String 行的内容
     * @throws UnsupportedEncodingException
     */
    private String readLineContent(String line) throws UnsupportedEncodingException {
        return line.substring(0, line.length() - 2);
    }

    /**
     * 对传入的Buffer进行拼接
     * @param oldBuffer
     * @param newBuffer
     * @return ByteBuffer 拼接后的Buffer
     */
    public static ByteBuffer mergeBuffer(ByteBuffer oldBuffer,ByteBuffer newBuffer) {
        // 如果原来的Buffer是null就直接返回
        if (oldBuffer == null) {
            return newBuffer;
        }
        // 如果原来的Buffer的剩余长度可容纳新的buffer则直接拼接
        newBuffer.rewind();
        if (oldBuffer.remaining() > (newBuffer.limit()-newBuffer.position())) {
            return oldBuffer.put(newBuffer);
        }

        // 如果不是以上两种情况就构建新的Buffer进行拼接
        int oldSize = oldBuffer != null?oldBuffer.limit():0;
        int newSize = newBuffer != null?newBuffer.limit():0;
        ByteBuffer result = ByteBuffer.allocate(oldSize+newSize);

        result.put(Arrays.copyOfRange(oldBuffer.array(), 0, oldSize));
        result.put(Arrays.copyOfRange(newBuffer.array(), 0, newSize));

        return result;
    }

    private void startServer() {
        try {
            serverSocketChannel.bind(new InetSocketAddress(port));
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            selectLoop();
        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }


    public static void main(String[] args) throws UnsupportedEncodingException {
        try {
            new NioServer(12345).startServer();

        } catch (IOException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325164753&siteId=291194637