2015 年 5 月,HTTP/2 发布。
2015 年第 3 季度,我所在企业的一个战略级客户(而且是第二大客户)说,他们需要在当年年底之前支持 HTTP/2(原因忘了,且与本文无关,从略)。
而在当时,Tomcat、Jetty、Undertow 等都还不支持 HTTP/2,Nginx 虽然已开始紧锣密鼓的添加对 HTTP/2 的支持,但等别人总不是个办法。依我对 Java 世界干什么都慢三拍的了解,一旦别人进度稍慢或者出现了隐蔽的坑,还是要自己来搞。更重要的原因是,我们需要更简单的部署方式,需要更好地应对弱网环境,作为一个中型企业,还需要形成现代的、可通用的、自主可控的技术积累。
求人不如求己,自己搞吧。
劝君免谈 Java EE
既然是搞 Java Web,那么 Servlet 总是被人们提起,作为 Java EE 中存在感最高的标准,它被众多 Servlet 容器所支持,在世界各地的机房闪闪发光。
而自从接触 Java 的第一天,我就在想,Servlet,乃至更大范围的 Java EE, 在这个年头到底还有什么用?
很多朋友可能知道,Java EE 是若干个标准的总称,包括而不限于:Servlet、JSP、EJB、JTA、JPA、JSF、JMS(完整列出来有好几十个)。随着服务化、多语言开发、前后端分离的兴起,Java EE 在技术比较新的公司(尤其是那几个知名的互联网公司)迅速被边缘化,其中的 Servlet 的重要性也日趋减小。要知道,标准的意义在于协作,但谁又会没事换容器玩?前后端、服务之间的调用一般通过 REST 或 RPC,前者与是否采用 Servlet 毫无关系,后者更没有 Servlet 什么事。
我大胆地下判断:是时候抛弃 Java EE 了。
事实上,后序几年的技术发展也印证了我的判断。
Java EE 被 Oracle 抛弃而独立发展,标志着不接地气的 Java EE 全面败给了 Spring 开源栈。
Spring Boot 内置容器方式使得 Java Web 程序在外部看来是 HTTP Server 而非 Servlet,如此一来,Servlet 容器的部分就可以被取代,例如,Spring 5 提供的 WebFlux 底层就可以在 netty 而非 Servlet 之上构建。
语言无关的基础设施的涌现,使得 Java EE 的用处越来越少。
越来越多的优秀 Java 工程师不再知道 Java EE 是什么。
当然,这些都是后话。当时我要做的,就是赶快把一个被定名为 Fxxxx 的框架搞出来,无论是否用 Servlet。
网络库上的抉择
我当时所任职的那家企业,有着深厚的桌面软件背景和服务器软件背景,内部有大量 C/C++ 基础设施,其中就包括高性能的 TCP、UDP 网络库。它分别在 Windows、macOS、Linux 下,封装了性能最好的网络模型 I/O 完成端口、kqueue、epoll,在多个重要产品中稳定运行。
我对这些设施当然是信任的,唯一要考虑的问题是,如果用 Java 来包装它们,不可避免要用到大量的 JNI,这样一来,维护成本大大增加,构建、调试都比较困难。而当时 netty 也很成熟了,它也封装了各个平台上的高性能网络模型。尽管 netty 内部也有 JNI,但它久经考验且外部感知不到,也节省了把 C++ 库包装为 Java 库的时间(这挺费事的,由于 C++ 多范式且特性丰富,两门语言不大容易优美地对话,另外,那些 C++ 代码时间太久了,有些地方也腐化了)。
netty 尽管也有一些坑,例如,服务端意外退出,ByteBuf 奇怪错误,连接池资源泄露,等等。不过,同事们当中有经验丰富的,基本上坑都踩过,所以问题不大。
最终的选择是 netty。同事和业界高手,我同样报以信任。
主要 API
HTTP 服务器 API 大概这个样子:
Server.createDefault()
.route("GET", ((resp, req) www.ysyl157.com-> {
//
}))
.andRoute("POST", ((resp, req) -> {
//
}))
.andRoute("PUT", ((resp, req) -> {
//
}))
.StartTLS(cert, key, 443);
或者这个样子:
@Namespace(prefix = "/model")
public class ModelNamespace {
@Router(pattern = "/{id}/predict", method = "POST")
public void predict(Context ctx, String id) {
//
}
@Router(pattern = "/{id}/info"www.dasheng178.com,www.tiaotiaoylzc.com/ method = "GET")
public void info(Response resp, Request req, String id) {
//
}
}
HTTP 客户端大概这个样子:
// 省略了错误处理
Client client = new Client();
Response resp = client.doSync(req); // 同步地
CompletableFuture<Response> respOther www.hengda157.com= client.doAsync(req); // 异步地
客户端方面,如果对方的服务器支持 HTTP/2,则运用多路复用,否则,会维护一个连接池。
翔一样的 java.net.HttpURLConnection 实在不想再用了。
实现 HTTP/2
实现 HTTP/2 协议解析,把握核心概念是关键:
数据流(stream)
消息(message)
帧(frame)
由于之前对 SPDY(HTTP/2 的前身)不熟,所以理解这些概念还是费了一些力气的,不过好在最后也弄清楚了。数据流的优先级,处理依赖和权重,帧的分割和组装,HPACK 中的霍夫曼树编码(好像又回到了学生时代),服务端推送……不知经过了多少次调试、检查、修改,最后在 Chrome 和 Firefox 上测试成功了。当然,最重要的是客户满意了,这个项目也作为我们的技术积累。
DAO 模块 & Spring 整合模块
这两个地方没什么可说的,把 Hibernate 和 MyBatis 浅浅包装了一下,再加上连接池。连接池这块,我抽象出了“策略”接口,这样,使用者如果对内置的连接池(c3p0 等)不满意,则可通过实现策略的方式自己来搞。
而整合 Spring 是为了要它的依赖注入、切面织入、事务管理等功能。
进化为 Spring Boot Starter
再后来,随着 Spring Boot 的日渐成熟完善,我们将其引入了生产环境。同时,将 Fxxxx 去掉 DAO 模块和 Spring 整合模块,专注 Web 层,改为了 Spring Boot Starter。如此一来,Fxxxx 和 Spring 全家桶整合到一起,用起来更加舒畅。
继往开来
后来,我离开了这个团队。
技术的发展却一直没有停下脚步,几乎所有的基础实施都开始支持 HTTP/2 了,如 Nginx 1.10,Tomcat 9,新版本的 Undertow。OkHttp 则提供了精美的支持 HTTP/1.1 和 HTTP/2 的客户端,而后 Java 9 也做到了这一点。
随着行业技术进步,这个项目的存在意义越来越小了。但它包含了我们技术人不畏艰难,别人没有就自己干的精神,激励着我前行。