Netty-01-快速掌握Java NIO

news2024/11/18 16:47:51

文章目录

  • 一、从传统I/O到Java NIO
  • 二、NIO 三大组件
    • 1. Channel(通道)
      • 1.1. FileChannel
        • 1.1.1. 获取 FileChannel
        • 1.1.2. FileChannel 读取 文件
        • 1.1.3. FileChannel写⽂件
        • 1.1.4. 通道之前传输数据-transferFrom
        • 1.1.5. 通道之前传输数据-transferTo
      • 1.2. SocketChannel
      • 1.3. ServerSocketChannel
      • 1.4. DatagramChannel
    • 2. Buffer(缓冲区)
      • 2.1. ByteBuffer 正确使用姿势
      • 2.2. 分配空间
      • 2.3. 向 buffer 写入数据
      • 2.4. 从 buffer 读数据
      • 2.5. 常见方法
      • 2.6. 字符串与 ByteBuffer 相互转换
    • 3. Selector(选择器)
      • 1、绑定 channel 事件
      • 2、监听 channel 事件
      • 3、SelectionKey 选择键
      • 4、使用示例
  • 三、NIO 实现网络编程
    • 1. 服务端实现
    • 2. 客户端实现
  • 四、NIO 包中其他类
    • 1. Pipe
    • 2. FileLock
    • 3. Path
    • 4. Files

一、从传统I/O到Java NIO

Blocking IO也称为BIO,即同步阻塞IO。Java的io包基于流模型实现,提供了File、FileInputStream、FileOutputStream等输⼊输出流的功能。Java的io包下提供的流操作,交互⽅式是同步且阻塞的⽅式,在输⼊输出流的操作进⾏读、写动作完成之前,线程会⼀直阻塞。因此io包中对流的操作容易造成性能的瓶颈

而 NIO是一种基于缓冲区、非阻塞的 I/O 操作方式,首先Java NIO提供管道channel,同时自身提供了一个缓冲区buffer。比如读数据时,数据先进入缓冲区,然后再用 Channel 从缓冲区读出数据,数据处理后放入任意的介质。其次 NIO为了实现非阻塞设计了组件Selector,Selector的具体工作是负责网络连接、网络读和网络写事件的注册和监测。网络连接、网络读、网络写这三类网络事件先要注册到 Selector上,然后由Selector监控这三类网络事件的发生。当网络事件发生时线程再处理,如果没发生,那么线程也不会阻塞,这样就可以充分地利用 CPU 资源了。

由此可见,NIO 充分利用了 CPU,所以在性能上有明显的提升

NIO,全拼是 non-blocking io,即 非阻塞IO。(网络上也有很多将NIO的N翻译成New,这是因为NIO是在Java 1.4版本中新增加的一种I/O模型。)与传统的I/O模型不同,NIO提供了一种基于缓冲区(Buffer)和通道(Channel)的I/O操作方式。

NIO的主要优势在于它可以实现非阻塞式I/O操作,从而提高了系统的并发处理能力。在传统的I/O模型中,当一个线程在进行I/O操作时,其他线程会被阻塞,直到I/O操作完成。而在NIO模型中,线程可以继续执行其他的任务,而不必等待I/O操作完成。这种非阻塞式的I/O操作方式可以提高系统的吞吐量和响应速度,特别是在高并发的情况下。
(Netty-01-快速掌握Java NIO.assets/image-20230823193756735.png)]

二、NIO 三大组件

根据上面讲述的,我们知道 Java NIO 主要有三个模块:BufferChannelSelector

buffer好比是一个蓄水池(它可以存储数据,以便在需要时进行处理),channel就是水管(用于传递数据),selector管理着多个水管,并根据一定的条件选择数据传递给相应的蓄水池(Buffer)进行处理。

Channel 1
Channel 2
Buffer 1
Buffer 2
Selector

1. Channel(通道)

channel 有一点类似于 stream,它就是读写数据的双向通道,可以从 channel 将数据读入 buffer,也可以将 buffer 的数据写入 channel,而之前的 stream 要么是输入,要么是输出,channel 比 stream 更为底层。

channel
buffer

Channel即通道,表示打开IO设备的连接,⽐如打开到⽂件、Socket套接字的连接。在使⽤NIO时,必须要获取⽤于连接IO设备的通道以及⽤于容纳数据的缓冲区。通过操作缓冲区,实现对数据的处理。也就是说数据是保存在buffer缓冲区中的,需要通过Channel来操作缓冲区中的数据。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0t8y1Kag-1692957566929)(Netty-01-快速掌握Java NIO.assets/image-20230822200734031.png)]

Channel的主要实现类有以下⼏个:

  • FileChannel:读写⽂件的通道
  • SocketChannel:读写TCP⽹络数据的通道
  • ServerSocketChannel:像web服务器⼀样,监听新进来的TCP连接,为连接创建SocketChannel
  • DatagramChannel:读写UDP⽹络数据的通道

1.1. FileChannel

FileChannel是Java NIO中用于对文件进行读写、映射和操作的通道(Channel)。它是连接应用程序与文件之间的桥梁,提供了更高效和灵活的文件操作方式。

注意:FileChannel只能工作在阻塞模式下

⽂件通道是连接到⽂件的可搜索字节通道。它在其⽂件中有⼀个当前位置,可以查询和修改。

⽂件本身包含可变⻓度的字节序列,可以读取和写⼊,并且可以查询其当前⼤⼩。当写⼊的字节超过其当前⼤⼩时,⽂件的⼤⼩增加;⽂件被截断时,其⼤⼩会减⼩。⽂件还可能具有⼀些相关联的元数据,如访问权限、内容类型和上次修改时间;此类不定义元数据访问的⽅法。

多个并发线程使⽤⽂件通道是安全的。根据通道接⼝的指定,可以随时调⽤close⽅法。在任何给定时间,只有⼀个涉及通道位置或可以改变其⽂件⼤⼩的操作正在进⾏;在第⼀个操作仍在进⾏时尝试发起第⼆个这样的操作将被阻⽌,直到第⼀个操作完成。其他⾏动,特别是采取明确⽴场的⾏动,可以同时进⾏;它们是否真的这样做取决于底层实现,因此没有具体说明。

