- 在java网络编程中,最常用的就是nio的一些工具类,今天先介绍一些nio的相关内容。
一、三大组件
1、channel:渠道,可以理解为通信的通道
2、buffer:数据流,可以理解为通信时候传输数据的载体
3、selector:多路复用器,也是nio的效率的保障
channel
常见的channel一共有四种
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
FileChannel用户文件传输,其他三个都是网络通信使用
buffer
常见的buffer分为一下几种
- ByteBuffer:MappedByteBuffer、DirectByteBuffer、HeapByteBuffer
- 类型Buffer
最常用的是ByteBuffer,其他的Buffer可以暂时不考虑。
HeapByteBuffer和DirectByteBuffer的区别:
① HeapByteBuffer初始化使用allocate()方法,使用的堆内存,会受到GC的影响,分配速度快,读写效率低
② DirectByteBuffer初始化使用allocateDirect()方法,使用真实内存,不会受到GC影响,分配速度慢,读写效率高
首选需要了解byteBuffer内部结构,同时在使用的时候需要注意,读写的切换。
- capacity:缓冲区的容量,一旦创建,就没有办法修改
- limit:缓冲区的界限,limit之后的数据是不可读的
- position:读写的索引位置
- mark:记录当前position的位置,通过mark()方法记录后,reset()方法恢复到mark记录的位置
四个属性满足条件是:mark<=position<=limit<=capacity
简单使用代码:
public class TestByteBuffer {
public static void main(String[] args) {
// 获得FileChannel
try (FileChannel channel = new FileInputStream("stu.txt").getChannel()) {
// 获得缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
int hasNext = 0;
StringBuilder builder = new StringBuilder();
while((hasNext = channel.read(buffer)) > 0) {
// 切换模式 limit=position, position=0
buffer.flip();
// 当buffer中还有数据时,获取其中的数据
while(buffer.hasRemaining()) {
builder.append((char)buffer.get());
}
// 切换模式 position=0, limit=capacity
buffer.clear();
}
System.out.println(builder.toString());
} catch (IOException e) {
}
}
}
1、allocate 分配一个byteBuffer ,大小为10
2、循环查看channel中是否还存在数据,如果存在继续使用channle.read(buffer)读到buffer中
3、buffer.filp(),切换到读模式
4、循环从buffer中读出数据,并写入到其他地方
核心API:
1、put():向byteBuffer中放入数据,position+1,同时limit=capcaity,结构如下:
2、flip():把byteBuffer切换成写模式,这个时候position=0,limit=3,结构如下:
3、get():从byteBuffer中读取一个元素,当未指定下标的时候,position+1,当执行下标的时候也就是get(i),position位置不变,读取一个元素后结构如下:
4、rewind():恢复position=0,其它值不变,这个时候可以从头读,或者切换到写模式覆盖,结构如下:
5、clear():恢复position、limit、capacity的初始位置,但是数据不清空,结构如下:
6、mark()、reset():mark会记录当前position的值,reset会恢复mark记录的position的值。
7、compact():该方法是把已读的数据弹出,未读的数据向前压缩,但是未读数据的原位置保存的还是原来的数据,写的时候就会覆盖,同时切换到写模式(可理解为接着写),结构如下:
可以看到,4、5虽然向前压缩了,但是原来的位置还是存放的4、5,但是position的位置上变为写的位置,所以4、5会被覆盖。
clear和compact比较:
① clear是清空,compact是接着写
② 因为涉及到数据的移动,所以compact更加消耗性能
String和byteBuffer相互转换
1、String转换为ByteBuffer:
- str.getBytes();
- StandardCharsets.UTF_8.encode("abc")
- ByteBuffer.wrap("abc".getBytes(StandardCharsets.UTF_8));
2、ByteBuffer转换为String:
- StandardCharsets.UTF_8.decode(wrap).toString();
selector
1、单线程处理网络连接:
- 连接数多的时候,会造成线程开启太多,内存占用高
- 线程之间切换消耗很大
2、线程池处理网络连接:
- 线程需要等待第一个连接完成后,才能处理第二个连接,适合短连接的情况,
- 当一个连接处理未完成的时候,其它连接都是阻塞的
3、多路复用器selector:
Selector的作用就是用来轮询每个注册的Channel,一旦发现Channel有注册的事件发生,便获取事件然后进行处理。这样一来,只是用一个单线程就可以管理多个通道,也就是管理多个连接。这样使得只有在连接真正有读写事件发生时,才会调用函数来进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了多线程之间的上下文切换导致的开销。
二、黏包与半包
因为网络传输中,会把数据积攒到一定量之后一次性的发送出去,这样就会出现一次性发送的数据,一次性发送多条数据,这就是黏包,同样的也可能一次性发送数据含有下一条数据的一半,这就是半包。其实知道这个概念之后,就知道怎么处理了,在读写的时候需要注意一下即可,下面是一个简单的解决小例子:
public class ByteBufferDemo {
public static void main(String[] args) {
ByteBuffer buffer = ByteBuffer.allocate(32);
// 模拟粘包+半包
buffer.put("Hello,world\nI'm Nyima\nHo".getBytes());
// 调用split函数处理
split(buffer);
buffer.put("w are you?\n".getBytes());
split(buffer);
}
private static void split(ByteBuffer buffer) {
// 切换为读模式
buffer.flip();
for(int i = 0; i < buffer.limit(); i++) {
// 遍历寻找分隔符
// get(i)不会移动position
if (buffer.get(i) == '\n') {
// 缓冲区长度
int length = i+1-buffer.position();
ByteBuffer target = ByteBuffer.allocate(length);
// 将前面的内容写入target缓冲区
for(int j = 0; j < length; j++) {
// 将buffer中的数据写入target中
target.put(buffer.get());
}
// 打印查看结果
ByteBufferUtil.debugAll(target);
}
}
// 切换为写模式,但是缓冲区可能未读完,这里需要使用compact
buffer.compact();
}
}
然后,就是还有一个思想,数据分而治之,就是把数据分开处理,不论读写都可以分段的去处理,其实在平时处理大批数据的时候也会用到这个思想。