如何用java.nio包中的类来创建客户程序EchoClient,本节提供了两种实现方式:

  • 采用阻塞模式,单线程。
  • 采用非阻塞模式,单线程。


文章目录

  • 1 采用阻塞模式,单线程
  • 2 采用非阻塞模式,单线程


1 采用阻塞模式,单线程

这个已经在服务端代码Demo(NIO)中提到了,代码如下:

package study.wyy.net.nio.client;

import lombok.Builder;
import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.channels.SocketChannel;

/**
 * @author wyaoyao
 * @date 2021/3/17 17:59
 */
@Slf4j
public class BlockEchoClient {

    private final SocketChannel socketChannel;
    private final String serverHost;
    private final int serverPort;

    public BlockEchoClient(String serverHost, int serverPort) throws IOException {
        this.serverHost = serverHost;
        this.serverPort = serverPort;
        this.socketChannel = SocketChannel.open();
        // 连接服务器
        SocketAddress remote = new InetSocketAddress(serverHost, serverPort);
        socketChannel.connect(remote);
        log.info("connect echo server success");
    }

    public void send(String message) {
        try {
            BufferedReader reader = getReader(socketChannel.socket());
            PrintWriter writer = getWriter(socketChannel.socket());
            // 发送数据
            writer.println(message);
            log.info("send request success; content is {}", message);
            // 读取服务端的响应
            String s1 = reader.readLine();
            log.info("get response success; response is {}", s1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void close() throws IOException {
        if(socketChannel != null){
            socketChannel.close();
        }
    }

    public BufferedReader getReader(Socket socket) throws IOException {
        InputStream inputStream = socket.getInputStream();
        return new BufferedReader(new InputStreamReader(inputStream));
    }

    public PrintWriter getWriter(Socket socket) throws IOException {
        return new PrintWriter(socket.getOutputStream(), true);
    }
}

调用了 socketChannel.connect(remote);方法连接远程服务器,该方法在阻塞模式下,将等到与远程服务器的连接成功建立之后才返回。
在send方法中,通过socketChannel.socket()获取与socketChannel关联的Socket对象,然后从Socket对象中获取输入和输出流,进行数据的发送和接收。

2 采用非阻塞模式,单线程

对于客户与服务器之间的通信,按照它们收发数据的协调程度来区分,可分为同步通信和异步通信:

  • 同步通信是指甲方向乙方发送了一批数据后,必须等接收到了乙方的响应数据后,再发送下一批数据。
  • 异步通信是指发送数据和接收数据的操作互不干扰,各自独立进行。

同步通信要求一个I/O操作完成之后,才能完成下一个I/O操作,用阻塞模式更容易实现它。异步通信允许发送数据和接收数据的操作各自独立进行,用非阻塞模式更容易实现它。

上面的就是一个同步通信,每当客户端发送一条数据之后,都必须等待响应数据才会发送下一条数据。

现在就使用非阻塞模式,来实现异步通信。

package study.wyy.net.nio.client;

import lombok.extern.slf4j.Slf4j;

import java.io.*;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;
import java.util.Set;

/**
 * @author wyaoyao
 * @date 2021/3/17 17:59
 */
@Slf4j
public class NoBlockEchoClient {

    private final SocketChannel socketChannel;
    private final String serverHost;
    private final int serverPort;

    private final Charset charset = Charset.forName("UTF-8");

    private ByteBuffer sendBuffer = ByteBuffer.allocate(1024);
    private ByteBuffer responseBuffer = ByteBuffer.allocate(1024);
    /**
     * 委托给Selector来负责接收连接就绪事件,读就绪事件,写就绪事件
     */
    private final Selector selector;

    private final Object LOCK = new Object();

    private Thread sendThread =  new Thread(() -> {
        try {
            send();
        } catch (IOException e) {
            e.printStackTrace();
        }
    });

    public NoBlockEchoClient(String serverHost, int serverPort) throws IOException {
        this.serverHost = serverHost;
        this.serverPort = serverPort;
        this.socketChannel = SocketChannel.open();

        // 连接服务器
        SocketAddress remote = new InetSocketAddress(serverHost, serverPort);
        socketChannel.connect(remote);
        // 设置socketChannel为非阻塞模式
        socketChannel.configureBlocking(false);
        log.info("connect echo server success");
        // 创建一个Selector对象
        this.selector = Selector.open();
        socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
        sendThread.start();
    }

    public void submit(String message) throws IOException {
        // 编码,在用户提交的数据上追加一个结束符号,这里就以换行符吧
        log.info("submit :{}",message);
        ByteBuffer encode = encode(message + "\r\n");
        // 将要发送的内容添加到sendBuffer中
        synchronized (LOCK) {
            sendBuffer.put(encode);
        }
    }

    public void send() throws IOException {
        while (true) {
            //  需要加锁,因为selector.select();这个方法时操作的selector的all-keys集合
            int select = selector.select();
            if (select == 0) {
                continue;
            }
            // 获取已经捕获到的事件的SelectionKey的数量
            // 数量大于0,捕获到事件发生 取出捕获的事件
            Set<SelectionKey> readyKeys = selector.selectedKeys();
            // 遍历事件,进行处理
            Iterator<SelectionKey> iterator = readyKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = null;
                try {
                    key = iterator.next();
                    iterator.remove();
                    if (key.isReadable()) {
                        // 读取服务响应结果
                        receive(key);
                    }
                    if (key.isWritable()) {
                        // 发送数据给服务端
                        sendMessage(key);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                    if (key != null) {
                        key.cancel();
                        key.channel().close();
                    }
                }
            }
        }
    }

    /**
     * 读取服务端数据
     *
     * @param key
     */
    private void receive(SelectionKey key) throws IOException {
        // 获取SelectionKey关联的SocketChannel
        SocketChannel channel = (SocketChannel) key.channel();
        // 服务端的数据读取到responseBuffer中,如果responseBuffer中的数据有一行数据
        // 就输出这一行数据,并删除这部分数据
        channel.read(responseBuffer);
        // 把极限设置为位置,位置设置为0
        responseBuffer.flip();
        // 解码
        String response = decode(responseBuffer);
        if (response.indexOf("\n") == -1) {
            // 不足一行,就返回,等待下次
            return;
        }
        // 截取出一行数据
        String line = response.substring(0, response.indexOf("\n") + 1);
        log.info("get response success; response is {}", line);
        ByteBuffer temp = encode(line);
        responseBuffer.position(temp.limit());
        responseBuffer.compact();
    }

    /**
     * 从sendBuffer缓冲中获取数据,向服务端发送数据
     *
     * @param key
     */
    private void sendMessage(SelectionKey key) throws IOException {
        // 获取SelectionKey关联的SocketChannel
        SocketChannel channel = (SocketChannel) key.channel();
        synchronized (LOCK) {
            // 把极限设置为位置,位置设置为0
            sendBuffer.flip();
            // 发送sendBuffer中的数据
            channel.write(sendBuffer);
            // 删除已经发送的数据
            sendBuffer.compact();
        }
    }


    private ByteBuffer encode(String content) {
        ByteBuffer encode = charset.encode(content);
        return encode;
    }

    /**
     * 解码
     *
     * @param byteBuffer
     * @return
     */
    private String decode(ByteBuffer byteBuffer) {
        CharBuffer decode = charset.decode(byteBuffer);
        return decode.toString();
    }

}

测试:

public static void main(String[] args) throws IOException {
        NoBlockEchoClient client = null;
        try {
            client = new NoBlockEchoClient("localhost", 10010);
            client.submit("hello! from " + Thread.currentThread().getName());
            client.submit("你好! from " + Thread.currentThread().getName());
            client.submit("bye! from " + Thread.currentThread().getName());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {

        }
    }