1.1.1. 获取 FileChannel

不能直接打开 FileChannel,必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法

  • 通过 FileInputStream 获取的 channel 只能读
  • 通过 FileOutputStream 获取的 channel 只能写
  • 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定

1.1.2. FileChannel 读取 文件

会从 channel 读取数据填充 ByteBuffer,返回值表示读到了多少字节,-1 表示到达了文件的末尾

int readBytes = channel.read(buffer);

如下~

public class FileChannelDemo01 {
    public static void main(String[] args) throws IOException {
        // 创建随机访问流
        RandomAccessFile file = new RandomAccessFile("1.txt", "rw");
        // 获取FileChannel
        FileChannel fileChannel = file.getChannel();
        // 创建Buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 读取数据到buffer中
        while ((fileChannel.read(buffer)) != -1) {
            // 将写模式切换成读模式
            buffer.flip();
            while (buffer.hasRemaining()) {
                System.out.print((char) buffer.get());
            }
            buffer.clear();
        }
        file.close();
        System.out.println("\n" + "读取完毕");
    }
}

1.1.3. FileChannel写⽂件

写入的正确姿势如下,在 while 中调用 channel.write 是因为 write 方法并不能保证一次将 buffer 中的内容全部写入 channel

ByteBuffer buffer = ...;
buffer.put(...); // 存入数据
buffer.flip();   // 切换读模式

while(buffer.hasRemaining()) {
    channel.write(buffer);
}

如下~

public class FileChannelDemo02 {
    public static void main(String[] args) throws IOException {
        // 创建随机访问流
        RandomAccessFile file = new RandomAccessFile("2.txt", "rw");
        // 获取FileChannel
        FileChannel fileChannel = file.getChannel();
        // 创建Buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        String data = "hello file channel";
        // 存入buffer
        buffer.put(data.getBytes(StandardCharsets.UTF_8));
        // 读写模式转换
        buffer.flip();
        // 把buffer中的数据写入文件
        fileChannel.write(buffer);
        // 关闭
        file.close();
    }
}

1.1.4. 通道之前传输数据-transferFrom

public class FileChannelDemo03 {
    public static void main(String[] args) throws IOException {
        // 获取两个文件的FileChannel
        RandomAccessFile srcFile = new RandomAccessFile("1.txt", "rw");
        FileChannel srcFileChannel = srcFile.getChannel();
        RandomAccessFile descFile = new RandomAccessFile("2.txt", "rw");
        FileChannel descFileChannel = descFile.getChannel();
        // src->dest
        descFileChannel.transferFrom(srcFileChannel, 0, srcFileChannel.size());
        // 关闭
        srcFileChannel.close();
        descFileChannel.close();
        System.out.println("传输完成");
    }
}

1.1.5. 通道之前传输数据-transferTo

效率高,底层会利用操作系统的零拷贝进行优化,有个上限是 2G

public class FileChannelDemo04 {
    public static void main(String[] args) throws IOException {
        // 获取两个文件的FileChannel
        RandomAccessFile srcFile = new RandomAccessFile("1.txt", "rw");
        FileChannel srcFileChannel = srcFile.getChannel();
        RandomAccessFile descFile = new RandomAccessFile("2.txt", "rw");
        FileChannel descFileChannel = descFile.getChannel();
        // src->dest
        srcFileChannel.transferTo(0, srcFileChannel.size(), descFileChannel);
        // 关闭
        srcFileChannel.close();
        descFileChannel.close();
        System.out.println("传输完成");
    }
}

1.2. SocketChannel

SocketChannel是连接到TCP网络套接字的通道,主要用于处理网络IO操作。它代表了客户端与服务器之间的通信通道,通过Socket套接字实现双向的数据传输。SocketChannel可以被选择器(Selector)多路复用,使得单个线程可以同时管理多个通道的事件。它基于TCP连接传输数据,提供高性能、可靠的网络通信功能。SocketChannel在Java NIO中扮演着重要的角色,被广泛应用于构建网络客户端和其他需要进行网络IO操作的应用程序。

注意

  • 不能在已经存在的Socket上再创建SocketChannel
  • SocketChannel需要指明关联的服务器地址及端⼝后才能使⽤
  • 未进⾏连接的SocketChannel进⾏IO操作时将抛出NotYetConnectedException异常
  • SocketChannel⽀持阻塞和⾮阻塞两种模式
  • SocketChannel⽀持异步关闭
  • SocketChannel⽀持设定参数
    • SO_SNDBUF:Socket发送缓冲区的⼤⼩
    • SO_RCVBUF:Socket接受缓冲区的⼤⼩
    • SO_KEEPALIVE:保活连接
    • SO_REUSEADDR:复⽤地址
    • SO_LINGER:有数据传输时延缓关闭Channel (只在⾮阻塞模式下使⽤)
    • TCP_NODELAY:禁⽤Nagle算法

可以通过以下方式创建SocketChannel:

SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8080));
// 或者
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));

SocketChannel⽀持阻塞和⾮阻塞两种模式

// 设置非阻塞
socketChannel.configureBlocking(false);

读取操作:

public class SocketChannelDemo02 {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
        socketChannel.configureBlocking(true);
        if (socketChannel.isConnectionPending()) {
            socketChannel.finishConnect();
        }
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        socketChannel.read(byteBuffer);
        System.out.println("读取到的内容:" + new String(byteBuffer.array()));
        socketChannel.close();
    }
}

通过以下方法进行判断连接状态~

  • socketChannel.isOpen(): 判断SocketChannel是否为open状态

  • socketChannel.isConnected(): 判断SocketChannel是否已连接

  • socketChannel.isConnectionPending(): 判断SocketChannel是否正在进⾏连接

  • socketChannel.finishConnect(): 完成连接,如果此通道已连接,则此⽅法将不会阻塞,并将⽴即返回true。如果此通道处于⾮阻塞模式,则如果连接过程尚未完成,则此⽅法将返回false。如果此通道处于阻塞模式,则此⽅法将阻塞,直到连接完成或失败,并且将始终返回true或抛出⼀个描述失败的检查异常。

