深入剖析Java通信架构下的三种IO模式2

简介: 文章摘要: Java NIO(New I/O)是Java 1.4引入的高效I/O API,支持非阻塞操作,核心组件包括Channel(通道)、Buffer(缓冲区)和Selector(选择器)。与BIO(阻塞I/O)相比,NIO以块方式处理数据,效率更高;通过Selector单线程可监听多通道事件,实现高并发。Buffer提供数据存取,Channel负责传输,支持双向读写;文件操作通过FileChannel实现,网络通信结合SocketChannel和Selector实现非阻塞多路复用。文章还对比了BIO/

 1.NIO基本介绍

  • Java NIO(New IO)也有人称之为 java non-blocking IO是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。
  • NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。
  • NIO 有三大核心部分:Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器)
  • Java NIO 的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
  • 通俗理解:NIO 是可以做到用一个线程来处理多个操作的。假设有 1000 个请求过来,根据实际情况,可以分配20 或者 80个线程来处理。不像之前的阻塞 IO 那样,非得分配 1000 个。

NIO与BIO对比:

  • BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多
  • BIO 是阻塞的,NIO 则是非阻塞的
  • BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道 读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
NIO BIO
面向缓冲区(Buffer) 面向流(Stream)
非阻塞(Non Blocking IO) 阻塞IO(Blocking IO)
选择器(Selectors)

2.NIO三大核心组件

NIO 有三大核心部分:Channel( 通道) ,Buffer( 缓冲区), Selector( 选择器)

Buffer缓冲区

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。相比较直接对数组的操作,Buffer API更加容易操作和管理。

Channel(通道)

Java NIO的通道类似流,但又有些不同:既可以从通道中读取数据,又可以写数据到通道。但流的(input或output)读写通常是单向的。 通道可以非阻塞读取和写入通道,通道可以支持读取或写入缓冲区,也支持异步地读写。

Selector选择器

Selector是 一个Java NIO组件,可以能够检查一个或多个 NIO 通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率

image.gif 编辑

  • 每个 channel 都会对应一个 Buffer
  • 一个线程对应Selector , 一个Selector对应多个 channel(连接)
  • 程序切换到哪个 channel 是由事件决定的
  • Selector 会根据不同的事件,在各个通道上切换
  • Buffer 就是一个内存块 , 底层是一个数组
  • 数据的读取写入是通过 Buffer完成的 , BIO 中要么是输入流,或者是输出流, 不能双向,但是 NIO 的 Buffer 是可以读也可以写。
  • Java NIO系统的核心在于:通道(Channel)和缓冲区 (Buffer)。通道表示打开到 IO 设备(例如:文件、 套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲 区。然后操作缓冲区,对数据进行处理。简而言之,Channel 负责传输, Buffer 负责存取数据

2.1 缓冲区Buffer

2.1.1 基本概念

一个用于特定基本数据类型的容器。由 java.nio 包定义的,所有缓冲区都是 Buffer 抽象类的子类.。Java NIO 中的 Buffer 主要用于与 NIO 通道进行交互,数据是从通道读入缓冲区,从缓冲区写入通道中的 image.gif 编辑

Buffer 就像一个数组,可以保存多个相同类型的数据。根据数据类型不同 ,有以下 Buffer 常用子类:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

上述 Buffer 类他们都采用相似的方法进行管理数据,只是各自管理的数据类型不同而已。都是通过如下方法获取一个 Buffer 对象:

static XxxBuffer allocate(int capacity) : 创建一个容量为capacity 的 XxxBuffer 对象

image.gif

2.1.2 缓冲区基本属性

Buffer 中的重要概念:

  • 容量 (capacity) :作为一个内存块,Buffer具有一定的固定大小,也称为"容量",缓冲区容量不能为负,并且创建后不能更改。
  • 限制 (limit):表示缓冲区中可以操作数据的大小(limit 后数据不能进行读写)。缓冲区的限制不能为负,并且不能大于其容量。 写入模式,限制等于buffer的容量。读取模式下,limit等于写入的数据量
  • 位置 (position):下一个要读取或写入的数据的索引。缓冲区的位置不能为负,并且不能大于其限制
  • 标记 (mark)与重置 (reset):标记是一个索引,通过 Buffer 中的 mark() 方法 指定 Buffer 中一个特定的 position,之后可以通过调用 reset() 方法恢复到这 个 position. 标记、位置、限制、容量遵守以下不变式: 0 <= mark <= position <= limit <= capacity
  • 图示:

image.gif 编辑

2.1.3 Buffer常见方法

Buffer clear() 清空缓冲区并返回对缓冲区的引用
Buffer flip() 为 将缓冲区的界限设置为当前位置,并将当前位置充值为 0
int capacity() 返回 Buffer 的 capacity 大小
boolean hasRemaining() 判断缓冲区中是否还有元素
int limit() 返回 Buffer 的界限(limit) 的位置
Buffer limit(int n) 将设置缓冲区界限为 n, 并返回一个具有新 limit 的缓冲区对象
Buffer mark() 对缓冲区设置标记
int position() 返回缓冲区的当前位置 position
Buffer position(int n) 将设置缓冲区的当前位置为 n , 并返回修改后的 Buffer 对象
int remaining() 返回 position 和 limit 之间的元素个数
Buffer reset() 将位置 position 转到以前设置的 mark 所在的位置
Buffer rewind() 将位置设为为 0, 取消设置的 mark

image.gif

2.1.4 Buffer的数据操作

Buffer 所有子类提供了两个用于数据操作的方法:get() put() 方法
获取 Buffer中的数据
get() :读取单个字节
get(byte[] dst):批量读取多个字节到 dst 中
get(int index):读取指定索引位置的字节(不会移动 position)
    
放入数据到 Buffer 中 
put(byte b):将给定单个字节写入缓冲区的当前位置
put(byte[] src):将 src 中的字节写入缓冲区的当前位置
put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动 position)

image.gif

使用Buffer读写数据一般遵循以下四个步骤:

  • 1.写入数据到Buffer
  • 2.调用flip()方法,转换为读取模式
  • 3.从Buffer中读取数据
  • 4.调用buffer.clear()方法或者buffer.compact()方法清除缓冲区

2.1.5 代码演示

