Socket(NIO)实现的客户端与服务端之间通信

NIO 介绍

在介绍NIO之前,先澄清一个概念,有的人叫NIO为 New IO,有的人把NIO叫做Non-Block IO,这里我们还是习惯说后者,即非阻塞IO。

学习NIO编程,我们先要了解几个概念:

Buffer(缓冲区)、Channel(管道、通道)Selector(选择器、多路复用器)

Buffer

       Buffer 是一个对象,它包含一些要写入或者要读取的数据。在NIO类库中加入Buffer对象,体现了新库与原IO的一个重要的区别。在面向流的IO中,可以将数据直接写入或者读取到Stream对象中。 在NIO库中,所有数据都是用缓冲区处理的(读写)。 缓冲区实质上就是一个数组,通常它是一个字节数组(ByteBuffer),也可以使用其他类型的数组。这个数组为缓冲区提供了数据的访问读写操作属性,如位置、容量、上限等概念,参考api文档。

       Buffer类型:我们常用的就是ByteBuffer,实际上每一种java基本类型都对应了一种缓冲区(除了Boolean类型):

ByteBuffer 、CharBuffer 、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer

Channel

        通道(Channel),它就像自来水管道一样,网络数据通过Channel读取和写入,通道与流不同之处在于通道是双向的,而流只是一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读、写、或者二者同时进行,最关键的是可以与多路复用器结合起来,有多种状态位,方便多路复用器去识别。

        事实上通道分为两大类,一类是网络读写的(SelectableChannel),一类用户文件操作(FileChannel),我们使用的是SocketChannel和ServerSocketChannel都是SelectableChannel的子类。

Selector

        多路复用器(Selector),他是NIO编程的基础,非常重要,多路复用器提供选择已经就绪的任务的能力。

        简单说,就是Selector会不断地轮训注册在其上的通道(Channel),如果某个通道发生了读写操作,这个通道就是处于就绪状态,会被Selector轮训出来,然后通过SelectionKey可以取得就绪的Channel集合,从而进行后续的IO操作。

        一个多路复用器(selector)可以负责成千上万的Channel通道,没有上限,这也是JDK使用了epoll代替了传统的select的实现,获得连接句柄没有限制,这也就意味着我们只要一个线程负责Selector的轮询,就可以接入成千上万的客户端,这是JDK NIO 库的巨大进步。

服务端

package com.example.netty.socket.nio;

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.util.Iterator;

/**
 * NIO 服务端
 *
 * @author lanx
 * @date 2022/3/19
 */
public class Server implements Runnable {

    //1.读取缓冲区
    private ByteBuffer readBuf = ByteBuffer.allocate(1024);
    //写入缓冲区
    private ByteBuffer writeBuf = ByteBuffer.allocate(1024);
    //2. 多路复用器
    private Selector selector;

    public Server(int port) throws IOException {
        // 初始化服务过程

        // 1 打开多路复用器
        this.selector = Selector.open();
        //2 打开服务器端通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        //3 设置通道阻塞模式
        ssc.configureBlocking(false);
        //4 绑定地址
        ssc.bind(new InetSocketAddress(port));
        //5.把服务器通道注册到多路复用器上,并且监听阻塞事件
        ssc.register(this.selector, SelectionKey.OP_ACCEPT);

    }

    @Override
    public void run() {
        while (true) {
            try {
                //1 必须要让多路复用器开始监听
                this.selector.select();
                //2 返回多路复用器所有注册的通道key
                Iterator<SelectionKey> it = this.selector.selectedKeys().iterator();
                //3遍历所有获取的key
                while (it.hasNext()) {
                    //4 获取key值
                    SelectionKey key = it.next();
                    // 5 从容器中移除已经选中的key(客戶端如果下次还要和服务端通讯,需要客户端再次注册客户端通道到多路复用器上才能再次监听到)
                    it.remove();
                    // 6 验证操作 判断管道是否有效 true 有效,false 无效
                    if (key.isValid()) {
                        /**
                         * 管道状态
                         * SelectionKey.OP_CONNECT 是否连接
                         * SelectionKey.OP_ACCEPT  是否阻塞
                         * SelectionKey.OP_READ    是否可读
                         *  SelectionKey.OP_WRITE  是否可写
                         */
                        // 7 如果为 OP_ACCEPT 阻塞状态
                        if (key.isAcceptable()) {
                            this.accept(key);
                        }
                        // 8 如果为 OP_READ  可读状态
                        if (key.isReadable()) {
                            this.read(key);
                        }
                    }

                }

            } catch (IOException e) {
                e.printStackTrace();
            }

        }

    }