1.3. ServerSocketChannel

ServerSocketChannel是⼀个基于通道的Socket监听器,能够实现⾮阻塞模式。ServerSocketChannel的主要作⽤是⽤来监听端⼝的连接,来创建SocketChannel。也就是说,可以调⽤ServerSocketChannel的accept⽅法,来创建SocketChannel对象。

public class ServerSocketChannelDemo01 {
    public static void main(String[] args) throws IOException {
        ByteBuffer byteBuffer = ByteBuffer.wrap("Hello server socket".getBytes());
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));
        serverSocketChannel.configureBlocking(false);
        while (true) {
            System.out.println("等待连接");
            SocketChannel socketChannel = serverSocketChannel.accept();
            if (socketChannel != null) {
                System.out.println("有新的连接:" + socketChannel.socket().getRemoteSocketAddress());
                socketChannel.write(byteBuffer);
            } else {
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

在这里插入图片描述

1.4. DatagramChannel

DatagramChannel对象关联着⼀个DatagramSocket对象。DatagramChannel基于UDP⽆连接协议,每个数据报都是⼀个⾃包含的实体,拥有它⾃⼰的⽬的地址及数据负载。DatagramChannel可以发送单独的数据报给不同的⽬的地,同样也可以接受来⾃于任意地址的数据报。

接收方

public class DatagramChannelReceive {
    public static void main(String[] args) throws IOException {
        // 获得channel
        DatagramChannel datagramChannel = DatagramChannel.open();
        // 绑定端口
        datagramChannel.bind(new InetSocketAddress(8080));
        // 接收消息
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        while (true) {
            SocketAddress address = datagramChannel.receive(byteBuffer);
            if (address == null) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            } else {
                byteBuffer.flip();
                System.out.println(address.toString() + "发来消息:" + new String(byteBuffer.array()));
                byteBuffer.clear();
            }
        }
    }
}

发送方

public class DatagramChannelSend {
    public static void main(String[] args) throws IOException {
        DatagramChannel datagramChannel = DatagramChannel.open();
        InetSocketAddress socketAddress = new InetSocketAddress("127.0.0.1", 8080);
        ByteBuffer byteBuffer = ByteBuffer.wrap("Hello datagram channel".getBytes());
        datagramChannel.send(byteBuffer, socketAddress);
    }
}

在这里插入图片描述

2. Buffer(缓冲区)

Buffer 在读写的过程中起到的是数据缓冲的作用。首先,我们可以通过内存把数据写入我们事先定义的一个 Buffer 中(当然,Buffer 也是内存的一部分),然后把 Buffer 的数据读出来再写入磁盘保存起来,或者通过网卡发送出去。也就是说,Buffer 是用来读写的缓冲区。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MXUTdTHD-1692957715232)(Netty-01-快速掌握Java NIO.assets/image-20230605202258742.png)]

Buffer(缓冲区)本质上是一个内存块,既可以写入数据,也可以读取数据。代表缓冲区的 Buffer 类是一个抽象类,位于 java.nio 包中。类与普通的内存块(Java 数组)不同的是: Buffer 类对象提供了一组比较有效的方法,用来进行写入和读取。

Buffer 类同时也是一个非线程安全类,对应于 Java 的主要数据类型,在 NIO 中有 8 种缓冲区类,分别如下:ByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、 ShortBuffer 和 MappedByteBuffer。

其中,我们用得最普遍的是 ByteBuffer,其他数据类型的 Buffer 很少用。

ByteBuffer 内部维护的是一个字节数组(byte array),内部维护了以下几个重要参数:

  • capacity:表示ByteBuffer内部缓冲区的容量大小,即它最多能够存放多少字节数据。
  • limit:表示ByteBuffer中当前有效数据的大小,即缓冲区中存放的字节数据的数量。
    • 写数据时:limit与capacity相同
    • 读数据时:limit表示可读的数据位置,因此在上次写操作后需要通过flip⽅法,将position值赋给limit。
  • position:表示当前读写位置的索引,即下一个要读写的字节数据的位置。
  • mark:一个标记,用于记录某一个位置,以便稍后回到该位置。

2.1. ByteBuffer 正确使用姿势

  1. 向 buffer 写入数据,例如调用 channel.read(buffer)
  2. 调用 flip() 切换至读模式
  3. 从 buffer 读取数据,例如调用 buffer.get()
  4. 调用 clear() 或 compact() 切换至写模式
  5. 重复 1~4 步骤

2.2. 分配空间

// class java.nio.HeapByteBuffer —— java堆内存,读写效率低,受到GC影响
// 数据的拷贝路径是这样的:JVM 空间-->操作系统控制的直接内存-->磁盘
ByteBuffer heapByteBuffer = ByteBuffer.allocate(10);
// class java.nio.DirectByteBuffer —— 直接内存,读写效率高(少了一次数据拷贝)
// 操作系统控制的直接内存-->磁盘
ByteBuffer directByteBuffer = ByteBuffer.allocateDirect(10);

下图展示了 Buffer 分配在直接内存的功能和作用:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CyAVxBmX-1692957715233)(Netty-01-快速掌握Java NIO.assets/image-20230606100908123.png)]

2.3. 向 buffer 写入数据

有三种办法:

  • 调用 buffer 的 put(数据) 方法,将数据存⼊到buffer,此时position随之变化。
  • 调用 buffer 的 wrap(数据) 方法,将数据存⼊数据并返回buffer,此时position为0,limit为数据的⻓度
  • 调用 channel 的 channel.read(buffer) 方法:将数据读⼊到buffer中。
// 调用 buffer 自己的 put方法
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
byteBuffer.put((byte) 127);
// 调用 buffer 的 wrap(数据) 方法,将数据存⼊数据并返回buffer
ByteBuffer byteBuffer = ByteBuffer.wrap("hello".getBytes());
// 调用 channel 的 read方法
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
RandomAccessFile file = new RandomAccessFile("1.txt", "rw");
FileChannel fileChannel = file.getChannel();
fileChannel.read(byteBuffer);

2.4. 从 buffer 读数据