public class TestBuffer {
   @Test
   public void test3(){
      //分配直接缓冲区
      ByteBuffer buf = ByteBuffer.allocateDirect(1024);
      System.out.println(buf.isDirect());
   }
   
   @Test
   public void test2(){
      String str = "itheima";
      
      ByteBuffer buf = ByteBuffer.allocate(1024);
      
      buf.put(str.getBytes());
      
      buf.flip();
      
      byte[] dst = new byte[buf.limit()];
      buf.get(dst, 0, 2);
      System.out.println(new String(dst, 0, 2));
      System.out.println(buf.position());
      
      //mark() : 标记
      buf.mark();
      
      buf.get(dst, 2, 2);
      System.out.println(new String(dst, 2, 2));
      System.out.println(buf.position());
      
      //reset() : 恢复到 mark 的位置
      buf.reset();
      System.out.println(buf.position());
      
      //判断缓冲区中是否还有剩余数据
      if(buf.hasRemaining()){
         //获取缓冲区中可以操作的数量
         System.out.println(buf.remaining());
      }
   }
    
   @Test
   public void test1(){
      String str = "itheima";
      //1. 分配一个指定大小的缓冲区
      ByteBuffer buf = ByteBuffer.allocate(1024);
      System.out.println("-----------------allocate()----------------");
      System.out.println(buf.position());
      System.out.println(buf.limit());
      System.out.println(buf.capacity());
      
      //2. 利用 put() 存入数据到缓冲区中
      buf.put(str.getBytes());
      System.out.println("-----------------put()----------------");
      System.out.println(buf.position());
      System.out.println(buf.limit());
      System.out.println(buf.capacity());
      
      //3. 切换读取数据模式
      buf.flip();
      System.out.println("-----------------flip()----------------");
      System.out.println(buf.position());
      System.out.println(buf.limit());
      System.out.println(buf.capacity());
      
      //4. 利用 get() 读取缓冲区中的数据
      byte[] dst = new byte[buf.limit()];
      buf.get(dst);
      System.out.println(new String(dst, 0, dst.length));
      System.out.println("-----------------get()----------------");
      System.out.println(buf.position());
      System.out.println(buf.limit());
      System.out.println(buf.capacity());
      //5. rewind() : 可重复读
      buf.rewind();
      System.out.println("-----------------rewind()----------------");
      System.out.println(buf.position());
      System.out.println(buf.limit());
      System.out.println(buf.capacity());
      
      //6. clear() : 清空缓冲区. 但是缓冲区中的数据依然存在,但是处于“被遗忘”状态
      buf.clear();
      System.out.println("-----------------clear()----------------");
      System.out.println(buf.position());
      System.out.println(buf.limit());
      System.out.println(buf.capacity());
      System.out.println((char)buf.get());
      
   }
}

image.gif

2.1.6 直接与非直接缓冲区

什么是直接内存与非直接内存

根据官方文档的描述:

byte byffer可以是两种类型,一种是基于直接内存(也就是非堆内存);另一种是非直接内存(也就是堆内存)。对于直接内存来说,JVM将会在IO操作上具有更高的性能,因为它直接作用于本地系统的IO操作。而非直接内存,也就是堆内存中的数据,如果要作IO操作,会先从本进程内存复制到直接内存,再利用本地IO处理。

从数据流的角度,非直接内存是下面这样的作用链:

本地IO-->直接内存-->非直接内存-->直接内存-->本地IO

image.gif

而直接内存是:

本地IO-->直接内存-->本地IO

image.gif

很明显,在做IO处理时,比如网络发送大量数据时,直接内存会具有更高的效率。直接内存使用allocateDirect创建,但是它比申请普通的堆内存需要耗费更高的性能。不过,这部分的数据是在JVM之外的,因此它不会占用应用的内存。所以呢,当你有很大的数据要缓存,并且它的生命周期又很长,那么就比较适合使用直接内存。只是一般来说,如果不是能带来很明显的性能提升,还是推荐直接使用堆内存。字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。

使用场景

  • 1 有很大的数据需要存储,它的生命周期又很长
  • 2 适合频繁的IO操作,比如网络并发场景

2.2 通道Channel

2.2.1 基本概念

通道(Channel):由 java.nio.channels 包定义 的。Channel 表示 IO 源与目标打开的连接。 Channel 类似于传统的“流”。只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互。

1、 NIO 的通道类似于流,但有些区别如下:

  • 通道可以同时进行读写,而流只能读或者只能写
  • 通道可以实现异步读写数据
  • 通道可以从缓冲读数据,也可以写数据到缓冲:

2、BIO 中的 stream 是单向的,例如 FileInputStream 对象只能进行读取数据的操作,而 NIO 中的通道(Channel) 是双向的,可以读操作,也可以写操作。

3、Channel 在 NIO 中是一个接口

public interface Channel extends Closeable{}

image.gif

常用的Channel实现类:

  • FileChannel:用于读取、写入、映射和操作文件的通道。
  • DatagramChannel:通过 UDP 读写网络中的数据通道。
  • SocketChannel:通过 TCP 读写网络中的数据。
  • ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。 【ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket】

2.2.2 FileChannel类

获取通道的一种方式是对支持通道的对象调用getChannel() 方法。支持通道的类如下:

  • FileInputStream
  • FileOutputStream
  • RandomAccessFile
  • DatagramSocket
  • Socket
  • ServerSocket 获取通道的其他方式是使用 Files 类的静态方法 newByteChannel() 获取字节通道。或者通过通道的静态方法 open() 打开并返回指定通道

FileChannel常用方法:

int read(ByteBuffer dst)  从Channel 到 中读取数据到  ByteBuffer
long  read(ByteBuffer[] dsts)  将Channel 到 中的数据“分散”到  ByteBuffer[]
int  write(ByteBuffer src) 将ByteBuffer 到 中的数据写入到  Channel
long write(ByteBuffer[] srcs) 将ByteBuffer[] 到 中的数据“聚集”到  Channel
long position() 返回此通道的文件位置
FileChannel position(long p) 设置此通道的文件位置
long size() 返回此通道的文件的当前大小
FileChannel truncate(long s) 将此通道的文件截取为给定大小
void force(boolean metaData) 强制将所有对此通道的文件更新写入到存储设备中

