Java NIO SocketChannel简述及示例

java 同时被 3 个专栏收录
1 篇文章 0 订阅
1 篇文章 0 订阅
1 篇文章 0 订阅

JAVA NIO之SocketChannel

1. 简述

NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。
Java NIO中的SocketChannel是一个连接到TCP网络套接字的通道

2. 特点

1. 非阻塞
2. 单线程
3. 轮循

3. 解决问题

当我们使用socket进行通信时, 建立后的链接需保持通信状态则不可关闭, 会启动分线程进行并发通信, 其中大部分时间会是阻塞状态,等待接收消息, 会造成大量的线程阻塞, 对于cpu需要维护线程之间的切换和调用, 在高并发是性能非常低, 因此使用nio可有效的提高效率

4. demo功能

客户端每隔2秒发送一次数据到服务器端, 服务器在其消息前加hello并回复客户端信息

  1. server端使用非阻塞模式单线程轮循的方式进行与client端通信处理
  2. client端使用2个线程进行运作, 一个线程用于定时发送消息到server端, 另一个线程用于读取server端消息

5. 工作原理

1. client注册SocketChannel到server中
在这里插入图片描述
第一步: 创建ServerSocketChannel服务器端并绑定通信端口8000, 存入selector挑选器keys中, 并注册accept事件
第二步: 客户端链接服务器的8000端口, 与服务器端进行通信,触发服务器端的accept事件
第三步: 挑选器挑选出本次需要执行的keys (ServerSocketChannel)存入挑选结果及selectedKeys中, 等待执行
第四步: 执行ServerSockerChannel.accept处理方式, 即获取与客户端client1通信的SocketChannel对象, 并未其注册read事件, 存储到挑选器中
第五步: 执行完本次挑选的结果集后, 清空selectedKeys集合即可
后续客户端连接服务器便重复第二,三,四步即可

2. client与server端的通信
在这里插入图片描述
第一步: client1和client3发送消息到server, 触发服务器端的read事件
第二步: 挑选器挑选出本次需要执行的keys (ServerSocketChannel)存入挑选结果及selectedKeys中, 等待执行
第三步: 遍历筛选出来的client1和client2的链接, 分别读取数据, 并返回响应消息写入到SocketChannel中
第四步: client通过SocketChannel接收server端发送的消息, 进行处理
重复以上步骤即可完成server和client端的通信

3. 特点
在server端, 使用selector的select方法进行轮循挑选触发事件的链接, 使用单线程即可完成与客户端的通信过程, 极大的提高了服务器端的性能, 在高并发时非常实用

6. 代码示例

1. ServerChannel

package pro.nio.socket;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

/**
 * nio之socket通信服务器端
 */
public class ServerChannel {
    public static void main(String[] args) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        // 开启挑选器
        Selector selector = Selector.open();
        // 开启ServerSocketChannel通道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress("localhost", 8000));
            // 设置非阻塞模式
        serverSocketChannel.configureBlocking(false);
            // 在挑选器中注册通道(服务器通道)
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        SelectableChannel sc = null ;
        while (true) {
            // 阻塞的挑选方法
            selector.select();
            Iterator<SelectionKey> iterable = selector.selectedKeys().iterator();
            while (iterable.hasNext()) {
                SelectionKey key = iterable.next();
                try {
                    if(key.isAcceptable()) {
                        // 服务器通道
                        sc = key.channel();
                        SocketChannel socketChannel = ((ServerSocketChannel)sc).accept();
                        socketChannel.configureBlocking(false); //设置非阻塞
                        // 注册读事件监听
                        //  socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); //注册读写时间
                        socketChannel.register(selector, SelectionKey.OP_READ );
                    }
                    if(key.isReadable()){
                        // 读时间监听获取
                        SocketChannel sc1 = (SocketChannel)key.channel();
                        while (sc1.read(byteBuffer) != 0) {
                            byteBuffer.flip();
                            bos.write(byteBuffer.array());
                            byteBuffer.clear();
                        }
                        System.out.println(new String("客户端请求: " + new String(bos.toByteArray())));
                        byte[] msg = new String("hello " + new String(bos.toByteArray())).getBytes();
                        bos.reset();
                        sc1.write(ByteBuffer.wrap(msg, 0, msg.length));
                    }
                } catch (Exception e) {
                    // 存在异常时, 清除该sokect
                    selector.keys().remove(key);
                }
            }
            // 处理完后清空挑选器内容
            selector.selectedKeys().clear();
        }
    }
}

2. ClientChannel

package pro.nio.socket;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Scanner;
/**
 * nio之socket通信客户端
 */