从Buffer中读取数据有以下⼏种⽅式:

  • 调用 buffer 的 get相关的⽅法,获得当前position或指定position的数据
  • 调用 buffer 的 array() 方法,返回整个数组内容
  • 调用 channel 的 channel.write(buffer) 方法,使⽤channel获得buffer中的内容并写⼊到指定⽬标
ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[]{'A', 'B', 'C', 'D', 'E'});
// 获得当前position位置
System.out.println(Character.toChars(byteBuffer.get()));
// 返回整个数组内容
System.out.println(new String(byteBuffer.array()));
// channel获得buffer中的内容并写⼊到指定⽬标
RandomAccessFile file = new RandomAccessFile("1.txt", "rw");
FileChannel fileChannel = file.getChannel();
fileChannel.write(byteBuffer);
byteBuffer.clear();

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bCWtGOCg-1692957766784)(Netty-01-快速掌握Java NIO.assets/image-20230823201850572.png)]

大家可以思考一下为什么最终写入文件的内容是"BCDE",而不是"ABCDE"呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cR7umfZE-1692957766784)(Netty-01-快速掌握Java NIO.assets/image-20230823202046517.png)]

如果不需要移动指针,可以使用 get(int i) 方法获取索引 i 的内容,它不会移动读指针!

2.5. 常见方法

  1. rewind 从头开始
public final Buffer rewind() {
    position = 0;
    mark = -1;
    return this;
}

我们查看该方法将position至为0,从而达到从头开始读的效果。

  1. mark 标记、reset 回到标记处

mark 是在读取时,做一个标记,即使 position 改变,只要调用 reset 就能回到 mark 的位置

注意:rewind 和 flip 都会清除 mark 位置

/**
 * Sets this buffer's mark at its position.
 *
 * @return  This buffer
 */
public final Buffer mark() {
    mark = position;
    return this;
}
/**
 * Resets this buffer's position to the previously-marked position.
 *
 * <p> Invoking this method neither changes nor discards the mark's
 * value. </p>
 *
 * @return  This buffer
 *
 * @throws  InvalidMarkException
 *          If the mark has not been set
 */
public final Buffer reset() {
    int m = mark;
    if (m < 0)
        throw new InvalidMarkException();
    position = m;
    return this;
}

mark 函数将当前当前读写位置的索引赋值给mack,在reset 函数中将当前读写位置设置为mack。从而实现标记和恢复标记位。

2.6. 字符串与 ByteBuffer 相互转换

ByteBuffer buffer1 = StandardCharsets.UTF_8.encode("你好");
ByteBuffer buffer2 = Charset.forName("utf-8").encode("你好");
CharBuffer buffer3 = StandardCharsets.UTF_8.decode(buffer1);
System.out.println(buffer3.getClass());
System.out.println(buffer3.toString());

3. Selector(选择器)

Selector选择器,也可以称为多路复⽤器。它是Java NIO的核⼼组件之⼀,⽤于检查⼀个或多个Channel的状态是否处于可读、可写、可连接、可接收等。通过⼀个Selector选择器管理多个Channel,可以实现⼀个线程管理多个Channel对应的⽹络连接。使⽤单线程管理多个Channel可以避免多线程的线程上下⽂切换带来的额外开销。

selector 单从字面意思不好理解,需要结合服务器的设计演化来理解它的用途

1、多线程版本,它具有以下缺点⚠️

  • 线程上下文切换成本高
  • 只适合连接数少的场景
多线程版
socket1
thread
socket2
thread
socket3
thread

2、线程池版本,它具有以下缺点⚠️

  • 阻塞模式下,线程仅能处理一个 socket 连接
  • 仅适合短连接场景
线程池版
socket1
thread
socket2
thread
socket3
socket4

3、selector版本,selector 的作用就是配合一个线程来管理多个 channel,获取这些 channel 上发生的事件,这些 channel 工作在非阻塞模式下,不会让线程吊死在一个 channel 上。适合连接数特别多,但流量低的场景(low traffic)

调用 selector 的 select() 会阻塞直到 channel 发生了读写就绪事件,这些事件发生,select 方法就会返回这些事件交给 thread 来处理

selector 版
selector
thread
channel
channel
channel

注意

只有SelectableChannel才能被Selector管理,⽐如所有的SocketChannel。⽽FileChannel并没有继承SelectableChannel,因此不能被Selector管理。

1、绑定 channel 事件

也称之为注册事件,绑定的事件 selector 才会关心。⼀个Channel可以注册到多个Selector上,但在某⼀个Selector上只能注册⼀次。注册时需要告知Selector,Selector需要对通道的哪个操作感兴趣。

channel.configureBlocking(false);
SelectionKey key = channel.register(selector, 绑定事件);

注意

  • channel必须处于⾮阻塞模式才能注册到Selector上。 FileChannel并没有继承SelectableChannel,没有非阻塞模式,因此不能配合 selector 一起使用
  • 绑定的事件类型操作枚举类java.nio.channels.SelectionKey
    • OP_READ : 可读,数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况
    • OP_WRITE :可写,数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况
    • OP_CONNECT :可连接,客户端连接成功时触发
    • OP_ACCEPT :可接收,服务器端成功接受连接时触发

⽐如channel调⽤register⽅法进⾏注册到Selector,并告知Selector对哪个操作感兴趣

channel.register(selector, SelectionKey.OP_READ);

也可以同时注册多个操作

channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);

选择器会查询每个⼀个channel的操作事件,如果是该channel注册的操作已就绪,则进⾏响应。注意,这⾥channel的操作指的是channel完成某个操作的条件,表示该channel对于该操作已处于就绪状态。⽐如ServerSocketChannel已准备好接收新的连接,那么它注册的 SelectionKey.OP_ACCEPT 操作就处于就绪状态。⼜⽐如SocketChannel已准备好去连接Server服务器,那么它注册的SelectionKey.OP_CONNECT 操作就处于就绪状态。于是Selector就可以触发之后的动作。

2、监听 channel 事件

可以通过下面三种方法来监听是否有事件发生,方法的返回值代表有多少 channel 发生了事件

方法1,阻塞直到绑定事件发生

