【网络编程】NIO三大组件:Channel、Buffer、Selector

一、前置知识
1、进程间通信
进程间通信分为本地的进程通信(在同台机器上)以及网络间的进程通信。
本地的进程通信主要有以下3种方式:

  • 管道(匿名管道和有名管道FIFO)。
  • 消息队列。
  • 共享区域(通常搭配信号量使用)。
    网络间的进程通信可以通过socket来实现。

2、socket
简单来说,socket是一个接口,用于实现网络间的进程通信,我们可以通过调用Socket模拟TCP和UDP通信。

socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭)。
在这里插入图片描述

**对于服务器端的socket编写:**新建一个socket(open()方法),给这个socket绑定一个端口(bind()方法),接收客户端请求(accept()方法)。注意,在接收客户端请求时,由于我们希望可以不断地接收请求,因此一般会写一个循环不断地调用accept()方法。

对于客户端的socket编写:新建一个socket(open()方法),与服务器socket建立连接(connect()方法)。

参考资料:

  • https://blog.csdn.net/pashanhu6402/article/details/96428887?spm=1001.2014.3001.5502【可以通过这篇博客理解Socket,但里面的函数是针对C语言的,在Java中可能不太一样。】
  • https://blog.csdn.net/u014209205/article/details/80461122?spm=1001.2014.3001.5502
  • https://www.bilibili.com/video/BV1vL4y1a7G7?p=3【Java编写一个Socket的Demo】

二、NIO
1、三大组件(Channel、Buffer、Selector)
Channel
Channel是数据传输的双向通道,可以从 channel 将数据读入 buffer,也可以将 buffer 的数据写入 channel。
常见的Channel有:

  • FileChannel
  • DatagramChannel(通常用于UDP)
  • SocketChannel(通常用于TCP)
  • ServerSocketChannel(通常用于TCP)

Buffer
内存缓存区,用于暂存从Channel中读取的数据、以及即将写入Channel中的数据。
常见的Buffer有:

  • ByteBuffer(主要使用)
    • MappedByteBuffer
    • DirectByteBuffer
    • HeapByteBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer
  • CharBuffer
    从名字上可以看出,这些Buffer支持的是不同数据类型的缓冲区,如short、int等。

Selector
在NIO出现之前,在开发服务器端程序时,会采用一种叫做“多线程版设计”的思路,即服务器端每当需要和一个新的客户端进行通信时,就会开启一个新的Socket连接(通过Socket就能读写数据了),并启动一个新的线程来为这个Socket服务(执行读写操作等)。
在这里插入图片描述
这种思路的弊端在于,如果客户端太多的话,那么服务器端就需要开启许多线程才能进行服务,这会带来以下问题:

  • 内存占用高。
  • 线程上下文切换成本高。
  • 只适合连接数少的场景。
    注意:线程并不是越多越好,因为CPU能并行处理的线程是有限的,一旦线程数量超出CPU能并行处理的数量,多出来的线程就需要等待CPU,而这就会导致线程上下文切换,从而带来额外开销。

为解决进程数量过多的问题,我们可以采用线程池来进行优化:
在这里插入图片描述
但这种方式中,每个线程每次只能服务一个socket,只有当正在服务的socket断开连接了,这个线程才能去服务其他socket。
线程池版的缺点:

  • 阻塞模式下,线程仅能处理一个socket连接
  • 仅适合短连接场景

由于上两种方式的弊端,因此人们想出了“selector”,先来看一下selector是如何为多个客户端提供服务的:
在这里插入图片描述
上图中,channel的功能类似于前面两张图中的socket,主要用于读写数据。我们重点关注的是selector,selector在这里就像一个哨兵,它监督着各个channel上发生的事件,一旦这些channel需要进行读写数据等操作,就会发送一个通知给selector,selector接收到通知之后就会去告诉thread,然后thread就会为对应的channel提供服务。
简单来说,selector 的作用就是配合一个线程来管理多个 channel,获取这些 channel 上发生的事件,这些 channel 工作在非阻塞模式下,不会让线程吊死在一个 channel 上。这种模式适合连接数特别多,但流量低的场景(low traffic)

2、bytebuffer
基本使用
假设我们目前有一个txt文件,我们要读取这个文件的内容。
首先,我们需要新建一个channel(本例中采用FileChannel),来完成读操作。
然后,我们需要新建一个buffer来暂存channel中读取到的数据,本例中采用ByteBuffer。
channel读取到数据之后,写入到buffer中。我们在从buffer中读取数据时,需要先把buffer切换成读模式,读完数据之后要把buffer切回写模式。
我们在创建buffer时,会给buffer分配一个大小,一旦存满了,channel中的数据就没办法再存入buffer中,因此,通常需要重复多次上述操作,才能把数据真正读完。
总结一下就是(ByteBuffer 正确使用姿势):
在这里插入图片描述

内部结构
ByteBuffer的内部结构类似于一个数组,主要有3个重要属性:

  • capacity:代表buffer的容量。
  • position:一个读写指针,可以理解为读写到buffer中的哪个下标。
  • limit:读写限制,position不能超过limit。

