From d6f874e7dfd2b983d6e0f84b2df844ed63a6b577 Mon Sep 17 00:00:00 2001 From: tim-qtp <2469100031@qq.com> Date: Tue, 21 Jan 2025 01:06:03 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20add:=20=E5=85=89=E7=8C=AB&?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E5=99=A8=E6=8B=A8=E5=8F=B7=20=E5=B1=80?= =?UTF-8?q?=E5=9F=9F=E7=BD=91=E5=85=B1=E4=BA=AB=20=E8=B7=AF=E7=94=B1&?= =?UTF-8?q?=E6=A1=A5=E6=8E=A5=20R2S=E5=AE=89=E8=A3=85OpenWrt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/.vuepress/sidebar/zh.ts | 6 +- ...rialization and deserialization in Java.md | 96 + src/Java/basic/10.md | 10 + src/Java/basic/11.md | 10 + src/Java/basic/12.md | 10 + src/Java/basic/13.md | 10 + src/Java/basic/14.md | 10 + src/Java/basic/15.md | 10 + src/Java/basic/16.md | 10 + src/Java/basic/17.md | 10 + src/Java/basic/18.md | 10 + src/Java/basic/2.Immutable classes in Java.md | 170 ++ src/Java/basic/3.Exception&Error.md | 64 + src/Java/basic/3.md | 10 + .../4.What are the advantages of Java.md | 28 + ...ncapsulation, inheritance, polymorphism.md | 146 ++ src/Java/basic/6.Inner Class.md | 175 ++ src/Java/basic/7.md | 10 + src/Java/basic/8.md | 10 + src/Java/basic/9.md | 10 + src/framework/netty/1.NIO.md | 2325 +++++++++++++++++ src/frontend/react/1.React Basics - Part 1.md | 1045 ++++++++ .../network/{http1-3.md => 1.http1-3.md} | 0 .../network/{tcp udp.md => 2.tcp udp.md} | 0 ...d => 3.three handshakes and four waves.md} | 0 ...Prerequisite Home Network Communication.md | 57 +- .../fundamental/network/6.DHCP protocol.md | 6 +- src/fundamental/network/7.DHCP attack.md | 12 + .../8.Home broadband Internet access.md | 29 + ...mode and bridging mode of optical modem.md | 20 + src/life/README.md | 36 +- src/opensource/tool.md | 4 +- .../scaffold/4.Md document font color.md | 73 + src/rain/1.VPN.md | 14 +- ...Prerequisite Home Network Communication.md | 196 +- .../clash/{Openclash.md => 1.Openclash.md} | 7 + src/rain/clash/2.IPLeak.md | 24 + src/rain/clash/3.LAN sharing copy 2.md | 9 + src/rain/clash/3.LAN sharing.md | 92 + src/rain/clash/4.Clash Verge Rev.md | 180 ++ .../clash/5.R2S install openwrt system.md | 122 + 41 files changed, 4976 insertions(+), 90 deletions(-) create mode 100644 src/Java/basic/1.What is serialization and deserialization in Java.md create mode 100644 src/Java/basic/10.md create mode 100644 src/Java/basic/11.md create mode 100644 src/Java/basic/12.md create mode 100644 src/Java/basic/13.md create mode 100644 src/Java/basic/14.md create mode 100644 src/Java/basic/15.md create mode 100644 src/Java/basic/16.md create mode 100644 src/Java/basic/17.md create mode 100644 src/Java/basic/18.md create mode 100644 src/Java/basic/2.Immutable classes in Java.md create mode 100644 src/Java/basic/3.Exception&Error.md create mode 100644 src/Java/basic/3.md create mode 100644 src/Java/basic/4.What are the advantages of Java.md create mode 100644 src/Java/basic/5.Encapsulation, inheritance, polymorphism.md create mode 100644 src/Java/basic/6.Inner Class.md create mode 100644 src/Java/basic/7.md create mode 100644 src/Java/basic/8.md create mode 100644 src/Java/basic/9.md create mode 100644 src/framework/netty/1.NIO.md create mode 100644 src/frontend/react/1.React Basics - Part 1.md rename src/fundamental/network/{http1-3.md => 1.http1-3.md} (100%) rename src/fundamental/network/{tcp udp.md => 2.tcp udp.md} (100%) rename src/fundamental/network/{three handshakes and four waves.md => 3.three handshakes and four waves.md} (100%) rename "src/fundamental/network/8.DHCP\345\215\217\350\256\256.md" => src/fundamental/network/6.DHCP protocol.md (93%) create mode 100644 src/fundamental/network/7.DHCP attack.md create mode 100644 src/fundamental/network/8.Home broadband Internet access.md create mode 100644 src/fundamental/network/9.Routing mode and bridging mode of optical modem.md rename src/rain/clash/{Openclash.md => 1.Openclash.md} (88%) create mode 100644 src/rain/clash/2.IPLeak.md create mode 100644 src/rain/clash/3.LAN sharing copy 2.md create mode 100644 src/rain/clash/3.LAN sharing.md create mode 100644 src/rain/clash/4.Clash Verge Rev.md create mode 100644 src/rain/clash/5.R2S install openwrt system.md diff --git a/src/.vuepress/sidebar/zh.ts b/src/.vuepress/sidebar/zh.ts index d0c1630..883efe8 100644 --- a/src/.vuepress/sidebar/zh.ts +++ b/src/.vuepress/sidebar/zh.ts @@ -329,7 +329,11 @@ export const zhSidebar = sidebar({ prefix: "clash", collapsible: true, children: [ - "Openclash" + "1.Openclash", + "2.IPLeak", + "3.LAN sharing", + "4.Clash Verge Rev", + "5.R2S install openwrt system" ], }, "1.VPN", diff --git a/src/Java/basic/1.What is serialization and deserialization in Java.md b/src/Java/basic/1.What is serialization and deserialization in Java.md new file mode 100644 index 0000000..f6a2cbf --- /dev/null +++ b/src/Java/basic/1.What is serialization and deserialization in Java.md @@ -0,0 +1,96 @@ +--- +order: 1 +author: +title: "序列化和反序列化" +category: + - Java基础 + - 序列化 + - 反序列化 + +--- + +主要是解决网络通信中对象传输的问题,网络传输的数据必须是二进制的,但是在java中都是对象,是没办法传输对象的! + +**序列化**:将Java对象转化成可传输的字节序列格式(字节流、JSON、xml),以便于传输和存储 + +**反序列化**:将字节序列化数据,里面的描述信息和状态,转化成Java对象的过程 + + + +### serialVersionUlD又有什么用? + +`private static final long serialVersionUID 1L;` + +经常会看到这样的代码,这个ID其实就是用来验证序列化的对象和反序列化对应的对象的心是否是一致的。 + +所以这个ID的数字其实不重要,无论是1L还是idea自动生成的,只要序列化的时候对象的serialVersionUID和反序列化的时候对象的serialVersionUlD一致的话就行。 + +如果没有显式指定serialVersionUlD,则编译器会根据类的相关信息自动生成一个。 + +所以如果你没有定义一个serialVersionUlD然后序列化一个对象之后,在反序列化,之前把对象的类的结构改了,比如增加了一个成员变量,则此时的反序列化会失败。 + +因为类的结构变了,所以serialVersionUlD就不一致了。所以serialVersionUlD就是起验证作用。 + + + +### Java序列化不包含静态变量 + +简单地说就是序列化之后存储的内容不包含静态变量的值,看一下下面的代码就很清晰了。 + +```java +public class Test implements Serializable { + private static final long serialVersionUID = 1L; // 序列化版本号 + public static int yes = 1; // 静态变量 + + public static void main(String[] args) { + try { + // 序列化 + ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("tim的無級攻略")); + out.writeObject(new Test()); // 将 Test 对象写入文件 + out.close(); + + // 修改静态变量的值 + Test.yes = 2; + + // 反序列化 + ObjectInputStream oin = new ObjectInputStream(new FileInputStream("tim的無級攻略")); + Test t = (Test) oin.readObject(); // 从文件中读取对象 + oin.close(); + + // 输出静态变量的值 + System.out.println(t.yes); // 输出 2,而不是 1 + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + + + +### 解释一下序列化的过程和作用? + +第一步,实现 Serializable 接口。 + +```java +public class Person implements Serializable { + private String name; + private int age; + + // 省略构造方法、getters和setters +} +``` + +第二步,使用 ObjectOutputStream 来将对象写入到输出流中。 + +```java +ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("person.ser")); +``` + +第三步,调用 ObjectOutputStream 的 writeObject 方法,将对象序列化并写入到输出流中。 + +```java +Person person = new Person("沉默王二", 18); +out.writeObject(person); +``` + diff --git a/src/Java/basic/10.md b/src/Java/basic/10.md new file mode 100644 index 0000000..8db2da5 --- /dev/null +++ b/src/Java/basic/10.md @@ -0,0 +1,10 @@ +--- +order: 2 +author: +title: "Java" +category: + - Java基础 +--- + + + diff --git a/src/Java/basic/11.md b/src/Java/basic/11.md new file mode 100644 index 0000000..8db2da5 --- /dev/null +++ b/src/Java/basic/11.md @@ -0,0 +1,10 @@ +--- +order: 2 +author: +title: "Java" +category: + - Java基础 +--- + + + diff --git a/src/Java/basic/12.md b/src/Java/basic/12.md new file mode 100644 index 0000000..8db2da5 --- /dev/null +++ b/src/Java/basic/12.md @@ -0,0 +1,10 @@ +--- +order: 2 +author: +title: "Java" +category: + - Java基础 +--- + + + diff --git a/src/Java/basic/13.md b/src/Java/basic/13.md new file mode 100644 index 0000000..8db2da5 --- /dev/null +++ b/src/Java/basic/13.md @@ -0,0 +1,10 @@ +--- +order: 2 +author: +title: "Java" +category: + - Java基础 +--- + + + diff --git a/src/Java/basic/14.md b/src/Java/basic/14.md new file mode 100644 index 0000000..8db2da5 --- /dev/null +++ b/src/Java/basic/14.md @@ -0,0 +1,10 @@ +--- +order: 2 +author: +title: "Java" +category: + - Java基础 +--- + + + diff --git a/src/Java/basic/15.md b/src/Java/basic/15.md new file mode 100644 index 0000000..8db2da5 --- /dev/null +++ b/src/Java/basic/15.md @@ -0,0 +1,10 @@ +--- +order: 2 +author: +title: "Java" +category: + - Java基础 +--- + + + diff --git a/src/Java/basic/16.md b/src/Java/basic/16.md new file mode 100644 index 0000000..8db2da5 --- /dev/null +++ b/src/Java/basic/16.md @@ -0,0 +1,10 @@ +--- +order: 2 +author: +title: "Java" +category: + - Java基础 +--- + + + diff --git a/src/Java/basic/17.md b/src/Java/basic/17.md new file mode 100644 index 0000000..8db2da5 --- /dev/null +++ b/src/Java/basic/17.md @@ -0,0 +1,10 @@ +--- +order: 2 +author: +title: "Java" +category: + - Java基础 +--- + + + diff --git a/src/Java/basic/18.md b/src/Java/basic/18.md new file mode 100644 index 0000000..8db2da5 --- /dev/null +++ b/src/Java/basic/18.md @@ -0,0 +1,10 @@ +--- +order: 2 +author: +title: "Java" +category: + - Java基础 +--- + + + diff --git a/src/Java/basic/2.Immutable classes in Java.md b/src/Java/basic/2.Immutable classes in Java.md new file mode 100644 index 0000000..178fac2 --- /dev/null +++ b/src/Java/basic/2.Immutable classes in Java.md @@ -0,0 +1,170 @@ +--- +order: 2 +author: +title: "Java中的不可变类" +category: + - Java基础 + - String +--- + +### 不可变类就是一个对象创建后字段属性无法修改的类 + +关键特征: + +1. 声明类为 final,防止子类继承。 + +2. 类的所有字段都是 private 和 final,确保它们在初始化后不能被更改。 + +3. 通过构造函数初始化所有字段。 + +4. 不提供任何修改对象状态的方法(如 setter 方法)。 + + +Java 中的经典不可变类有:String、Integer、BigDecimal、LocalDate 等。 + +String怎么就不可变了,我声明一个String,然后重新赋值,不就可以改变了吗 + +```java +String a = "过年爽死了"; +a = "找工作你就不爽了!" +System.out.println(a) +``` + +将变量重新赋值,那只是创建了一个新对象,然后将新对象的引用,赋值给了变量,之前的对象是没有受到影响的。 + +### 为什么String是不可变的呢? + +- String 类被 [final 关键字](https://javabetter.cn/oo/final.html)修饰,所以它不会有子类,这就意味着没有子类可以[重写](https://javabetter.cn/basic-extra-meal/override-overload.html)它的方法,改变它的行为。 +- String 类的数据存储在 `char[]` 数组中,而这个数组也被 final 关键字修饰了,这就表示 String 对象是没法被修改的,只要初始化一次,值就确定了。 + +```java +public final class String + implements java.io.Serializable, Comparable, CharSequence { + /** The value is used for character storage. */ + private final char value[]; +} +``` + +第一,可以保证 String 对象的安全性,避免被篡改,毕竟像密码这种隐私信息一般就是用字符串存储的。 + +```java +public class SecurityExample { + public static void main(String[] args) { + String password = "mySecretPassword"; + + authenticateUser(password); + + // 假如 String 是可变的,可能会发生以下情况 + System.out.println("Original password: " + password); // 仍然应该是 "mySecretPassword" + } + + public static void authenticateUser(String password) { + // 进行验证逻辑(这里只是举例) + if ("mySecretPassword".equals(password)) { + System.out.println("Authentication successful"); + } + + // 假如 String 是可变的,恶意代码可能会篡改字符串内容 + password.replace("mySecretPassword", "hackedPassword"); + } +} +``` + +第二,保证哈希值不会频繁变更。毕竟要经常作为[哈希表](https://javabetter.cn/collection/hashmap.html)的键值,经常变更的话,哈希表的性能就会很差劲。 + +在 String 类中,哈希值是在第一次计算时缓存的,后续对该哈希值的请求将直接使用缓存值。这有助于提高哈希表等数据结构的性能。以下是一个简单的示例,演示了字符串的哈希值缓存机制: + +```java +String text1 = "沉默王二"; +String text2 = "沉默王二"; + +// 计算字符串 text1 的哈希值,此时会进行计算并缓存哈希值 +int hashCode1 = text1.hashCode(); +System.out.println("第一次计算 text1 的哈希值: " + hashCode1); + +// 再次计算字符串 text1 的哈希值,此时直接返回缓存的哈希值 +int hashCode1Cached = text1.hashCode(); +System.out.println("第二次计算: " + hashCode1Cached); + +// 计算字符串 text2 的哈希值,由于字符串常量池的存在,实际上 text1 和 text2 指向同一个字符串对象 +// 所以这里直接返回缓存的哈希值 +int hashCode2 = text2.hashCode(); +System.out.println("text2 直接使用缓存: " + hashCode2); +``` + +在这个示例中,创建了两个具有相同内容的字符串 text1 和 text2。首次计算 text1 的哈希值时,会进行实际计算并缓存该值。当再次计算 text1 的哈希值或计算具有相同内容的 text2 的哈希值时,将直接返回缓存的哈希值,而不进行重新计算。 + +由于 String 对象是不可变的,其哈希值在创建后不会发生变化。这使得 String 类可以缓存哈希值,提高哈希表等数据结构的性能。如果 String 是可变的,那么在每次修改时都需要重新计算哈希值,这会降低性能。 + +第三,可以实现[字符串常量池](https://javabetter.cn/string/constant-pool.html),Java 会将相同内容的字符串存储在字符串常量池中。这样,具有相同内容的字符串变量可以指向同一个 String 对象,节省内存空间。 + +“由于字符串的不可变性,String 类的一些方法实现最终都返回了新的字符串对象。” + +“就拿 `substring()` 方法来说。” + +```java +public String substring(int beginIndex) { + if (beginIndex < 0) { + throw new StringIndexOutOfBoundsException(beginIndex); + } + int subLen = value.length - beginIndex; + if (subLen < 0) { + throw new StringIndexOutOfBoundsException(subLen); + } + return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); +} +``` + +`substring()` 方法用于截取字符串,最终返回的都是 new 出来的新字符串对象。 + +“还有 `concat()` 方法。” + +```java +public String concat(String str) { + int olen = str.length(); + if (olen == 0) { + return this; + } + if (coder() == str.coder()) { + byte[] val = this.value; + byte[] oval = str.value; + int len = val.length + oval.length; + byte[] buf = Arrays.copyOf(val, len); + System.arraycopy(oval, 0, buf, val.length, oval.length); + return new String(buf, coder); + } + int len = length(); + byte[] buf = StringUTF16.newBytesFor(len + olen); + getBytes(buf, 0, UTF16); + str.getBytes(buf, len, UTF16); + return new String(buf, UTF16); +} +``` + +`concat()` 方法用于拼接字符串,不管编码是否一致,最终也返回的是新的字符串对象。 + +`replace()` 替换方法其实也一样。 + +这就意味着,不管是截取、拼接,还是替换,都不是在原有的字符串上进行的,而是重新生成了新的字符串对象。也就是说,这些操作执行过后,**原来的字符串对象并没有发生改变**。 + + + +### 如何实现一个不可变类? + +看String就知道了 + +```java +public final class String + implements java.io.Serializable, Comparable, CharSequence { + /** The value is used for character storage. */ + private final char value[]; +} +``` + +String类用final修饰,表示无法被继承; + +String本身是一个char数组,然后用final修饰,不过final只能限制引用不可变,限制不了数组内部的数据,所以还不够; + +所以 `value` 是用 `private` 修饰的,并且没有暴露出 `set` 方法,这样外部其实就接触不到 `value`,所以无法修改。 + +当然还是有修改的需求,比如 `replace` 方法,所以这时候就需要返回一个新对象来作为结果。 \ No newline at end of file diff --git a/src/Java/basic/3.Exception&Error.md b/src/Java/basic/3.Exception&Error.md new file mode 100644 index 0000000..39c9b6f --- /dev/null +++ b/src/Java/basic/3.Exception&Error.md @@ -0,0 +1,64 @@ +--- +order: 3 +author: +title: "Exception和Error的区别" +category: + - Java基础 + - Exception +--- + +在 Java 中,`Exception` 和 `Error` 都是 `Throwable` 类的子类(只有继承了 `Throwable` 类的实例才可以被 `throw` 或者被 `catch`)。它们表示在程序运行时发生的异常或错误情况。 + +总结来看:`Exception` 表示**可以被处理**的程序异常,`Error` 表示系统级的**不可恢复错误**。 + +详细说明: + +1) **Exception**:表示程序中可以处理的异常情况,通常是由于程序逻辑或外部环境中的问题引起的,可以通过代码进行恢复或处理。 + +常见的子类有:`IOException`、`SQLException`、`NullPointerException`、`IndexOutOfBoundsException` 等。 + +`Exception` 又分为 **Checked Exception**(编译期异常)和 **Unchecked Exception**(运行时异常)。 + +- **Checked Exception**:在编译时必须进行处理(例如使用 `try-catch` 块或通过 `throws` 声明抛出),例如 `IOException`。 +- **Unchecked Exception**:运行时异常,不需要显式处理,常见的如 `NullPointerException`、`IllegalArgumentException` 等,继承自 `RuntimeException`。 + +2) **Error**:表示严重的错误,通常是 JVM 层次内系统级的、无法预料的错误,程序无法通过代码进行处理或恢复。例如内存耗尽(`OutOfMemoryError`)、栈溢出(`StackOverflowError`)。`Error` 不应该被程序捕获或处理,因为一般出现这种错误时,程序无法继续运行。 + +这个解释清晰地说明了 Java 中 `Exception` 和 `Error` 的区别,强调了它们在 Java 应用程序中的角色以及如何处理它们。 + + + +**异常处理时需要注意的点:** + +#### 1. **尽量不要捕获通用的异常(如 `Exception`)** + +- 例如,只捕获 `IOException` 或 `SQLException`,而不是捕获所有类型的异常。 + +```java +try { + // 执行文件操作 +} catch (IOException e) { + // 处理特定的IO异常 +} catch (Exception e) { + // 如果真需要捕获一般异常,可以放在最后,捕获未知错误 +} +``` + +### 2. logger.error() 做日志保存 + +- 捕获异常后,至少记录异常信息(例如日志),甚至可以重新抛出异常以便让上层调用者继续处理。 +- `e.printStackTrace()` 只是打印,不会进行日志保存。 + +```java +try { + // 执行可能抛出异常的代码 +} catch (IOException e) { + // 记录异常信息 + logger.error("IO Error occurred", e); + // 重新抛出异常或做适当处理 + throw e; +} +``` + + + diff --git a/src/Java/basic/3.md b/src/Java/basic/3.md new file mode 100644 index 0000000..9befb5f --- /dev/null +++ b/src/Java/basic/3.md @@ -0,0 +1,10 @@ +--- +order: 3 +author: +title: "Java" +category: + - Java基础 +--- + + + diff --git a/src/Java/basic/4.What are the advantages of Java.md b/src/Java/basic/4.What are the advantages of Java.md new file mode 100644 index 0000000..8b718b2 --- /dev/null +++ b/src/Java/basic/4.What are the advantages of Java.md @@ -0,0 +1,28 @@ +--- +order: 4 +author: +title: "Java的优势是什么" +category: + - Java基础 +--- + +**跨平台、垃圾回收、生态、面向对象** + +### 跨平台 + +不同平台执行的机器码是不一样的,而Java因为加了一层JVM,所以可以做到一次编写多端运行, + +### 垃圾回收 + +- 自动内存管理:无需手动分配和释放内存,减少内存泄漏和指针错误的风险。 +- 提高开发效率:开发者可专注于业务逻辑,无需关心内存回收细节。 +- 通过分代回收机制(如年轻代、老年代),提升内存分配和回收效率。GC 会在对象不再被引用时清理,防止悬空指针问题。 +- 通过定期回收无用对象,降低因内存耗尽导致程序崩溃的风险,提高稳定性。 + +### 生态 + +生态圈太大了,各种领域都有:web,大数据,安卓。社区非常庞大,第三方那个类库,企业级框架,各种中间件, + +### 面向对象 + +支持封装、继承、多态,具有清晰的类、对象、接口这些概念 \ No newline at end of file diff --git a/src/Java/basic/5.Encapsulation, inheritance, polymorphism.md b/src/Java/basic/5.Encapsulation, inheritance, polymorphism.md new file mode 100644 index 0000000..0d823ab --- /dev/null +++ b/src/Java/basic/5.Encapsulation, inheritance, polymorphism.md @@ -0,0 +1,146 @@ +--- +order: 5 +author: +title: "封装、继承、多态" +category: + - Java基础 +--- + +### 封装(Encapsulation): + +封装是将数据和方法包装在一个单元内的概念。通过使用访问修饰符(public、private、protected等),可以限制对类内部的访问,仅提供对外部的有限接口。你只需要调用类或者说库中的某个方法,传入正确的参数,即可让方法运行,达到你想要的结果,至于方法内部进行了怎样的操作,你不知道,也无需知道,这就是封装。 + +有啥好处呢: + +1. **数据隐藏**:内部数据设为私有(private),防止外部直接访问和修改,从而保护数据的完整性和安全性。 +2. get,set方法可以实现对成员**更精准的控制**。 + +### 继承: + +允许新创建的类继承现有类的属性和方法,促进代码重用 + +子类继承父类后,就拥有父类的非私有的**属性和方法**。 + +### 单继承 + +如下: + +![单继承:在类层次结构上比较清晰](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/extends-bigsai-bf43b473-4a05-4727-a543-c4edd44e5437.png) + +```java +class Animal +{ + public int id; + public String name; + public int age; + public int weight; + + public Animal(int id, String name, int age, int weight) { + this.id = id; + this.name = name; + this.age = age; + this.weight = weight; + } + //这里省略get set方法 + public void sayHello() + { + System.out.println("hello"); + } + public void eat() + { + System.out.println("I'm eating"); + } + public void sing() + { + System.out.println("sing"); + } +} +``` + +而 Dog,Cat,Chicken 类可以这样设计: + +```java +class Dog extends Animal//继承animal +{ + public Dog(int id, String name, int age, int weight) { + super(id, name, age, weight);//调用父类构造方法 + } +} +class Cat extends Animal{ + + public Cat(int id, String name, int age, int weight) { + super(id, name, age, weight);//调用父类构造方法 + } +} +class Chicken extends Animal{ + + public Chicken(int id, String name, int age, int weight) { + super(id, name, age, weight);//调用父类构造方法 + } + //鸡下蛋 + public void layEggs() + { + System.out.println("我是老母鸡下蛋啦,咯哒咯!咯哒咯!"); + } +} +``` + +各自的类继承 Animal 后可以直接使用 Animal 类的属性和方法而不需要重复编写,各个类如果有自己的方法也可很容易地拓展。 + +#### 多继承 + +一个子类有多个直接的父类。这样做的好处是子类拥有所有父类的特征,**子类的丰富度很高,但是缺点就是容易造成混乱**。下图为一个混乱的例子。 + +![多继承容易造成混乱](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/extends-bigsai-ab4c9fef-63be-4bba-a871-7e5fb9bf711a.png) + +Java 虽然不支持多继承,但是 Java 有三种实现多继承效果的方式,**分别是**内部类、多层继承和实现接口。 + + + + + +##### implements 关键字 + +使用 implements 关键字可以变相使 Java 拥有多继承的特性,一个类可以实现多个接口(接口与接口之间用逗号分开)。 + +```java +interface doA{ + void sayHello(); +} +interface doB{ + void eat(); + //以下会报错 接口中的方法不能具体定义只能声明 + //public void eat(){System.out.println("eating");} +} +class Cat2 implements doA,doB{ + @Override//必须重写接口内的方法 + public void sayHello() { + System.out.println("hello!"); + } + @Override + public void eat() { + System.out.println("I'm eating"); + } +} +public class test2 { + public static void main(String[] args) { + Cat2 cat=new Cat2(); + cat.sayHello(); + cat.eat(); + } +} +``` + +Cat 类实现 doA 和 doB 接口的时候,需要实现其声明的方法 + + + + + +### 多态: + +多态是同一个行为具有多个不同表现形式或形态的能力。Java语言中含有方法重载与对象多态两种形式的多态: + +- 1.**方法重载**:在一个类中,允许多个方法使用同一个名字,但方法的参数不同,完成的功能也不同。 +- 2.**对象多态**:子类对象可以与父类对象进行转换,而且根据其使用的子类不同完成的功能也不同(重写父类的方法)。 + diff --git a/src/Java/basic/6.Inner Class.md b/src/Java/basic/6.Inner Class.md new file mode 100644 index 0000000..5af6eaf --- /dev/null +++ b/src/Java/basic/6.Inner Class.md @@ -0,0 +1,175 @@ +--- +order: 6 +author: +title: "内部类" +category: + - Java基础 +--- + +### 1、成员内部类 + +成员内部类是最常见的内部类,看下面的代码: + +```java +class Wanger { + int age = 18; + + class Wangxiaoer { + int age = 81; + } +} +``` + +看起来内部类 Wangxiaoer 就好像 Wanger 的一个成员,成员内部类可以无限制访问外部类的所有成员属性。 + +```java +public class Wanger { + int age = 18; + private String name = "沉默王二"; + static double money = 1; + + class Wangxiaoer { + int age = 81; + + public void print() { + System.out.println(name); + System.out.println(money); + } + } +} +``` + +内部类可以随心所欲地访问外部类的成员,但外部类想要访问内部类的成员,就不那么容易了,必须先创建一个成员内部类的对象,再通过这个对象来访问: + +```java +public class Wanger { + int age = 18; + private String name = "沉默王二"; + static double money = 1; + + public Wanger () { + new Wangxiaoer().print(); + } + + class Wangxiaoer { + int age = 81; + + public void print() { + System.out.println(name); + System.out.println(money); + } + } +} +``` + +这也就意味着,如果想要在静态方法中访问成员内部类的时候,就必须先得创建一个外部类的对象,因为内部类是依附于外部类的。 + +```java +public class Wanger { + int age = 18; + private String name = "沉默王二"; + static double money = 1; + + public Wanger () { + new Wangxiaoer().print(); + } + + public static void main(String[] args) { + Wanger wanger = new Wanger(); + Wangxiaoer xiaoer = wanger.new Wangxiaoer(); + xiaoer.print(); + } + + class Wangxiaoer { + int age = 81; + + public void print() { + System.out.println(name); + System.out.println(money); + } + } +} +``` + +这种创建内部类的方式在实际开发中并不常用,因为内部类和外部类紧紧地绑定在一起,使用起来非常不便。 + +### 2、局部内部类 + +局部内部类是定义在一个方法或者一个作用域里面的类,所以局部内部类的生命周期仅限于作用域内。 + +```java +public class Wangsan { + public Wangsan print() { + class Wangxiaosan extends Wangsan{ + private int age = 18; + } + return new Wangxiaosan(); + } +} +``` + +局部内部类就好像一个局部变量一样,它是不能被权限修饰符修饰的,比如说 public、protected、private 和 static 等。 + +![img](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/inner-class-26fc0242-134a-4588-a52d-7da962fc3fb9.png) + +### [**3)匿名内部类**](https://javabetter.cn/oo/inner-class.html#_3-匿名内部类) + +匿名内部类是我们平常用得最多的,尤其是启动多线程的时候,会经常用到,并且 IDE 也会帮我们自动生成。 + + + +``` +public class ThreadDemo { + public static void main(String[] args) { + Thread t = new Thread(new Runnable() { + @Override + public void run() { + System.out.println(Thread.currentThread().getName()); + } + }); + t.start(); + } +} +``` + +匿名内部类就好像一个方法的参数一样,用完就没了,以至于我们都不需要为它专门写一个构造方法,它的名字也是由系统自动命名的。仔细观察编译后的字节码文件也可以发现,匿名内部类连名字都不配拥有,哈哈,直接借用的外部类,然后 `$1` 就搞定了。 + +![img](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/inner-class-c0b9bdf5-cb12-45fc-b362-cb14d5d44fdc.png) + +匿名内部类是唯一一种没有构造方法的类。就上面的写法来说,匿名内部类也不允许我们为其编写构造方法,因为它就像是直接通过 new 关键字创建出来的一个对象。 + +匿名内部类的作用主要是用来继承其他类或者实现接口,并不需要增加额外的方法,方便对继承的方法进行实现或者重写。 + +### [**4)静态内部类**](https://javabetter.cn/oo/inner-class.html#_4-静态内部类) + +静态内部类和成员内部类类似,只是多了一个 [static 关键字](https://javabetter.cn/oo/static.html)。 + + + +``` +public class Wangsi { + static int age; + double money; + + static class Wangxxiaosi { + public Wangxxiaosi (){ + System.out.println(age); + } + } +} +``` + +由于 static 关键字的存在,静态内部类是不允许访问外部类中非 static 的变量和方法的,这一点也非常好理解:你一个静态的内部类访问我非静态的成员变量干嘛? + +![img](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/inner-class-69523196-37fe-43c6-a52e-5a8c94fdd2d8.png) + +“为什么要使用内部类呢?”三妹问。 + +三妹这个问题问的非常妙,是时候引经据典了。 + +在《Think in java》中有这样一句话: + +> 使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。 + +在我们程序设计中有时候会存在一些使用接口很难解决的问题,这个时候我们可以利用内部类提供的、可以继承多个具体的或者抽象的类的能力来解决这些程序设计问题。可以这样说,接口只是解决了部分问题,而内部类使得多重继承的解决方案变得更加完整。 + diff --git a/src/Java/basic/7.md b/src/Java/basic/7.md new file mode 100644 index 0000000..b231e13 --- /dev/null +++ b/src/Java/basic/7.md @@ -0,0 +1,10 @@ +--- +order: 7 +author: +title: "Java" +category: + - Java基础 +--- + + + diff --git a/src/Java/basic/8.md b/src/Java/basic/8.md new file mode 100644 index 0000000..8db2da5 --- /dev/null +++ b/src/Java/basic/8.md @@ -0,0 +1,10 @@ +--- +order: 2 +author: +title: "Java" +category: + - Java基础 +--- + + + diff --git a/src/Java/basic/9.md b/src/Java/basic/9.md new file mode 100644 index 0000000..8db2da5 --- /dev/null +++ b/src/Java/basic/9.md @@ -0,0 +1,10 @@ +--- +order: 2 +author: +title: "Java" +category: + - Java基础 +--- + + + diff --git a/src/framework/netty/1.NIO.md b/src/framework/netty/1.NIO.md new file mode 100644 index 0000000..9221335 --- /dev/null +++ b/src/framework/netty/1.NIO.md @@ -0,0 +1,2325 @@ +--- +order: 1 +author: +title: "NIO" +category: + - NIO + - 网络编程 +--- + +现在的互联网环境下,分布式系统大行其道,而分布式系统的根基在于网络编程,而Netty恰恰是Java领域网络 +编程的王者。如果要致力于开发高性能的服务器程序、高性能的客户端程序,必须掌握Netty。 + +**路线**: + +第一部分是NIO编程、NIO的Selector、ByteBuffer和Channel三大组件。 +第二部分进入Netty入门学习,会介绍EventLoop、Channel、Future、.Pipeline、Handler、ByteBuf等重 +要组件,同样能回答你以前琢磨不清的一些问题 +第三部分是Nty进阶学习,会介绍粘包半包的解决方法、协议的设计、序列化知识,使用聊天室案例将这些 +知识点串起来。 +第四部分是Netty常见参数的学习以及优化 +第五部分是源码分析,这里的源码分析侧重与Netty的服务器启动、建立连接、读取数据、EventLoop处理 +事件的流程,不牵扯更多组件的源码 + +**学习目的**: + +- 使用Netty开发基本网络应用程序 +- 彻底理解阻塞、非阻塞的区别,并跟Ntty、NIO的编码联系起来 +- 懂得多路复用在服务器开发时的优势,为什么在此基础上还要加多线程 +- Netty中是如何实现异步的,异步处理的优势是什么 +- Netty中是如何管理线程的,EventLoop如何运作 +- Netty中是如何管理内存的,ByteBuf特点和分配时机看源码、调试的一些技巧,让自己也能去看、去 +- 跟源码 + + + +# 一. NIO 基础 + +non-blocking io 非阻塞 IO + +## 1. 三大组件 + +### 1.1 Channel & Buffer + +channel 有一点类似于 stream,它就是读写数据的**双向通道**,可以从 channel 将数据读入 buffer,也可以将 buffer 的数据写入 channel,而之前的 stream 要么是输入,要么是输出,channel 比 stream 更为底层 + +```mermaid +graph LR +channel --> buffer +buffer --> channel +``` + +常见的 Channel 有 + +* FileChannel +* DatagramChannel +* SocketChannel +* ServerSocketChannel + + + +buffer 则用来缓冲读写数据,常见的 buffer 有 + +* ByteBuffer + * MappedByteBuffer + * DirectByteBuffer + * HeapByteBuffer +* ShortBuffer +* IntBuffer +* LongBuffer +* FloatBuffer +* DoubleBuffer +* CharBuffer + + + +### 1.2 Selector + +selector 单从字面意思不好理解,需要结合服务器的设计演化来理解它的用途 + +#### 多线程版设计 + +```mermaid +graph TD +subgraph 多线程版 +t1(thread) --> s1(socket1) +t2(thread) --> s2(socket2) +t3(thread) --> s3(socket3) +end +``` + +#### ⚠️ 多线程版缺点 + +* 内存占用高 +* 线程上下文切换成本高 +* 只适合连接数少的场景 + + + + + + + +#### 线程池版设计 + +```mermaid +graph TD +subgraph 线程池版 +t4(thread) --> s4(socket1) +t5(thread) --> s5(socket2) +t4(thread) -.-> s6(socket3) +t5(thread) -.-> s7(socket4) +end +``` + +#### ⚠️ 线程池版缺点 + +* 阻塞模式下,线程仅能处理一个 socket 连接 +* 仅适合短连接场景 + + + + + + + + + +#### selector 版设计 + +selector 的作用就是配合一个线程来管理多个 channel,获取这些 channel 上发生的事件,这些 channel 工作在非阻塞模式下,不会让线程吊死在一个 channel 上。适合连接数特别多,但流量低的场景(low traffic) + +```mermaid +graph TD +subgraph selector 版 +thread --> selector +selector --> c1(channel) +selector --> c2(channel) +selector --> c3(channel) +end +``` + + + +调用 selector 的 select() 会阻塞直到 channel 发生了读写就绪事件,这些事件发生,select 方法就会返回这些事件交给 thread 来处理 + + + + + + + +## 2. ByteBuffer + +有一普通文本文件 data.txt,内容为 + +``` +1234567890abcd +``` + +使用 FileChannel 来读取文件内容 + +```java +@Slf4j +public class ChannelDemo1 { + public static void main(String[] args) { + try (RandomAccessFile file = new RandomAccessFile("helloword/data.txt", "rw")) { + FileChannel channel = file.getChannel(); + ByteBuffer buffer = ByteBuffer.allocate(10); + do { + // 向 buffer 写入 + int len = channel.read(buffer); + log.debug("读到字节数:{}", len); + if (len == -1) { + break; + } + // 切换 buffer 读模式 + buffer.flip(); + while(buffer.hasRemaining()) { + log.debug("{}", (char)buffer.get()); + } + // 切换 buffer 写模式 + buffer.clear(); + } while (true); + } catch (IOException e) { + e.printStackTrace(); + } + } +} +``` + +输出 + +``` +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 +``` + + + +### 2.1 ByteBuffer 正确使用姿势 + +1. 向 buffer 写入数据,例如调用 channel.read(buffer) +2. 调用 flip() 切换至**读模式** +3. 从 buffer 读取数据,例如调用 buffer.get() +4. 调用 clear() 或 compact() 切换至**写模式** +5. 重复 1~4 步骤 + + + +### 2.2 ByteBuffer 结构 + +ByteBuffer 有以下重要属性 + +* capacity +* position +* limit + +一开始 + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/0021.png) + +写模式下,position 是写入位置,limit 等于容量,下图表示写入了 4 个字节后的状态 + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/0018.png) + +flip 动作发生后,position 切换为读取位置,limit 切换为读取限制 + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/0019.png) + +读取 4 个字节后,状态 + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/0020.png) + +clear 动作发生后,状态 + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/0021.png) + +compact 方法,是把未读完的部分向前压缩,然后切换至写模式 + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/0022.png) + + + +#### 💡 调试工具类 + +```java +public class ByteBufferUtil { + private static final char[] BYTE2CHAR = new char[256]; + private static final char[] HEXDUMP_TABLE = new char[256 * 4]; + private static final String[] HEXPADDING = new String[16]; + private static final String[] HEXDUMP_ROWPREFIXES = new String[65536 >>> 4]; + private static final String[] BYTE2HEX = new String[256]; + private static final String[] BYTEPADDING = new String[16]; + + static { + final char[] DIGITS = "0123456789abcdef".toCharArray(); + for (int i = 0; i < 256; i++) { + HEXDUMP_TABLE[i << 1] = DIGITS[i >>> 4 & 0x0F]; + HEXDUMP_TABLE[(i << 1) + 1] = DIGITS[i & 0x0F]; + } + + int i; + + // Generate the lookup table for hex dump paddings + for (i = 0; i < HEXPADDING.length; i++) { + int padding = HEXPADDING.length - i; + StringBuilder buf = new StringBuilder(padding * 3); + for (int j = 0; j < padding; j++) { + buf.append(" "); + } + HEXPADDING[i] = buf.toString(); + } + + // Generate the lookup table for the start-offset header in each row (up to 64KiB). + for (i = 0; i < HEXDUMP_ROWPREFIXES.length; i++) { + StringBuilder buf = new StringBuilder(12); + buf.append(NEWLINE); + buf.append(Long.toHexString(i << 4 & 0xFFFFFFFFL | 0x100000000L)); + buf.setCharAt(buf.length() - 9, '|'); + buf.append('|'); + HEXDUMP_ROWPREFIXES[i] = buf.toString(); + } + + // Generate the lookup table for byte-to-hex-dump conversion + for (i = 0; i < BYTE2HEX.length; i++) { + BYTE2HEX[i] = ' ' + StringUtil.byteToHexStringPadded(i); + } + + // Generate the lookup table for byte dump paddings + for (i = 0; i < BYTEPADDING.length; i++) { + int padding = BYTEPADDING.length - i; + StringBuilder buf = new StringBuilder(padding); + for (int j = 0; j < padding; j++) { + buf.append(' '); + } + BYTEPADDING[i] = buf.toString(); + } + + // Generate the lookup table for byte-to-char conversion + for (i = 0; i < BYTE2CHAR.length; i++) { + if (i <= 0x1f || i >= 0x7f) { + BYTE2CHAR[i] = '.'; + } else { + BYTE2CHAR[i] = (char) i; + } + } + } + + /** + * 打印所有内容 + * @param buffer + */ + public static void debugAll(ByteBuffer buffer) { + int oldlimit = buffer.limit(); + buffer.limit(buffer.capacity()); + StringBuilder origin = new StringBuilder(256); + appendPrettyHexDump(origin, buffer, 0, buffer.capacity()); + System.out.println("+--------+-------------------- all ------------------------+----------------+"); + System.out.printf("position: [%d], limit: [%d]\n", buffer.position(), oldlimit); + System.out.println(origin); + buffer.limit(oldlimit); + } + + /** + * 打印可读取内容 + * @param buffer + */ + public static void debugRead(ByteBuffer buffer) { + StringBuilder builder = new StringBuilder(256); + appendPrettyHexDump(builder, buffer, buffer.position(), buffer.limit() - buffer.position()); + System.out.println("+--------+-------------------- read -----------------------+----------------+"); + System.out.printf("position: [%d], limit: [%d]\n", buffer.position(), buffer.limit()); + System.out.println(builder); + } + + private static void appendPrettyHexDump(StringBuilder dump, ByteBuffer buf, int offset, int length) { + if (isOutOfBounds(offset, length, buf.capacity())) { + throw new IndexOutOfBoundsException( + "expected: " + "0 <= offset(" + offset + ") <= offset + length(" + length + + ") <= " + "buf.capacity(" + buf.capacity() + ')'); + } + if (length == 0) { + return; + } + dump.append( + " +-------------------------------------------------+" + + NEWLINE + " | 0 1 2 3 4 5 6 7 8 9 a b c d e f |" + + NEWLINE + "+--------+-------------------------------------------------+----------------+"); + + final int startIndex = offset; + final int fullRows = length >>> 4; + final int remainder = length & 0xF; + + // Dump the rows which have 16 bytes. + for (int row = 0; row < fullRows; row++) { + int rowStartIndex = (row << 4) + startIndex; + + // Per-row prefix. + appendHexDumpRowPrefix(dump, row, rowStartIndex); + + // Hex dump + int rowEndIndex = rowStartIndex + 16; + for (int j = rowStartIndex; j < rowEndIndex; j++) { + dump.append(BYTE2HEX[getUnsignedByte(buf, j)]); + } + dump.append(" |"); + + // ASCII dump + for (int j = rowStartIndex; j < rowEndIndex; j++) { + dump.append(BYTE2CHAR[getUnsignedByte(buf, j)]); + } + dump.append('|'); + } + + // Dump the last row which has less than 16 bytes. + if (remainder != 0) { + int rowStartIndex = (fullRows << 4) + startIndex; + appendHexDumpRowPrefix(dump, fullRows, rowStartIndex); + + // Hex dump + int rowEndIndex = rowStartIndex + remainder; + for (int j = rowStartIndex; j < rowEndIndex; j++) { + dump.append(BYTE2HEX[getUnsignedByte(buf, j)]); + } + dump.append(HEXPADDING[remainder]); + dump.append(" |"); + + // Ascii dump + for (int j = rowStartIndex; j < rowEndIndex; j++) { + dump.append(BYTE2CHAR[getUnsignedByte(buf, j)]); + } + dump.append(BYTEPADDING[remainder]); + dump.append('|'); + } + + dump.append(NEWLINE + + "+--------+-------------------------------------------------+----------------+"); + } + + private static void appendHexDumpRowPrefix(StringBuilder dump, int row, int rowStartIndex) { + if (row < HEXDUMP_ROWPREFIXES.length) { + dump.append(HEXDUMP_ROWPREFIXES[row]); + } else { + dump.append(NEWLINE); + dump.append(Long.toHexString(rowStartIndex & 0xFFFFFFFFL | 0x100000000L)); + dump.setCharAt(dump.length() - 9, '|'); + dump.append('|'); + } + } + + public static short getUnsignedByte(ByteBuffer buffer, int index) { + return (short) (buffer.get(index) & 0xFF); + } +} +``` + + + +### 2.3 ByteBuffer 常见方法 + +#### 分配空间 + +可以使用 allocate 方法为 ByteBuffer 分配空间,其它 buffer 类也有该方法 + +```java +Bytebuffer buf = ByteBuffer.allocate(16); +``` + + + +#### 向 buffer 写入数据 + +有两种办法 + +* 调用 channel 的 read 方法 +* 调用 buffer 自己的 put 方法 + +```java +int readBytes = channel.read(buf); +``` + +和 + +```java +buf.put((byte)127); +``` + + + +#### 从 buffer 读取数据 + +同样有两种办法 + +* 调用 channel 的 write 方法 +* 调用 buffer 自己的 get 方法 + +```java +int writeBytes = channel.write(buf); +``` + +和 + +```java +byte b = buf.get(); +``` + +get 方法会让 position 读指针向后走,如果想重复读取数据 + +* 可以调用 rewind 方法将 position 重新置为 0 +* 或者调用 get(int i) 方法获取索引 i 的内容,它不会移动读指针 + + + +#### mark 和 reset + +mark 是在读取时,做一个标记,即使 position 改变,只要调用 reset 就能回到 mark 的位置 + +> **注意** +> +> rewind 和 flip 都会清除 mark 位置 + + + +#### 字符串与 ByteBuffer 互转 + +```java +ByteBuffer buffer1 = StandardCharsets.UTF_8.encode("你好"); +ByteBuffer buffer2 = Charset.forName("utf-8").encode("你好"); + +debug(buffer1); +debug(buffer2); + +CharBuffer buffer3 = StandardCharsets.UTF_8.decode(buffer1); +System.out.println(buffer3.getClass()); +System.out.println(buffer3.toString()); +``` + +输出 + +``` + +-------------------------------------------------+ + | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ++--------+-------------------------------------------------+----------------+ +|00000000| e4 bd a0 e5 a5 bd |...... | ++--------+-------------------------------------------------+----------------+ + +-------------------------------------------------+ + | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ++--------+-------------------------------------------------+----------------+ +|00000000| e4 bd a0 e5 a5 bd |...... | ++--------+-------------------------------------------------+----------------+ +class java.nio.HeapCharBuffer +你好 +``` + + + +#### ⚠️ Buffer 的线程安全 + +> Buffer 是**非线程安全的** + + + +### 2.4 Scattering Reads + +分散读取,有一个文本文件 3parts.txt + +``` +onetwothree +``` + +使用如下方式读取,可以将数据填充至多个 buffer + +```java +try (RandomAccessFile file = new RandomAccessFile("helloword/3parts.txt", "rw")) { + FileChannel channel = file.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(); + debug(a); + debug(b); + debug(c); +} catch (IOException e) { + e.printStackTrace(); +} +``` + +结果 + +``` + +-------------------------------------------------+ + | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ++--------+-------------------------------------------------+----------------+ +|00000000| 6f 6e 65 |one | ++--------+-------------------------------------------------+----------------+ + +-------------------------------------------------+ + | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ++--------+-------------------------------------------------+----------------+ +|00000000| 74 77 6f |two | ++--------+-------------------------------------------------+----------------+ + +-------------------------------------------------+ + | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ++--------+-------------------------------------------------+----------------+ +|00000000| 74 68 72 65 65 |three | ++--------+-------------------------------------------------+----------------+ +``` + + + +### 2.5 Gathering Writes + +使用如下方式写入,可以将多个 buffer 的数据填充至 channel + +```java +try (RandomAccessFile file = new RandomAccessFile("helloword/3parts.txt", "rw")) { + FileChannel channel = file.getChannel(); + ByteBuffer d = ByteBuffer.allocate(4); + ByteBuffer e = ByteBuffer.allocate(4); + channel.position(11); + + d.put(new byte[]{'f', 'o', 'u', 'r'}); + e.put(new byte[]{'f', 'i', 'v', 'e'}); + d.flip(); + e.flip(); + debug(d); + debug(e); + channel.write(new ByteBuffer[]{d, e}); +} catch (IOException e) { + e.printStackTrace(); +} +``` + +输出 + +``` + +-------------------------------------------------+ + | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ++--------+-------------------------------------------------+----------------+ +|00000000| 66 6f 75 72 |four | ++--------+-------------------------------------------------+----------------+ + +-------------------------------------------------+ + | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ++--------+-------------------------------------------------+----------------+ +|00000000| 66 69 76 65 |five | ++--------+-------------------------------------------------+----------------+ +``` + +文件内容 + +``` +onetwothreefourfive +``` + + + +### 2.6 练习 + +网络上有多条数据发送给服务端,数据之间使用 \n 进行分隔 +但由于某种原因这些数据在接收时,被进行了重新组合,例如原始数据有3条为 + +* Hello,world\n +* I'm zhangsan\n +* How are you?\n + +变成了下面的两个 byteBuffer (黏包,半包) + +* Hello,world\nI'm zhangsan\nHo +* w are you?\n + +现在要求你编写程序,将错乱的数据恢复成原始的按 \n 分隔的数据 + +```java +public static void main(String[] args) { + ByteBuffer source = ByteBuffer.allocate(32); + // 11 24 + 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(); + int oldLimit = source.limit(); + for (int i = 0; i < oldLimit; i++) { + if (source.get(i) == '\n') { + System.out.println(i); + ByteBuffer target = ByteBuffer.allocate(i + 1 - source.position()); + // 0 ~ limit + source.limit(i + 1); + target.put(source); // 从source 读,向 target 写 + debugAll(target); + source.limit(oldLimit); + } + } + source.compact(); +} +``` + + + +## 3. 文件编程 + +### 3.1 FileChannel + +#### ⚠️ FileChannel 工作模式 + +> FileChannel 只能工作在阻塞模式下 + + + +#### 获取 + +不能直接打开 FileChannel,必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法 + +* 通过 FileInputStream 获取的 channel 只能读 +* 通过 FileOutputStream 获取的 channel 只能写 +* 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定 + + + +#### 读取 + +会从 channel 读取数据填充 ByteBuffer,返回值表示读到了多少字节,-1 表示到达了文件的末尾 + +```java +int readBytes = channel.read(buffer); +``` + + + +#### 写入 + +写入的正确姿势如下, SocketChannel + +```java +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 方法 + + + +#### 位置 + +获取当前位置 + +```java +long pos = channel.position(); +``` + +设置当前位置 + +```java +long newPos = ...; +channel.position(newPos); +``` + +设置当前位置时,如果设置为文件的末尾 + +* 这时读取会返回 -1 +* 这时写入,会追加内容,但要注意如果 position 超过了文件末尾,再写入时在新内容和原末尾之间会有空洞(00) + + + +#### 大小 + +使用 size 方法获取文件的大小 + + + +#### 强制写入 + +操作系统出于性能的考虑,会将数据缓存,不是立刻写入磁盘。可以调用 force(true) 方法将文件内容和元数据(文件的权限等信息)立刻写入磁盘 + + + +### 3.2 两个 Channel 传输数据 + +```java +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); +``` + +输出 + +``` +transferTo 用时:8.2011 +``` + + + +超过 2g 大小的文件传输 + +```java +public class TestFileChannelTransferTo { + 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(); + } + } +} +``` + +实际传输一个超大文件 + +``` +position:0 left:7769948160 +position:2147483647 left:5622464513 +position:4294967294 left:3474980866 +position:6442450941 left:1327497219 +``` + + + +### 3.3 Path + +jdk7 引入了 Path 和 Paths 类 + +* Path 用来表示文件路径 +* Paths 是工具类,用来获取 Path 实例 + +```java +Path source = Paths.get("1.txt"); // 相对路径 使用 user.dir 环境变量来定位 1.txt + +Path source = Paths.get("d:\\1.txt"); // 绝对路径 代表了 d:\1.txt + +Path source = Paths.get("d:/1.txt"); // 绝对路径 同样代表了 d:\1.txt + +Path projects = Paths.get("d:\\data", "projects"); // 代表了 d:\data\projects +``` + +* `.` 代表了当前路径 +* `..` 代表了上一级路径 + +例如目录结构如下 + +``` +d: + |- data + |- projects + |- a + |- b +``` + +代码 + +```java +Path path = Paths.get("d:\\data\\projects\\a\\..\\b"); +System.out.println(path); +System.out.println(path.normalize()); // 正常化路径 +``` + +会输出 + +``` +d:\data\projects\a\..\b +d:\data\projects\b +``` + + + +### 3.4 Files + +检查文件是否存在 + +```java +Path path = Paths.get("helloword/data.txt"); +System.out.println(Files.exists(path)); +``` + + + +创建一级目录 + +```java +Path path = Paths.get("helloword/d1"); +Files.createDirectory(path); +``` + +* 如果目录已存在,会抛异常 FileAlreadyExistsException +* 不能一次创建多级目录,否则会抛异常 NoSuchFileException + + + +创建多级目录用 + +```java +Path path = Paths.get("helloword/d1/d2"); +Files.createDirectories(path); +``` + + + +拷贝文件 + +```java +Path source = Paths.get("helloword/data.txt"); +Path target = Paths.get("helloword/target.txt"); + +Files.copy(source, target); +``` + +* 如果文件已存在,会抛异常 FileAlreadyExistsException + +如果希望用 source 覆盖掉 target,需要用 StandardCopyOption 来控制 + +```java +Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); +``` + + + +移动文件 + +```java +Path source = Paths.get("helloword/data.txt"); +Path target = Paths.get("helloword/data.txt"); + +Files.move(source, target, StandardCopyOption.ATOMIC_MOVE); +``` + +* StandardCopyOption.ATOMIC_MOVE 保证文件移动的原子性 + + + +删除文件 + +```java +Path target = Paths.get("helloword/target.txt"); + +Files.delete(target); +``` + +* 如果文件不存在,会抛异常 NoSuchFileException + + + +删除目录 + +```java +Path target = Paths.get("helloword/d1"); + +Files.delete(target); +``` + +* 如果目录还有内容,会抛异常 DirectoryNotEmptyException + + + +遍历目录文件 + +```java +public static void main(String[] args) throws IOException { + Path path = Paths.get("C:\\Program Files\\Java\\jdk1.8.0_91"); + AtomicInteger dirCount = new AtomicInteger(); + AtomicInteger fileCount = new AtomicInteger(); + Files.walkFileTree(path, new SimpleFileVisitor(){ + @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(dirCount); // 133 + System.out.println(fileCount); // 1479 +} +``` + + + +统计 jar 的数目 + +```java +Path path = Paths.get("C:\\Program Files\\Java\\jdk1.8.0_91"); +AtomicInteger fileCount = new AtomicInteger(); +Files.walkFileTree(path, new SimpleFileVisitor(){ + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) + throws IOException { + if (file.toFile().getName().endsWith(".jar")) { + fileCount.incrementAndGet(); + } + return super.visitFile(file, attrs); + } +}); +System.out.println(fileCount); // 724 +``` + + + +删除多级目录 + +```java +Path path = Paths.get("d:\\a"); +Files.walkFileTree(path, new SimpleFileVisitor(){ + @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); + } +}); +``` + + + +#### ⚠️ 删除很危险 + +> 删除是危险操作,确保要递归删除的文件夹没有重要内容 + + + +拷贝多级目录 + +```java +long start = System.currentTimeMillis(); +String source = "D:\\Snipaste-1.16.2-x64"; +String target = "D:\\Snipaste-1.16.2-x64aaa"; + +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(); + } +}); +long end = System.currentTimeMillis(); +System.out.println(end - start); +``` + + + + + +## 4. 网络编程 + +### 4.1 非阻塞 vs 阻塞 + +#### 阻塞 + +* 阻塞模式下,相关方法都会导致线程暂停 + * ServerSocketChannel.accept 会在没有连接建立时让线程暂停 + * SocketChannel.read 会在没有数据可读时让线程暂停 + * 阻塞的表现其实就是线程暂停了,暂停期间不会占用 cpu,但线程相当于闲置 +* 单线程下,阻塞方法之间相互影响,几乎不能正常工作,需要多线程支持 +* 但多线程下,有新的问题,体现在以下方面 + * 32 位 jvm 一个线程 320k,64 位 jvm 一个线程 1024k,如果连接数过多,必然导致 OOM,并且线程太多,反而会因为频繁上下文切换导致性能降低 + * 可以采用线程池技术来减少线程数和线程上下文切换,但治标不治本,如果有很多连接建立,但长时间 inactive,会阻塞线程池中所有线程,因此不适合长连接,只适合短连接 + + + +服务器端 + +```java +// 使用 nio 来理解阻塞模式, 单线程 +// 0. ByteBuffer +ByteBuffer buffer = ByteBuffer.allocate(16); +// 1. 创建了服务器 +ServerSocketChannel ssc = ServerSocketChannel.open(); + +// 2. 绑定监听端口 +ssc.bind(new InetSocketAddress(8080)); + +// 3. 连接集合 +List channels = new ArrayList<>(); +while (true) { + // 4. accept 建立与客户端连接, SocketChannel 用来与客户端之间通信 + log.debug("connecting..."); + SocketChannel sc = ssc.accept(); // 阻塞方法,线程停止运行 + log.debug("connected... {}", sc); + channels.add(sc); + for (SocketChannel channel : channels) { + // 5. 接收客户端发送的数据 + log.debug("before read... {}", channel); + channel.read(buffer); // 阻塞方法,线程停止运行 + buffer.flip(); + debugRead(buffer); + buffer.clear(); + log.debug("after read...{}", channel); + } +} +``` + +客户端 + +```java +SocketChannel sc = SocketChannel.open(); +sc.connect(new InetSocketAddress("localhost", 8080)); +System.out.println("waiting..."); +``` + + + +#### 非阻塞 + +* 非阻塞模式下,相关方法都会不会让线程暂停 + * 在 ServerSocketChannel.accept 在没有连接建立时,会返回 null,继续运行 + * SocketChannel.read 在没有数据可读时,会返回 0,但线程不必阻塞,可以去执行其它 SocketChannel 的 read 或是去执行 ServerSocketChannel.accept + * 写数据时,线程只是等待数据写入 Channel 即可,无需等 Channel 通过网络把数据发送出去 +* 但非阻塞模式下,即使没有连接建立,和可读数据,线程仍然在不断运行,白白浪费了 cpu +* 数据复制过程中,线程实际还是阻塞的(AIO 改进的地方) + + + +服务器端,客户端代码不变 + +```java +// 使用 nio 来理解非阻塞模式, 单线程 +// 0. ByteBuffer +ByteBuffer buffer = ByteBuffer.allocate(16); +// 1. 创建了服务器 +ServerSocketChannel ssc = ServerSocketChannel.open(); +ssc.configureBlocking(false); // 非阻塞模式 +// 2. 绑定监听端口 +ssc.bind(new InetSocketAddress(8080)); +// 3. 连接集合 +List channels = new ArrayList<>(); +while (true) { + // 4. accept 建立与客户端连接, SocketChannel 用来与客户端之间通信 + SocketChannel sc = ssc.accept(); // 非阻塞,线程还会继续运行,如果没有连接建立,但sc是null + if (sc != null) { + log.debug("connected... {}", sc); + sc.configureBlocking(false); // 非阻塞模式 + channels.add(sc); + } + for (SocketChannel channel : channels) { + // 5. 接收客户端发送的数据 + int read = channel.read(buffer);// 非阻塞,线程仍然会继续运行,如果没有读到数据,read 返回 0 + if (read > 0) { + buffer.flip(); + debugRead(buffer); + buffer.clear(); + log.debug("after read...{}", channel); + } + } +} +``` + + + +#### 多路复用 + +单线程可以配合 Selector 完成对多个 Channel 可读写事件的监控,这称之为多路复用 + +* 多路复用仅针对网络 IO、普通文件 IO 没法利用多路复用 +* 如果不用 Selector 的非阻塞模式,线程大部分时间都在做无用功,而 Selector 能够保证 + * 有可连接事件时才去连接 + * 有可读事件才去读取 + * 有可写事件才去写入 + * 限于网络传输能力,Channel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件 + + + +### 4.2 Selector + +```mermaid +graph TD +subgraph selector 版 +thread --> selector +selector --> c1(channel) +selector --> c2(channel) +selector --> c3(channel) +end +``` + + + +好处 + +* 一个线程配合 selector 就可以监控多个 channel 的事件,事件发生线程才去处理。避免非阻塞模式下所做无用功 +* 让这个线程能够被充分利用 +* 节约了线程的数量 +* 减少了线程上下文切换 + + + +#### 创建 + +```java +Selector selector = Selector.open(); +``` + + + +#### 绑定 Channel 事件 + +也称之为注册事件,绑定的事件 selector 才会关心 + +```java +channel.configureBlocking(false); +SelectionKey key = channel.register(selector, 绑定事件); +``` + +* channel 必须工作在非阻塞模式 +* FileChannel 没有非阻塞模式,因此不能配合 selector 一起使用 +* 绑定的事件类型可以有 + * connect - 客户端连接成功时触发 + * accept - 服务器端成功接受连接时触发 + * read - 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况 + * write - 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况 + + + +#### 监听 Channel 事件 + +可以通过下面三种方法来监听是否有事件发生,方法的返回值代表有多少 channel 发生了事件 + +方法1,阻塞直到绑定事件发生 + +```java +int count = selector.select(); +``` + + + +方法2,阻塞直到绑定事件发生,或是超时(时间单位为 ms) + +```java +int count = selector.select(long timeout); +``` + + + +方法3,不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件 + +```java +int count = selector.selectNow(); +``` + + + +#### 💡 select 何时不阻塞 + +> * 事件发生时 +> * 客户端发起连接请求,会触发 accept 事件 +> * 客户端发送数据过来,客户端正常、异常关闭时,都会触发 read 事件,另外如果发送的数据大于 buffer 缓冲区,会触发多次读取事件 +> * channel 可写,会触发 write 事件 +> * 在 linux 下 nio bug 发生时 +> * 调用 selector.wakeup() +> * 调用 selector.close() +> * selector 所在线程 interrupt + + + +### 4.3 处理 accept 事件 + +客户端代码为 + +```java +public class Client { + public static void main(String[] args) { + try (Socket socket = new Socket("localhost", 8080)) { + System.out.println(socket); + socket.getOutputStream().write("world".getBytes()); + System.in.read(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} +``` + + + +服务器端代码为 + +```java +@Slf4j +public class ChannelDemo6 { + public static void main(String[] args) { + try (ServerSocketChannel channel = ServerSocketChannel.open()) { + channel.bind(new InetSocketAddress(8080)); + System.out.println(channel); + Selector selector = Selector.open(); + channel.configureBlocking(false); + channel.register(selector, SelectionKey.OP_ACCEPT); + + while (true) { + int count = selector.select(); +// int count = selector.selectNow(); + log.debug("select count: {}", count); +// if(count <= 0) { +// continue; +// } + + // 获取所有事件 + Set keys = selector.selectedKeys(); + + // 遍历所有事件,逐一处理 + Iterator iter = keys.iterator(); + while (iter.hasNext()) { + SelectionKey key = iter.next(); + // 判断事件类型 + if (key.isAcceptable()) { + ServerSocketChannel c = (ServerSocketChannel) key.channel(); + // 必须处理 + SocketChannel sc = c.accept(); + log.debug("{}", sc); + } + // 处理完毕,必须将事件移除 + iter.remove(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} +``` + + + +#### 💡 事件发生后能否不处理 + +> 事件发生后,要么处理,要么取消(cancel),不能什么都不做,否则下次该事件仍会触发,这是因为 nio 底层使用的是水平触发 + + + +### 4.4 处理 read 事件 + +```java +@Slf4j +public class ChannelDemo6 { + public static void main(String[] args) { + try (ServerSocketChannel channel = ServerSocketChannel.open()) { + channel.bind(new InetSocketAddress(8080)); + System.out.println(channel); + Selector selector = Selector.open(); + channel.configureBlocking(false); + channel.register(selector, SelectionKey.OP_ACCEPT); + + while (true) { + int count = selector.select(); +// int count = selector.selectNow(); + log.debug("select count: {}", count); +// if(count <= 0) { +// continue; +// } + + // 获取所有事件 + Set keys = selector.selectedKeys(); + + // 遍历所有事件,逐一处理 + Iterator iter = keys.iterator(); + while (iter.hasNext()) { + SelectionKey key = iter.next(); + // 判断事件类型 + if (key.isAcceptable()) { + ServerSocketChannel c = (ServerSocketChannel) key.channel(); + // 必须处理 + SocketChannel sc = c.accept(); + sc.configureBlocking(false); + sc.register(selector, SelectionKey.OP_READ); + log.debug("连接已建立: {}", sc); + } else if (key.isReadable()) { + SocketChannel sc = (SocketChannel) key.channel(); + ByteBuffer buffer = ByteBuffer.allocate(128); + int read = sc.read(buffer); + if(read == -1) { + key.cancel(); + sc.close(); + } else { + buffer.flip(); + debug(buffer); + } + } + // 处理完毕,必须将事件移除 + iter.remove(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } +} +``` + +开启两个客户端,修改一下发送文字,输出 + +``` +sun.nio.ch.ServerSocketChannelImpl[/0:0:0:0:0:0:0:0:8080] +21:16:39 [DEBUG] [main] c.i.n.ChannelDemo6 - select count: 1 +21:16:39 [DEBUG] [main] c.i.n.ChannelDemo6 - 连接已建立: java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:60367] +21:16:39 [DEBUG] [main] c.i.n.ChannelDemo6 - select count: 1 + +-------------------------------------------------+ + | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ++--------+-------------------------------------------------+----------------+ +|00000000| 68 65 6c 6c 6f |hello | ++--------+-------------------------------------------------+----------------+ +21:16:59 [DEBUG] [main] c.i.n.ChannelDemo6 - select count: 1 +21:16:59 [DEBUG] [main] c.i.n.ChannelDemo6 - 连接已建立: java.nio.channels.SocketChannel[connected local=/127.0.0.1:8080 remote=/127.0.0.1:60378] +21:16:59 [DEBUG] [main] c.i.n.ChannelDemo6 - select count: 1 + +-------------------------------------------------+ + | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ++--------+-------------------------------------------------+----------------+ +|00000000| 77 6f 72 6c 64 |world | ++--------+-------------------------------------------------+----------------+ +``` + + + +#### 💡 为何要 iter.remove() + +> 因为 select 在事件发生后,就会将相关的 key 放入 selectedKeys 集合,但不会在处理完后从 selectedKeys 集合中移除,需要我们自己编码删除。例如 +> +> * 第一次触发了 ssckey 上的 accept 事件,没有移除 ssckey +> * 第二次触发了 sckey 上的 read 事件,但这时 selectedKeys 中还有上次的 ssckey ,在处理时因为没有真正的 serverSocket 连上了,就会导致空指针异常 + + + +#### 💡 cancel 的作用 + +> cancel 会取消注册在 selector 上的 channel,并从 keys 集合中删除 key 后续不会再监听事件 + + + +#### ⚠️ 不处理边界的问题 + +以前有同学写过这样的代码,思考注释中两个问题,以 bio 为例,其实 nio 道理是一样的 + +```java +public class Server { + public static void main(String[] args) throws IOException { + ServerSocket ss=new ServerSocket(9000); + while (true) { + Socket s = ss.accept(); + InputStream in = s.getInputStream(); + // 这里这么写,有没有问题 + byte[] arr = new byte[4]; + while(true) { + int read = in.read(arr); + // 这里这么写,有没有问题 + if(read == -1) { + break; + } + System.out.println(new String(arr, 0, read)); + } + } + } +} +``` + +客户端 + +```java +public class Client { + public static void main(String[] args) throws IOException { + Socket max = new Socket("localhost", 9000); + OutputStream out = max.getOutputStream(); + out.write("hello".getBytes()); + out.write("world".getBytes()); + out.write("你好".getBytes()); + max.close(); + } +} +``` + +输出 + +``` +hell +owor +ld� +�好 + +``` + +为什么? + + + +#### 处理消息的边界 + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/0023.png) + +* 一种思路是固定消息长度,数据包大小一样,服务器按预定长度读取,缺点是浪费带宽 +* 另一种思路是按分隔符拆分,缺点是效率低 +* TLV 格式,即 Type 类型、Length 长度、Value 数据,类型和长度已知的情况下,就可以方便获取消息大小,分配合适的 buffer,缺点是 buffer 需要提前分配,如果内容过大,则影响 server 吞吐量 + * Http 1.1 是 TLV 格式 + * Http 2.0 是 LTV 格式 + + + +```mermaid +sequenceDiagram +participant c1 as 客户端1 +participant s as 服务器 +participant b1 as ByteBuffer1 +participant b2 as ByteBuffer2 +c1 ->> s: 发送 01234567890abcdef3333\r +s ->> b1: 第一次 read 存入 01234567890abcdef +s ->> b2: 扩容 +b1 ->> b2: 拷贝 01234567890abcdef +s ->> b2: 第二次 read 存入 3333\r +b2 ->> b2: 01234567890abcdef3333\r +``` + +服务器端 + +```java +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++) { + target.put(source.get()); + } + debugAll(target); + } + } + source.compact(); // 0123456789abcdef position 16 limit 16 +} + +public static void main(String[] args) throws IOException { + // 1. 创建 selector, 管理多个 channel + Selector selector = Selector.open(); + ServerSocketChannel ssc = ServerSocketChannel.open(); + ssc.configureBlocking(false); + // 2. 建立 selector 和 channel 的联系(注册) + // SelectionKey 就是将来事件发生后,通过它可以知道事件和哪个channel的事件 + SelectionKey sscKey = ssc.register(selector, 0, null); + // key 只关注 accept 事件 + sscKey.interestOps(SelectionKey.OP_ACCEPT); + log.debug("sscKey:{}", sscKey); + ssc.bind(new InetSocketAddress(8080)); + while (true) { + // 3. select 方法, 没有事件发生,线程阻塞,有事件,线程才会恢复运行 + // select 在事件未处理时,它不会阻塞, 事件发生后要么处理,要么取消,不能置之不理 + selector.select(); + // 4. 处理事件, selectedKeys 内部包含了所有发生的事件 + Iterator iter = selector.selectedKeys().iterator(); // accept, read + while (iter.hasNext()) { + SelectionKey key = iter.next(); + // 处理key 时,要从 selectedKeys 集合中删除,否则下次处理就会有问题 + iter.remove(); + log.debug("key: {}", key); + // 5. 区分事件类型 + if (key.isAcceptable()) { // 如果是 accept + ServerSocketChannel channel = (ServerSocketChannel) key.channel(); + SocketChannel sc = channel.accept(); + sc.configureBlocking(false); + ByteBuffer buffer = ByteBuffer.allocate(16); // attachment + // 将一个 byteBuffer 作为附件关联到 selectionKey 上 + SelectionKey scKey = sc.register(selector, 0, buffer); + scKey.interestOps(SelectionKey.OP_READ); + log.debug("{}", sc); + log.debug("scKey:{}", scKey); + } else if (key.isReadable()) { // 如果是 read + try { + SocketChannel channel = (SocketChannel) key.channel(); // 拿到触发事件的channel + // 获取 selectionKey 上关联的附件 + ByteBuffer buffer = (ByteBuffer) key.attachment(); + int read = channel.read(buffer); // 如果是正常断开,read 的方法的返回值是 -1 + if(read == -1) { + key.cancel(); + } else { + split(buffer); + // 需要扩容 + if (buffer.position() == buffer.limit()) { + ByteBuffer newBuffer = ByteBuffer.allocate(buffer.capacity() * 2); + buffer.flip(); + newBuffer.put(buffer); // 0123456789abcdef3333\n + key.attach(newBuffer); + } + } + + } catch (IOException e) { + e.printStackTrace(); + key.cancel(); // 因为客户端断开了,因此需要将 key 取消(从 selector 的 keys 集合中真正删除 key) + } + } + } + } +} +``` + +客户端 + +```java +SocketChannel sc = SocketChannel.open(); +sc.connect(new InetSocketAddress("localhost", 8080)); +SocketAddress address = sc.getLocalAddress(); +// sc.write(Charset.defaultCharset().encode("hello\nworld\n")); +sc.write(Charset.defaultCharset().encode("0123\n456789abcdef")); +sc.write(Charset.defaultCharset().encode("0123456789abcdef3333\n")); +System.in.read(); +``` + + + + + +#### ByteBuffer 大小分配 + +* 每个 channel 都需要记录可能被切分的消息,因为 ByteBuffer 不能被多个 channel 共同使用,因此需要为每个 channel 维护一个独立的 ByteBuffer +* ByteBuffer 不能太大,比如一个 ByteBuffer 1Mb 的话,要支持百万连接就要 1Tb 内存,因此需要设计大小可变的 ByteBuffer + * 一种思路是首先分配一个较小的 buffer,例如 4k,如果发现数据不够,再分配 8k 的 buffer,将 4k buffer 内容拷贝至 8k buffer,优点是消息连续容易处理,缺点是数据拷贝耗费性能,参考实现 [http://tutorials.jenkov.com/java-performance/resizable-array.html](http://tutorials.jenkov.com/java-performance/resizable-array.html) + * 另一种思路是用多个数组组成 buffer,一个数组不够,把多出来的内容写入新的数组,与前面的区别是消息存储不连续解析复杂,优点是避免了拷贝引起的性能损耗 + + + + + +### 4.5 处理 write 事件 + + + +#### 一次无法写完例子 + +* 非阻塞模式下,无法保证把 buffer 中所有数据都写入 channel,因此需要追踪 write 方法的返回值(代表实际写入字节数) +* 用 selector 监听所有 channel 的可写事件,每个 channel 都需要一个 key 来跟踪 buffer,但这样又会导致占用内存过多,就有两阶段策略 + * 当消息处理器第一次写入消息时,才将 channel 注册到 selector 上 + * selector 检查 channel 上的可写事件,如果所有的数据写完了,就取消 channel 的注册 + * 如果不取消,会每次可写均会触发 write 事件 + + + +```java +public class WriteServer { + + public static void main(String[] args) throws IOException { + ServerSocketChannel ssc = ServerSocketChannel.open(); + ssc.configureBlocking(false); + ssc.bind(new InetSocketAddress(8080)); + + Selector selector = Selector.open(); + ssc.register(selector, SelectionKey.OP_ACCEPT); + + while(true) { + selector.select(); + + Iterator iter = selector.selectedKeys().iterator(); + while (iter.hasNext()) { + SelectionKey key = iter.next(); + iter.remove(); + if (key.isAcceptable()) { + SocketChannel sc = ssc.accept(); + sc.configureBlocking(false); + SelectionKey sckey = sc.register(selector, SelectionKey.OP_READ); + // 1. 向客户端发送内容 + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < 3000000; i++) { + sb.append("a"); + } + ByteBuffer buffer = Charset.defaultCharset().encode(sb.toString()); + int write = sc.write(buffer); + // 3. write 表示实际写了多少字节 + System.out.println("实际写入字节:" + write); + // 4. 如果有剩余未读字节,才需要关注写事件 + if (buffer.hasRemaining()) { + // read 1 write 4 + // 在原有关注事件的基础上,多关注 写事件 + sckey.interestOps(sckey.interestOps() + SelectionKey.OP_WRITE); + // 把 buffer 作为附件加入 sckey + sckey.attach(buffer); + } + } else if (key.isWritable()) { + ByteBuffer buffer = (ByteBuffer) key.attachment(); + SocketChannel sc = (SocketChannel) key.channel(); + int write = sc.write(buffer); + System.out.println("实际写入字节:" + write); + if (!buffer.hasRemaining()) { // 写完了 + key.interestOps(key.interestOps() - SelectionKey.OP_WRITE); + key.attach(null); + } + } + } + } + } +} +``` + +客户端 + +```java +public class WriteClient { + public static void main(String[] args) throws IOException { + Selector selector = Selector.open(); + SocketChannel sc = SocketChannel.open(); + sc.configureBlocking(false); + sc.register(selector, SelectionKey.OP_CONNECT | SelectionKey.OP_READ); + sc.connect(new InetSocketAddress("localhost", 8080)); + int count = 0; + while (true) { + selector.select(); + Iterator iter = selector.selectedKeys().iterator(); + while (iter.hasNext()) { + SelectionKey key = iter.next(); + iter.remove(); + if (key.isConnectable()) { + System.out.println(sc.finishConnect()); + } else if (key.isReadable()) { + ByteBuffer buffer = ByteBuffer.allocate(1024 * 1024); + count += sc.read(buffer); + buffer.clear(); + System.out.println(count); + } + } + } + } +} +``` + + + +#### 💡 write 为何要取消 + +只要向 channel 发送数据时,socket 缓冲可写,这个事件会频繁触发,因此应当只在 socket 缓冲区写不下时再关注可写事件,数据写完之后再取消关注 + + + + + + + + + + + +### 4.6 更进一步 + + + +#### 💡 利用多线程优化 + +> 现在都是多核 cpu,设计时要充分考虑别让 cpu 的力量被白白浪费 + + + +前面的代码只有一个选择器,没有充分利用多核 cpu,如何改进呢? + +分两组选择器 + +* 单线程配一个选择器,专门处理 accept 事件 +* 创建 cpu 核心数的线程,每个线程配一个选择器,轮流处理 read 事件 + + + +```java +public class ChannelDemo7 { + public static void main(String[] args) throws IOException { + new BossEventLoop().register(); + } + + + @Slf4j + static class BossEventLoop implements Runnable { + private Selector boss; + private WorkerEventLoop[] workers; + private volatile boolean start = false; + AtomicInteger index = new AtomicInteger(); + + public void register() throws IOException { + if (!start) { + ServerSocketChannel ssc = ServerSocketChannel.open(); + ssc.bind(new InetSocketAddress(8080)); + ssc.configureBlocking(false); + boss = Selector.open(); + SelectionKey ssckey = ssc.register(boss, 0, null); + ssckey.interestOps(SelectionKey.OP_ACCEPT); + workers = initEventLoops(); + new Thread(this, "boss").start(); + log.debug("boss start..."); + start = true; + } + } + + public WorkerEventLoop[] initEventLoops() { +// EventLoop[] eventLoops = new EventLoop[Runtime.getRuntime().availableProcessors()]; + WorkerEventLoop[] workerEventLoops = new WorkerEventLoop[2]; + for (int i = 0; i < workerEventLoops.length; i++) { + workerEventLoops[i] = new WorkerEventLoop(i); + } + return workerEventLoops; + } + + @Override + public void run() { + while (true) { + try { + boss.select(); + Iterator iter = boss.selectedKeys().iterator(); + while (iter.hasNext()) { + SelectionKey key = iter.next(); + iter.remove(); + if (key.isAcceptable()) { + ServerSocketChannel c = (ServerSocketChannel) key.channel(); + SocketChannel sc = c.accept(); + sc.configureBlocking(false); + log.debug("{} connected", sc.getRemoteAddress()); + workers[index.getAndIncrement() % workers.length].register(sc); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } + + @Slf4j + static class WorkerEventLoop implements Runnable { + private Selector worker; + private volatile boolean start = false; + private int index; + + private final ConcurrentLinkedQueue tasks = new ConcurrentLinkedQueue<>(); + + public WorkerEventLoop(int index) { + this.index = index; + } + + public void register(SocketChannel sc) throws IOException { + if (!start) { + worker = Selector.open(); + new Thread(this, "worker-" + index).start(); + start = true; + } + tasks.add(() -> { + try { + SelectionKey sckey = sc.register(worker, 0, null); + sckey.interestOps(SelectionKey.OP_READ); + worker.selectNow(); + } catch (IOException e) { + e.printStackTrace(); + } + }); + worker.wakeup(); + } + + @Override + public void run() { + while (true) { + try { + worker.select(); + Runnable task = tasks.poll(); + if (task != null) { + task.run(); + } + Set keys = worker.selectedKeys(); + Iterator iter = keys.iterator(); + while (iter.hasNext()) { + SelectionKey key = iter.next(); + if (key.isReadable()) { + SocketChannel sc = (SocketChannel) key.channel(); + ByteBuffer buffer = ByteBuffer.allocate(128); + try { + int read = sc.read(buffer); + if (read == -1) { + key.cancel(); + sc.close(); + } else { + buffer.flip(); + log.debug("{} message:", sc.getRemoteAddress()); + debugAll(buffer); + } + } catch (IOException e) { + e.printStackTrace(); + key.cancel(); + sc.close(); + } + } + iter.remove(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } + } +} +``` + + + +#### 💡 如何拿到 cpu 个数 + +> * Runtime.getRuntime().availableProcessors() 如果工作在 docker 容器下,因为容器不是物理隔离的,会拿到物理 cpu 个数,而不是容器申请时的个数 +> * 这个问题直到 jdk 10 才修复,使用 jvm 参数 UseContainerSupport 配置, 默认开启 + + + +### 4.7 UDP + +* UDP 是无连接的,client 发送数据不会管 server 是否开启 +* server 这边的 receive 方法会将接收到的数据存入 byte buffer,但如果数据报文超过 buffer 大小,多出来的数据会被默默抛弃 + +首先启动服务器端 + +```java +public class UdpServer { + public static void main(String[] args) { + try (DatagramChannel channel = DatagramChannel.open()) { + channel.socket().bind(new InetSocketAddress(9999)); + System.out.println("waiting..."); + ByteBuffer buffer = ByteBuffer.allocate(32); + channel.receive(buffer); + buffer.flip(); + debug(buffer); + } catch (IOException e) { + e.printStackTrace(); + } + } +} +``` + +输出 + +``` +waiting... +``` + + + +运行客户端 + +```java +public class UdpClient { + public static void main(String[] args) { + try (DatagramChannel channel = DatagramChannel.open()) { + ByteBuffer buffer = StandardCharsets.UTF_8.encode("hello"); + InetSocketAddress address = new InetSocketAddress("localhost", 9999); + channel.send(buffer, address); + } catch (Exception e) { + e.printStackTrace(); + } + } +} +``` + +接下来服务器端输出 + +``` + +-------------------------------------------------+ + | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ++--------+-------------------------------------------------+----------------+ +|00000000| 68 65 6c 6c 6f |hello | ++--------+-------------------------------------------------+----------------+ +``` + + + + + +## 5. NIO vs BIO + +### 5.1 stream vs channel + +* stream 不会自动缓冲数据,channel 会利用系统提供的发送缓冲区、接收缓冲区(更为底层) +* stream 仅支持阻塞 API,channel 同时支持阻塞、非阻塞 API,网络 channel 可配合 selector 实现多路复用 +* 二者均为全双工,即读写可以同时进行 + + + +### 5.2 IO 模型 + +同步阻塞、同步非阻塞、同步多路复用、异步阻塞(没有此情况)、异步非阻塞 + +* 同步:线程自己去获取结果(一个线程) +* 异步:线程自己不去获取结果,而是由其它线程送结果(至少两个线程) + + + +当调用一次 channel.read 或 stream.read 后,会切换至操作系统内核态来完成真正数据读取,而读取又分为两个阶段,分别为: + +* 等待数据阶段 +* 复制数据阶段 + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/0033.png) + +* 阻塞 IO + + ![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/0039.png) + +* 非阻塞 IO + + ![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/0035.png) + +* 多路复用 + + ![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/0038.png) + +* 信号驱动 + +* 异步 IO + + ![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/0037.png) + +* 阻塞 IO vs 多路复用 + + ![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/0034.png) + + ![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/0036.png) + +#### 🔖 参考 + +UNIX 网络编程 - 卷 I + + + +### 5.3 零拷贝 + +#### 传统 IO 问题 + +传统的 IO 将一个文件通过 socket 写出 + +```java +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); +``` + +内部工作流程是这样的: + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/0024.png) + +1. java 本身并不具备 IO 读写能力,因此 read 方法调用后,要从 java 程序的**用户态**切换至**内核态**,去调用操作系统(Kernel)的读能力,将数据读入**内核缓冲区**。这期间用户线程阻塞,操作系统使用 DMA(Direct Memory Access)来实现文件读,其间也不会使用 cpu + + > DMA 也可以理解为硬件单元,用来解放 cpu 完成文件 IO + +2. 从**内核态**切换回**用户态**,将数据从**内核缓冲区**读入**用户缓冲区**(即 byte[] buf),这期间 cpu 会参与拷贝,无法利用 DMA + +3. 调用 write 方法,这时将数据从**用户缓冲区**(byte[] buf)写入 **socket 缓冲区**,cpu 会参与拷贝 + +4. 接下来要向网卡写数据,这项能力 java 又不具备,因此又得从**用户态**切换至**内核态**,调用操作系统的写能力,使用 DMA 将 **socket 缓冲区**的数据写入网卡,不会使用 cpu + + + +可以看到中间环节较多,java 的 IO 实际不是物理设备级别的读写,而是缓存的复制,底层的真正读写是操作系统来完成的 + +* 用户态与内核态的切换发生了 3 次,这个操作比较重量级 +* 数据拷贝了共 4 次 + + + +#### NIO 优化 + +通过 DirectByteBuf + +* ByteBuffer.allocate(10) HeapByteBuffer 使用的还是 java 内存 +* ByteBuffer.allocateDirect(10) DirectByteBuffer 使用的是操作系统内存 + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/0025.png) + +大部分步骤与优化前相同,不再赘述。唯有一点:java 可以使用 DirectByteBuf 将堆外内存映射到 jvm 内存中来直接访问使用 + +* 这块内存不受 jvm 垃圾回收的影响,因此内存地址固定,有助于 IO 读写 +* java 中的 DirectByteBuf 对象仅维护了此内存的虚引用,内存回收分成两步 + * DirectByteBuf 对象被垃圾回收,将虚引用加入引用队列 + * 通过专门线程访问引用队列,根据虚引用释放堆外内存 +* 减少了一次数据拷贝,用户态与内核态的切换次数没有减少 + + + +进一步优化(底层采用了 linux 2.1 后提供的 sendFile 方法),java 中对应着两个 channel 调用 transferTo/transferFrom 方法拷贝数据 + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/0026.png) + +1. java 调用 transferTo 方法后,要从 java 程序的**用户态**切换至**内核态**,使用 DMA将数据读入**内核缓冲区**,不会使用 cpu +2. 数据从**内核缓冲区**传输到 **socket 缓冲区**,cpu 会参与拷贝 +3. 最后使用 DMA 将 **socket 缓冲区**的数据写入网卡,不会使用 cpu + +可以看到 + +* 只发生了一次用户态与内核态的切换 +* 数据拷贝了 3 次 + + + +进一步优化(linux 2.4) + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/0027.png) + +1. java 调用 transferTo 方法后,要从 java 程序的**用户态**切换至**内核态**,使用 DMA将数据读入**内核缓冲区**,不会使用 cpu +2. 只会将一些 offset 和 length 信息拷入 **socket 缓冲区**,几乎无消耗 +3. 使用 DMA 将 **内核缓冲区**的数据写入网卡,不会使用 cpu + +整个过程仅只发生了一次用户态与内核态的切换,数据拷贝了 2 次。所谓的【零拷贝】,并不是真正无拷贝,而是在不会拷贝重复数据到 jvm 内存中,零拷贝的优点有 + +* 更少的用户态与内核态的切换 +* 不利用 cpu 计算,减少 cpu 缓存伪共享 +* 零拷贝适合小文件传输 + + + +### 5.3 AIO + +AIO 用来解决数据复制阶段的阻塞问题 + +* 同步意味着,在进行读写操作时,线程需要等待结果,还是相当于闲置 +* 异步意味着,在进行读写操作时,线程不必等待结果,而是将来由操作系统来通过回调方式由另外的线程来获得结果 + +> 异步模型需要底层操作系统(Kernel)提供支持 +> +> * Windows 系统通过 IOCP 实现了真正的异步 IO +> * Linux 系统异步 IO 在 2.6 版本引入,但其底层实现还是用多路复用模拟了异步 IO,性能没有优势 + + + +#### 文件 AIO + +先来看看 AsynchronousFileChannel + +```java +@Slf4j +public class AioDemo1 { + public static void main(String[] args) throws IOException { + try{ + AsynchronousFileChannel s = + AsynchronousFileChannel.open( + Paths.get("1.txt"), StandardOpenOption.READ); + ByteBuffer buffer = ByteBuffer.allocate(2); + log.debug("begin..."); + s.read(buffer, 0, null, new CompletionHandler() { + @Override + public void completed(Integer result, ByteBuffer attachment) { + log.debug("read completed...{}", result); + buffer.flip(); + debug(buffer); + } + + @Override + public void failed(Throwable exc, ByteBuffer attachment) { + log.debug("read failed..."); + } + }); + + } catch (IOException e) { + e.printStackTrace(); + } + log.debug("do other things..."); + System.in.read(); + } +} +``` + +输出 + +``` +13:44:56 [DEBUG] [main] c.i.aio.AioDemo1 - begin... +13:44:56 [DEBUG] [main] c.i.aio.AioDemo1 - do other things... +13:44:56 [DEBUG] [Thread-5] c.i.aio.AioDemo1 - read completed...2 + +-------------------------------------------------+ + | 0 1 2 3 4 5 6 7 8 9 a b c d e f | ++--------+-------------------------------------------------+----------------+ +|00000000| 61 0d |a. | ++--------+-------------------------------------------------+----------------+ +``` + +可以看到 + +* 响应文件读取成功的是另一个线程 Thread-5 +* 主线程并没有 IO 操作阻塞 + + + +#### 💡 守护线程 + +默认文件 AIO 使用的线程都是守护线程,所以最后要执行 `System.in.read()` 以避免守护线程意外结束 + + + +#### 网络 AIO + +```java +public class AioServer { + public static void main(String[] args) throws IOException { + AsynchronousServerSocketChannel ssc = AsynchronousServerSocketChannel.open(); + ssc.bind(new InetSocketAddress(8080)); + ssc.accept(null, new AcceptHandler(ssc)); + System.in.read(); + } + + private static void closeChannel(AsynchronousSocketChannel sc) { + try { + System.out.printf("[%s] %s close\n", Thread.currentThread().getName(), sc.getRemoteAddress()); + sc.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private static class ReadHandler implements CompletionHandler { + private final AsynchronousSocketChannel sc; + + public ReadHandler(AsynchronousSocketChannel sc) { + this.sc = sc; + } + + @Override + public void completed(Integer result, ByteBuffer attachment) { + try { + if (result == -1) { + closeChannel(sc); + return; + } + System.out.printf("[%s] %s read\n", Thread.currentThread().getName(), sc.getRemoteAddress()); + attachment.flip(); + System.out.println(Charset.defaultCharset().decode(attachment)); + attachment.clear(); + // 处理完第一个 read 时,需要再次调用 read 方法来处理下一个 read 事件 + sc.read(attachment, attachment, this); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void failed(Throwable exc, ByteBuffer attachment) { + closeChannel(sc); + exc.printStackTrace(); + } + } + + private static class WriteHandler implements CompletionHandler { + private final AsynchronousSocketChannel sc; + + private WriteHandler(AsynchronousSocketChannel sc) { + this.sc = sc; + } + + @Override + public void completed(Integer result, ByteBuffer attachment) { + // 如果作为附件的 buffer 还有内容,需要再次 write 写出剩余内容 + if (attachment.hasRemaining()) { + sc.write(attachment); + } + } + + @Override + public void failed(Throwable exc, ByteBuffer attachment) { + exc.printStackTrace(); + closeChannel(sc); + } + } + + private static class AcceptHandler implements CompletionHandler { + private final AsynchronousServerSocketChannel ssc; + + public AcceptHandler(AsynchronousServerSocketChannel ssc) { + this.ssc = ssc; + } + + @Override + public void completed(AsynchronousSocketChannel sc, Object attachment) { + try { + System.out.printf("[%s] %s connected\n", Thread.currentThread().getName(), sc.getRemoteAddress()); + } catch (IOException e) { + e.printStackTrace(); + } + ByteBuffer buffer = ByteBuffer.allocate(16); + // 读事件由 ReadHandler 处理 + sc.read(buffer, buffer, new ReadHandler(sc)); + // 写事件由 WriteHandler 处理 + sc.write(Charset.defaultCharset().encode("server hello!"), ByteBuffer.allocate(16), new WriteHandler(sc)); + // 处理完第一个 accpet 时,需要再次调用 accept 方法来处理下一个 accept 事件 + ssc.accept(null, this); + } + + @Override + public void failed(Throwable exc, Object attachment) { + exc.printStackTrace(); + } + } +} +``` + + + + + + + + + + + diff --git a/src/frontend/react/1.React Basics - Part 1.md b/src/frontend/react/1.React Basics - Part 1.md new file mode 100644 index 0000000..bd4fdb5 --- /dev/null +++ b/src/frontend/react/1.React Basics - Part 1.md @@ -0,0 +1,1045 @@ +--- +order: 1 +title: React基础-上 +category: + - 前端 + - React +--- + +## React介绍 + +React由Meta公司开发,是一个用于 构建Web和原生交互界面的库 +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/01.png) + +### React的优势 + +相较于传统基于DOM开发的优势 + +1. 组件化的开发方式 +2. 不错的性能 + +相较于其它前端框架的优势 + +1. 丰富的生态 +2. 跨平台支持 + +### React的市场情况 + +全球最流行,大厂必备 + + +## 开发环境创建 + +create-react-app是一个快速创建React开发环境的工具,底层由Webpack构件,封装了配置细节,开箱即用 +执行命令: + +```bash +npx create-react-app react-basic +``` + +1. npx - Node.js工具命令,查找并执行后续的包命令 +2. create-react-app - 核心包(固定写法),用于创建React项目 +3. react-basic React项目的名称(可以自定义) + +:::warning + +创建 React 项目的更多方式 + +官方文档: + +::: + + + +根组件 + +```js +//index.js +import React from 'react' +import ReactDOM from 'react-dom/client' +// 导入项目的根组件 +import App from './App' +// 把App根组件渲染到id为root的dom节点上 +const root = ReactDOM.createRoot(document.getElementById('root')) +root.render() +``` + +```js +//App.js +// 项目的根组件 +//App-> index.js -> public/index.html(root) +function App() { + return ( +
+ this one is a app +
+ ); +} +export default App +``` + + + +## JSX基础 + +### 什么是JSX + +> 概念:JSX是JavaScript和XMl(HTML)的缩写,表示在JS代码中编写HTML模版结构,它是React中构建UI的方式 + +```jsx +const message = 'this is message' + +function App(){ + return ( +
+

this is title

+ {message} +
+ ) +} +``` + +优势: + +1. HTML的声明式模版写法 +2. JavaScript的可编程能力 + +### JSX的本质 + +> JSX并不是标准的JS语法,它是 JS的语法扩展,浏览器本身不能识别,需要通过==解析工具==做解析之后才能在浏览器中使用 + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/03.png) + +### JSX高频场景-JS表达式 + +> 在JSX中可以通过 `大括号语法{}` ==识别==JavaScript中的表达式,比如常见的变量、函数调用、方法调用等等 + +1. 使用引号传递字符串 +2. 使用JS变量 +3. 函数调用和方法调用 +4. 使用JavaScript对象 + :::warning + 注意:if语句、switch语句、变量声明不属于表达式,不能出现在{}中 + ::: + +```jsx +const message = 'this is message' + +function getAge(){ + return 18 +} + +function App(){ + return ( +
+

this is title

+ {/* 字符串识别 */} + {'this is str'} + {/* 变量识别 */} + {message} + {/* 函数调用 渲染为函数的返回值 */} + {getAge()} + {/*使用js对象*/} +
this is div
+
+ ) +} +``` + +### JSX高频场景-列表渲染 + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/04.png) + +> 在JSX中可以使用原生js种的`map方法` 实现列表渲染 + +```jsx +const list = [ + {id:1001, name:'Vue'}, + {id:1002, name: 'React'}, + {id:1003, name: 'Angular'} +] + +function App(){ + return ( +
    + // 这里需要绑定一个key + {list.map(item=>
  • {item.name}
  • )} +