int count = selector.select();

方法2,阻塞直到绑定事件发生,或是超时(时间单位为 ms)

int count = selector.select(long timeout);

方法3,不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件

int count = selector.selectNow();

select 何时不阻塞呢?

  • 事件发生时
    • 客户端发起连接请求,会触发 accept 事件
    • 客户端发送数据过来,客户端正常、异常关闭时,都会触发 read 事件,另外如果发送的数据大于 buffer 缓冲区,会触发多次读取事件
    • channel 可写,会触发 write 事件
    • 在 linux 下 nio bug 发生时
  • 调用 selector.wakeup()
  • 调用 selector.close()
  • selector 所在线程 interrupt

3、SelectionKey 选择键

SelectionKey封装了channel和注册的操作。当selector调用select()方法时,会轮训所有注册在它身上的channel,查看是否有处于某个操作(已注册到selector上的)就绪状态的channel,把这些channel放入到selectionKey的集合中。

4、使用示例

首先,通过Selector的open⽅法创建Selector对象。

// 创建Selector
Selector selector = Selector.open();

其次,将Channel注册到Selector上

public class SelectorDemo01 {
    public static void main(String[] args) throws IOException {
        // 创建Selector
        Selector selector = Selector.open();
        // 创建ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8080));
        // 注册channel并指明注册的操作
        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    }
}

Selector通过调⽤select⽅法轮询已就绪的通道操作。select⽅法是阻塞的,直到⾄少有⼀个通道的注册操作已就绪。当完成select⽅法调⽤后,被选中的已就绪的所有channel通过Selector的selectedKeys()⽅法获得,该⽅法获得的是⼀个SelectionKey集合,其中每⼀个SelectionKey都表示⼀个Channel。于是可以根据SelectionKey的注册操作来做具体的业务处理。

public class SelectorDemo01 {
    public static void main(String[] args) throws IOException {
        // 创建Selector
        Selector selector = Selector.open();
        // 创建ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.configureBlocking(false);
        serverSocketChannel.bind(new InetSocketAddress(8080));
        // 注册channel并指明注册的操作
        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            // 阻塞等待某个操作就绪状态的channel
            selector.select();
            // 获得这次阻塞等待发生操作的多个channel集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            // 获取集合迭代器
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                if (key.isAcceptable()) {
                    // 处理连接状态的业务
                } else if (key.isConnectable()) {
                    // 处理接受状态的业务
                } else if (key.isReadable()) {
                    // 处理读状态的业务
                } else if (key.isWritable()) {
                    // 处理写状态的业务
                }
                // 保证下次channel就绪可以再被选中
                iterator.remove();
            }
        }
    }
}

为何要 iterator.remove()?

因为 select 在事件发生后,就会将相关的 key 放入 selectedKeys 集合,但不会在处理完后从 selectedKeys 集合中移除,需要我们自己编码删除。例如

  • 第一次触发了 ssckey 上的 accept 事件,没有移除 ssckey
  • 第二次触发了 sckey 上的 read 事件,但这时 selectedKeys 中还有上次的 ssckey ,在处理时因为没有真正的 serverSocket 连上了,就会导致空指针异常

事件发生后能否不处理?

事件发生后,要么处理,要么取消(cancel),不能什么都不做,否则下次该事件仍会触发,这是因为 nio 底层使用的是水平触发。

cancel 会取消注册在 selector 上的 channel,并从 keys 集合中删除 key 后续不会再监听事件

三、NIO 实现网络编程

客户端1 服务器 1.发送连接请求,并注册OP_CONNECT事件 2.处理客户端连接事件,建立起了服务端和客户端的连接通道并注册OP_READ事件 3.OP_CONNECT事件触发,向客户端发送hello, client。并注册OP_READ事件 4.socketChannel的OP_READ事件触发,读取数据至缓冲区 5.向客户端发送数据 6.OP_READ事件触发,读取数据至缓冲区 客户端1 服务器

1. 服务端实现

/**
 * Description: NIO服务端
 *
 * @author LinHuiBa-YanAn
 * @date 2023/5/9 14:26
 */
public class NIOServer {
    public static void main(String[] args) throws IOException {
        // 创建服务端的Socket通道,ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 设置为非阻塞
        serverSocketChannel.configureBlocking(false);
        // 设置端口号为9090
        serverSocketChannel.socket().bind(new InetSocketAddress(8080));
        // 创建Selector多路复用器
        Selector selector = Selector.open();
        // 把ServerSocketChannel注册到Selector上,并且监听客户端的连接时间操作
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        while (true) {
            System.out.println("等待事件发生...");
            // 轮训监听所有注册到selector上的channel的SelectionKey,此方法是阻塞的
            int count = selector.select();
            System.out.print("触发事件:");
            // 获得所有发生事件的channel的key,遍历key的集合并处理每一个key的channel
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                handle(key);
                // 删除本次处理的key,防止重复处理
                iterator.remove();
            }
        }
    }

    public static void handle(SelectionKey selectionKey) throws IOException {
        // 判断channel发生的是什么事件
        if (selectionKey.isAcceptable()) {
            System.out.println("客户端请求连接");
            // 服务端处理客户端的连接,得到ServerSocketChannel,代表着服务端
            ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey.channel();
            // 服务端处理连接请求,建立SocketChannel(建立起了服务端和客户端的连接通道)。该方法是阻塞的,但该阻塞是在连接事件发生后马上执行的,相当于是非阻塞。且接收完后不需要阻塞等待客户端的读写操作
            SocketChannel socketChannel = serverSocketChannel.accept();
            // 把socketChannel设置成非阻塞
            socketChannel.configureBlocking(false);
            // 把socketChannel注册读事件到Selector上,当读时间发生时(针对于服务端),触发事件
            socketChannel.register(selectionKey.selector(), SelectionKey.OP_READ);
        } else if (selectionKey.isReadable()) {
            System.out.println("客户端向服务端发送数据");
            // 获取服务端和客户端之间的通道
            SocketChannel socketChannel = (SocketChannel) selectionKey.channel();
            // 创建Buffer
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // NIO非阻塞的体现,read本身就是非阻塞的
            int len = socketChannel.read(buffer);
            if (len != -1) {
                System.out.println("读取客户端的数据:" + new String(buffer.array(), 0, len));
            }
            // 服务端返回数据给客户端
            ByteBuffer byteBuffer = ByteBuffer.wrap("hello nio".getBytes(StandardCharsets.UTF_8));
            socketChannel.write(byteBuffer);
            System.out.println("向客户端发送数据:" + new String(buffer.array()));
            // 监听下一次事件,读或者写
            selectionKey.interestOps(SelectionKey.OP_READ);
        }
    }
}

