NIO入门
Netty网络框架致力于开发高性能的服务器程序、高性能的客户端程序。
Netty底层-NIO
NIO: none-blocking io 非阻塞IO
非阻塞体现在当线程需要去进行IO操作而尚未获取到数据时,并不让线程阻塞,而是让线程继续去执行其它事件
Channel & Buffer
Channel:数据传输通道
Channel是一个对象,作用是用于源节点和目标节点的连接,在javaNIO中负责缓冲区数据的传递,Channel本身不存储数据,因此需要配合缓冲区进行传输
Buffer: 缓冲区,暂存数据的区域
NIO的Buffer(缓冲区)本质上是一个内存块,既可以写入数据,也可以从中读取数据
Java NIO中代表缓冲区的Buffer类是一个抽象类,位于java.nio包中
Selector
Selector中能够检测到一到多个 NIO 通道,并能够知道通道是否为读写
事件做好准备
这样,一个单独的线程可以管理多个 Channel,从而管理多个网络连接
原来的设计一个线程只管理一个socket,这样子会造成线程开销的内存浪费,线程上下文切换成本高,只适合连接数少的场景
因此我们要让一个线程管理多个连接就需要使用Selector,适合连接数多,但是流量低的场景
由于阻塞模式下,线程只能处理一个socket连接,导致了线程利用率不高
引入selector可以实现单线程的非阻塞式多信道管理,实现了多路复用
引入Netty依赖
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.0.23.Final</version>
</dependency>
使用FileChannel
FileChannel只能工作在阻塞模式下,无法和Selector一起用
必须通过FileInputStream,FileOutputStream或者RandomAccessFile来获取FileChannel,它们都有getChannel的方法
- RandomAccessFile可以指定读写模式,”rw”即可以读写
//fileChannel
//获取输入流
try {
FileChannel channel = new FileInputStream("data.txt").getChannel();
//准备缓冲区 10bytes Buffer
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
while (true){
//读取channel数据,即写入Buffer
int read = channel.read(byteBuffer);
if (read == -1){
break;//无内容读取则退出循环
}
//打印buffer内容
//切换到缓冲区读模式
byteBuffer.flip();
//检查是否还有数据
while (byteBuffer.hasRemaining()){
//每次读一个字节
byte b = byteBuffer.get();
System.out.println((char) b);
}
//切换回写模式
byteBuffer.clear();
}
} catch (IOException e) {
}
缓冲区使用步骤
1.向Buffer写入数据,如调用channel.read(buffer)
2.调用flip()切换至读模式
3.从buffer读取数据,如调用buffer.get()
4.调用clear()或compact()切换至写模式
5.重复以上步骤,直到读空为止
ByteBuffer结构
属性
- Capacity 数据容量
- position 读写指针
- limit 读写限制
开始Buffer为空
Position处于0,Limit = 容量大小
当写入4个字节后,调用flip,position从4回到0,Limit指向4,Buffer处于读模式
clear调用后,容器回到Buffer为空的写模式状态
compact调用后,则是把未读完的部分向前压缩,然后切换至写模式
常用方法:
- 分配空间
Bytebuffer buf = ByteBuffer.allocate(16);
- 读取channel数据到buffer中
int read = channel.read(buf);
buf.put((byte)127);
- 向channel写入buffer中的数据
int writeBytes = channel.write(buf);
//get方法会使得position指针向后走
//get(int i)会获取索引i的内容,指针不移动
byte b = buf.get();
字符串转换为ByteBuffer
public class StringToByteBuffer {
public static void main(String[] args) {
//方法一
//创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(16);
//变为字节数组写入数据
buffer.put("hello".getBytes());
//方法二,使用标准字符集来编码字符串为byte数组
ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode("hello");
//方法三,包装
ByteBuffer byteBuffer1 = ByteBuffer.wrap("hello".getBytes());
}
}
使用charset方法后buffer会自动切换到读模式
Scattering Reads
分散读取
channel.read(new ByteBuffer[]{b1,b2,b3});
GatheringWrites
整合写入
channel.write(new ByteBuffer[]{b1,b2,b3});
不需要合并多个buffer,这样子可以减少数据在ByteBuffer之间的拷贝复制,一次性写入
粘包和半包问题
粘包发送数据是因为这样能一次性发送多个包,提高了网络传输的效率
半包现象是由于服务器缓冲区存在大小限制因此必须拆分发送
public static void main(String[] args) {
ByteBuffer source = ByteBuffer.allocate(32);
source.put("Hello,world\nI'm zhangsan\nHo".getBytes());
split(source);
source.put("w are you?\n".getBytes());
split(source);
}
private static void split(ByteBuffer source) {
//切换为写模式
source.flip();
//这种解决方案每次都需要匹配换行符,存在效率低下的问题
for (int i = 0;i < source.limit() ; i++) {
if (source.get(i) == '\n'){
int length = i - source.position() + 1;
ByteBuffer target = ByteBuffer.allocate(length);
for (int j = 0; j < length; j++) {
target.put(source.get());
}
}
}
source.compact();
}
compact():使得未读完的数据前移,切换为写模式
limit():buffer存储写入数据的大小,即limit指针索引
position():当前读取的索引位置,即position指针索引
Channel之间的数据传输
try {
FileChannel from = new FileInputStream("data.txt").getChannel();
FileChannel to = new FileOutputStream("to.txt").getChannel();
//这种方法效率高,底层会利用操作系统的零拷贝进行优化
from.transferTo(0,from.size(),to);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
改进后
try {
FileChannel from = new FileInputStream("data.txt").getChannel();
FileChannel to = new FileOutputStream("to.txt").getChannel();
//这种方法效率高,底层会利用操作系统的零拷贝进行优化,上限为2G数据
long size = from.size();
for (long left = size; left > 0;){
left -= from.transferTo(size - left, left, to);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
这种可以支持更大数据的传输
文件编程
Path
jdk7引入了Path和Paths类
- Path用来表示文件路径
- Paths是工具类,用来获取Path实例
Path source = Paths.get("1.txt"); //相对路径
Path source = Paths.get("d:\\1.txt"); //绝对路径
Path source = Paths.get("d:/1.txt");
Files
检查文件是否存在
Path path = Paths.get("helloword/data.txt");
System.out.println(Files.exists(path));
创建一级目录
//目录已存在会抛出异常
//只能创建一级目录
Path path = Paths.get("helloword/d1");
Files.createDirectory(path);
创建多级目录
Path path = Paths.get("helloword/d1/d2");
Files.createDirectories(path);
拷贝文件
Path source = Path.get("helloword/data.txt");
Path target = Path.get("helloword/target.txt");
//高效率,使用的是操作系统的底层实现
//文件已存在则抛出异常
Files.copy(source,target);
//覆盖已存在
Files.copy(source,target,StandardCopyOption.REPLACE_EXISTING);
移动文件
//保证文件移动原子性
Files.move(source,target,StandardCopyOption.ATOMIC_MOVE);
删除文件
Path target = Path.get("helloword/target.txt");
//文件不存在则会报出异常
Files.delete(target);
删除目录(空目录)
Path target = Path.get("helloword/d1");
Files.delete(target);
更加方便的删除非空目录方法
walkFileTree访问多级目录
public static void main(String[] args) throws IOException {
//由于匿名内部类对外部引用都需要复制一份拷贝,为了保持一致性
//默认为final,因此不能用int类型变量作为计数器
AtomicInteger dirCount = new AtomicInteger();
AtomicInteger fileCount = new AtomicInteger();
//参数一为起点文件位置,参数二为设定的遍历规则,需要重写内部类方法
Path path = Files.walkFileTree(Paths.get("D://xxx/xxx"), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
//此处添加逻辑即可
System.out.println("====>"+dir);
dirCount.incrementAndGet();
return super.preVisitDirectory(dir, attrs);
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
//此处添加逻辑即可
System.out.println(file);
fileCount.incrementAndGet();
return super.visitFile(file, attrs);
}
});
System.out.println("dir count:" + dirCount);
System.out.println("file count:"+ fileCount);
}
使用了访问者设计模式
相比于我们自己写递归访问更加的方便
删除多级目录
public static void main(String[] args) throws IOException {
Path path = Files.walkFileTree(Paths.get("D://xxx/xxx"), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return super.visitFile(file, attrs);
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
Files.delete(dir);
return super.postVisitDirectory(dir, exc);
}
});
}
拷贝文件
public static void main(String[] args) throws IOException {
String source = "D:\\xxx1";
String target = "D:\\xxx2";
Files.walk(Paths.get(source)).forEach(path -> {
try {
String targetName = path.toString().replace(source,target);
//是目录
if (Files.isDirectory(path)){
Files.createDirectory(Paths.get(targetName));
}
//是普通文件
else if (Files.isRegularFile(path)){
Files.copy(path,Paths.get(targetName));
}
}catch (IOException e) {
e.printStackTrace();
}
});
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以邮件至 1300452403@qq.com