    /**
     * 监听 可读状态
     *
     * @param key
     */
    private void read(SelectionKey key) {
        //1 清空缓冲区
        this.readBuf.clear();
        //2 获取之前注册的SocketChannel通道对象
        SocketChannel sc = (SocketChannel) key.channel();
        try {
            //3 从通道获取数据放入缓冲区
            int index = sc.read(this.readBuf);
            if (index == -1) {
                //关闭通道
                key.channel().close();
                //取消key
                key.cancel();
                return;
            }
            //4 由于 sc 通道里的数据流入到 readBuf 容器中,所以 readBuf里面的 position一定发生了变化, 必须进行复位
            readBuf.flip();
            // 读取readBuf数据 然后打印数据
            byte[] bytes = new byte[this.readBuf.remaining()];
            this.readBuf.get(bytes);
            String body = new String(bytes).trim();
            System.out.println("服务器读取到客户SocketChannel端数据:" + body);
            // 5 写出数据
            //清空缓冲区
            this.writeBuf.clear();
            byte[] writeBytes = "服务端发送数据".getBytes();
            this.writeBuf.put(writeBytes);
            this.writeBuf.flip();
            sc.write(this.writeBuf);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 监听阻塞状态
     *
     * @param key
     */
    private void accept(SelectionKey key) {

        try {
            // 1 由于目前是 server端,那么一定是server端启动,并且处于阻塞状态,所以获取阻塞状态的key 一定是 :服务端管道 ServerSocketChannel
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            // 2 通过调用 accept 方法,返回一个具体的客户端连接管道
            SocketChannel sc = ssc.accept();
            // 3 设置为非阻塞
            sc.configureBlocking(false);
            // 4 设置当前获取的客户端连接管道为可读状态
            sc.register(this.selector, SelectionKey.OP_READ);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        new Thread(new Server(8765)).start();
    }
}

客户端

package com.example.netty.socket.nio;

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

/**
 * NIO 客户端
 *
 * @author lanx
 * @date 2022/3/19
 */
public class Client implements Runnable {

    private String ip;
    private int port;
    private Selector selector;
    private SocketChannel socketChannel;

    public Client(String ip, int port) {
        this.ip = ip;
        this.port = port;
        try {
            // 1 打开多路复用器
            selector = Selector.open();
            //2 打开服务器端通道
            socketChannel = SocketChannel.open();
            //3 设置通道阻塞模式
            socketChannel.configureBlocking(false);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }


    @Override
    public void run() {
        try {
            //请求建立连接
            this.doConnect();
        } catch (Exception e) {
            e.printStackTrace();
        }

        while (true) {
            try {
                //休眠1秒  无论是否有读写事件发生 selector每隔1秒被唤醒
                selector.select(1000);
                //获取注册在selector上的所有的就绪状态的serverSocketChannel中发生的事件
                Set<SelectionKey> set = selector.selectedKeys();
                Iterator<SelectionKey> it = set.iterator();
                while (it.hasNext()) {
                    // 获取key值
                    SelectionKey key = it.next();
                    it.remove();

                    // 6 验证操作 判断管道是否有效 true 有效,false 无效
                    if (key.isValid()) {
                        /**
                         * 管道状态
                         * SelectionKey.OP_CONNECT 是否连接
                         * SelectionKey.OP_ACCEPT  是否阻塞
                         * SelectionKey.OP_READ    是否可读
                         *  SelectionKey.OP_WRITE  是否可写
                         */
                        this.handleInput(key);
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
                System.exit(1);
            }
        }

    }

    /**
     * 处理数据
     */
    private void handleInput(SelectionKey key) throws IOException {

        SocketChannel sc = (SocketChannel) key.channel();
        if (key.isConnectable()) { //处于连接状态
            if (sc.finishConnect()) {//客户端连接成功
                //注册到selector为 可读状态
                sc.register(selector, SelectionKey.OP_READ);
                byte[] requestBytes = "客户端发送数据.".getBytes();
                ByteBuffer bf = ByteBuffer.allocate(requestBytes.length);
                bf.put(requestBytes);
                //缓冲区复位
                bf.flip();
                //發送數據
                sc.write(bf);
            }
        }

        if (key.isReadable()) {//如果客户端接收到了服务器端发送的应答消息 则SocketChannel是可读的
            ByteBuffer bf = ByteBuffer.allocate(1024);
            int bytes = sc.read(bf);
            if (bytes > 0) {
                bf.flip();
                byte[] byteArray = new byte[bf.remaining()];
                bf.get(byteArray);
                String resopnseMessage = new String(byteArray, "UTF-8");
                System.out.println("接收到服务端数据:" + resopnseMessage);
            } else if (bytes < 0) {
                key.cancel();
                sc.close();
            }
        }

    }

    /**
     * 创建连接
     * @throws Exception
     */
    private void doConnect() throws Exception {
        //如果直连接连接成功,则注册到多路复用器上,并注册SelectionKey.OP_READ操作
        if (socketChannel.connect(new InetSocketAddress(ip, port))) {
            socketChannel.register(selector, SelectionKey.OP_READ);
            //发送请求消息 读应答
            byte[] requestBytes = "客户端发送数据.".getBytes();
            ByteBuffer bf = ByteBuffer.allocate(requestBytes.length);
            bf.put(requestBytes);
            bf.flip();
            //发送数据
            socketChannel.write(bf);

        } else {//如果直连接连接未成功,则注册到多路复用器上,并注册SelectionKey.OP_CONNECT操作
            socketChannel.register(selector, SelectionKey.OP_CONNECT);
        }
    }

    public static void main(String[] args) {

        new Thread(new Client("127.0.0.1", 8765)).start();
    }

}

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

终遇你..

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值