2. 客户端实现

/**
 * Description: NIO客户端1
 *
 * @author LinHuiBa-YanAn
 * @date 2023/5/10 19:30
 */
public class NIOClient1 {
    public static void main(String[] args) throws IOException {
        // 获得Channel通道
        SocketChannel socketChannel = SocketChannel.open();
        // 设置成非阻塞
        socketChannel.configureBlocking(false);
        // 获得多路复用器 Selector
        Selector selector = Selector.open();
        // 将channel注册到Selector上,并且监听连接事件
        socketChannel.register(selector, SelectionKey.OP_CONNECT);
        // 客户端连接服务器,需要在监听方法中调用channel的finishConnect()才能完成连接
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 8080));
        System.out.println("向客户端请求链接");
        // 轮询访问Selector
        while (true) {
            // 阻塞等待,实际上selector只对应着客户端这一个channel
            int count = selector.select();
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            // 遍历所有的事件
            while (iterator.hasNext()) {
                handle(iterator.next());
                // 删除本次处理的key,防止重复处理
                iterator.remove();
            }
        }
    }

    private static void handle(SelectionKey key) throws IOException {
        if (key.isConnectable()) {
            // 如果是连接
            SocketChannel channel = (SocketChannel) key.channel();
            // 如果是正在连接,则完成连接
            if (channel.isConnectionPending()) {
                channel.finishConnect();
                // 设置成非阻塞
                channel.configureBlocking(false);
                // 给服务端发送消息
                ByteBuffer buffer = ByteBuffer.wrap("hello, client".getBytes(StandardCharsets.UTF_8));
                // 缓存区中的数据写到通道里
                channel.write(buffer);
                System.out.println("向客户端发送数据:" + new String(buffer.array()));
                // 监听读事件,可以获取服务器返回的数据
                channel.register(key.selector(), SelectionKey.OP_READ);
            }
        } else if (key.isReadable()) {
            // 读服务端返回的数据
            SocketChannel channel = (SocketChannel) key.channel();
            // 创建缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // 读取缓冲区中的数据
            int len = channel.read(buffer);
            if (len != -1) {
                System.out.println("服务端返回的数据:" + new String(buffer.array(), 0, len));
            }
        }
    }
}

服务端运行结果~

等待事件发生...
触发事件:客户端请求连接
等待事件发生...
触发事件:客户端向服务端发送数据
读取客户端的数据:hello, client
向客户端发送数据:hello, client
等待事件发生...

客户端运行结果~

向客户端请求链接
向客户端发送数据:hello, client
服务端返回的数据:hello nio

四、NIO 包中其他类

1. Pipe

Java NIO包中提供了Pipe类,⽤来实现两个线程之间的单向数据连接。Pipe包含了一个输入流和一个输出流,其中,输出流被连接到输入流的另一端,通过输出流写入的数据可以在输入流中读取到。Pipe的输入流和输出流是线程安全的,多个线程可以同时读写,因此可以用来实现线程间的数据传输和同步。分别是Sink Channel和Source Channel

  • Sink Channel:线程将数据写⼊到Sink Channel
  • Source Channel:线程从Source Channel中获取数据

具体的两个线程通过Pipe管道实现数据传输的例⼦如下:

public class PipeDemo {
    public static void main(String[] args) throws IOException {
        // 获取Pipe
        Pipe pipe = Pipe.open();
        // 创建写数据的线程
        Thread1 t1 = new Thread1(pipe);
        // 创建读数据的线程
        Thread2 t2 = new Thread2(pipe);
        t1.start();
        t2.start();
    }
}

/**
 * 向pipe写数据
 */
class Thread1 extends Thread {
    private Pipe pipe;

    public Thread1(Pipe pipe) {
        this.pipe = pipe;
    }

