NIO 应用-网络通信

1. 服务端处理器关注读事件

在这里插入图片描述

package org.example;

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.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Iterator;
import java.util.Set;

//nio服务端处理器
public class NioServerHandle implements Runnable {
    
    
    private volatile boolean start;
    private ServerSocketChannel serverSocketChannel;
    private Selector selector;

    //构造方法,指定要监听的端口号
    public NioServerHandle(int port) {
    
    
        try {
    
    
            //创建选择器实例
            selector = Selector.open();
            //创建ServerSocketChannel实例
            serverSocketChannel = ServerSocketChannel.open();

            //设置false,通道为非阻塞;如果设置为true,为阻塞模式,但是启动会报错
            serverSocketChannel.configureBlocking(false);
            //绑定端口号
            serverSocketChannel.socket().bind(new InetSocketAddress(port));
            //注册事件,关心客户端连接事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            start = true;
            System.out.println("服务端已启动,端口号:" + port);
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }

    //处理事件
    private void handleInput(SelectionKey key) throws IOException {
    
    
        if (key.isValid()) {
    
    
            //处理新接入的客户端请求事件
            if (key.isAcceptable()) {
    
    
                //获取关心当前事件的channel
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                //接受连接
                SocketChannel accept = ssc.accept();
                System.out.println("建立连接");
                accept.configureBlocking(false);
                //关注读事件
                accept.register(selector, SelectionKey.OP_READ);
            }
            //处理客户端发送的数据,读事件
            if (key.isReadable()) {
    
    
                SocketChannel sc = (SocketChannel) key.channel();
                //创建一个bytebuffer,开辟一个缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //从通道中读数据,然后写入buffer
                int readBytes = sc.read(buffer);
                //读取到数据
                if (readBytes > 0) {
    
    
                    //buffer切换为读模式,将buffer当前的limit设置为position,position=0,用于后续对buffer的读操作
                    buffer.flip();
                    //根据buffer可读字节数创建字节数组
                    byte[] bytes = new byte[buffer.remaining()];
                    //将buffer可读字节数组复制到新建的数组中
                    buffer.get(bytes);
                    String message = new String(bytes, StandardCharsets.UTF_8);
                    System.out.println("服务器接收到的消息:" + message);
                    String result = "Hello " + message + ", now is " + new Date();
                    //发送应答消息
                    doWrite(sc, result);
                }
                //读取数据结束
                else if (readBytes < 0) {
    
    
                    //取消特定的注册关系
                    key.cancel();
                    //关闭通道
                    sc.close();
                }
            }
        }
    }

    //发送应答消息
    private void doWrite(SocketChannel socketChannel, String response) throws IOException {
    
    
        //将消息编码为字节数组
        byte[] bytes = response.getBytes();
        //根据数组容量创建ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
        //将字节数组复制到缓冲区
        buffer.put(bytes);
        //写切换为读
        buffer.flip();
        //channel读取缓冲区的字节数组
        socketChannel.write(buffer);
    }

    @Override
    public void run() {
    
    
        while (start) {
    
    
            try {
    
    
                //如果没有读写事件,selector会阻塞,但是1s后会被唤醒
                selector.select(1000);
                //获取当前事件的集合
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
    
    
                    SelectionKey selectionKey = iterator.next();
                    //删除已处理过的SelectionKey,否则这个key还是一个激活状态,下次循环还会再次处理这个key
                    handleInput(selectionKey);
                    iterator.remove();
                }
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }

}

2. 服务端启动类

在这里插入图片描述

package org.example;

public class NioServer {
    
    
    private static NioServerHandle nioServerHandle;

    public static void main(String[] args) {
    
    

        //使用9001端口
        nioServerHandle = new NioServerHandle(9001);
        new Thread(nioServerHandle).start();

    }
}

3. 客户端处理器

在这里插入图片描述

package org.example;

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.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;

//客户端处理器
public class NioClientHandle implements Runnable {
    
    
    private String host;
    private int port;
    private volatile boolean started;
    private Selector selector;
    private SocketChannel socketChannel;

    public NioClientHandle(String ip, int port) {
    
    
        this.host = ip;
        this.port = port;

        try {
    
    
            //创建选择器实例
            selector = Selector.open();
            //创建SocketChannel实例
            socketChannel = SocketChannel.open();
            //设置通道为非阻塞
            socketChannel.configureBlocking(false);
            started = true;
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
    
    
        try {
    
    
            doConnect();
        } catch (IOException e) {
    
    
            e.printStackTrace();
            System.exit(1);
        }
        //循环遍历selector
        while (started) {
    
    
            try {
    
    
                //如果没有读写事件,selector会阻塞,但是1s后会被唤醒
                selector.select(1000);
                //获取当前事件的集合
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                SelectionKey key = null;
                while (iterator.hasNext()) {
    
    
                    key = iterator.next();
                    try {
    
    
                        handleInput(key);
                        iterator.remove();
                    } catch (Exception e) {
    
    
                        if (key != null) {
    
    
                            key.cancel();
                            if (key.channel() != null) {
    
    
                                key.channel().close();
                            }
                        }
                    }
                }
            } catch (IOException e) {
    
    
                e.printStackTrace();
                System.exit(1);
            }
        }
        //selector关闭后会自动释放里面管理的资源
        if (selector != null) {
    
    
            try {
    
    
                selector.close();
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }

    //具体的事件处理方法
    private void handleInput(SelectionKey key) throws IOException {
    
    
        if (key.isValid()) {
    
    
            //获取关心当前事件的channel
            SocketChannel sc = (SocketChannel) key.channel();
            //连接事件
            if (key.isConnectable()) {
    
    
                if (sc.finishConnect()) {
    
    
                    sc.register(selector, SelectionKey.OP_READ);
                } else {
    
    
                    System.exit(1);
                }
            }
            //有数据可读事件
            if (key.isReadable()) {
    
    
                //创建一个bytebuffer,开辟一个1M的缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //从通道中读数据,然后写入buffer
                int readBytes = sc.read(buffer);
                //读取到数据
                if (readBytes > 0) {
    
    
                    //buffer切换为读模式,将buffer当前的limit设置为position,position=0,用于后续对buffer的读操作
                    buffer.flip();
                    //根据buffer可读字节数创建字节数组
                    byte[] bytes = new byte[buffer.remaining()];
                    //将buffer可读字节数组复制到新建的数组中
                    buffer.get(bytes);
                    String message = new String(bytes, StandardCharsets.UTF_8);
                    System.out.println("客户器接收到的消息:" + message);
                }
                //读取数据结束
                else if (readBytes < 0) {
    
    
                    //取消特定的注册关系
                    key.cancel();
                    //关闭通道
                    sc.close();
                }
            }
        }
    }

    //发送应答消息
    private void doWrite(SocketChannel channel, String request)
            throws IOException {
    
    
        //将消息编码为字节数组
        byte[] bytes = request.getBytes();
        //根据数组容量创建ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
        //将字节数组复制到缓冲区
        buffer.put(bytes);
        //写切换为读
        buffer.flip();
        //channel读取缓冲区的字节数组
        channel.write(buffer);
    }

    private void doConnect() throws IOException {
    
    
    	//如果连接成功就注册读事件
        if (socketChannel.connect(new InetSocketAddress(host, port))) {
    
    
            socketChannel.register(selector, SelectionKey.OP_READ);
        }
        //连接失败就注册连接事件 
        else {
    
    
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
        }
    }

    //写数据对外暴露的API
    public void sendMsg(String msg) throws Exception {
    
    
        doWrite(socketChannel, msg);
    }
    
}

4. 客户端启动类

在这里插入图片描述

package org.example;

import java.util.Scanner;

public class NioClient {
    
    
    private static NioClientHandle nioClientHandle;

    public static void start() {
    
    
        nioClientHandle = new NioClientHandle("127.0.0.1", 9001);
        new Thread(nioClientHandle, "Server").start();
    }

    //向服务器发送消息
    public static boolean sendMsg(String msg) throws Exception {
    
    
        nioClientHandle.sendMsg(msg);
        return true;
    }

    public static void main(String[] args) throws Exception {
    
    
        start();
        Scanner scanner = new Scanner(System.in);
        while (NioClient.sendMsg(scanner.next())) ;
    }

}

5. 先启动服务端,再启动客户端

  • 服务端打印

在这里插入图片描述

  • 客户端启动

在这里插入图片描述

  • 再看服务端打印,提示连接已建立

在这里插入图片描述

  • 客户端输入要发送的消息后,接收到服务端返回的消息

在这里插入图片描述

  • 查看服务端打印,提示接收到了客户端的消息

在这里插入图片描述

6. 新建一个服务端处理器,同时关注读事件和写事件

  • 需要修改的两处代码

在这里插入图片描述

在这里插入图片描述

package org.example;

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.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Iterator;
import java.util.Set;

//nio服务端处理器
public class NioServerHandleWrite implements Runnable {
    
    
    private volatile boolean start;
    private ServerSocketChannel serverSocketChannel;
    private Selector selector;

    //构造方法,指定要监听的端口号
    public NioServerHandleWrite(int port) {
    
    
        try {
    
    
            //创建选择器实例
            selector = Selector.open();
            //创建ServerSocketChannel实例
            serverSocketChannel = ServerSocketChannel.open();

            //设置通道为非阻塞,如果设置为true启动会报错
            serverSocketChannel.configureBlocking(false);
            //绑定端口号
            serverSocketChannel.socket().bind(new InetSocketAddress(port));
            //注册事件,关心客户端连接事件
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            start = true;
            System.out.println("服务端已启动,端口号:" + port);
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }

    //处理事件
    private void handleInput(SelectionKey key) throws IOException {
    
    
        System.out.println("当前通道内的事件:" + key.interestOps());
        if (key.isValid()) {
    
    
            //处理新接入的客户端请求事件
            if (key.isAcceptable()) {
    
    
                //获取关心当前事件的channel
                ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
                //接受连接
                SocketChannel accept = ssc.accept();
                System.out.println("建立连接");
                accept.configureBlocking(false);
                //关注读事件
                accept.register(selector, SelectionKey.OP_READ);
            }
            //处理客户端发送的数据
            if (key.isReadable()) {
    
    
                SocketChannel sc = (SocketChannel) key.channel();
                //创建一个bytebuffer,开辟一个缓冲区
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                //从通道中读数据,然后写入buffer
                int readBytes = sc.read(buffer);
                //读取到数据
                if (readBytes > 0) {
    
    
                    //buffer切换为读模式,将buffer当前的limit设置为position,position=0,用于后续对buffer的读操作
                    buffer.flip();
                    //根据buffer可读字节数创建字节数组
                    byte[] bytes = new byte[buffer.remaining()];
                    //将buffer可读字节数组复制到新建的数组中
                    buffer.get(bytes);
                    String message = new String(bytes, StandardCharsets.UTF_8);
                    System.out.println("服务器接收到的消息:" + message);
                    String result = "Hello " + message + ", now is " + new Date();
                    //发送应答消息
                    doWrite(sc, result);
                }
                //读取数据结束
                else if (readBytes < 0) {
    
    
                    //取消特定的注册关系
                    key.cancel();
                    //关闭通道
                    sc.close();
                }
            }
            if (key.isWritable()) {
    
    
                SocketChannel sc = (SocketChannel) key.channel();
                //从key中拿到需要写的数据
                ByteBuffer buffer = (ByteBuffer) key.attachment();
                if (buffer.hasRemaining()) {
    
    
                    int write = sc.write(buffer);
                    System.out.println("write :" + write + " byte, remaining :" + buffer.hasRemaining());
                } else {
    
    
                    //如果一条数据写完,取消对写的注册,只保留读事件
                    key.interestOps(SelectionKey.OP_READ);
                }
            }
            System.out.println("当前通道内的事件2:" + key.interestOps());
        }
    }

    //发送应答消息
    private void doWrite(SocketChannel socketChannel, String response) throws IOException {
    
    
        //将消息编码为字节数组
        byte[] bytes = response.getBytes();
        //根据数组容量创建ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
        //将字节数组复制到缓冲区
        buffer.put(bytes);
        //写切换为读
        buffer.flip();
        //注册写事件和读事件,并附着buffer数据
        socketChannel.register(selector,
                SelectionKey.OP_WRITE | SelectionKey.OP_READ, buffer);
    }

    @Override
    public void run() {
    
    
        while (start) {
    
    
            try {
    
    
                //如果没有读写事件,selector会阻塞,但是1s后会被唤醒
                selector.select(1000);
                //获取当前事件的集合
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
    
    
                    SelectionKey selectionKey = iterator.next();
                    //删除已处理过的SelectionKey,否则这个key还是一个激活状态,下次循环还会再次处理这个key
                    iterator.remove();
                    handleInput(selectionKey);
                }
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        }
    }

}

7. 启动类

在这里插入图片描述

package org.example;

public class NioServerWrite {
    
    
    private static NioServerHandleWrite nioServerHandleWrite;

    public static void main(String[] args) {
    
    
        nioServerHandleWrite = new NioServerHandleWrite(9001);
        new Thread(nioServerHandleWrite).start();
    }
}

8. 先启动服务端,再启动客户端

  • 服务端打印

在这里插入图片描述

  • 这时候selector中只有接收事件OP_ACCEPT,1左移4位就是2的4次方,等于16

在这里插入图片描述

  • 客户端输入要发送的消息

在这里插入图片描述

  • 服务端打印

在这里插入图片描述

//接收到消息后,从通道中读数据,读事件OP_READ等于1
当前通道内的事件11
服务器接收到的消息:fisher
当前通道内的事件25
//消息读结束,向客户端发送应答消息,同时注册了写事件和读事件,写事件OP_WRITE等于4,读事件OP_READ等于1,相加等于5
当前通道内的事件15
write :49 byte, remaining :false
当前通道内的事件25
当前通道内的事件15
//这里为什么会等于1,是因为数据全部写完后,取消了对写的注册,只保留读事件
当前通道内的事件21

在这里插入图片描述

  • 这里必须要有取消写事件的注册,不然只要buffer中没有填满,程序就会不停出发写事件操作

在这里插入图片描述

  • 这里注释掉 key.interestOps(SelectionKey.OP_READ);这行代码,再发消息看服务端打印,程序会一直出发写事件,进到if (key.isWritable()) {}这个代码块中,但是又没有数据可以写

在这里插入图片描述

代码下载地址

https://gitee.com/fisher3652/netWork.git

猜你喜欢

转载自blog.csdn.net/qq_40977118/article/details/109449002