拆包粘包到底在解决什么问题

”TCP 拆包粘包“网络上的争论挺大的,原因在于这一概念在 RFC 中并未提到过,(* ̄︶ ̄)有点民科的赶脚。但实际上它是挺常见的一个问题,比较准确的描述应该是应用层分包问题。那么为什么提到这一概念总是会带上 TCP 而不是 UDP 呢?因为应用层分包问题本质是接收方在某个时刻接收到的数据不构成一条完整的消息

  • 对于 UDP 来说,它并未在 IP 协议外作出额外的可靠性特性,所以无论发送的数据荷载是多少都会完整的交由 IP 层处理,而 IP 层虽然会根据 MTU 进行分包,但是接收方会确保在 IP 层重组完成才会交由 UDP 处理,否则会直接丢弃。所以基于 UDP 协议实现的应用层协议每次接收到的数据都是完整的消息,无需应用层做额外的分包和重组

  • 对于 TCP 来说,它是无边界的字节流协议,为了解决可靠性的问题以及各种性能优化会导致接收方收到的数据不是完整的消息,比如:

    • 若数据荷载过小时,TCP 根据 Nagle 算法整合多个小数据包统一发送
    • 若数据荷载过大时,TCP 会根据 MSS 大小对数据进行分段发送

    这也就是为啥”拆包粘包“总是碰瓷 TCP 的原因了

Tomcat 线程模型详解

Tomcat 作为最常见的 Servlet 容器,在 6.x 版本就支持了 NIO 模式的 Connector 和 Reactor 模式相比有什么特殊之处吗?首先来看下面的表格:

Java Nio Connector NIO Java Nio2 Connector NIO2 APR/native Connector APR
Classname Http11NioProtocol Http11Nio2Protocol Http11AprProtocol
Tomcat Version since 6.0.x since 8.0.x since 5.5.x
Support Polling YES YES YES
Polling Size maxConnections maxConnections maxConnections
Read Request Headers Non Blocking Non Blocking Non Blocking
Read Request Body Blocking Blocking Blocking
Write Response Headers and Body Blocking Blocking Blocking
Wait for next Request Non Blocking Non Blocking Non Blocking
SSL Support Java SSL or OpenSSL Java SSL or OpenSSL OpenSSL
SSL Handshake Non blocking Non blocking Blocking
Max Connections maxConnections maxConnections maxConnections

Reactor模式

标准 I/O 会存在两个阶段:(以 TCP Socket 举例)

  • 数据在内核态和用户态的复制:TCP 协议栈维护着 Send Buffer(发送缓冲区)和 Recv Buffer(接收缓冲区),因此 read/write 都只是将用户态数据复制到内核态的 Socket Buffer(Send/Recv Buffer)。而 Socket Buffer 和网卡之间会通过 DMA 进行数据传输(不占用 CPU)
  • 等待数据就绪:对于 read 操作来说,就绪是指 Recv Buffer 没有可读的数据;而对于 write 操作来说,就绪是指 Send Buffer 已满无法写入

BIO 称为同步阻塞 I/O,它在上述的两个阶段均会阻塞。因此 BIO 必须为每个 TCP 连接创建新线程并阻塞等待其可读或可写。服务端若想支持大量客户端连接,在 BIO 的前提下使用多线程来解决是必然的事情,下面用一个简单的例子展示:

DNS是如何解析主机名的

DNS 解析的作用就是将主机名转换为 IP 地址,那么 DNS 解析过程是怎么样的,DNS 服务器又是如何快速响应全球用户的请求的呢?

DNS 解析原理

在解释 DNS 解析原理前首先需要知道的是,DNS 服务器只是一种统称,实际上 DNS 服务器的不同类型负责完全不同的功能,这对于后续理解整个原理具有非常关键的作用。下面根据 DNS 服务器功能不同主要分为 4 种类型:

  • 根域名服务器:根域名服务器负责维护顶级域名(如 .com, .net, .org 等)对应的 TLD 域名服务器地址,即当客户端向其发送 DNS 解析请求时,根域名服务器会返回其顶级域名对应的 TLD 域名服务器地址。目前全球的根域名服务器的任播(Anycast)地址共有 13 个

  • TLD 域名服务器(顶级域名服务器):不同顶级域名由不同的托管商负责,在根域名服务器响应了 TLD 域名服务器后的域名后,用户可以在 TLD 域名服务器获取到定向到权威服务器的信息,比如返回 zzcoder.cn 所在的权威服务器域名

  • 权威服务器:权威服务器维护了特定顶级域名下的所有子域名(比如 zzcoder.cn)的 DNS 记录。比如用户在域名注册商(如 DNSPod)下注册 zzcoder.cn 实际上就是在权威服务器添加各类 DNS 记录(A 记录,CNAME 记录等等)

  • 递归解析器:递归解析器也称为 DNS 解析器。递归解析器作为客户端与 DNS 域名服务器的中间人,是面向客户端最近的一站,它负责接收客户端的 DNS 查询,并分别向根域名服务器,TLD 域名服务器,权威服务器发送解析请求,然后将最终的响应返回给客户端。常见的公开的 DNS 解析器有 Google 的 8.8.8.8, Cloudflare 的 1.1.1.1

    每个递归解析器都会内置 DNS 根域名服务器的 13 个 任播地址

HTTP协议与无状态

大家常提起 HTTP 协议是无状态的,其指代的“无状态”是什么?常见的观点有:

  • 无状态代表相同的请求参数总是能返回相同的结果
  • HTTP 本身的设计是无状态的,增加了有状态协议头(Cookie/Session)后变成了有状态协议

