分布式根基于网络编程,Netty恰是java网络编程的王者,致力于高性能编程。
适用于网络开发,服务器开发。多线程,线程池,maven。
non-clocking io:非阻塞IO
Channel是读写数据的双向通道。常见通道的有File,Datagram,Socket,ServerSocket。
Buffer是用来缓冲读写数据的。常见的有Byte(Mapped,Direct,Heap),Int,Float,Double,Char,
多线程版本:早期的服务器基于多线程实现。内存占用高,线程上下文切换成本高,只适合连接少的场景。
线程池版本:阻塞模式下,线程仅能处理一个socket连接,仅适合短链接场景。
selector版本:selector作用就是配合一个线程来管理多个channel,获取channel上发生的事件,这些channel工作在非阻塞下,不会让线程吊死在一个channel上适合连接多但流量低的场景。
调用selector的select()会阻塞直到channel发生了读写就绪事件,select方法会返回这些事件交给thread来处理。
Buffer/ByteBuffer/ByteBuf详解
ByteBuffer有以下重要属性:capacity容量,position读写指针,limit限制。
flip:position切换读取位置,limit切换为读取限制
compact:把未读完的部分向前压缩,然后切换写模式
分配空间:ByteBuffer buf = ByteBuffer.allocate(16);(堆,低效,会GC)ByteBufferDirect(16); (直接内存,高效,不会GC,分配低效)
写入数据:
* 调用channel的read方法:channel.read();
* 调用buffer自己的put方法:buf.put();
读取数据:
* 调用channel的write方法:channel.write();
* 调用buffer自己的get方法:buf.get();
* 注:get方法会将position指针向后走,想重复读可调用rewind方法将position置0或get(i);
* 标记position:mark
* 回到标记位置:reset
字符串与ByteBuffer转换:
* 直转方法:buffer.put(“hello”.getBytes());
* Charset方法:ByteBuffer buf = StandardCharsets.UTF_8.encode(“hello”);
* wrap方法:ByteBuffer buf = ByteBuffer.wrap(“hello”.getBytes());
* 回转String:StandardCharsets.UTF_8.decode(buf).toString();
public class TestByteBufferExam {
public static void main(String[] args) {
/**
* 网络上多条数据发送客户端使用/n进行分割,但由于某种原因,被进行重新组合,例如
* Hello,world\n
* I'm aric\n
* How are you?\n
* 变成下面的两个 byteBuffer(粘包,半包)
* Hello,world\nI'm aric\nHo
* w are you?\n
* 现要求将错乱的数据恢复按\n分割数据
*/
ByteBuffer source = ByteBuffer.allocate(32);
source.put("Hello,world\nI'm aric\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 + 1 - source.position(); //消息长度
//把这条完整的消息存入新的ByteBuffer
ByteBuffer target = ByteBuffer.allocate(length);
//从source读,向target写
for (int j = 0; j < length; j++) {
byte b = source.get();
target.put(b);
}
debugAll(target);
}
}
source.compact();
}
}
注:FileChannel只能工作在阻塞模式下。
获取
不能直接打开FIleChannel,必须通过FileInputStream,FileOutputStream或RandomAccessFile来获取FileChannel,他们都有getChannel();
读取
会从channel读取数据填充ByteBuffer,返回值表示读到多少字节,-1表示达到了文件的末尾。
int readBytes = channel.read(buffer);
写入
ByteBuffer buffer = ...;
buffer.put(); //存入数据
buffer.filp(); //切换读模式
while(buffer.hasRemaining()){ //用while因为buffer无法保证一次读取channel中全部内容。
channel.write(buffer);
}
关闭
channel必须关闭。
位置
获取当前位置:long pos = hannel.position();
设置当前位置:channel.position(pos);
大小
size方法
强制写入
数据先会缓存,调用force(true)方法可将文件内容和元数据立刻写入磁盘。
try (FileChannel from = new FileInputStream("data.txt").getChannel();
FileChannel to = new FileOutputStream("to.txt").getChannel();
) {
//transferTo底层采用零拷贝,效率高
//from.transferTo(0, from.size(), to); 最大只能传输2G
long size = from.size();
for (long left = size; left > 0; ) { //left 表示剩余多少字节
left -= from.transferTo((size - left), left, to);
}
} catch (IOException e) {
e.printStackTrace();
}
jdk7引入Path和Paths类,Path表示文件路径,Paths是工具类,用来获取path实例。
检查文件是否存在
Path path = Paths.get(“data.txt”);
System.out.println(Files.ex);
拷贝文件
Files.copy(source, target);
移动文件
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
删除文件
Files.delete(target);
public static void main(String[] args) throws IOException {
walkFile(); //遍历文件
deleteFile(); //删除文件
String source = "I:\\BaiduNetdiskDownload";
String target = "I:\\BaiduNetdisk";
copyFile(source, target); //拷贝文件
}
private static void walkFile() throws IOException {
AtomicInteger dirCount = new AtomicInteger();
AtomicInteger fileCount = new AtomicInteger();
AtomicInteger jarCount = new AtomicInteger();
Files.walkFileTree(Paths.get("I:\\BaiduNetdiskDownload"), 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 {
if (file.toString().endsWith(".jar")) {
System.out.println(file);
jarCount.incrementAndGet();
}
System.out.println(file);
fileCount.incrementAndGet();
return super.visitFile(file, attrs);
}
});
System.out.println(dirCount);
System.out.println(fileCount);
System.out.println(jarCount);
}
private static void deleteFile() throws IOException {
Files.walkFileTree(Paths.get("I:\\BaiduNetdisk"), 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);
}
});
}
private static void copyFile(String source, String target) throws IOException {
Files.walk(Paths.get(source)).forEach(path -> {
try {
String targetName = path.toString().replace(source, target);
//是目录
if (Files.isDirectory(path)) {
Files.createDirectories(Paths.get(targetName))
}
//是普通文件
else if (Files.isRegularFile(path)) {
Files.copy(path, Paths.get(targetName));
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
线程必须配合Selector才能完成对多个Channel可读写事件的监控,即多路复用。
//nio 阻塞模式&非阻塞
public static void main(String[] args) throws IOException {
//消息缓冲区
ByteBuffer buffer = ByteBuffer.allocate(16);
//创建服务器
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false); //非阻塞模式开启,没有建立连接时,sc返回null
//绑定监听端口
ssc.bind(new InetSocketAddress(8080));
ArrayList<SocketChannel> channels = new ArrayList<>();
while (true) {
//循环监听客户端连接 accept socket用来与客户端通信
SocketChannel sc = ssc.accept(); //阻塞方法:线程停止运行,没链接时阻塞
if(sc != null){
sc.configureBlocking(false); //将socketChannel设为非阻塞模式。如果没有读到数据,read返回0
channels.add(sc);
}
for (SocketChannel channel : channels) {
channel.read(buffer); //阻塞方法:线程停止运行,没有数据时阻塞
buffer.flip();
System.out.println(buffer);
buffer.clear();
}
}
}
void selectorEdition() throws IOException {
//1. 创建selector
Selector selector = Selector.open();
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
//2. 建立selector和channel的注册,sscKey是事件的句柄,是将来事件发生后,通过它可以知道事件和哪个channel的事件
SelectionKey sscKey = ssc.register(selector, 0, null);
//表示sscKey只关注accept事件
sscKey.interestOps(SelectionKey.OP_ACCEPT);
ssc.bind(new InetSocketAddress(8080));
while (true) {
//3. select 方法,没有事件发生,线程阻塞,有事件,线程才会恢复运行
//selector在事件未处理时,不会阻塞,事件发生后要么处理,要么取消,不能置之不理
selector.select();
//4. 处理事件,selectedKeys内部包含了所有发生的事件
Iterator<SelectionKey> iter = selector.selectedKeys().iterator(); //selector发生事件后,selectedKeys集合只有加入,不会删除
while (iter.hasNext()) {
SelectionKey key = iter.next();
//处理完key一定要移除调,不然下次处理时会报空指针异常
iter.remove();
//5. 区分事件类型
if (key.isAcceptable()) { //如果时accept
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
//读取事件
sc.configureBlocking(false);
SelectionKey scKey = sc.register(selector, 0, null);
scKey.interestOps(SelectionKey.OP_READ);
} else if (key.isReadable()) {
try {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(16);
int read = channel.read(buffer); //正常断开,read返回-1
if (read == -1) {
key.cancel();
} else {
buffer.flip();
System.out.println(buffer);
//split(buffer); //使用分隔符方式接收消息
}
} catch (IOException e) {
e.printStackTrace();
//客户端出异常情况,需手动从selectedKeys集合取消key
key.cancel();
}
}
}
}
}
channel 必须工作在非阻塞模式
FileChannel 没有非阻塞模式,因此不能配合 selector 一起使用
绑定的事件类型可以有
可以通过下面三种方法来监听是否有事件发生,方法的返回值代表有多少 channel 发生了事件
方法1:阻塞直到绑定事件发生-selector.select();
方法2:阻塞直到绑定事件发生,或是超时(时间单位为 ms)-selector.select(long timeout);
方法3:不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件-selector.selectNow();
事件发生时
事件发生后,要么处理,要么取消(cancel),不能什么都不做,否则下次该事件仍会触发,这是因为 nio 底层使用的是水平触发
因为 select 在事件发生后,就会将相关的 key 放入 selectedKeys 集合,但不会在处理完后从 selectedKeys 集合中移除,需要我们自己编码删除。例如
cancel 会取消注册在 selector 上的 channel,并从 keys 集合中删除 key 后续不会再监听事件。
if (key.isAcceptable()) { //如果时accept
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel sc = channel.accept();
//读取事件
sc.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(16); //attachment buffer和sc绑定
SelectionKey scKey = sc.register(selector, 0, null, buffer);
scKey.interestOps(SelectionKey.OP_READ);
} else if (key.isReadable()) {
try {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment(); //从key中获取独有的ByteBuffer
int read = channel.read(buffer); //正常断开,read返回-1
if (read == -1) {
key.cancel();
} else {
//buffer.flip();
//System.out.println(buffer);
split(buffer); //使用分隔符方式接收消息
if(buffer.position() == buffer.limit()) { //扩容
ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2);
buffer.flip();
newBuffer.put(buffer);
key.attach(newBuffer); //替换原key中的ByteBuffer
}
}
} catch (IOException e) {
e.printStackTrace();
//客户端出异常情况,需手动从selectedKeys集合取消key
key.cancel();
}
}
一种思想是首先分配一个较小的buffer,不够再扩容,优点是消息连续容易处理,缺点是数据拷贝耗性能。
另一种思想是用多个数组组成buffer,一个数组不够,把多出来的内容写入新的数组,区别是消息存储不连续,解析复杂,优点是避免拷贝
只要向 channel 发送数据时,socket 缓冲可写,这个事件会频繁触发,因此应当只在 socket 缓冲区写不下时再关注可写事件,数据写完之后再取消关注
当调用一次channel.read或stream.read后,会切换至操作系统内核态完成真正的数据读取,而读取又分为:等待数据阶段、复制数据阶段。
阻塞IO:用户态调用内核态阻塞,等待内核态数据就绪复制完成后才能返回。期间用户和内核都阻塞
非阻塞IO:用户态调用内核态阻塞会立刻返回并循环直到有数据。用户态只有在等待数据时非阻塞,复制数据时还是阻塞。但是内核和用户切换很频繁。
多路复用:先调用select方法,用户态调用内核阻塞,有事件才返回用户态,用户态再读取内核态阻塞等待复制数据完后才返回。期间用户和内核都阻塞。
传统IO:将文件先通过accessfile读入byte数组中,再通过socket输出流i写出客户端。
File f = new File("data.txt");
RandomAccessFile file = new RandomAccessFile(file, "r");
byte[] buf = new byte[(int)f.length()];
file.read(buf);
Socket socket = ...;
socket.getOutputStream().write(buf);
通过DirectByteBuf
底层词用linux提供的sendFile方法,java中对应两个channel调用transferTo/transferFrom方法拷贝数据。
AIO用来解决数据复制阶段的阻塞问题。
异步模型需底层系统kernel支持
- Windows通过IOCP实现真正的异步IO
- Linux系统异步IO在2.6版本中引入,但其底层还是多路复用模拟异步IO,性能没优势