    /**
     * 向pipe写数据
     */
    @Override
    public void run() {
        Pipe.SinkChannel sinkChannel = pipe.sink();
        try {
            System.out.println(Thread.currentThread().getName()+":写数据");
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            buffer.put("hello pipe".getBytes());
            buffer.flip();
            sinkChannel.write(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                sinkChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

/**
 * 从pipe读数据
 */
class Thread2 extends Thread {
    private Pipe pipe;

    public Thread2(Pipe pipe) {
        this.pipe = pipe;
    }

    @Override
    public void run() {
        Pipe.SourceChannel sourceChannel = pipe.source();
        try {
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int len = 0;
            while ((len = sourceChannel.read(buffer))> 0) {
                buffer.flip();
                System.out.println(Thread.currentThread().getName()+":"+new String(buffer.array(), 0, len));
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                sourceChannel.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

2. FileLock

FileLock表示⽂件锁,通过JVM进程对要操作的⽂件上锁,在同⼀时间只允许⼀个进程访问上锁的⽂件。FileLock⽂件锁是进程级别,在同⼀个进程中的多个线程可以同时对⽂件进⾏操作。

FileLock⽂件锁⼜分成了排它锁和共享锁。

  • 排它锁,只有获得锁的进程能读写文件
//获得排它锁⽅式⼀:阻塞⽅式获得锁,如果锁被其他进程持有则阻塞
FileLock fileLock = fisChannel.lock();
//获得排它锁⽅式⼆:参数1:锁⽂件的具体哪⼀部分内容的起始位置,参数2:⻓度,参数3:是否共享
fisChannel.lock(0, file.length(), false);
//获得排它锁⽅式三:⾮阻塞的⽅式获得锁,如果锁被其他进程持有则直接返回null,不阻塞
FileLock fileLock = fisChannel.tryLock();
//获得排它锁⽅式四:⾮阻塞的⽅式获得锁
FileLock fileLock = fisChannel.tryLock(0, file.length(), false);
  • 共享锁,获得锁的线程可以读文件,但不能写文件
//获得共享锁方式一,参数1:锁⽂件的具体哪⼀部分内容的起始位置,参数2:⻓度,参数3:是否共享
fisChannel.lock(0,file.length(),true)
//获得共享锁方式二:⾮阻塞的⽅式获得锁,如果锁被其他进程持有则直接返回null,不阻塞
FileLock fileLock = fisChannel.tryLock(0,file.length(),true);

示例

首先,进程一获得锁后阻塞

public class FileLockDemo1 {
    public static void main(String[] args) throws IOException, InterruptedException {
        File file = new File("1.txt");
        FileOutputStream fis = new FileOutputStream(file);
        FileChannel fisChannel = fis.getChannel();
        // 获得排他锁
        FileLock fileLock = fisChannel.lock(0, file.length(), false);
        fisChannel.write(ByteBuffer.wrap("hello,lock!进程1".getBytes()));
        Thread.sleep(5000);
        fileLock.release();
    }
}

然后,编写进程二尝试获得锁并写数据

public class FileLockDemo2 {
    public static void main(String[] args) throws IOException {
        File file = new File("1.txt");
        FileOutputStream fis = new FileOutputStream(file);
        FileChannel fisChannel = fis.getChannel();
        FileLock fileLock = fisChannel.lock();
        fisChannel.write(ByteBuffer.wrap("hello,lock!进程2".getBytes()));
        System.out.println("写入完毕");
        fileLock.close();
        fisChannel.close();
    }
}

3. Path

Path类在JDK7中已经加⼊到NIO包内,Path表示⽂件系统中的路径,和java的io包下的File类⼗分相似。

Path可以表示绝对路径,即盘符在内的完整路径。Path也可以表示相对路径,即相对于其他路径的⽂件或⽬录的路径。

public class PathDemo {
    public static void main(String[] args) {
        //创建相对路径
        Path path = Paths.get("1.txt");
        //判断是否是绝对路径
        System.out.println(path.isAbsolute());
        Path path1 = Paths.get("/Users/gwh/Documents/yanAn/学习资料/Netty/code/io-demo/1.txt");
        System.out.println(path1.isAbsolute());
        //创建Path的另⼀种⽅式
        Path path2 = Paths.get("/Users/gwh/Documents/yanAn/学习资料/Netty/code/io-demo/", "1.txt");
        System.out.println(path2.isAbsolute());
        //获得父文件夹路径
        System.out.println(path2.getParent());
        //获得文件名
        System.out.println(path2.getFileName());
    }
}

4. Files

NIO包中的Files类提供了操作⽂件及⽬录的⼀些常⽤⽅法。Files需要和Path⼀起使⽤。

1、创建⽬录的⽅法: createDirectory(),根据Path对象创建⼀个新⽬录

try {
    Path path = Paths.get("myDir");
    // 创建目录
    Files.createDirectory(path);
} catch (IOException e) {
    e.printStackTrace();
}

2、拷贝文件 copy()

实现拷⻉⽂件的功能,可以通过可选的参数实现⽂件的覆盖。

try {
    Path srcPath = Paths.get("1.txt");
    Path destPath = Paths.get("3.txt");
    Files.copy(srcPath, destPath, StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
    e.printStackTrace();
}

3、删除文件 delete()

try {
   Path path = Paths.get("11.txt");
   Files.delete(path);
} catch (IOException e) {
	e.printStackTrace();
}

4、遍历⽬录 walkFileTree()

通过 walkFileTree(Path path,FileVisitor fv) ⽅法可以实现⽬录的遍历,通过提供FileVisitor接⼝的实现类对象来告知遍历⽂件的具体措施。其中通过返回的FileVisitResult告知遍历每⼀个⽂件时的具体下⼀步动作是什么,⽐如:继续(CONTINUE)、终⽌(TERMINATE)、跳过同级(SKIP_SIBLING)、跳过⼦级(SKIP_SUBTREE)。

Path path = Files.walkFileTree(Paths.get("/Users/gwh/Documents/yanAn/hgw/大学"), new SimpleFileVisitor<Path>() {

    /**
     * 访问目录前
     * @param dir
     * @param attrs
     * @return
     * @throws IOException
     */
    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
        System.out.println("访问文件夹:" + dir);
        return super.preVisitDirectory(dir, attrs);
    }

    /**
     * 访问文件
     * @param file
     * @param attrs
     * @return
     * @throws IOException
     */
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
        System.out.println("访问文件:" + file);
        return super.visitFile(file, attrs);
    }
});

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/928318.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MQTT Windows 安装及开机自启

一&#xff1a;搭建服务 下载地址&#xff1a; https://www.emqx.io/zh/downloads?osWindows 使用指南&#xff1a; https://www.emqx.io/docs/zh/v5.1/getting-started/getting-started.html#%E5%90%AF%E5%8A%A8-emqx 下载解压&#xff1a; 以管理员打开CMD&#xff0c;进…

c语言练习题28:杨氏矩阵

杨氏矩阵 从左到右增加 从上到下增加 思路&#xff1a; 代码&#xff1a; #include<stdio.h> int findNum(int(*arr)[3], int x, int y, int k) {int i 0;int j y - 1;while (i<x&&j>0) {if (arr[i][j] > k) {j--;}else if (arr[i][j] < k) {i;…

空时自适应处理用于机载雷达——元素空间空时自适应处理(Matla代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

保护隐私的第一步:从更新浏览器开始

当今社会已经进入了数字化和网络化的时代&#xff0c;而网络安全问题也日益突显。随着互联网在我们生活中的不断渗透&#xff0c;网络威胁变得愈发普遍和隐蔽。在这样的背景下&#xff0c;网络浏览器作为人们访问互联网的主要工具之一&#xff0c;不仅为我们提供了便捷的上网方…

2023CCF图形学启明星计划夏令营感想记录

这篇就是纯日记了&#xff0c;想记录一下参加这个夏令营的感想&#xff0c;中间的一些过程&#xff0c;毕竟这对我来说算是一段难忘的经历。 一、了解到的渠道 我个人是比较喜欢图形渲染的&#xff0c;之前也学过GAMES的课程&#xff0c;然后偶然的一天&#xff0c;GAMES101里…

后端开发有哪几种语言? - 易智编译EaseEditing

后端开发是构建应用程序的一部分&#xff0c;负责处理服务器端的逻辑、数据库交互和数据处理。有许多编程语言可用于后端开发&#xff0c;以下是一些常见的后端开发语言&#xff1a; Java&#xff1a; Java是一种广泛使用的面向对象编程语言&#xff0c;具有强大的跨平台能力。…

打造您的专属购物天堂!快来体验短视频商城app源码吧!

提倡个性化购物体验&#xff0c;短视频商城app源码助您建立独特购物天堂 尊敬的用户&#xff0c;您正在寻找一种能够打造您独特的购物天堂的方式吗?短视频商城app源码是您的最佳选择!不同于传统电商平台&#xff0c;短视频商城app源码将个性化体验与购物结合&#xff0c;为用户…

短视频商城App源码助您一站式打造自己的创业项目

您想要开始自己的创业项目&#xff0c;但是对于技术开发和设计可能会感到有些困惑。不用担心!我们为您提供了一种非常有效和快速的解决方案。迅速起步!短视频商城App源码将成为您打造创业项目的一站式解决方案。 1. 内容丰富全面 迅速起步!短视频商城App源码提供了丰富全面的功…

【Java从0到1学习】12 Java集合框架源码解读(6w字长文,快来收藏)

1. ArrayList源码剖析 1.1 ArrayList底层原理 利用空参创建的集合&#xff0c;在底层创建一个默认长度为0的数组名字为elementData&#xff0c;且还有一个底层变量size&#xff0c;用来记录元素的个数。添加第一个元素时&#xff0c;底层会创建一个新的长度为10的数组&#x…

树套树小结

树状数组套权值线段树&#xff0c;实现过程类似主席树&#xff0c;采用动态开点实现 https://www.luogu.com.cn/problem/P3380 树状数组部分 线段树部分

在项目中快速搭建机器学习的流程

在软件开发领域&#xff0c;机器学习框架发挥着关键作用&#xff0c;为开发人员提供强大的人工智能工具、库和算法&#xff0c;以有效地利用机器学习的潜力。从本质上讲&#xff0c;机器学习使计算机能够从数据中学习并做出预测或决策&#xff0c;而无需明确编程。 机器学习框…

Windows命令行调用main函数

通常C/C的入口函数都是main函数&#xff0c;平常一般使用的原型都是 int main() ;但是&#xff0c;实际上&#xff0c;main函数也可以有参数 int main(intargc[ ,char*argv[] [,char*envp[] ] ] ); int wmain(intargc[ ,wchar_t*argv[] [,wchar_t*envp[] ] ] );//适用于这种带…

手把手教你如何使用Fiddler及使用图文详解

01、什么是 Fiddler? Fiddler 是一个 HTTP 协议调试代理工具&#xff0c;它能够记录并检查所有你的电脑和互联网之间的 HTTP 通讯。Fiddler 提供了电脑端、移动端的抓包、包括 http 协议和 https 协议都可以捕获到报文并进行分析&#xff1b;可以设置断点调试、截取报文进行请…

java使用swing制作桌面图形应用的实例教程

本篇文章主要讲解&#xff0c;java编程语言通过swing制作桌面图形应用的实例教程&#xff0c;通过一个简单的个人信息提交表单界面&#xff0c;让你了解swing的布局管理、窗口图标设置、编译和运行以及窗口菜单的设置。 日期&#xff1a;2023年8月25日 实际效果 弹出新窗口帮助…

Linux网络编程:libevent事件通知库

文章目录&#xff1a; 一&#xff1a;libevent库 二&#xff1a;libevent框架 1.常规事件event 1.1 创建事件event&#xff08;event_new&#xff09; 1.2 添加事件到 event_base&#xff08;event_add&#xff09; 1.3 从event_base上摘下事件&#xff08;event_del&a…

Desnet模型详解

模型介绍 DenseNet的主要思想是密集连接&#xff0c;它在卷积神经网络&#xff08;CNN&#xff09;中引入了密集块&#xff08;Dense Block&#xff09;&#xff0c;在这些块中&#xff0c;每个层都与前面所有层直接连接。这种设计可以让信息更快速地传播&#xff0c;有助于解…

xargs命令解决“Argument list too long”

一、xargs命令概述 xargs命令是给其他命令传递参数的一个过滤器&#xff0c;也是组合多个命令的一个工具。它擅长将标准输入数据转换成命令行参数&#xff0c;xargs能够处理管道或者stdin并将其转换成特定命令的命令参数。空格是其默认定界符&#xff0c;管道传递给xargs的输入…

J2L3x助力企业业务协同,打破部门壁垒

在现代企业中&#xff0c;协同办公已经成为了一个关键的话题。在过去的几年里&#xff0c;我们已经看到了许多团队努力打破内部部门之间的壁垒&#xff0c;以更为高效的方式来协同工作。今天&#xff0c;我们要谈的是一种叫做J2L3x的企业沟通工具&#xff0c;这是一个旨在帮助团…

浅谈APP自动化测试工具的优势和应用

随着移动应用市场的迅速发展&#xff0c;APP的质量和性能变得至关重要。为了确保APP的稳定性和用户体验&#xff0c;自动化测试工具成为开发者和测试团队的关键利器。那么&#xff0c;APP自动化测试工具的优势和应用是什么?下面&#xff0c;就跟随掌控智能小编一起来看看具体介…

MySQL数据的导入导出mysqldump、mysqlimport into outfile和load data

0、概述 MySQL数据的导入导出方案通常是配套的&#xff0c;例如&#xff1a; 方案一&#xff1a;使用mysqldump导出数据&#xff0c;再使用mysql客户端导入数据 方案二&#xff1a;使用SELECT INTO OUTFILE命令导出数据&#xff0c;再使用LOAD DATA或mysqlimport导入数据 方案…