对于第一个观点显而易见是错误的,它的表示更倾向于“幂等性”,这往往无法由协议本身保证,还需要服务器进行“有状态”的响应(若服务器不进行状态的持久化,当然无法做到幂等);而第二个观点听起来就靠谱很多,通过 Cookie/Session 进行状态维护从而保证了有状态。但我们可以考虑下 Cookie 或 Session 保证的是谁的状态?一般情况下,它们保证了后端服务器的状态,而非 HTTP 协议的状态。因此对于最初的问题 HTTP 协议的”无状态“,我们是不是应该从协议的本身出发呢?

揭开try-catch-finally的神秘面纱

根据 JDK Tutorial 的描述,除非在执行 try 或 catch 代码时线程被中断或 JVM 退出,finally 中的逻辑始终会执行。因此 finally 关键字常被用于释放资源,防止程序出现异常时出现资源泄露。本文主要探讨其在 JVM 层面的实现原理,以及 synchronized 关键字在类似场景的处理手段。首先来看一段简单的 try-finally 代码

1
2
3
4
5
6
7
public void testWithTryFinally() {
try {
System.out.println("try");
} finally {
System.out.println("finally");
}
}

如何优雅的避免空指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class NPESolution {

public void withIf(Person person){
if(person != null){
// ...
}
// ...
}

public void withSpringAssert(Person person){
Assert.isTrue(person != null, "person must be not null.");
// ...
}

public void withOptional(Person person){
Optional<Person> personOptional = Optional.ofNullable(person);
// ...
}

public void withJsr305Annotation(@Nonnull Person person){
Optional<Person> personOptional = Optional.of(person);
// ...
}
}

上述的代码是我在日常用于避免空指针(NPE)的常用方式,很长时间内我都热衷于断言(Assert)这类防御性编程方式,防御性编程可以有效的保证方法的输入条件,并在毫无意义的边界情况能够给出有效的提示,何乐而不为呢?事实上防御性编程也确实是一种非常推荐的方式,并且其在 Spring 源码中随处可见。而 JDK8 的 Optional 是否会是一种更优雅的方式呢?亦或许,另有它人?

HSDB从入门到实战

HSDB(Hotspot Debugger),是一款内置于 SA 中的 GUI 调试工具,可用于调试 JVM 运行时数据,从而进行故障排除

启动HSDB

检测不同 JDK 版本需要使用不同的 HSDB 版本,否则容易出现无法扫描到对象等莫名其妙的问题

  • Mac:JDK7 和 JDK8 均可以采用以下的方式

    1
    $ sudo java -cp ,:/Library/Java/JavaVirtualMachines/jdk1.7.0_80.jdk/Contents/Home/lib/sa-jdi.jar sun.jvm.hotspot.HSDB

    事实上经过测试,即使通过 JDK8 自带的 sa-jdi.jar 去扫描对象(scanoops)的时候也会发生扫不到的情况,但可以通过其他手段代替

    而 JDK11 的启动方式有些区别

    1
    $ /Library/Java/JavaVirtualMachines/jdk-11.0.1.jdk/Contents/Home/bin/jhsdb hsdb

    事实上经过测试,该版本启动的 HSDB 会少支持一些指令(比如 mem, whatis),因此目前不推荐使用该版本

  • Windows:

    1
    $ java -classpath "%JAVA_HOME%/lib/sa-jdi.jar" sun.jvm.hotspot.HSDB

其中启动版本可以使用 /usr/libexec/java_home -V 获取

若遇到 Unable to locate an executable at “/Users/xx/.jenv/versions/1.7/bin/jhsdb” (-1) 可通过 Jenv 切换到当前 Jdk 版本即可解决

Static Nested Or Inner Classes

在 Java 中,在一个类中声明另一个类则称为嵌套类,被声明为 static 的嵌套类称为静态嵌套类(static nested classes ),与之相对的非静态嵌套类被称为内部类((inner classes

  • 非静态嵌套类每个实例都包含一个额外指向外围对象的引用,换句话说,要实例化一个非静态嵌套类必须首先实例化外部类

  • 静态嵌套类独立于外部类实例,可以看作嵌套在一个顶级类中的顶级类。因此,如果嵌套类不要求访问外部类的实例变量或方法,就要始终把 static 修饰符放在它的声明中,使它成为静态嵌套类。(如果该嵌套类不作为基类,那么更适合同时加上 final 修饰符)。JDK1.8 源码可见各种这样的设计,如 ReentrantLock 中

    1
    2
    3
    static final class NonfairSync extends Sync {
    ...
    }

我们从四个方面来更详细的讨论它们的区别:

  • 嵌套类访问外部类的范围

  • 嵌套类本身定义变量的范围

  • 实例化

  • 同名覆盖

泛型进阶

无限制通配符

无限通配符即: <?>,主要在不确定或不关心实际参数类型时使用,如:

1
2
3
public boolean removeAll(Collection<?> c){
...
}

由于它不确定具体类型,所以不能将任何元素(Null 除外)放入,即它是只读的,但在很多情况下需要放入对象,因此一种比较常见的方法是使用 类型参数 作为辅助函数

1
2
3
4
5
6
7
public static void swap(List<?> list, int i, int j){
swapHelper(list, i, j);
}

public static <E> void swapHelper(List<E> list, int i, int j){
list.set(i, list.get(j));
}

那么 List<?>List<Object> 有什么区别呢?

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×