+ ) +} +``` + +### JSX高频场景-条件渲染 + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/05.png) + +> 在React中,可以通过逻辑与运算符&&、三元表达式(?:) 实现基础的条件渲染 + +```jsx +const flag = true +const loading = false + +function App(){ + return ( + <> + {flag && this is span} + {loading ? loading...:this is span} + + ) +} +``` + +### JSX高频场景-复杂条件渲染 + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/06.png) + +> 需求:列表中需要根据文章的状态适配 +> 解决方案:自定义函数 + 判断语句 + +```jsx +const type = 1 // 0|1|3 + +function getArticleJSX(){ + if(type === 0){ + return
无图模式模版
+ }else if(type === 1){ + return
单图模式模版
+ }else(type === 3){ + return
三图模式模版
+ } +} + +function App(){ + return ( + <> + { getArticleJSX() } + + ) +} +``` + +## React的事件绑定 + +### 基础实现 + +> React中的事件绑定,通过语法 `on + 事件名称 = { 事件处理程序 }`,整体上遵循驼峰命名法 + +```jsx +function App(){ + const clickHandler = ()=>{ + console.log('button按钮点击了') + } + return ( + + ) +} +``` + +### 使用事件参数 + +> 在事件回调函数中设置形参e即可 + +```jsx +function App(){ + const clickHandler = (e)=>{ + console.log('button按钮点击了', e) + } + return ( + + ) +} +``` + +### 传递自定义参数 + +> 语法:事件绑定的位置改造成箭头函数的写法,在执行clickHandler实际处理业务函数的时候传递实参 + +```jsx +function App(){ + const clickHandler = (name)=>{ + console.log('button按钮点击了', name) + } + return ( + + ) +} +``` + +:::warning +注意:不能直接写函数调用,这里事件绑定需要一个函数引用 +::: + +### 同时传递事件对象和自定义参数 + +> 语法:在事件绑定的位置传递事件实参e和自定义参数,clickHandler中声明形参,注意顺序对应 + +```jsx +function App(){ + const clickHandler = (name,e)=>{ + console.log('button按钮点击了', name,e) + } + return ( + + ) +} +``` + +## React组件基础使用 + +### 组件是什么 + +概念:一个组件就是一个用户界面的一部分,它可以有自己的逻辑和外观,组件之间可以互相嵌套,也可以服用多次 +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/07.png) + +### 组件基础使用 + +> 在React中,一个组件就是**首字母大写的函数**,内部存放了组件的逻辑和视图UI, 渲染组件只需要把组件当成标签书写即可 + +```jsx +// 1. 定义组件 +// 函数组件 +function Button(){ + return +} +// 箭头函数组件 +const Button = ()=>{ + return +} + +// 2. 使用组件 +function App(){ + return ( +
+ {/* 自闭和 */} + +
+ ) +} +``` + +## 组件状态管理-useState + +### 基础使用 + +> useState 是一个 React Hook(函数),它允许我们向组件添加一个`状态变量`, 从而控制影响组件的渲染结果 +> 和普通JS变量不同的是,状态变量一旦发生变化组件的视图UI也会跟着变化(数据驱动视图) + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/08.png) + +```jsx +function App(){ + const [ count, setCount ] = React.useState(0) + return ( +
+ +
+ ) +} +``` + +### 状态的修改规则 + +> 在React中状态被认为是只读的,我们应该始终`替换它而不是修改它`, 直接修改状态不能引发视图更新 + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/09.png) + +### 修改对象状态 + +> 对于对象类型的状态变量,应该始终给set方法一个`全新的对象` 来进行修改 + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/10.png) + +## 组件的基础样式处理 + +> React组件基础的样式控制有俩种方式,行内样式和class类名控制 + +```jsx +
this is div
+``` + +```css +.foo{ + color: red; +} +``` + +```jsx +import './index.css' + +function App(){ + return ( +
+ this is span +
+ ) +} +``` + +## B站评论案例 + +![](https://qtp-1324720525.cos.ap-shanghai.myqcloud.com/blog/11.png) + +1. 渲染评论列表 +2. 删除评论实现 +3. 渲染导航Tab和高亮实现 +4. 评论列表排序功能实现 + +### 基础模版 + +```tsx +import { useState } from 'react' +import './App.scss' +import avatar from './images/bozai.png' + +/** + * 评论列表的渲染和操作 + * + * 1. 根据状态渲染评论列表 + * 2. 删除评论 + */ + +// 评论列表数据 +const defaultList = [ + { + // 评论id + rpid: 3, + // 用户信息 + user: { + uid: '13258165', + avatar: '', + uname: '周杰伦', + }, + // 评论内容 + content: '哎哟,不错哦', + // 评论时间 + ctime: '10-18 08:15', + like: 88, + }, + { + rpid: 2, + user: { + uid: '36080105', + avatar: '', + uname: '许嵩', + }, + content: '我寻你千百度 日出到迟暮', + ctime: '11-13 11:29', + like: 88, + }, + { + rpid: 1, + user: { + uid: '30009257', + avatar, + uname: '黑马前端', + }, + content: '学前端就来黑马', + ctime: '10-19 09:00', + like: 66, + }, +] +// 当前登录用户信息 +const user = { + // 用户id + uid: '30009257', + // 用户头像 + avatar, + // 用户昵称 + uname: '黑马前端', +} + +/** + * 导航 Tab 的渲染和操作 + * + * 1. 渲染导航 Tab 和高亮 + * 2. 评论列表排序 + * 最热 => 喜欢数量降序 + * 最新 => 创建时间降序 + */ + +// 导航 Tab 数组 +const tabs = [ + { type: 'hot', text: '最热' }, + { type: 'time', text: '最新' }, +] + +const App = () => { + return ( +
+ {/* 导航 Tab */} +
+
    +
  • + 评论 + {/* 评论数量 */} + {10} +
  • +
  • + {/* 高亮类名: active */} + 最新 + 最热 +
  • +
+
+ +
+ {/* 发表评论 */} +
+ {/* 当前用户头像 */} +
+
+ 用户头像 +
+
+
+ {/* 评论框 */} +