当我们新建一个buffer时,它的内部结构如下:
在这里插入图片描述
写模式下,position 是写入位置,limit 等于容量,下图表示写入了 4 个字节后的状态:
在这里插入图片描述
flip 动作发生后,position 切换为读取位置,limit 切换为读取限制:
在这里插入图片描述
读取 4 个字节后,状态变成:
在这里插入图片描述
clear 动作发生后,状态变成:
在这里插入图片描述
compact 方法,是把未读完的部分向前压缩,然后切换至写模式
在这里插入图片描述

**调试工具类:**黑马程序员提供了一个调试工具类,可以更直观地打印出ByteBuffer的相关信息。这个工具类需要用到netty,所以要引入netty的依赖。
代码获取:https://www.bilibili.com/video/BV1py4y1E7oA?p=8
这个工具类有如下方法:
debugall:以可视化形式打印出当前ByteBuffer的使用情况,包括position、limit等。

ByteBuffer常用方法

  • 分配空间:allocate()和allocateDirect()方法:这两个方法都是用于为ByteBuffer分配空间的,区别在于allocate()分配的空间位于Java堆内存中,而allocateDirect()分配的空间位于系统内存中。在这里插入图片描述
  • 写入数据:有两种方式:①调用 channel 的 read 方法。②调用 buffer 自己的 put 方法。
  • 读取数据:有两种方式:①调用 channel 的 write 方法。②调用 buffer 自己的 get 方法( get() 和 get(int i) 的效果也不太一样)。注意:
    在这里插入图片描述
  • mark()和reset()方法:mark 是在读取时,做一个标记,即使 position 改变,只要调用 reset 就能回到 mark 的位置。
    在这里插入图片描述
  • 字符串转换成ByteBuffer:
    ①调用ByteBuffer的put()方法:新建一个ByteBuffer,将字符串转换成byte数组 → 调用ByteBuffer的put方法,将刚得到的byte数组作为参数穿进去。在这里插入图片描述
    这种方式得到的buffer处于写模式中(如果要读模式需要手动进行转换)。
    ②调用Charset的encode()方法:
    在这里插入图片描述
    这种方式得到的buffer处于读模式中。
    ③调用ByteBuffer的wrap()方法:新建一个ByteBuffer,将字符串转换成byte数组 → 调用ByteBuffer的wrap()方法,将刚得到的byte数组作为参数穿进去。
    在这里插入图片描述
    这种方式得到的buffer处于读模式中。
  • ByteBuffer转换成字符串:与上述3种方法对应的,有3种将ByteBuffer转换成字符串的方法。比如可以调用charset的decode()方法,调用后得到一个CharBuffer类型的数据,所以还要通过toString()方法将这个CharBuffer转换成字符串。
    在这里插入图片描述
    但这种方法,传入的buffer必须处于读模式中,否则会得到一个空字符串。
    在这里插入图片描述
    Scattering Reads分散读取
    也就是把一个文件读取到多个buffer中去。要实现这个功能,最简单的思路就是把文件统一读取下来,然后再分别把数据存到不同的buffer中去。但这种方式比较繁琐,且涉及到多次字符串的分割、以及ByteBuffer的复制等,效率不高。
    在channel中提供的read方法,可以直接传入一个ByteBuffer数组,然后channel会按照数组里面各个ByteBuffer的空间大小自动将数据读到不同的ByteBuffer中去。
    看个例子:
    在这里插入图片描述
    得到的结果:
    在这里插入图片描述
    Gathering Writes集中写
    与分散读取相对应,集中写是要把多个ByteBuffer的内容一次性写入到一个文件中去。同样的,channel的wirte()方法也可以接收一个ByteBuffer数组作为参数:
    在这里插入图片描述
    在这里插入图片描述
    黏包和半包问题
    什么是黏包和半包?
    在这里插入图片描述
    要处理黏包和半包问题,就是要将错乱的数据恢复成原始的按 \n 分隔的数据。
    最基本的处理思路(下图中的split()方法):读取数据的时候判断当前读取到的是不是\n,如果是,说明已经读完一句了,所以我们就先把这一句存储起来,然后循环接着读下一句。
    在这里插入图片描述

3、FileChannel
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
两个channel传输数据
首先,可以使用transferTo()方法,如下面例子所示:
注意:transferTo()方法一次最多只能传送2G数据,因此,如果文件数量大于2G,就需要循环调用transferTo()方法。
在这里插入图片描述
在这里插入图片描述

4、JDK7引入的Path和Paths类

  • Path 用来表示文件路径
  • Paths 是工具类,用来获取 Path 实例

使用Path和Paths的例子:
在这里插入图片描述

配合Files使用:
获得Path之后,调用Files的方法可以实现如下功能:

检查文件是否存在:
在这里插入图片描述

创建一级目录或多级目录、删除目录:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

拷贝文件、移动文件、删除文件:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
此外,还能实现“遍历目录文件”、“统计jar包数目”、“删除多级目录”、“拷贝多级目录”等功能。
在这里插入图片描述
参考资料:
https://www.bilibili.com/video/BV1py4y1E7oA?p=7

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值