NIO三大组件
现在互联网环境下,分布式系统大相径庭,而分布式系统的根基在于网络编程,而netty恰恰是java领域的网络编程的王者,如果要致力于并发高性能的服务器程序、高性能的客户端程序,必须掌握netty网络编程。
NIO基础
NIO是从java1.4开始引入的一种新的I/O编程方式,相对于传统的IO来说,NIO更加灵活、高效、可靠,能够更好的处理海量的数据和高并发场景。简单来说,并发能力强。
三大组件
channel
Channel是数据传输的**双向通道,Stream要不就是读,要不就是写。Channel比Stream更加底层。**
常见的Channel有FileChannel、SocketChannel、DatagramChannel、ServerSocketChannel。FileChannel主要用于文件传输,其他三种用于网络通信。
Buffer
当我们有了连接通道,我们需要将拿到的数据放到一个缓冲区域,以便于程序对它的读取/写入操作
ByteBuffer
- MappedByteBuffer 、DirectByteBuffer、 HeapByteBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
CharBuffer
最常用的是ByteBuffer
select
Selector(选择器)是一个特殊的组件,用于采集各个通道的状态(或者说事件)。
这种方法的弊端:
内存占用高。每有一个socket连接,系统就要分配一个线程去对接。当出现大量连接时,会开辟大量线程,导致占用大量内存。
线程上下文切换成本高。
只适合连接数较少的场景。
什么是线程上下文切换?
一个CPU在同一个时刻是只能处理一个线程的,由于时间片耗尽或出现阻塞等情况,CPU 会转去执行另外一个线程,这个叫做线程上下文切换。
线程池技术
这种方法的弊端:
在阻塞模式下,线程只能处理一个连接。线程池中的线程获取任务,只有当任务完成/socket断开连接,才会去获取执行
下一个任务
只适合短链接的场景。
选择器(Selector)技术
为每个线程配合一个选择器,让选择器去管理多个channel。(注:FileChannel是阻塞式的,因此无法使用选择器。)
让选择器去管理多个工作在非阻塞式下的Channel,获取Channel上的事件,当一个Channel没有任务时,就转而去执行别的Channel上的任务。这种适合用在连接多,流量小的场景。
若事件未就绪,调用 selector 的 select() 方法会阻塞线程,直到 channel 发生了就绪事件。这些事件就绪后,select 方法就会返回这些事件交给 thread 来处理
ByteBuffer
属性
capacity:缓冲区的容量,不可变。(在netty中可变哦~)
limit:缓冲区的界限。limit之后的数据不允许读写
position:读写指针。position不可大于limit,且position不为负数。
mark:标记。记录当前position的值。position被改变后,可以通过调用reset() 方法恢复到mark的位置。
正确使用方法
- 向 buffer 写入数据,例如调用 channel.read(buffer)
- 调用 flip() 切换至读模式
- 从 buffer 读取数据,例如调用 buffer.get()
- 调用 clear() 切换至写模式
- 重复 1~4 步骤
// 1. 输入输出流
try(FileChannel channel = new FileInputStream("D:/Java/netty/src/test/resources/data.txt").getChannel()) {// 2. 准备缓冲区ByteBuffer buffer = ByteBuffer.allocate(10);while(true) {// 3. 从channel读取数据,读到buffer中去int len = channel.read(buffer);log.debug("读到的字节数 {}", len);if(len == -1) {break;}// 4. 切换buffer读模式,打印内容buffer.flip();while(buffer.hasRemaining()) {byte b = buffer.get();log.debug("实际字节 {}", (char)b);}// 切换回写模式buffer.clear();}
} catch (IOException e) {throw new RuntimeException(e);
}
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 读到字节数:10
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 1
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 2
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 3
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 4
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 5
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 6
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 7
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 8
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 9
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 0
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 读到字节数:4
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - a
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - b
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - c
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - d
10:39:03 [DEBUG] [main] c.i.n.ChannelDemo1 - 读到字节数:-1
一开始
写模式下,position 是写入位置,limit 等于容量,下图表示写入了 4 个字节后的状态
flip后变成读模式,position变成读取位置,limit变成读取限制。
读取四个字节后,状态
clear动作发生后,状态
compact方法,是把未读完的部分向前压缩,然后切换至写模式
常用方法
分配空间
// class java.nio.HeapByteBuffer - java 堆内存,读写效果低,受到GC影响
System.out.println(ByteBuffer.allocate(16).getClass());
// class java.nio.DirectByteBuffer - 直接内存,读写效率高(少一次拷贝),不会受GC影响。系统内存分配效率低,可能内存泄露
System.out.println(ByteBuffer.allocateDirect(16).getClass());
向buffer写入数据
int readBytes = channel.read(buf);
buf.put((byte)127);
向buffer读取数据
int writeBytes = channel.write(buf);
byte b = buf.get();
get 方法会让 position 读指针向后走,如果想重复读取数据
- 可以调用 rewind 方法将 position 重新置为 0
- 或者调用 get(int i) 方法获取索引 i 的内容,它不会移动读指针
- mark 和 reset(了解):
- mark 是在读取时,做一个标记,即使 position 改变,只要调用 reset 就能回到 mark 的位置
字符串与 ByteBuffer 互转
- mark 是在读取时,做一个标记,即使 position 改变,只要调用 reset 就能回到 mark 的位置
// 1. 简单转换为ByteBuffer -> 写模式
ByteBuffer buffer0 = ByteBuffer.allocate(16);
buffer0.put("hello".getBytes());// 切换读模式
buffer0.flip();
String s0 = StandardCharsets.UTF_8.decode(buffer0).toString();
System.out.println(s0);// 2. encode -> 读模式
ByteBuffer buffer1 = StandardCharsets.UTF_8.encode("world");
String s1 = StandardCharsets.UTF_8.decode(buffer1).toString();
System.out.println(s1);// 3. warp
ByteBuffer buffer2 = ByteBuffer.wrap("hello".getBytes());
String s2 = StandardCharsets.UTF_8.decode(buffer2).toString();
System.out.println(s2);
分散读集中写
分散读:把一个Channel读取到三个Buffer当中去,减少数据的复制
String baseUrl = "D:/Java/netty/src/test/resources/word.txt";
try (FileChannel channel = new RandomAccessFile(baseUrl, "rw").getChannel()) {ByteBuffer a = ByteBuffer.allocate(3);ByteBuffer b = ByteBuffer.allocate(3);ByteBuffer c = ByteBuffer.allocate(5);channel.read(new ByteBuffer[]{a, b, c});a.flip();b.flip();c.flip();debugAll(a);debugAll(b);debugAll(c);
} catch (IOException e) {
}
集中写:三个Buffer写到一个Channel里面去,减少数据的复制
String baseUrl = "D:/Java/netty/src/test/resources/word.txt";
try (FileChannel channel = new RandomAccessFile(baseUrl, "rw").getChannel()) {ByteBuffer d = ByteBuffer.allocate(4);ByteBuffer e = ByteBuffer.allocate(4);d.put(new byte[]{'f', 'o', 'u', 'r'});e.put(new byte[]{'f', 'i', 'v', 'e'});d.flip();e.flip();debugAll(d);debugAll(e);channel.write(new ByteBuffer[]{d, e});
} catch (IOException e) {
}
粘包与半包
网络上有多条数据发送给服务端,数据之间使用 \n 进行分隔
但由于某种原因这些数据在接收时,被进行了重新组合,例如原始数据有3条为
Hello,world\n
I’m Nyima\n
How are you?\n
变成了下面的两个 byteBuffer (粘包,半包)
Hello,world\nI’m Nyima\nHo
w are you?\n
出现原因
粘包
发送方在发送数据时,并不是一条一条地发送数据,而是将数据整合在一起,当数据达到一定的数量后再一起发送。这就会导致多条信息被放在一个缓冲区中被一起发送出去
半包
接收方的缓冲区的大小是有限的,当接收方的缓冲区满了以后,就需要将信息截断,等缓冲区空了以后再继续放入数据。这就会发生一段完整的数据最后被截断的现象
解决办法
通过get(index)方法遍历ByteBuffer,遇到分隔符时进行处理。
注意
:get(index)不会改变position的值
记录该段数据长度,以便于申请对应大小的缓冲区
将缓冲区的数据通过get()方法写入到target中
调用compact方法切换模式,因为缓冲区中可能还有未读的数据
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?\nhaha!\n".getBytes());split(source);
}private static void split(ByteBuffer source) {source.flip();ByteBuffer target = ByteBuffer.allocate(15);for(int i = 0; i < source.limit(); i++) {if(source.get(i) == '\n') {// 长度处理很关键int length = i + 1 - source.position();for(int j = 0; j < length; j++) {target.put(source.get());}// 打印字符debugAll(target);target.clear();}}source.compact();
}
文件编程
FileChannel
只能工作在堵塞模式下
不能直接打开 FileChannel,必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法
获取
不能直接打开 FileChannel,必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法
- 通过 FileInputStream 获取的 channel 只能读
- 通过 FileOutputStream 获取的 channel 只能写
- 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定
读取
会从 channel 读取数据填充 ByteBuffer,返回值表示读到了多少字节,-1 表示到达了文件的末尾
int readBytes = channel.read(buffer);
写入
写入的正确如下,socketchannel
ByteBuffer buffer = ...;
buffer.put(...); // 存入数据
buffer.flip(); // 切换读模式while(buffer.hasRemaining()) {channel.write(buffer);
}
在 while 中调用 channel.write 是因为 write 方法并不能保证一次将 buffer 中的内容全部写入 channel
- 关闭
channel 必须关闭,不过调用了 FileInputStream、FileOutputStream 或者 RandomAccessFile 的 close 方法会间接地调用 channel 的 close 方法
- 大小
使用 size 方法获取文件的大小
- 强制写入
操作系统出于性能的考虑,会将数据缓存,不是立刻写入磁盘。可以调用 force(true) 方法将文件内容和元数据(文件的权限等信息)立刻写入磁盘
两个 Channel 传输数据(有用)
小文件
String FROM = "helloword/data.txt";
String TO = "helloword/to.txt";
long start = System.nanoTime();
try (FileChannel from = new FileInputStream(FROM).getChannel();FileChannel to = new FileOutputStream(TO).getChannel();
) {from.transferTo(0, from.size(), to);
} catch (IOException e) {e.printStackTrace();
}
long end = System.nanoTime();
System.out.println("transferTo 用时:" + (end - start) / 1000_000.0);
大文件
public static void main(String[] args) {try (FileChannel from = new FileInputStream("data.txt").getChannel();FileChannel to = new FileOutputStream("to.txt").getChannel();) {// 效率高,底层会利用操作系统的零拷贝进行优化long size = from.size();// left 变量代表还剩余多少字节for (long left = size; left > 0; ) {System.out.println("position:" + (size - left) + " left:" + left);left -= from.transferTo((size - left), left, to);}} catch (IOException e) {e.printStackTrace();}
}
Files
查找检查文件是否存在
Path path = Paths.get("helloword/data.txt");
System.out.println(Files.exists(path));Copy
创建一级目录
Path path = Paths.get("helloword/d1");
Files.createDirectory(path);Copy
如果目录已存在,会抛异常 FileAlreadyExistsException
不能一次创建多级目录,否则会抛异常 NoSuchFileException
创建多级目录
Path path = Paths.get("helloword/d1/d2");
Files.createDirectories(path);Copy
拷贝及移动
Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/target.txt");Files.copy(source, target);Copy
如果希望用 source 覆盖掉 target,需要用 StandardCopyOption 来控制
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);Copy
移动文件
Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/data.txt");Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);Copy
遍历文件夹
// 要遍历的文件夹
Path path = Paths.get("D:\\Java\\netty");
// 文件夹个数
AtomicInteger dirCount = new AtomicInteger();
// 文件个数
AtomicInteger fileCount = new AtomicInteger();
// 开始遍历
Files.walkFileTree(path, new SimpleFileVisitor<Path>(){// 进入文件夹之前的操作@Overridepublic FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {System.out.println("====> " + dir);dirCount.incrementAndGet();return super.preVisitDirectory(dir, attrs);}// 遍历到文件的操作@Overridepublic FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {System.out.println(file);fileCount.incrementAndGet();return super.visitFile(file, attrs);}
});
System.out.println(dirCount);
System.out.println(fileCount);
网络编程
阻塞
阻塞模式下,相关方法都会导致线程暂停
ServerSocketChannel.accept 会在没有连接建立时让线程暂停
SocketChannel.read 会在通道中没有数据可读时让线程暂停
阻塞的表现其实就是线程暂停了,暂停期间不会占用 cpu,但线程相当于闲置
单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持
但多线程下,有新的问题,体现在以下方面
32 位 jvm 一个线程 320k,64 位 jvm 一个线程 1024k,如果连接数过多,必然导致 OOM,并且线程太多,反而会因为频繁上下文切换导致性能降低
可以采用线程池技术来减少线程数和线程上下文切换,但治标不治本,如果有很多连接建立,但长时间 inactive,会阻塞线程池中所有线程,因此不适合长连接,只适合短连接
服务端代码
public class Server {public static void main(String[] args) {// 创建缓冲区ByteBuffer buffer = ByteBuffer.allocate(16);// 获得服务器通道try(ServerSocketChannel server = ServerSocketChannel.open()) {// 为服务器通道绑定端口server.bind(new InetSocketAddress(8080));// 用户存放连接的集合ArrayList<SocketChannel> channels = new ArrayList<>();// 循环接收连接while (true) {System.out.println("before connecting...");// 没有连接时,会阻塞线程SocketChannel socketChannel = server.accept();System.out.println("after connecting...");channels.add(socketChannel);// 循环遍历集合中的连接for(SocketChannel channel : channels) {System.out.println("before reading");// 处理通道中的数据// 当通道中没有数据可读时,会阻塞线程channel.read(buffer);buffer.flip();ByteBufferUtil.debugRead(buffer);buffer.clear();System.out.println("after reading");}}} catch (IOException e) {e.printStackTrace();}}
}
客户端
public class Client {public static void main(String[] args) {try (SocketChannel socketChannel = SocketChannel.open()) {// 建立连接socketChannel.connect(new InetSocketAddress("localhost", 8080));System.out.println("waiting...");} catch (IOException e) {e.printStackTrace();}}
}
客户端-服务器建立连接前:服务器端因accept阻塞
客户端-服务器建立连接后,客户端发送消息前:服务器端因通道为空被阻塞
客户端发送数据后,服务器处理通道中的数据。再次进入循环时,再次被accept阻塞
前的客户端再次发送消息**,服务器端因为被accept阻塞**,无法处理之前客户端发送到通道中的信息
非阻塞
可以通过ServerSocketChannel的configureBlocking(false)方法将获得连接设置为非阻塞的。此时若没有连接,accept会返回null
可以通过SocketChannel的configureBlocking(false)方法将从通道中读取数据设置为非阻塞的。若此时通道中没有数据可读,read会返回-1
服务器代码如下
public class Server {public static void main(String[] args) {// 创建缓冲区ByteBuffer buffer = ByteBuffer.allocate(16);// 获得服务器通道try(ServerSocketChannel server = ServerSocketChannel.open()) {// 为服务器通道绑定端口server.bind(new InetSocketAddress(8080));// 用户存放连接的集合ArrayList<SocketChannel> channels = new ArrayList<>();// 循环接收连接while (true) {// 设置为非阻塞模式,没有连接时返回null,不会阻塞线程server.configureBlocking(false);SocketChannel socketChannel = server.accept();// 通道不为空时才将连接放入到集合中if (socketChannel != null) {System.out.println("after connecting...");channels.add(socketChannel);}// 循环遍历集合中的连接for(SocketChannel channel : channels) {// 处理通道中的数据// 设置为非阻塞模式,若通道中没有数据,会返回0,不会阻塞线程channel.configureBlocking(false);int read = channel.read(buffer);if(read > 0) {buffer.flip();ByteBufferUtil.debugRead(buffer);buffer.clear();System.out.println("after reading");}}}} catch (IOException e) {e.printStackTrace();}}
}
这样写存在一个问题,因为设置为了非阻塞,会一直执行while(true)中的代码,CPU一直处于忙碌状态,会使得性能变低,所以实际情况中不使用这种方法处理请求。
Selector
多路复用
单线程可以配合 Selector 完成对多个 Channel 可读写事件的监控,这称之为多路复用
多路复用仅针对网络 IO,普通文件 IO 无法利用多路复用
如果不用 Selector 的非阻塞模式,线程大部分时间都在做无用功,而 Selector 能够保证
有可连接事件时才去连接
有可读事件才去读取
有可写事件才去写入
使用及Accpet事件
使用Selector实现多路复用,服务端代码如下改进
public class SelectServer {public static void main(String[] args) {ByteBuffer buffer = ByteBuffer.allocate(16);// 获得服务器通道try(ServerSocketChannel server = ServerSocketChannel.open()) {server.bind(new InetSocketAddress(8080));// 创建选择器Selector selector = Selector.open();// 通道必须设置为非阻塞模式server.configureBlocking(false);// 将通道注册到选择器中,并设置感兴趣的事件server.register(selector, SelectionKey.OP_ACCEPT);while (true) {// 若没有事件就绪,线程会被阻塞,反之不会被阻塞。从而避免了CPU空转// 返回值为就绪的事件个数int ready = selector.select();System.out.println("selector ready counts : " + ready);// 获取所有事件Set<SelectionKey> selectionKeys = selector.selectedKeys();// 使用迭代器遍历事件Iterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();// 判断key的类型if(key.isAcceptable()) {// 获得key对应的channelServerSocketChannel channel = (ServerSocketChannel) key.channel();System.out.println("before accepting...");// 获取连接并处理,而且是必须处理,否则需要取消SocketChannel socketChannel = channel.accept();System.out.println("after accepting...");// 处理完毕后移除iterator.remove();}}}} catch (IOException e) {e.printStackTrace();}}
}
获得选择器Selector
Selector selector = Selector.open();
channel 必须工作在非阻塞模式
FileChannel 没有非阻塞模式,因此不能配合 selector 一起使用
获得的事件类型
connect - 客户端连接成功时触发
accept - 服务器端成功接受连接时触发
read - 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况
write - 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况
// 通道必须设置为非阻塞模式
server.configureBlocking(false);
// 将通道注册到选择器中,并设置感兴趣的实践
server.register(selector, SelectionKey.OP_ACCEPT);
Selector监听事件,并获得就绪的通道个数,若没有通道就绪,线程会被阻塞
int count = selector.select();
阻塞直到绑定事件发生,或是超时(时间单位为 ms)
int count = selector.select(long timeout);
不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件
int count = selector.selectNow();
获取就绪事件并得到对应的通道,然后进行处理
// 获取所有事件
Set<SelectionKey> selectionKeys = selector.selectedKeys();// 使用迭代器遍历事件
Iterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();// 判断key的类型,此处为Accept类型if(key.isAcceptable()) {// 获得key对应的channelServerSocketChannel channel = (ServerSocketChannel) key.channel();// 获取连接并处理,而且是必须处理,否则需要取消SocketChannel socketChannel = channel.accept();// 处理完毕后移除iterator.remove();}
}
事件发生后,要么处理,要么取消(cancel),不能什么都不做,否则下次该事件仍会触发,这是因为 nio 底层使用的是水平触发
加粗样式
在Accept事件中,若有客户端与服务器端建立了连接,需要将其对应的SocketChannel设置为非阻塞,并注册到选择其中
添加Read事件,触发后进行读取操作
public class SelectServer {public static void main(String[] args) {ByteBuffer buffer = ByteBuffer.allocate(16);// 获得服务器通道try(ServerSocketChannel server = ServerSocketChannel.open()) {server.bind(new InetSocketAddress(8080));// 创建选择器Selector selector = Selector.open();// 通道必须设置为非阻塞模式server.configureBlocking(false);// 将通道注册到选择器中,并设置感兴趣的实践server.register(selector, SelectionKey.OP_ACCEPT);// 为serverKey设置感兴趣的事件while (true) {// 若没有事件就绪,线程会被阻塞,反之不会被阻塞。从而避免了CPU空转// 返回值为就绪的事件个数int ready = selector.select();System.out.println("selector ready counts : " + ready);// 获取所有事件Set<SelectionKey> selectionKeys = selector.selectedKeys();// 使用迭代器遍历事件Iterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();// 判断key的类型if(key.isAcceptable()) {// 获得key对应的channelServerSocketChannel channel = (ServerSocketChannel) key.channel();System.out.println("before accepting...");// 获取连接SocketChannel socketChannel = channel.accept();System.out.println("after accepting...");// 设置为非阻塞模式,同时将连接的通道也注册到选择其中socketChannel.configureBlocking(false);socketChannel.register(selector, SelectionKey.OP_READ);// 处理完毕后移除iterator.remove();} else if (key.isReadable()) {SocketChannel channel = (SocketChannel) key.channel();System.out.println("before reading...");channel.read(buffer);System.out.println("after reading...");buffer.flip();ByteBufferUtil.debugRead(buffer);buffer.clear();// 处理完毕后移除iterator.remove();}}}} catch (IOException e) {e.printStackTrace();}}
}
消息边界
不处理消息边界存在的问题:将缓冲区的大小设置为4个字节,发送2个汉字(你好),通过decode解码并打印时,会出现乱码
这是因为UTF-8字符集下,1个汉字占用3个字节,此时缓冲区大小为4个字节,一次读时间无法处理完通道中的所有数据,所以一共会触发两次读事件。这就导致 你好 的 好 字被拆分为了前半部分和后半部分发送,解码时就会出现问题
处理消息边界
附件与扩容
Channel的register方法还有第三个参数:附件,可以向其中放入一个Object类型的对象,该对象会与登记的Channel以及其对应的SelectionKey绑定,可以从SelectionKey获取到对应通道的附件
public final SelectionKey register(Selector sel, int ops, Object att)
可通过SelectionKey的attachment()方法获得附件
ByteBuffer buffer = (ByteBuffer) key.attachment();
我们需要在Accept事件发生后,将通道注册到Selector中时,对每个通道添加一个ByteBuffer附件,让每个通道发生读事件时都使用自己的通道,避免与其他通道发生冲突而导致问题
// 设置为非阻塞模式,同时将连接的通道也注册到选择其中,同时设置附件
socketChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(16);
// 添加通道对应的Buffer附件
socketChannel.register(selector, SelectionKey.OP_READ, buffer);
当Channel中的数据大于缓冲区时,需要对缓冲区进行扩容操作。此代码中的扩容的判定方法:Channel调用compact方法后,的position与limit相等,说明缓冲区中的数据并未被读取(容量太小),此时创建新的缓冲区,其大小扩大为两倍。同时还要将旧缓冲区中的数据拷贝到新的缓冲区中,同时调用SelectionKey的attach方法将新的缓冲区作为新的附件放入SelectionKey中
// 如果缓冲区太小,就进行扩容
if (buffer.position() == buffer.limit()) {ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity()*2);// 将旧buffer中的内容放入新的buffer中ewBuffer.put(buffer);// 将新buffer作为附件放到key中key.attach(newBuffer);
}
改造后的服务器代码如下
public class SelectServer {public static void main(String[] args) {// 获得服务器通道try(ServerSocketChannel server = ServerSocketChannel.open()) {server.bind(new InetSocketAddress(8080));// 创建选择器Selector selector = Selector.open();// 通道必须设置为非阻塞模式server.configureBlocking(false);// 将通道注册到选择器中,并设置感兴趣的事件server.register(selector, SelectionKey.OP_ACCEPT);// 为serverKey设置感兴趣的事件while (true) {// 若没有事件就绪,线程会被阻塞,反之不会被阻塞。从而避免了CPU空转// 返回值为就绪的事件个数int ready = selector.select();System.out.println("selector ready counts : " + ready);// 获取所有事件Set<SelectionKey> selectionKeys = selector.selectedKeys();// 使用迭代器遍历事件Iterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();// 判断key的类型if(key.isAcceptable()) {// 获得key对应的channelServerSocketChannel channel = (ServerSocketChannel) key.channel();System.out.println("before accepting...");// 获取连接SocketChannel socketChannel = channel.accept();System.out.println("after accepting...");// 设置为非阻塞模式,同时将连接的通道也注册到选择其中,同时设置附件socketChannel.configureBlocking(false);ByteBuffer buffer = ByteBuffer.allocate(16);socketChannel.register(selector, SelectionKey.OP_READ, buffer);// 处理完毕后移除iterator.remove();} else if (key.isReadable()) {SocketChannel channel = (SocketChannel) key.channel();System.out.println("before reading...");// 通过key获得附件(buffer)ByteBuffer buffer = (ByteBuffer) key.attachment();int read = channel.read(buffer);if(read == -1) {key.cancel();channel.close();} else {// 通过分隔符来分隔buffer中的数据split(buffer);// 如果缓冲区太小,就进行扩容if (buffer.position() == buffer.limit()) {ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity()*2);// 将旧buffer中的内容放入新的buffer中buffer.flip();newBuffer.put(buffer);// 将新buffer放到key中作为附件key.attach(newBuffer);}}System.out.println("after reading...");// 处理完毕后移除iterator.remove();}}}} catch (IOException e) {e.printStackTrace();}}private static void split(ByteBuffer buffer) {buffer.flip();for(int i = 0; i < buffer.limit(); i++) {// 遍历寻找分隔符// get(i)不会移动positionif (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);}}// 切换为写模式,但是缓冲区可能未读完,这里需要使用compactbuffer.compact();}
}
Write事件
服务器通过Buffer向通道中写入数据时,可能因为通道容量小于Buffer中的数据大小,导致无法一次性将Buffer中的数据全部写入到Channel中,这时便需要分多次写入,具体步骤如下
执行一次写操作,向将buffer中的内容写入到SocketChannel中,然后判断Buffer中是否还有数据
若Buffer中还有数据,则需要将SockerChannel注册到Seletor中,并关注写事件,同时将未写完的Buffer作为附件一起放入到SelectionKey中
int write = socket.write(buffer);
// 通道中可能无法放入缓冲区中的所有数据
if (buffer.hasRemaining()) {// 注册到Selector中,关注可写事件,并将buffer添加到key的附件中socket.configureBlocking(false);socket.register(selector, SelectionKey.OP_WRITE, buffer);
}
添加写事件的相关操作key.isWritable(),对Buffer再次进行写操作
每次写后需要判断Buffer中是否还有数据(是否写完)。若写完,需要移除SelecionKey中的Buffer附件,避免其占用过多内存,同时还需移除对写事件的关注
SocketChannel socket = (SocketChannel) key.channel();
// 获得buffer
ByteBuffer buffer = (ByteBuffer) key.attachment();
// 执行写操作
int write = socket.write(buffer);
System.out.println(write);
// 如果已经完成了写操作,需要移除key中的附件,同时不再对写事件感兴趣
if (!buffer.hasRemaining()) {key.attach(null);key.interestOps(0);
}
整体代码如下
public class WriteServer {public static void main(String[] args) {try(ServerSocketChannel server = ServerSocketChannel.open()) {server.bind(new InetSocketAddress(8080));server.configureBlocking(false);Selector selector = Selector.open();server.register(selector, SelectionKey.OP_ACCEPT);while (true) {selector.select();Set<SelectionKey> selectionKeys = selector.selectedKeys();Iterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();// 处理后就移除事件iterator.remove();if (key.isAcceptable()) {// 获得客户端的通道SocketChannel socket = server.accept();// 写入数据StringBuilder builder = new StringBuilder();for(int i = 0; i < 500000000; i++) {builder.append("a");}ByteBuffer buffer = StandardCharsets.UTF_8.encode(builder.toString());// 先执行一次Buffer->Channel的写入,如果未写完,就添加一个可写事件int write = socket.write(buffer);System.out.println(write);// 通道中可能无法放入缓冲区中的所有数据if (buffer.hasRemaining()) {// 注册到Selector中,关注可写事件,并将buffer添加到key的附件中socket.configureBlocking(false);socket.register(selector, SelectionKey.OP_WRITE, buffer);}} else if (key.isWritable()) {SocketChannel socket = (SocketChannel) key.channel();// 获得bufferByteBuffer buffer = (ByteBuffer) key.attachment();// 执行写操作int write = socket.write(buffer);System.out.println(write);// 如果已经完成了写操作,需要移除key中的附件,同时不再对写事件感兴趣if (!buffer.hasRemaining()) {key.attach(null);key.interestOps(0);}}}}} catch (IOException e) {e.printStackTrace();}}
}
多线程优化
创建一个负责处理Accept事件的Boss线程,与多个负责处理Read事件的Worker线程
Boss线程执行的操作
接受并处理Accepet事件,当Accept事件发生后,调用Worker的register(SocketChannel socket)方法,让Worker去处理Read事件,其中需要根据标识robin去判断将任务分配给哪个Worker
// 创建固定数量的Worker
Worker[] workers = new Worker[4];
// 用于负载均衡的原子整数
AtomicInteger robin = new AtomicInteger(0);
// 负载均衡,轮询分配Worker
workers[robin.getAndIncrement()% workers.length].register(socket);
register(SocketChannel socket)方法会通过同步队列完成Boss线程与Worker线程之间的通信,让SocketChannel的注册任务被Worker线程执行。添加任务后需要调用selector.wakeup()来唤醒被阻塞的Selector
public void register(final SocketChannel socket) throws IOException {// 只启动一次if (!started) {// 初始化操作}// 向同步队列中添加SocketChannel的注册事件// 在Worker线程中执行注册事件queue.add(new Runnable() {@Overridepublic void run() {try {socket.register(selector, SelectionKey.OP_READ);} catch (IOException e) {e.printStackTrace();}}});// 唤醒被阻塞的Selector// select类似LockSupport中的park,wakeup的原理类似LockSupport中的unparkselector.wakeup();
}
Worker线程执行的操作: 从同步队列中获取注册任务,并处理Read事件
public class ThreadsServer {public static void main(String[] args) {try (ServerSocketChannel server = ServerSocketChannel.open()) {// 当前线程为Boss线程Thread.currentThread().setName("Boss");server.bind(new InetSocketAddress(8080));// 负责轮询Accept事件的SelectorSelector boss = Selector.open();server.configureBlocking(false);server.register(boss, SelectionKey.OP_ACCEPT);// 创建固定数量的WorkerWorker[] workers = new Worker[4];// 用于负载均衡的原子整数AtomicInteger robin = new AtomicInteger(0);for(int i = 0; i < workers.length; i++) {workers[i] = new Worker("worker-"+i);}while (true) {boss.select();Set<SelectionKey> selectionKeys = boss.selectedKeys();Iterator<SelectionKey> iterator = selectionKeys.iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();iterator.remove();// BossSelector负责Accept事件if (key.isAcceptable()) {// 建立连接SocketChannel socket = server.accept();System.out.println("connected...");socket.configureBlocking(false);// socket注册到Worker的Selector中System.out.println("before read...");// 负载均衡,轮询分配Workerworkers[robin.getAndIncrement()% workers.length].register(socket);System.out.println("after read...");}}}} catch (IOException e) {e.printStackTrace();}}static class Worker implements Runnable {private Thread thread;private volatile Selector selector;private String name;private volatile boolean started = false;/*** 同步队列,用于Boss线程与Worker线程之间的通信*/private ConcurrentLinkedQueue<Runnable> queue;public Worker(String name) {this.name = name;}public void register(final SocketChannel socket) throws IOException {// 只启动一次if (!started) {thread = new Thread(this, name);selector = Selector.open();queue = new ConcurrentLinkedQueue<>();thread.start();started = true;}// 向同步队列中添加SocketChannel的注册事件// 在Worker线程中执行注册事件queue.add(new Runnable() {@Overridepublic void run() {try {socket.register(selector, SelectionKey.OP_READ);} catch (IOException e) {e.printStackTrace();}}});// 唤醒被阻塞的Selector// select类似LockSupport中的park,wakeup的原理类似LockSupport中的unparkselector.wakeup();}@Overridepublic void run() {while (true) {try {selector.select();// 通过同步队列获得任务并运行Runnable task = queue.poll();if (task != null) {// 获得任务,执行注册操作task.run();}Set<SelectionKey> selectionKeys = selector.selectedKeys();Iterator<SelectionKey> iterator = selectionKeys.iterator();while(iterator.hasNext()) {SelectionKey key = iterator.next();iterator.remove();// Worker只负责Read事件if (key.isReadable()) {// 简化处理,省略细节SocketChannel socket = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(16);socket.read(buffer);buffer.flip();ByteBufferUtil.debugAll(buffer);}}} catch (IOException e) {e.printStackTrace();}}}}
}
socket.register(selector, SelectionKey.OP_READ);和selector.wakeup(); 都是在boss线程内部执行,selector.select();(这个方法会阻塞线程的执行) 在work线程内部执行,如果work线程先执行,会堵塞线程直到selector被唤醒,然后从同步队列中获取注册任务,并处理Read事件。若是boss线程运行在work线程前面,selector.wakeup()会一直作用,selector.select()就不会堵塞了。
NIO与BIO
IO模型
零拷贝
传统 IO 问题
File f = new File("helloword/data.txt");
RandomAccessFile file = new RandomAccessFile(file, "r");
byte[] buf = new byte[(int)f.length()];
file.read(buf);
Socket socket = ...;
socket.getOutputStream().write(buf);
- Java 本身并不具备 IO 读写能力,因此 read 方法调用后,要从 Java
程序的用户态切换至内核态,去调用操作系统(Kernel)的读能力,将数据读入内核缓冲区。这期间用户线程阻塞,操作系统使用DMA(Direct Memory Access)来实现文件读,其间也不会使用 CPU。DMA 也可以理解为硬件单元,用来解放 cpu 完成文件 IO - 从内核态切换回用户态,将数据从内核缓冲区读入用户缓冲区(即 byte[] buf),这期间 CPU 会参与拷贝,无法利用 DMA
- 调用 write 方法,这时将数据从用户缓冲区(byte[] buf)写入 socket 缓冲区,CPU 会参与拷贝
- 接下来要向网卡写数据,这项能力 Java 又不具备,因此又得从用户态切换至内核态,调用操作系统的写能力,使用 DMA 将 socket 缓冲区的数据写入网卡,不会使用 CPU
NIO优化
零拷贝指的是数据无需拷贝到 JVM 内存中,同时具有以下三个优点- - 更少的用户态与内核态的切换
- 不利用 cpu 计算,减少 cpu 缓存伪共享
- 零拷贝适合小文件传输
AIO
简易聊天室1.0
客户端
package chat1;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;public class ChatClient {private Selector selector;private SocketChannel socketChannel;private static final String HOST = "localhost";private static final int PORT = 8080;public ChatClient() {try {selector = Selector.open();socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT));socketChannel.configureBlocking(false);socketChannel.register(selector, SelectionKey.OP_READ);System.out.println("欢迎来到" + HOST + ":" + PORT+"的聊天室");} catch (IOException e) {e.printStackTrace();}}public void start(){new Thread(()->{try {while (true) {if (selector.select() > 0) {for (SelectionKey key : selector.selectedKeys()) {selector.selectedKeys().remove(key);if (key.isReadable()) {readMessage();}}}}}catch (IOException e) {throw new RuntimeException(e);}}).start();try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {String input;while ((input = reader.readLine()) != null) {sendMessage(input);}} catch (IOException e) {e.printStackTrace();}}public void sendMessage(String message) throws IOException {if(message!=null && !message.trim().isEmpty()){ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());socketChannel.write(buffer);}}public void readMessage() throws IOException {ByteBuffer buffer = ByteBuffer.allocate(1024);int read= socketChannel.read(buffer);if(read>0){buffer.flip();String msg = new String(buffer.array(), 0, read);System.out.println(msg);}}public static void main(String[] args) {new ChatClient().start();}
}
服务器
package chat1;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;public class ChatServer {private Selector selector;private ServerSocketChannel serverSocketChannel;private static final int PORT = 8080;public ChatServer() {try {selector= Selector.open();serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.socket().bind(new InetSocketAddress(PORT));serverSocketChannel.configureBlocking(false);serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("聊天室已经启动了"+PORT);} catch (IOException e) {e.printStackTrace();}}public void start(){try {while (true) {if (selector.select() > 0) {Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while (iterator.hasNext()) {SelectionKey key = iterator.next();iterator.remove();handleKey(key);}}}} catch (IOException e) {e.printStackTrace();}}public void handleKey(SelectionKey key) throws IOException {if (key.isAcceptable()) {SocketChannel socketChannel = serverSocketChannel.accept();socketChannel.configureBlocking(false);socketChannel.register(selector, SelectionKey.OP_READ);System.out.println("客户端连上了"+socketChannel.getRemoteAddress());}else if (key.isReadable()) {SocketChannel socketChannel = (SocketChannel) key.channel();ByteBuffer byteBuffer = ByteBuffer.allocate(1024);int read = socketChannel.read(byteBuffer);if (read > 0) {byteBuffer.flip();String msg = new String(byteBuffer.array(), 0, read);System.out.println("客户端说: " + msg);socketChannel.write(ByteBuffer.wrap(("服务端回复: " + msg).getBytes()));}}}public static void main(String[] args) {new ChatServer().start();}
}
简易聊天室2.0
客户端
package chatover;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;public class Client {public static void main(String[] args) throws IOException {SocketChannel socketChannel = SocketChannel.open();socketChannel.configureBlocking(false);socketChannel.connect(new InetSocketAddress("localhost", 8080));// 创建一个 SelectorSelector selector = Selector.open();socketChannel.register(selector, SelectionKey.OP_CONNECT);// 从控制台读取输入并发送给服务器端Thread sendMessageThread = new Thread(() -> {try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {while (true) {System.out.println("输入客户端消息: ");String message = reader.readLine();if (socketChannel.isConnected()) {ByteBuffer buffer = ByteBuffer.wrap((message + "\n").getBytes());socketChannel.write(buffer);}}} catch (IOException e) {e.printStackTrace();}});sendMessageThread.start();while (true) {int readyChannels = selector.select();if (readyChannels == 0) {continue;}Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> keyIterator = selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();if (key.isConnectable()) {// 连接到服务器socketChannel.finishConnect();socketChannel.register(selector, SelectionKey.OP_READ);System.out.println("已连接到服务器");} else if (key.isReadable()) {// 读取服务器端消息ByteBuffer buffer = ByteBuffer.allocate(1024);int bytesRead = socketChannel.read(buffer);if (bytesRead > 0) {buffer.flip();byte[] bytes = new byte[buffer.remaining()];buffer.get(bytes);String message = new String(bytes).trim();System.out.println("服务器端消息: " + message);}}keyIterator.remove();}}}
}
服务器端
package chatover;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;public class Server {public static void main(String[] args) throws IOException {// 创建一个 ServerSocketChannelServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false);serverSocketChannel.bind(new InetSocketAddress(8080));// 创建一个 SelectorSelector selector = Selector.open();serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("聊天室服务端启动了");// 客户端连接AtomicReference<SocketChannel> clientRef = new AtomicReference<>();// 从控制台读取输入并发送给客户端Thread sendMessageThread = new Thread(() -> {try (BufferedReader reader = new BufferedReader(new InputStreamReader(System.in))) {while (true) {System.out.println("输入服务器端消息: ");String message = reader.readLine();SocketChannel client = clientRef.get();if (client != null && client.isConnected()) {ByteBuffer buffer = ByteBuffer.wrap((message + "\n").getBytes());client.write(buffer);}}} catch (IOException e) {e.printStackTrace();}});sendMessageThread.start();while (true) {int readyChannels = selector.select();if (readyChannels == 0) {continue;}Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> keyIterator = selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();if (key.isAcceptable()) {// 接受客户端连接SocketChannel client = serverSocketChannel.accept();System.out.println("客户端已连接");client.configureBlocking(false);client.register(selector, SelectionKey.OP_READ);clientRef.set(client);} else if (key.isReadable()) {// 读取客户端消息SocketChannel channel = (SocketChannel) key.channel();ByteBuffer buffer = ByteBuffer.allocate(1024);int bytesRead = channel.read(buffer);if (bytesRead > 0) {buffer.flip();byte[] bytes = new byte[buffer.remaining()];buffer.get(bytes);String message = new String(bytes).trim();System.out.println("客户端消息: " + message);}}keyIterator.remove();}}}
}
下面这篇文章写的很不棒讲异步的
Java网络编程和NIO详解5:Java 非阻塞 IO 和异步 IO
通常,我们会有一个线程池用于执行异步任务,提交任务的线程将任务提交到线程池就可以立马返回,不必等到任务真正完成。如果想要知道任务的执行结果,通常是通过传递一个回调函数的方式,任务结束后去调用这个函数。
同样的原理,Java 中的异步 IO 也是一样的,都是由一个线程池来负责执行任务,然后使用回调或自己去查询结果。
异步聊天室
客户端
package Asynchronous;import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.io.IOException;
import java.net.InetSocketAddress;public class AsynchronousClient {public static void main(String[] args) throws IOException {try {AsynchronousSocketChannel client = AsynchronousSocketChannel.open();Future<Void> connectResult = client.connect(new InetSocketAddress("localhost", 8080));connectResult.get(); //.get()会阻塞线程,等待连接完成String message = "沉默王二,在吗?";ByteBuffer buffer = ByteBuffer.wrap(message.getBytes(StandardCharsets.UTF_8));//将字符串转为字节数组Future<Integer> writeResult = client.write(buffer);//异步返回的结果writeResult.get(); // 等待发送完成System.out.println("消息发送完毕");client.close();} catch (IOException | InterruptedException | ExecutionException e) {e.printStackTrace();}}
}
服务端
package Asynchronous;import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.AsynchronousServerSocketChannel;
import java.nio.channels.AsynchronousSocketChannel;
import java.nio.channels.CompletionHandler;
import java.util.concurrent.Future;public class AsynchronousServer {public static void main(String[] args) throws IOException, InterruptedException {AsynchronousServerSocketChannel server = AsynchronousServerSocketChannel.open();server.bind(new InetSocketAddress(8080));System.out.println("服务端启动");server.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {@Overridepublic void completed(AsynchronousSocketChannel result, Object attachment) {server.accept(null, this);ByteBuffer buffer = ByteBuffer.allocate(1024);Future<Integer> read = result.read(buffer);try {read.get();buffer.flip();String message = new String(buffer.array(), 0, buffer.remaining());System.out.println("接收到的消息: " + message);} catch (Exception e) {e.printStackTrace();}}@Overridepublic void failed(Throwable exc, Object attachment) {exc.printStackTrace();}});Thread.currentThread().join();//由于服务器通常需要持续运行以接受新的连接,使用 join() 方法可以防止 Java 程序在 main 方法执行完毕后立即退出。}
}
相关文章:
NIO三大组件
现在互联网环境下,分布式系统大相径庭,而分布式系统的根基在于网络编程,而netty恰恰是java领域的网络编程的王者,如果要致力于并发高性能的服务器程序、高性能的客户端程序,必须掌握netty网络编程。 NIO基础 NIO是从ja…...
智能呼叫中心是什么?
智能呼叫中心是什么? 作者:开源智能呼叫中心系统 FreeIPCC,Github地址:https://github.com/lihaiya/freeipcc 智能呼叫中心是指运用人工智能、大数据分析等技术,对来电进行智能分析和处理的客户服务中心。以下是对智能…...
LSTM原理解读与实战
在RNN详解及其实战中,简单讨论了为什么需要RNN这类模型、RNN的具体思路、RNN的简单实现等问题。同时,在文章结尾部分我们提到了RNN存在的梯度消失问题,及之后的一个解决方案:LSTM。因此,本篇文章主要结构如下ÿ…...
24.11.26 神经网络 参数初始化
神经网络 感知神经网络 神经网络(Neural Networks)是一种模拟人脑神经元网络结构的计算模型,用于处理复杂的模式识别、分类和预测等任务 生物学: 人脑可以看做是一个生物神经网络,由众多的神经元连接而成 树突&#…...
51单片机从入门到精通:理论与实践指南(一)
单片机在智能控制领域的应用已非常普遍,发展也很迅猛,学习和使用单片机的人员越来越多。虽然新型微控制器在不断推出,但51单片机价格低廉、易学易用、性能成熟,在家电和工业控制中有一定的应用,而且学好了51单片机&…...
wordpress获取文章总数、分类总数、tag总数等
在制作wordpress模板的时候会要调用网站的文章总数分类总数tag总数等这个数值,如果直接用count查询数据库那就太过分了。好在wordpress内置了一些标签可以直接获取到这些数值,本文整理了一些常用的wordpress网站总数标签。 文章总数 <?php $count_…...
Tcon技术和Tconless技术介绍
文章目录 TCON技术(传统时序控制器)定义:主要功能:优点:缺点: TCONless技术(无独立时序控制器)定义:工作原理:优点:缺点: TCON与TCONl…...
WinFrom调用webapi接口另一个方法及其应用实例
1.调用接口方法 代码如下: public class WebAPI{#region WebAPI调用 public async Task<string> Call_Webapi(string Url, string Json) //url传入的是接口名称,json传入的是接口参数{string responseBody string.Empty; //responseBod…...
上海乐鑫科技一级代理商飞睿科技,ESP32-C61高性价比WiFi6芯片高性能、大容量
在当今快速发展的物联网市场中,无线连接技术的不断进步对智能设备的性能和能效提出了更高要求。为了满足这一需求,乐鑫科技推出了ESP32-C61——一款高性价比的Wi-Fi 6芯片,旨在为用户设备提供更出色的物联网性能,并满足智能设备连…...
【机器学习】数据集合集!
本文将为您介绍经典、热门的数据集,希望对您在选择适合的数据集时有所帮助。 1 privacy 更新时间:2024-11-26 访问地址: GitHub 描述: 此存储库包含 TensorFlow Privacy(一种 Python)的源代码 库,其中包…...
GitLab使用操作v1.0
1.前置条件 Gitlab 项目地址:http://******/req Gitlab账户信息:例如 001/******自己的分支名称:例如 001-master(注:master只有项目创建者有权限更新,我们只能更新自己分支,然后创建合并请求&…...
企业后端多租户管理平台
1 简介 此系统在企业后端管理系统上进行的更改,用于快速开发租户管理平台。项目中详细的功能请查看文章:企业后端系统通用模版_后端模板-CSDN博客 支持多租户,支持多租户切换,支持多租户数据隔离,支持多租户数据同步等…...
【模型学习之路】PyG的使用+基于点的任务
这一篇是关于PyG的基本使用 目录 前言 PyG的数据结构 演示 图的可视化 基于点的任务 任务分析 MLP GCN 前言 对图结构感兴趣的朋友可以学一下常用的有关图结构的库:networkx详细介绍 networkx 库,探讨它的基本功能、如何创建图、操作图以及其常…...
【ubuntu】数学人的环境搭建
Python 语言环境 python 的 pip 第三方库管理 sudo apt install python3-pippython 的 idle 界面 sudo apt install idle3R 语言环境 sudo apt install r-cran-zoo### RStudio 界面 ubuntu sudo snap install rstudio --classicJulia 语言环境 sudo snap install julia --…...
Android 常用命令和工具解析之Trace相关
目录 1、Perfetto基本用法 1.1 perfetto抓取命令 1.2 Perfetto主界面 1.3 Perfetto常用技巧 1.3.1 CPU的运行状态 1.3.2 CPU的频率 1.3.3 CPU的所有任务 1.3.4 判断是否低内存 1.3.5 CPU的负载计算 1.3.6 查看某进程是否运行在大核 1.3.7 CPU的大核占用率计算 2、应…...
使用IDEA构建springboot项目+整合Mybatis
目录 目录 1.Springboot简介 2.SpringBoot的工作流程 3.SpringBoot框架的搭建和配置 4.用Springboot实现一个基本的select操作 5.SpringBoot项目部署非常简单,springBoot内嵌了 Tomcat、Jetty、Undertow 三种容器,其默认嵌入的容器是 Tomcat,…...
Python学习34天
import random class Game: peo0 rob0 # # def __init__(self,peo,rob): # self.peopeo # self.robrob def Play(self): """ 石头剪刀布游戏,0代表石头,1代见到,2代表石头 …...
leetcode hot100【LeetCode 215.数组中的第K个最大元素】java实现
LeetCode 215.数组中的第K个最大元素 题目描述 给定一个整数数组 nums 和一个整数 k,请返回数组中第 k 个最大的元素。 请注意,要求排名是从大到小的,因此第 k 个最大元素是排序后的第 k 个元素。你需要设计一个高效的算法来解决这个问题。…...
科技赋能:企业如何通过新技术提升竞争力的策略与实践
引言 在当今瞬息万变的商业环境中,科技的迅猛发展正在重新定义行业的游戏规则。无论是小型企业还是跨国巨头,都感受到数字化转型的迫切需求。过去,企业竞争力更多依赖于成本控制、资源调配或市场覆盖,而如今,新技术的引…...
SAP开发语言ABAP开发入门
1. 了解ABAP开发环境和基础知识 - ABAP简介 - ABAP(Advanced Business Application Programming)是SAP系统中的编程语言,主要用于开发企业级的业务应用程序,如财务、物流、人力资源等模块的定制开发。 - 开发环境搭建 - 首先需…...
快速理解微服务中Ribbon的概念
一.基本概念 1.在微服务架构中,Ribbon 是一个客户端负载均衡器,用于控制服务间的通信方式。 2.Ribbon 是一个开源的库,最早由 Netflix 开发,用于实现客户端负载均衡。 3.Ribbon 主要解决的是在微服务架构中,多个服务…...
MTK主板_安卓主板方案_MTK联发科主板定制开发
联发科(MTK)主板以其强大的性能和多样化的功能而受到广泛关注。该平台包括多个型号,例如MT6761、MT8766、MT6762、MT6765、MT8768和MT8788等,均配置了四核或八核64位处理器,主频可高达2.0GHz。采用先进的12nm工艺,搭载Android 11.…...
Linux:文件管理(一)——文件描述符fd
目录 一、文件基础认识 二、C语言操作文件的接口 1.> 和 >> 2.理解“当前路径” 三、相关系统调用 1.open 2.文件描述符 3.一切皆文件 4.再次理解重定向 一、文件基础认识 文件 内容 属性。换句话说,如果在电脑上新建了一个空白文档࿰…...
Excel的图表使用和导出准备
目的 导出Excel图表是很多软件要求的功能之一,那如何导出Excel图表呢?或者说如何使用Excel图表。 一种方法是软件生成图片,然后把图片写到Excel上,这种方式,因为格式种种原因,导出的图片不漂亮,…...
Python + 深度学习从 0 到 1(00 / 99)
希望对你有帮助呀!!💜💜 如有更好理解的思路,欢迎大家留言补充 ~ 一起加油叭 💦 欢迎关注、订阅专栏 【深度学习从 0 到 1】谢谢你的支持! ⭐ 什么是深度学习? 人工智能、机器学习与…...
高级AI记录笔记(五)
学习位置 B站位置:红豆丨泥 UE AI 教程原作者Youtube位置:https://youtu.be/-t3PbGRazKg?siRVoaBr4476k88gct素材自备 改良近战AI格挡行为 把近战AI的格挡行为从行为树中单独一个任务分块中给删除掉,因为我们希望敌人在受到伤害后立即进行…...
uniapp vue2项目迁移vue3项目
uniapp vue2项目迁移vue3项目,必须适配的部分 一、main.js 创建应用实例 // 之前 - Vue 2 import Vue from vue import App from ./App Vue.config.productionTip false // vue3 不再需要 App.mpType app // vue3 不再需要 const app new Vue({ ...App }) …...
平安科技Java面试题及参考答案
多个线程 a++,单个线程不管别的线程怎么改变 a 的值,只管自己的 a 的值,但是只有一个对象 在 Java 中,当多个线程对同一个对象的共享变量 a 进行 a++ 操作时,如果不进行适当的同步处理,就会出现数据不一致的问题。因为 a++ 操作并非原子操作,它实际上包含了读取 a 的值、…...
Element UI 打包探索【1】
目录 第一个命令 第二个命令 node build/bin/iconInit.js node build/bin/build-entry.js node build/bin/i18n.js node build/bin/version.js 总结 最近在接触组件库的项目,所以特意拿来Element UI借鉴学习一下,它算是做前端的同学们离不开的一…...
【通俗理解】神经网络中步长缩小的奥秘:优化算法与卷积操作的影响
【通俗理解】神经网络中步长缩小的奥秘:优化算法与卷积操作的影响 关键词提炼 #神经网络 #步长缩小 #优化算法 #卷积操作 #AdaGrad #感受野 第一节:步长缩小的类比与核心概念【尽可能通俗】 在神经网络中,步长缩小就像是运动员在赛跑中逐…...
C语言:C语言实现对MySQL数据库表增删改查功能
基础DOME可以用于学习借鉴; 具体代码 #include <stdio.h> #include <mysql.h> // mysql 文件,如果配置ok就可以直接包含这个文件//宏定义 连接MySQL必要参数 #define SERVER "localhost" //或 127.0.0.1 #define USER "roo…...
2023.11 Graph-Enriched Biomedical Language Models: A Research Proposal
Proceedings of the 13th International Joint Conference on Natural Language Processing and the 3rd Conference of the Asia-Pacific Chapter of the Association for Computational Linguistics: Student Research Workshop, pages 82–92 November 1–4, 2023. ©20…...
HTML详解(1)
1.HTML定义 HTML:超文本标记语言。超文本:通过链接可以把多个网页链接到一起标记:标签,带括号的文本后缀:.html 标签语法:<strong>需加粗文字</strong> 成对出现,中间包裹内容&l…...
Taro 鸿蒙技术内幕系列(三) - 多语言场景下的通用事件系统设计
基于 Taro 打造的京东鸿蒙 APP 已跟随鸿蒙 Next 系统公测,本系列文章将深入解析 Taro 如何实现使用 React 开发高性能鸿蒙应用的技术内幕 背景 在鸿蒙生态系统中,虽然原生应用通常基于 ArkTS 实现,但在实际研发过程中发现,使用 C…...
Java图书管理系统(简易保姆级)
前面学习了这么多知识,为了巩固之前的知识,我们就要写一个图书管理系统来帮助大家复习,让大家的知识融会贯通~~~ 话不多说,直接开始今天的内容~ 首先呢,我们要有一个大体的思路: 实现效果思路有两种情况&a…...
maven 中<packaging>pom</packaging>配置使用
在 Maven 项目的 pom.xml 文件中, 元素用于指定项目的打包类型。默认情况下,如果 元素没有被显式定义,Maven 会假设其值为 jar。但是,当您设置 pom 时,这意味着该项目是一个 POM(Project Object Model&…...
uniapp使用腾讯云获取位置转为省市区
腾讯云获取位置转为省市区 腾讯位置服务提供了多种SDK程序包,其中的JavaScript版本的SDK适用于微信小程序,所以我们下载这个SDK包。 下载地址 在小程序项目中,创建lib目录,把SDK文件放入其中 <script>var QQMapWX requ…...
【git实践】分享一个适用于敏捷开发的分支管理策略
文章目录 1. 背景2. 分支管理实践2.1. 敏捷开发中分支管理面临的问题2.2. 分支管理策略2.3. 还需要注意的一些问题 3.总结 1. 背景 在实际的开发工作中,我们往往会面临多任务并行研发,多个环境管理的情况,这种情况下,一个合适的分…...
rpm方式安装postgres数据库及普通用户管理数据库
一、安装postgres 数据库 下载rpm安装包 wget https://ftp.postgresql.org/pub/repos/yum/15/redhat/rhel-7.9-x86_64/postgresql15-libs-15.5-1PGDG.rhel7.x86_64.rpm wget https://ftp.postgresql.org/pub/repos/yum/15/redhat/rhel-7.9-x86_64/postgresql15-15.5-1PGDG.rh…...
使用PgBackRest备份远程PG数据库
文章目录 环境准备总体要求1. 在 PostgreSQL 服务器上配置1.1 配置 PostgreSQL1.2 配置 SSH 密钥1.4 安装PgBackRest1.4.1 使用源码编译1.4.2 直接安装 配置文件 2. 在 PgBackRest 服务器安装PgBackRest2.1 安装 PgBackRest2.2 创建必要的目录2.3 编辑配置文件2.4 配置 SSH 密钥…...
笔记mfc11
Subclass(子类化)是MFC中最常用的窗体技术之一。子类化完成两个工作:一是把窗体类对象attach到一个windows窗体实体中(即把一个窗体的hwnd赋给该类)。另外就是把该类对象的消息加入到消息路由中,使得该类可以捕获消息。 让edit能…...
Softing线上研讨会 | Ethernet-APL:推动数字时代的过程自动化
| (免费)线上研讨会时间:2024年11月19日 16:00~16:30 / 23:00~23:30 Ethernet-APL以10Mb/s的传输速率为过程工业中的现场设备带来了无缝以太网连接和本质安全电源,这不仅革新了新建工厂,也适用于改造现有工厂。 与现…...
Spring Boot 整合 ELK 全面指南:实现日志采集、分析与可视化
一、ELK简介 1.1 什么是ELK? ELK 是三个开源工具的组合: Elasticsearch:一个分布式全文搜索和分析引擎,用于存储和查询日志数据。Logstash:一个数据处理管道工具,用于收集、解析和处理日志数据。Kibana&…...
D80【 python 接口自动化学习】- python基础之HTTP
day80 requests请求加入headers 学习日期:20241126 学习目标:http定义及实战 -- requests请求加入headers 学习笔记: requests请求加入headers import requestsurlhttps://movie.douban.com/j/search_subjects params{"type":…...
在Windows环境下打包Qt C++项目为独立可执行文件的完整指南
目录 1. 配置Qt环境变量步骤: 2. 使用Release模式编译项目步骤: 3. 使用windeployqt工具收集依赖项步骤: 4. 精简复制后的文件目录方法一:使用windeployqt的选项方法二:手动删除不必要的文件方法三:使用静态…...
已存大量数据的mysql库实现主从各种报错----解决方案(看评论)
背景何谓“先死后生”本文使用技术1、实施流程图2、实施2.1、数据库备份2.2、搭建Mysql的Master-Slave2.2.1、准备工作2.2.2、开始部署2.2.3、账号配置2.2.4、slave 同步配置2.2.5、验证 2.3、Master做数据恢复 结语 背景 计划对已有大量数据的mysql库的主从搭建,使…...
Java爬虫:深入探索1688接口的奥秘
在数字化时代,数据成为了企业最宝贵的资产之一。对于电商企业来说,获取和分析数据的能力直接关系到其市场竞争力。阿里巴巴旗下的1688平台,作为中国领先的批发贸易平台,拥有海量的商家和商品信息,成为了众多企业获取数…...
Linux——基础命令(1)
目录 一、认识Linux 终端命令格式 查阅命令帮助信息 -help 辅助操作 自动补全 清屏和查看当前工作目录 二、基本命令 文件和目录常用命令 1.ls-查看文件与目录 2.cd切换目录 (1)touc创建文件或修改文件时间 (2)mkdir创…...
Java知识及热点面试题总结(一)
今天开始对Java进行总复习,主要针对热点面试题,我们再根据试题内容延申相关知识; 1.、Java中equals与“”的区别详解 讲二者区别之前,先大概讲解一下基本数据类型和引用类型的概念,当然,有一定基础的朋友…...
基于 AI 的软件工程: 超级程序员
徐昊 《AI时代的软件工程》-极客时间课程学习总结 帮助你更好地利用 LLM 提高效率,还可以站在一个更全面的立场上,讨论如何将 LLM 引入团队或是组织。 核心观点: AI 辅助业务建模:通过将模型转化为 Mermaid 格式,将我们的模型表达为大语言模型能够理解的形式。通过添加注…...