image.gif

2.2.3 案例-本地文件写数据

需求:使用前面学习后的 ByteBuffer(缓冲) 和 FileChannel(通道), 将 "hello,黑马Java程序员!" 写入到 data.txt 中.

import org.junit.Test;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class ChannelTest {
    @Test
    public void write(){
        try {
            // 1、字节输出流通向目标文件
            FileOutputStream fos = new FileOutputStream("data01.txt");
            // 2、得到字节输出流对应的通道Channel
            FileChannel channel = fos.getChannel();
            // 3、分配缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            buffer.put("hello,黑马Java程序员!".getBytes());
            // 4、把缓冲区切换成写出模式
            buffer.flip();
            channel.write(buffer);
            channel.close();
            System.out.println("写数据到文件中!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

image.gif

2.2.4 案例-本地文件读数据

需求:使用前面学习后的 ByteBuffer(缓冲) 和 FileChannel(通道), 将 data01.txt 中的数据读入到程序,并显示在控制台屏幕。

public class ChannelTest {
    @Test
    public void read() throws Exception {
        // 1、定义一个文件字节输入流与源文件接通
        FileInputStream is = new FileInputStream("data01.txt");
        // 2、需要得到文件字节输入流的文件通道
        FileChannel channel = is.getChannel();
        // 3、定义一个缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 4、读取数据到缓冲区
        channel.read(buffer);
        buffer.flip();
        // 5、读取出缓冲区中的数据并输出即可
        String rs = new String(buffer.array(),0,buffer.remaining());
        System.out.println(rs);
    }

image.gif

2.2.5 案例-使用Buffer完成文本复制

使用 FileChannel(通道) ,完成文件的拷贝。

@Test
public void copy() throws Exception {
    // 源文件
    File srcFile = new File("C:\\Users\\dlei\\Desktop\\BIO,NIO,AIO\\文件\\壁纸.jpg");
    File destFile = new File("C:\\Users\\dlei\\Desktop\\BIO,NIO,AIO\\文件\\壁纸new.jpg");
    // 得到一个字节字节输入流
    FileInputStream fis = new FileInputStream(srcFile);
    // 得到一个字节输出流
    FileOutputStream fos = new FileOutputStream(destFile);
    // 得到的是文件通道
    FileChannel isChannel = fis.getChannel();
    FileChannel osChannel = fos.getChannel();
    // 分配缓冲区
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    while(true){
        // 必须先清空缓冲然后再写入数据到缓冲区
        buffer.clear();
        // 开始读取一次数据
        int flag = isChannel.read(buffer);
        if(flag == -1){
            break;
        }
        // 已经读取了数据 ,把缓冲区的模式切换成可读模式
        buffer.flip();
        // 把数据写出到
        osChannel.write(buffer);
    }
    isChannel.close();
    osChannel.close();
    System.out.println("复制完成!");
}

image.gif

2.2.6 案例-分散 (Scatter) 和聚集 (Gather)

分散读取(Scatter ):是指把Channel通道的数据读入到多个缓冲区中去

聚集写入(Gathering )是指将多个 Buffer 中的数据“聚集”到 Channel

//分散和聚集
@Test
public void test() throws IOException{
    RandomAccessFile raf1 = new RandomAccessFile("1.txt", "rw");
  //1. 获取通道
  FileChannel channel1 = raf1.getChannel();
  
  //2. 分配指定大小的缓冲区
  ByteBuffer buf1 = ByteBuffer.allocate(100);
  ByteBuffer buf2 = ByteBuffer.allocate(1024);
  
  //3. 分散读取
  ByteBuffer[] bufs = {buf1, buf2};
  channel1.read(bufs);
  
  for (ByteBuffer byteBuffer : bufs) {
    byteBuffer.flip();
  }
  
  System.out.println(new String(bufs[0].array(), 0, bufs[0].limit()));
  System.out.println("-----------------");
  System.out.println(new String(bufs[1].array(), 0, bufs[1].limit()));
  
  //4. 聚集写入
  RandomAccessFile raf2 = new RandomAccessFile("2.txt", "rw");
  FileChannel channel2 = raf2.getChannel();
  
  channel2.write(bufs);
}

image.gif

2.2.7 案例-transferFrom()

从目标通道中去复制原通道数据

@Test
public void test02() throws Exception {
    // 1、字节输入管道
    FileInputStream is = new FileInputStream("data01.txt");
    FileChannel isChannel = is.getChannel();
    // 2、字节输出流管道
    FileOutputStream fos = new FileOutputStream("data03.txt");
    FileChannel osChannel = fos.getChannel();
    // 3、复制
    osChannel.transferFrom(isChannel,isChannel.position(),isChannel.size());
    isChannel.close();
    osChannel.close();
}

image.gif

2.2.8 案例-transferTo()

把原通道数据复制到目标通道

@Test
public void test02() throws Exception {
    // 1、字节输入管道
    FileInputStream is = new FileInputStream("data01.txt");
    FileChannel isChannel = is.getChannel();
    // 2、字节输出流管道
    FileOutputStream fos = new FileOutputStream("data04.txt");
    FileChannel osChannel = fos.getChannel();
    // 3、复制
    isChannel.transferTo(isChannel.position() , isChannel.size() , osChannel);
    isChannel.close();
    osChannel.close();
}

image.gif

2.3 选择器Selector

2.3.1 概述

选择器(Selector) 是 SelectableChannel 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心

image.gif 编辑

  • Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)
  • Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个 Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
  • 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
  • 避免了多线程之间的上下文切换导致的开销

2.3.2 应用

创建 Selector :通过调用 Selector.open() 方法创建一个 Selector。

Selector selector = Selector.open();

image.gif

向选择器注册通道:SelectableChannel.register(Selector sel, int ops)

//1. 获取通道
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//2. 切换非阻塞模式
ssChannel.configureBlocking(false);
//3. 绑定连接
ssChannel.bind(new InetSocketAddress(9898));
//4. 获取选择器
Selector selector = Selector.open();
//5. 将通道注册到选择器上, 并且指定“监听接收事件”
ssChannel.register(selector, SelectionKey.OP_ACCEPT);

image.gif

当调用 register(Selector sel, int ops) 将通道注册选择器时,选择器对通道的监听事件,需要通过第二个参数 ops 指定。可以监听的事件类型(用 可使用 SelectionKey 的四个常量 表示):

  • 读 : SelectionKey.OP_READ (1)
  • 写 : SelectionKey.OP_WRITE (4)
  • 连接 : SelectionKey.OP_CONNECT (8)
  • 接收 : SelectionKey.OP_ACCEPT (16)
  • 若注册时不止监听一个事件,则可以使用“位或”操作符连接。
int interestSet = SelectionKey.OP_READ|SelectionKey.OP_WRITE

image.gif

2.4 NIO非阻塞式网络通信原理分析

Selector可以实现: 一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。

image.gif 编辑

2.4.1 服务端流程

1、当客户端连接服务端时,服务端会通过 ServerSocketChannel 得到 SocketChannel:

ServerSocketChannel ssChannel = ServerSocketChannel.open();

image.gif

2、切换非阻塞模式

ssChannel.configureBlocking(false);

image.gif

3、绑定连接

ssChannel.bind(new InetSocketAddress(9999));

image.gif

4、 获取选择器

Selector selector = Selector.open();

image.gif

5、 将通道注册到选择器上, 并且指定“监听接收事件”

ssChannel.register(selector, SelectionKey.OP_ACCEPT);

image.gif

6、 轮询式的获取选择器

//轮询式的获取选择器上已经“准备就绪”的事件
 while (selector.select() > 0) {
        System.out.println("轮一轮");
        //7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
        Iterator<SelectionKey> it = selector.selectedKeys().iterator();
        while (it.hasNext()) {
            //8. 获取准备“就绪”的是事件
            SelectionKey sk = it.next();
            //9. 判断具体是什么事件准备就绪
            if (sk.isAcceptable()) {
                //10. 若“接收就绪”,获取客户端连接
                SocketChannel sChannel = ssChannel.accept();
                //11. 切换非阻塞模式
                sChannel.configureBlocking(false);
                //12. 将该通道注册到选择器上
                sChannel.register(selector, SelectionKey.OP_READ);
            } else if (sk.isReadable()) {
                //13. 获取当前选择器上“读就绪”状态的通道
                SocketChannel sChannel = (SocketChannel) sk.channel();
                //14. 读取数据
                ByteBuffer buf = ByteBuffer.allocate(1024);
                int len = 0;
                while ((len = sChannel.read(buf)) > 0) {
                    buf.flip();
                    System.out.println(new String(buf.array(), 0, len));
                    buf.clear();
                }
            }
            //15. 取消选择键 SelectionKey
            it.remove();
        }
    }
}

image.gif

    2.4.2 客户端流程

    获取通道:

    SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));

    image.gif

    切换非阻塞模式:

    sChannel.configureBlocking(false);

    image.gif

    分配指定大小的缓冲区:

    ByteBuffer buf = ByteBuffer.allocate(1024);

    image.gif

      发送数据给服务端:

      Scanner scan = new Scanner(System.in);
      while(scan.hasNext()){
        String str = scan.nextLine();
        buf.put((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(System.currentTimeMillis())
            + "\n" + str).getBytes());
        buf.flip();
        sChannel.write(buf);
        buf.clear();
      }
      //关闭通道
      sChannel.close();

      image.gif

      3. NIO编程案例

      3.1 入门案例

      需求:服务端接收客户端的连接请求,并接收多个客户端发送过来的事件。

      /**
        客户端
       */
      public class Client {
        public static void main(String[] args) throws Exception {
          //1. 获取通道
          SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
          //2. 切换非阻塞模式
          sChannel.configureBlocking(false);
          //3. 分配指定大小的缓冲区
          ByteBuffer buf = ByteBuffer.allocate(1024);
          //4. 发送数据给服务端
          Scanner scan = new Scanner(System.in);
          while(scan.hasNext()){
            String str = scan.nextLine();
            buf.put((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(System.currentTimeMillis())
                + "\n" + str).getBytes());
            buf.flip();
            sChannel.write(buf);
            buf.clear();
          }
          //5. 关闭通道
          sChannel.close();
        }
      }
      /**
       服务端
       */
      public class Server {
          public static void main(String[] args) throws IOException {
              //1. 获取通道
              ServerSocketChannel ssChannel = ServerSocketChannel.open();
              //2. 切换非阻塞模式
              ssChannel.configureBlocking(false);
              //3. 绑定连接
              ssChannel.bind(new InetSocketAddress(9999));
              //4. 获取选择器
              Selector selector = Selector.open();
              //5. 将通道注册到选择器上, 并且指定“监听接收事件”
              ssChannel.register(selector, SelectionKey.OP_ACCEPT);
              //6. 轮询式的获取选择器上已经“准备就绪”的事件
              while (selector.select() > 0) {
                  System.out.println("轮一轮");
                  //7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”
                  Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                  while (it.hasNext()) {
                      //8. 获取准备“就绪”的是事件
                      SelectionKey sk = it.next();
                      //9. 判断具体是什么事件准备就绪
                      if (sk.isAcceptable()) {
                          //10. 若“接收就绪”,获取客户端连接
                          SocketChannel sChannel = ssChannel.accept();
                          //11. 切换非阻塞模式
                          sChannel.configureBlocking(false);
                          //12. 将该通道注册到选择器上
                          sChannel.register(selector, SelectionKey.OP_READ);
                      } else if (sk.isReadable()) {
                          //13. 获取当前选择器上“读就绪”状态的通道
                          SocketChannel sChannel = (SocketChannel) sk.channel();
                          //14. 读取数据
                          ByteBuffer buf = ByteBuffer.allocate(1024);
                          int len = 0;
                          while ((len = sChannel.read(buf)) > 0) {
                              buf.flip();
                              System.out.println(new String(buf.array(), 0, len));
                              buf.clear();
                          }
                      }
                      //15. 取消选择键 SelectionKey
                      it.remove();
                  }
              }
          }
      }

      image.gif

      3.2 基于NIO实现IM群聊系统

      目标:

      需求:进一步理解 NIO 非阻塞网络编程机制,实现多人群聊

      • 编写一个 NIO 群聊系统,实现客户端与客户端的通信需求(非阻塞)
      • 服务器端:可以监测用户上线,离线,并实现消息转发功能
      • 客户端:通过 channel 可以无阻塞发送消息给其它所有客户端用户,同时可以接受其它客户端用户通过服务端转发来的消息

      服务端实现:

      public class Server {
          //定义属性
          private Selector selector;
          private ServerSocketChannel ssChannel;
          private static final int PORT = 9999;
          //构造器
          //初始化工作
          public Server() {
              try {
                  // 1、获取通道
                  ssChannel = ServerSocketChannel.open();
                  // 2、切换为非阻塞模式
                  ssChannel.configureBlocking(false);
                  // 3、绑定连接的端口
                  ssChannel.bind(new InetSocketAddress(PORT));
                  // 4、获取选择器Selector
                  selector = Selector.open();
                  // 5、将通道都注册到选择器上去,并且开始指定监听接收事件
                  ssChannel.register(selector , SelectionKey.OP_ACCEPT);
              }catch (IOException e) {
                  e.printStackTrace();
              }
          }
          //监听
          public void listen() {
              System.out.println("监听线程: " + Thread.currentThread().getName());
              try {
                  while (selector.select() > 0){
                      System.out.println("开始一轮事件处理~~~");
                      // 7、获取选择器中的所有注册的通道中已经就绪好的事件
                      Iterator<SelectionKey> it = selector.selectedKeys().iterator();
                      // 8、开始遍历这些准备好的事件
                      while (it.hasNext()){
                          // 提取当前这个事件
                          SelectionKey sk = it.next();
                          // 9、判断这个事件具体是什么
                          if(sk.isAcceptable()){
                              // 10、直接获取当前接入的客户端通道
                              SocketChannel schannel = ssChannel.accept();
                              // 11 、切换成非阻塞模式
                              schannel.configureBlocking(false);
                              // 12、将本客户端通道注册到选择器
                              System.out.println(schannel.getRemoteAddress() + " 上线 ");
                              schannel.register(selector , SelectionKey.OP_READ);
                              //提示
                          }else if(sk.isReadable()){
                              //处理读 (专门写方法..)
                              readData(sk);
                          }
                          it.remove(); // 处理完毕之后需要移除当前事件
                      }
                  }
              }catch (Exception e) {
                  e.printStackTrace();
              }finally {
                  //发生异常处理....
              }
          }
          //读取客户端消息
          private void readData(SelectionKey key) {
              //取到关联的channle
              SocketChannel channel = null;
              try {
                 //得到channel
                  channel = (SocketChannel) key.channel();
                  //创建buffer
                  ByteBuffer buffer = ByteBuffer.allocate(1024);
                  int count = channel.read(buffer);
                  //根据count的值做处理
                  if(count > 0) {
                      //把缓存区的数据转成字符串
                      String msg = new String(buffer.array());
                      //输出该消息
                      System.out.println("form 客户端: " + msg);
                      //向其它的客户端转发消息(去掉自己), 专门写一个方法来处理
                      sendInfoToOtherClients(msg, channel);
                  }
              }catch (IOException e) {
                  try {
                      System.out.println(channel.getRemoteAddress() + " 离线了..");
                      e.printStackTrace();
                      //取消注册
                      key.cancel();
                      //关闭通道
                      channel.close();
                  }catch (IOException e2) {
                      e2.printStackTrace();;
                  }
              }
          }
          //转发消息给其它客户(通道)
          private void sendInfoToOtherClients(String msg, SocketChannel self ) throws  IOException{
              System.out.println("服务器转发消息中...");
              System.out.println("服务器转发数据给客户端线程: " + Thread.currentThread().getName());
              //遍历 所有注册到selector 上的 SocketChannel,并排除 self
              for(SelectionKey key: selector.keys()) {
                  //通过 key  取出对应的 SocketChannel
                  Channel targetChannel = key.channel();
                  //排除自己
                  if(targetChannel instanceof  SocketChannel && targetChannel != self) {
                      //转型
                      SocketChannel dest = (SocketChannel)targetChannel;
                      //将msg 存储到buffer
                      ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                      //将buffer 的数据写入 通道
                      dest.write(buffer);
                  }
              }
          }
          public static void main(String[] args) {
              //创建服务器对象
              Server groupChatServer = new Server();
              groupChatServer.listen();
          }
      }

      image.gif

      客户端实现:

      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.Scanner;
      public class Client {
          //定义相关的属性
          private final String HOST = "127.0.0.1"; // 服务器的ip
          private final int PORT = 9999; //服务器端口
          private Selector selector;
          private SocketChannel socketChannel;
          private String username;
          //构造器, 完成初始化工作
          public Client() throws IOException {
              selector = Selector.open();
              //连接服务器
              socketChannel = socketChannel.open(new InetSocketAddress("127.0.0.1", PORT));
              //设置非阻塞
              socketChannel.configureBlocking(false);
              //将channel 注册到selector
              socketChannel.register(selector, SelectionKey.OP_READ);
              //得到username
              username = socketChannel.getLocalAddress().toString().substring(1);
              System.out.println(username + " is ok...");
          }
          //向服务器发送消息
          public void sendInfo(String info) {
              info = username + " 说:" + info;
              try {
                  socketChannel.write(ByteBuffer.wrap(info.getBytes()));
              }catch (IOException e) {
                  e.printStackTrace();
              }
          }
          //读取从服务器端回复的消息
          public void readInfo() {
              try {
                  int readChannels = selector.select();
                  if(readChannels > 0) {//有可以用的通道
                      Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                      while (iterator.hasNext()) {
                          SelectionKey key = iterator.next();
                          if(key.isReadable()) {
                              //得到相关的通道
                             SocketChannel sc = (SocketChannel) key.channel();
                             //得到一个Buffer
                              ByteBuffer buffer = ByteBuffer.allocate(1024);
                              //读取
                              sc.read(buffer);
                              //把读到的缓冲区的数据转成字符串
                              String msg = new String(buffer.array());
                              System.out.println(msg.trim());
                          }
                      }
                      iterator.remove(); //删除当前的selectionKey, 防止重复操作
                  } else {
                      //System.out.println("没有可以用的通道...");
                  }
              }catch (Exception e) {
                  e.printStackTrace();
              }
          }
          public static void main(String[] args) throws Exception {
              //启动我们客户端
              Client chatClient = new Client();
              //启动一个线程, 每个3秒,读取从服务器发送数据
              new Thread() {
                  public void run() {
                      while (true) {
                          chatClient.readInfo();
                          try {
                              Thread.currentThread().sleep(3000);
                          }catch (InterruptedException e) {
                              e.printStackTrace();
                          }
                      }
                  }
              }.start();
              //发送数据给服务器端
              Scanner scanner = new Scanner(System.in);
              while (scanner.hasNextLine()) {
                  String s = scanner.nextLine();
                  chatClient.sendInfo(s);
              }
          }
      }

      image.gif

      4. BIO VS NIO

      4.1 Stream vs channel

      • stream 不会自动缓冲数据,channel 会利用系统提供的发送缓冲区、接收缓冲区(更为底层)
      • stream 仅支持阻塞 API,channel 同时支持阻塞、非阻塞 API,网络 channel 可配合 selector 实现多路复用
      • 二者均为全双工,即读写可以同时进行

      4.2 IO模型

      同步阻塞、同步非阻塞、同步多路复用、异步阻塞(没有此情况)、异步非阻塞

      • 同步:线程自己去获取结果(一个线程)
      • 异步:线程自己不去获取结果,而是由其它线程送结果(至少两个线程)

      当调用一次 channel.read 或 stream.read 后,会切换至操作系统内核态来完成真正数据读取,而读取又分为两个阶段,分别为:

      • 等待数据阶段
      • 复制数据阶段

      image.gif 编辑

      阻塞 IO:

      image.gif 编辑

      非阻塞 IO: image.gif 编辑

      多路复用:

      image.gif 编辑

      信号驱动

      异步 IO:

      image.gif 编辑

      阻塞 IO vs 多路复用:

      image.gif 编辑

      4.3 零拷贝

      传统 IO 问题:

      传统的 IO 将一个文件通过 socket 写出

      File f = new File("helloword/data.txt");
      RandomAccessFile file = new RandomAccessFile(file, "r");
      byte[] buf = new byte[(int)f.length()];
      file.read(buf);
      Socket socket = ...;
      socket.getOutputStream().write(buf);

      image.gif

      内部工作流程是这样的:

      image.gif 编辑

      1. java 本身并不具备 IO 读写能力,因此 read 方法调用后,要从 java 程序的用户态切换至内核态,去调用操作系统(Kernel)的读能力,将数据读入内核缓冲区。这期间用户线程阻塞,操作系统使用 DMA(Direct Memory Access)来实现文件读,其间也不会使用 cpu。DMA 也可以理解为硬件单元,用来解放 cpu 完成文件 IO
      2. 内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(即 byte[] buf),这期间 cpu 会参与拷贝,无法利用 DMA
      3. 调用 write 方法,这时将数据从用户缓冲区(byte[] buf)写入 socket 缓冲区,cpu 会参与拷贝
      4. 接下来要向网卡写数据,这项能力 java 又不具备,因此又得从用户态切换至内核态,调用操作系统的写能力,使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 cpu

      可以看到中间环节较多,java 的 IO 实际不是物理设备级别的读写,而是缓存的复制,底层的真正读写是操作系统来完成的

      • 用户态与内核态的切换发生了 3 次,这个操作比较重量级
      • 数据拷贝了共 4 次

      NIO 优化:

      通过 DirectByteBuf

      • ByteBuffer.allocate(10) HeapByteBuffer 使用的还是 java 内存
      • ByteBuffer.allocateDirect(10) DirectByteBuffer 使用的是操作系统内存 image.gif 编辑

      大部分步骤与优化前相同,不再赘述。唯有一点:java 可以使用 DirectByteBuf 将堆外内存映射到 jvm 内存中来直接访问使用

      • 这块内存不受 jvm 垃圾回收的影响,因此内存地址固定,有助于 IO 读写
      • java 中的 DirectByteBuf 对象仅维护了此内存的虚引用,内存回收分成两步
      • DirectByteBuf 对象被垃圾回收,将虚引用加入引用队列
      • 通过专门线程访问引用队列,根据虚引用释放堆外内存
      • 减少了一次数据拷贝,用户态与内核态的切换次数没有减少

      进一步优化(底层采用了 linux 2.1 后提供的 sendFile 方法),java 中对应着两个 channel 调用 transferTo/transferFrom 方法拷贝数据

      image.gif 编辑

      1. java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 cpu
      2. 数据从内核缓冲区传输到 socket 缓冲区,cpu 会参与拷贝
      3. 最后使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 cpu

      可以看到

      • 只发生了一次用户态与内核态的切换
      • 数据拷贝了 3 次

      进一步优化(linux 2.4)

      image.gif 编辑

      1. java 调用 transferTo 方法后,要从 java 程序的用户态切换至内核态,使用 DMA将数据读入内核缓冲区,不会使用 cpu
      2. 只会将一些 offset 和 length 信息拷入 socket 缓冲区,几乎无消耗
      3. 使用 DMA 将 内核缓冲区的数据写入网卡,不会使用 cpu

      整个过程仅只发生了一次用户态与内核态的切换,数据拷贝了 2 次。所谓的【零拷贝】,并不是真正无拷贝,而是在不会拷贝重复数据到 jvm 内存中,零拷贝的优点有

      • 更少的用户态与内核态的切换
      • 不利用 cpu 计算,减少 cpu 缓存伪共享
      • 零拷贝适合小文件传输

      5.AIO

      • Java AIO(NIO.2) : 异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
      AIO
      异步非阻塞,基于NIO的,可以称之为NIO2.0
          BIO                   NIO                              AIO        
      Socket                SocketChannel                    AsynchronousSocketChannel
      ServerSocket          ServerSocketChannel        AsynchronousServerSocketChannel

      image.gif

      与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可, 这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序

      即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称作NIO.2,主要在Java.nio.channels包下增加了下面四个异步通道:

      AsynchronousSocketChannel
        AsynchronousServerSocketChannel
        AsynchronousFileChannel
        AsynchronousDatagramChannel

      image.gif

      AIO 用来解决数据复制阶段的阻塞问题

      • 同步意味着,在进行读写操作时,线程需要等待结果,还是相当于闲置
      • 异步意味着,在进行读写操作时,线程不必等待结果,而是将来由操作系统来通过回调方式由另外的线程来获得结果

      异步模型需要底层操作系统(Kernel)提供支持

      Windows 系统通过 IOCP 实现了真正的异步 IO

      Linux 系统异步 IO 在 2.6 版本引入,但其底层实现还是用多路复用模拟了异步 IO,性能没有优势

      文件 AIO

      先来看看 AsynchronousFileChannel

      @Slf4j
      public class AioDemo1 {
          public static void main(String[] args) throws IOException {
              try{
                  AsynchronousFileChannel s = 
                      AsynchronousFileChannel.open(
                        Paths.get("1.txt"), StandardOpenOption.READ);
                  ByteBuffer buffer = ByteBuffer.allocate(2);
                  log.debug("begin...");
                  s.read(buffer, 0, null, new CompletionHandler<Integer, ByteBuffer>() {
                      @Override
                      public void completed(Integer result, ByteBuffer attachment) {
                          log.debug("read completed...{}", result);
                          buffer.flip();
                          debug(buffer);
                      }
                      @Override
                      public void failed(Throwable exc, ByteBuffer attachment) {
                          log.debug("read failed...");
                      }
                  });
              } catch (IOException e) {
                  e.printStackTrace();
              }
              log.debug("do other things...");
              System.in.read();
          }
      }

      image.gif

      输出:

      13:44:56 [DEBUG] [main] c.i.aio.AioDemo1 - begin...
      13:44:56 [DEBUG] [main] c.i.aio.AioDemo1 - do other things...
      13:44:56 [DEBUG] [Thread-5] c.i.aio.AioDemo1 - read completed...2
               +-------------------------------------------------+
               |  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f |
      +--------+-------------------------------------------------+----------------+
      |00000000| 61 0d                                           |a.              |
      +--------+-------------------------------------------------+----------------+

      image.gif

      可以看到

      • 响应文件读取成功的是另一个线程 Thread-5
      • 主线程并没有 IO 操作阻塞

      守护线程:

      默认文件 AIO 使用的线程都是守护线程,所以最后要执行 System.in.read() 以避免守护线程意外结束

      网络 AIO

      public class AioServer {
          public static void main(String[] args) throws IOException {
              AsynchronousServerSocketChannel ssc = AsynchronousServerSocketChannel.open();
              ssc.bind(new InetSocketAddress(8080));
              ssc.accept(null, new AcceptHandler(ssc));
              System.in.read();
          }
          private static void closeChannel(AsynchronousSocketChannel sc) {
              try {
                  System.out.printf("[%s] %s close\n", Thread.currentThread().getName(), sc.getRemoteAddress());
                  sc.close();
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
          private static class ReadHandler implements CompletionHandler<Integer, ByteBuffer> {
              private final AsynchronousSocketChannel sc;
              public ReadHandler(AsynchronousSocketChannel sc) {
                  this.sc = sc;
              }
              @Override
              public void completed(Integer result, ByteBuffer attachment) {
                  try {
                      if (result == -1) {
                          closeChannel(sc);
                          return;
                      }
                      System.out.printf("[%s] %s read\n", Thread.currentThread().getName(), sc.getRemoteAddress());
                      attachment.flip();
                      System.out.println(Charset.defaultCharset().decode(attachment));
                      attachment.clear();
                      // 处理完第一个 read 时,需要再次调用 read 方法来处理下一个 read 事件
                      sc.read(attachment, attachment, this);
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
              }
              @Override
              public void failed(Throwable exc, ByteBuffer attachment) {
                  closeChannel(sc);
                  exc.printStackTrace();
              }
          }
          private static class WriteHandler implements CompletionHandler<Integer, ByteBuffer> {
              private final AsynchronousSocketChannel sc;
              private WriteHandler(AsynchronousSocketChannel sc) {
                  this.sc = sc;
              }
              @Override
              public void completed(Integer result, ByteBuffer attachment) {
                  // 如果作为附件的 buffer 还有内容,需要再次 write 写出剩余内容
                  if (attachment.hasRemaining()) {
                      sc.write(attachment);
                  }
              }
              @Override
              public void failed(Throwable exc, ByteBuffer attachment) {
                  exc.printStackTrace();
                  closeChannel(sc);
              }
          }
          private static class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, Object> {
              private final AsynchronousServerSocketChannel ssc;
              public AcceptHandler(AsynchronousServerSocketChannel ssc) {
                  this.ssc = ssc;
              }
              @Override
              public void completed(AsynchronousSocketChannel sc, Object attachment) {
                  try {
                      System.out.printf("[%s] %s connected\n", Thread.currentThread().getName(), sc.getRemoteAddress());
                  } catch (IOException e) {
                      e.printStackTrace();
                  }
                  ByteBuffer buffer = ByteBuffer.allocate(16);
                  // 读事件由 ReadHandler 处理
                  sc.read(buffer, buffer, new ReadHandler(sc));
                  // 写事件由 WriteHandler 处理
                  sc.write(Charset.defaultCharset().encode("server hello!"), ByteBuffer.allocate(16), new WriteHandler(sc));
                  // 处理完第一个 accpet 时,需要再次调用 accept 方法来处理下一个 accept 事件
                  ssc.accept(null, this);
              }
              @Override
              public void failed(Throwable exc, Object attachment) {
                  exc.printStackTrace();
              }
          }
      }

      image.gif


      相关文章
      |
      27天前
      |
      存储 JSON 自然语言处理
      大模型应用开发-LangChain框架基础
      本文摘要: 文章系统介绍了大模型技术应用与开发的全流程,涵盖云端/本地模型部署、Prompt工程、LangChain框架及RAG项目实战。主要内容包括: 模型部署 阿里云百炼平台API接入与安全配置 Ollama本地模型部署方案 OpenAI兼容SDK的多平台调用方法 Prompt工程 Zero-shot/Few-shot提示技巧 金融文本分类/信息抽取实战案例 JSON数据结构处理与模板设计 LangChain框架 组件化架构:Models/Prompts/Memory/Vectorstores 链式调用
      |
      27天前
      |
      前端开发 Java Maven
      MinIO的预签名直传机制
      我们传统使用MinIo做OSS对象存储的应用方式往往都是在后端配置与MinIO的连接和文件上传下载的相关接口,然后我们在前端调用这些接口完成文件的上传下载机制,但是,当并发量过大,频繁访问会对后端的并发往往会对服务器造成极大的压力,大文件传输场景下,服务器被迫承担数据中转的角色,既消耗大量带宽资源,又形成单点性能瓶颈。这时,我们引入了MinIO的一种预签名机制。
      MinIO的预签名直传机制
      |
      27天前
      |
      Linux API 云计算
      零基础保姆级|阿里云计算巢+MacOS/Linux/Windows11部署OpenClaw 技能集成+大模型配置全流程
      2026年,AI自动化框架OpenClaw(原Clawdbot)凭借云端+本地双部署、多模型兼容与Skills插件化扩展能力,成为个人与团队实现复杂任务自动化的核心工具。阿里云计算巢提供OpenClaw官方一键部署方案,无需手动配置环境,5分钟即可完成云端部署;本地则支持MacOS、Linux、Windows11全系统部署,搭配阿里云千问、免费Coding Plan大模型API,再通过Skills扩展能力,可实现从信息查询、文件处理到流程自动化的全场景能力。
      988 15
      |
      26天前
      |
      人工智能 JavaScript Linux
      OpenClaw(Clawdbot)阿里云秒级部署图文教程|千问Qwen3-Max一键接入+常见问题解答
      2026年,OpenClaw(原Clawdbot)凭借轻量化架构、秒级部署能力与强大的大模型集成生态,成为搭建专属AI助手的首选工具。阿里云依托计算巢与轻量应用服务器,为OpenClaw提供官方专属部署模板,实现真正意义上的秒级部署,全程无需手动配置环境、无需处理复杂依赖,搭配阿里云千问Qwen3-Max大模型,可快速构建具备深度理解、长文本生成、复杂逻辑推理能力的AI机器人,满足个人与企业的智能交互、任务自动化需求。
      471 6
      |
      27天前
      |
      人工智能 Linux API
      OpenClaw多Agent协作系统实操:本地+阿里云部署与千问/Coding Plan API配置全指南
      2026年OpenClaw(原Clawdbot)推出的多Agent协作系统,彻底打破了单一AI智能体的能力边界,让多个AI Agent像人类团队一样实现智能分工、实时信息同步与灵活角色配置,可高效完成内容创作、软件开发、数据分析等复杂复合型任务。在实际落地过程中,开发者不仅需要掌握多Agent协作系统的基础使用逻辑,更需要完成OpenClaw在本地多系统(MacOS/Linux/Windows11)与阿里云的稳定部署,同时实现与阿里云千问大模型API、免费Coding Plan API的无缝对接,才能真正发挥多Agent协作的核心价值。本文将深度解析OpenClaw多Agent协作系统的核心
      879 10
      |
      27天前
      |
      存储 人工智能 NoSQL
      大模型应用开发3-LangChain4j实战
      本文介绍了LangChain4j框架的使用方法,主要包括以下内容:1. 基础配置:创建SpringBoot项目并配置OpenAI聊天模型;2. AIServices工具类:简化模型调用,支持流式和阻塞式两种调用方式;3. 会话记忆功能:实现多轮对话记忆,支持会话隔离和Redis持久化存储;4. RAG检索增强:通过向量数据库存储和检索专业领域知识,提升大模型回答质量;5. Tools工具:通过Function Calling机制实现业务功能调用。文章详细讲解了每个功能的实现步骤,包括代码示例和配置方法,帮助
      |
      27天前
      |
      人工智能 Linux API
      阿里云+本地全平台部署OpenClaw|iMessage集成+大模型千问/Coding Plan API+避坑指南
      2026年,AI自动化框架OpenClaw(原Clawdbot)凭借云端+本地双部署、多模型兼容与iMessage深度集成能力,成为连接苹果生态与AI能力的核心工具。阿里云提供轻量服务器、ECS、计算巢三种一键部署方案,本地支持MacOS、Linux、Windows11全系统运行,搭配阿里云千问大模型、免费Coding Plan API,可实现iMessage消息收发、自然语言交互、任务自动化执行,满足个人效率管理、移动AI助手、轻量业务开发等场景需求。
      254 14
      阿里云+本地全平台部署OpenClaw|iMessage集成+大模型千问/Coding Plan API+避坑指南
      |
      27天前
      |
      人工智能 Linux API
      阿里云+本地全平台部署OpenClaw|iMessage集成+千问/Coding Plan API+避坑指南
      2026年,AI自动化框架OpenClaw(原Clawdbot)凭借云端+本地双部署、多模型兼容与iMessage深度集成能力,成为连接苹果生态与AI能力的核心工具。阿里云提供轻量服务器、ECS、计算巢三种一键部署方案,本地支持MacOS、Linux、Windows11全系统运行,搭配阿里云千问大模型、免费Coding Plan API,可实现iMessage消息收发、自然语言交互、任务自动化执行,满足个人效率管理、移动AI助手、轻量业务开发等场景需求。
      276 19
      |
      27天前
      |
      存储 监控 前端开发
      大文件上传下载处理方案-断点续传,秒传,分片,合并
      本文介绍了大文件上传下载的断点续传技术方案。上传方面,通过前端将大文件分块(如5MB/块),后端使用MinIO存储分块并合并,实现断点续传和秒传功能。下载方面,采用Range请求分片下载,前端合并分片触发下载。技术要点包括:1)前端分块计算MD5;2)后端MinIO存储管理;3)分片校验与合并;4)进度监控和异常处理。该方案解决了大文件传输中断问题,提升用户体验,适用于视频等大文件传输场景,完整代码示例包含前后端实现。
      |
      16天前
      |
      存储 弹性计算 监控
      阿里云云服务器ECS实例介绍及选择建议
      阿里云ECS是安全可靠、弹性伸缩的云服务器服务,提供通用型、计算型、内存型等8类实例,适配Web、数据库、AI、大数据等场景。支持免费试用,兼顾性能与成本,助力企业降本增效。
      292 13

      热门文章

      最新文章

      下一篇
      开通oss服务