public class ClientChannel {
    public static void main(String[] args) throws Exception {
        ByteBuffer buf = ByteBuffer.allocate(1024);
        // 开启客户端socket
        SocketChannel socketChannel = SocketChannel.open();
        // 绑定通信端口
        socketChannel.connect(new InetSocketAddress("localhost", 8000));
        socketChannel.configureBlocking(false);
        // 向服务器发送信息
        new Thread() {
            @Override
            public void run() {
                int i = 0;
                while (true) {
                    try {
                        buf.put(("test " + (i++)).getBytes());
                        buf.flip();
                        socketChannel.write(buf);
                        buf.clear();
                        Thread.sleep(2000);
                    } catch (Exception e) {
                        e.printStackTrace();
                        System.exit(0);
                    }
                }
            }
        }.start();
        // 读数据
        new Thread() {
            @Override
            public void run() {
                int i = 0;
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                while (true) {
                    ByteBuffer buf = ByteBuffer.allocate(1024 * 8);
                    try {
                        while (socketChannel.read(buf) != 0) {
                            buf.flip();
                            bos.write(buf.array());
                            buf.clear();
                        }
                        if (bos.size() > 0) {
                            System.out.println("服务器端回复: " + new String(bos.toByteArray()));
                            bos.reset();
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                        System.exit(0);
                    }
                }
            }
        }.start();
    }
}

3. 运行结果展示

server端

客户端请求: test0
客户端请求: test1
客户端请求: test2
客户端请求: test3
客户端请求: test4
客户端请求: test5
客户端请求: test6
客户端请求: test7
客户端请求: test8

client端:

服务器端回复: hello test0
服务器端回复: hello test1
服务器端回复: hello test2
服务器端回复: hello test3
服务器端回复: hello test4
服务器端回复: hello test5
服务器端回复: hello test6
服务器端回复: hello test7
服务器端回复: hello test8

7. 涉及知识扩充

1. Channel通道
Java NIO Channel通道和流非常相似,主要有以下几点区别:
通道可以读也可以写,流一般是单向的。
通道可以异步读写。
通道总是基于缓冲区Buffer来读写。
在这里插入图片描述
通道和buffer之间是双向的, 通道中读取数据,写入到buffer;也可以从buffer内读数据,写入到通道中

2. Buffer缓冲区
内存中预留指定大小的存储空间用来对输入/输出(I/O)的数据作临时存储,这部分预留的内存空间就叫做缓冲区
使用缓冲区有这么两个好处:

  1. 减少实际的物理读写次数
  2. 缓冲区在创建时就被分配内存,这块内存区域一直被重用,可以减少动态分配和回收内存的次数
    在Java NIO中,缓冲区的作用也是用来临时存储数据,可以理解为是I/O操作中数据的中转站。缓冲区直接为通道(Channel)服务,写入数据到通道或从通道读取数据,这样的操利用缓冲区数据来传递就可以达到对数据高效处理的目的。
  3. 所有缓冲区都有4个属性:
    capacity、limit、position、mark,并遵循:mark <= position <= limit <= capacity,下表格是对着4个属性的解释
属性描述
capacity容量,可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变
limit缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。可以修改
position位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变改值
mark标记,调用reset()可以让position恢复到标记的位置
  1. 常用方法
方法描述
allocate(int capacity)从堆空间中分配一个容量大小为capacity的byte数组作为缓冲区的byte数据存储器
flip()limit = position;position = 0;mark = -1;排版,也就是让flip之后的position到limit这块区域变成之前的0到position这块,处于准备取数据的状态
array()缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。可以修改的
wrap(byte[] array, int offset, int length)这个缓冲区的数据会存放在byte数组中,bytes数组或buff缓冲区任何一方中数据的改动都会影响另一方

3. Selector选择器
Selector是Java NIO中的一个组件,用于检查一个或多个NIO Channel的状态是否处于可读、可写。如此可以实现单线程管理多个channels,也就是可以管理多个网络链接。从操作系统的角度来看,切换线程开销是比较昂贵的,并且每个线程都需要占用系统资源,因此暂用线程越少越好。

  1. 单线程处理三个channel的示例图:
    在这里插入图片描述
  2. 常用方法
方法描述
open()创建一个Selector
Select()返回值是一个int整形,代表有多少channel处于就绪了
selectedKeys()在调用select并返回了有channel就绪之后,可以通过选中的key集合来获取channel,返回的SelectionKey
close()当操作Selector完毕后,需要调用close方法。close的调用会关闭Selector并使相关的SelectionKey都无效。channel本身不管被关闭。
  1. 使用注意事项
    使用Selector时,必须先把Channel注册到Selector上,这个操作使用SelectableChannel.register():

channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);

channel.configureBlocking(false)切换为非阻塞模式。

  1. channel状态
    注意register的第二个参数,代表我们关注的channel状态,有四种基础类型可供监听:
监听事件监听引用
ConnectSelectionKey.OP_CONNECT
AcceptSelectionKey.OP_ACCEPT
ReadSelectionKey.OP_READ
WriteSelectionKey.OP_WRITE
  • 1
    点赞
  • 0
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值