为什么要写这个呢?因为室友在秋招简历上写了个关于HTTP服务器的项目,在很多次面试中都被问到了,我对此甚是好奇,加上有时面试的时候也会被问到Spring这个常被用来作为Web开发的框架,觉得对于Web开发这些还是有必须要好好梳理一次。
主要参考:
廖雪峰老师的Java教程 本篇的内容很多都是来自廖老师,额外加上了个人理解,仅作个人记录。
实现一个Web服务器
当我们访问一个网页的时候,其实是我们的浏览器与远程的Web服务器建立了网络连接,现在一般是HTTPS连接,关于网络连接具体的流程是我之前写的另一篇文章,现在的网页一般是HTML的格式,HTML即超文本标记语言,是一种用于创建网页的标准标记语言,它一般和CSS、JavaScript一起来显示完整的网页,我们本地发送了GET请求拿到的第一个资源往往就是对应网页的HTML。
简单来说,对于浏览器而言,展示页面的流程大致如下:
- 与Web服务器建立连接
- 发送HTTP GET请求
- 将请求结果在浏览器渲染出来
上面是从一个客户端的角度来描述Web请求的整个过程。今天主要想说的是在作为Web服务器,Web服务器需要去处理客户端的请求,我们要怎么去做呢?
多线程实现Web服务器
下面的代码来自 廖雪峰老师的Java教程
最简单的方式,监听特定端口,来了一个请求之后,新启一个线程去处理:
1 | public class Server { |
只需要在handle()
方法中,用Reader读取HTTP请求,用Writer发送HTTP响应,即可实现一个最简单的HTTP服务器。
1 | private void handle(InputStream input, OutputStream output) throws IOException { |
Servlet开发
从上面的例子可以看到,除开特定的业务和展示页面外,编写一个HTTP服务要做的事其实总体来讲是相对固定的,都需要:
- 建立网络连接
- 监听特定端口
- 识别HTTP请求和HTTP头
- 复用TCP连接
- 异常处理
- …
无论是一个建立的 HTML 还是一个复杂的展示页面都需要实现这些步骤,完全可以把这些固定的步骤都封装起来,只留下处理请求部门的接口就好了,在 Java 中,这个接口就是 Servlet
。至于其它部分,处理 TCP 连接啊,解析HTTP协议啊都可以交给现成的Web服务器去做。
如果还不理解 Servlet 是怎样的一个概念,推荐阅读这个:
servlet的本质是什么,它是如何工作的? - bravo1988的回答 - 知乎 https://www.zhihu.com/question/21416727/answer/690289895
一个简单的 Servlet 例子:
1 | // WebServlet注解表示这是一个Servlet,并映射到地址/: |
一个Servlet总是继承自HttpServlet
,然后覆写doGet()
或doPost()
方法。注意到doGet()
方法传入了HttpServletRequest
和HttpServletResponse
两个对象,分别代表HTTP请求和响应。我们使用Servlet API时,并不直接与底层TCP交互,也不需要解析HTTP协议,因为HttpServletRequest
和HttpServletResponse
就已经封装好了请求和响应。以发送响应为例,我们只需要设置正确的响应类型,然后获取PrintWriter
,写入响应即可。
Servlet 的 API 来自 javax 包,它也是 Java 标准的一部分,属于核心库,但是没有直接放在 jdk 中,这是为了避免 jdk 太大。
1 | <project xmlns="http://maven.apache.org/POM/4.0.0" |
这个pom.xml
与普通 Java 程序有个区别,打包类型不是jar
,而是war
,表示 Java Web Application Archive。
依赖中<scope>
指定为provided
,表示编译时使用,但不会打包到.war
文件中,因为运行期Web服务器本身已经提供了Servlet API相关的jar包。
还需要在工程目录下创建一个web.xml
描述文件,放到src/main/webapp/WEB-INF
目录下(固定目录结构,不要修改路径,注意大小写)。文件内容可以固定如下:
1 | <!DOCTYPE web-app PUBLIC |
整个工程结构如下:
1 | web-servlet-hello |
运行Maven命令mvn clean package
,在target
目录下得到一个hello.war
文件,这个文件就是我们编译打包后的 Web应用程序。
我们应该如何运行这个war
文件?
普通的Java程序是通过启动JVM,然后执行main()
方法开始运行。但是Web应用程序有所不同,我们无法直接运行war
文件,必须先启动Web服务器,再由Web服务器加载我们编写的HelloServlet
,这样就可以让HelloServlet
处理浏览器发送的请求。
因此,我们首先要找一个支持Servlet API的Web服务器。目前最常用的开源免费的 Web服务器是 Tomcat,它是由Apache开发的开源免费服务器。还有 Jetty,GlassFish,收费的 WebLogic 等等都可以作为 Web服务器。无论哪一个服务器,只要它支持Servlet API 4.0(因为我们引入的Servlet版本是4.0),我们的war包都可以在上面运行。
将我们写的程序放到 Tomcat 的webapps 目录下,再启动 Tomcat服务器,我们就可以在浏览器上看到我们的展示页面啦~
1 | 浏览器键入 localhost:8080/hello |
大功告成!页面展示也有啦~
等等!我的 main 方法呢?Tomcat 到底是怎么调用我的 Servlet 的?Tomcat 里面真的和我写的多线程是一样的吗?
如果理解了 Servlet 的话,很容易看出,Servlet 只是完整 Web 程序的一部分,只是一个拼图,Tomcat 这类 Web 服务器将我们写的代码拼上去就完事儿,main方法当然也是 Tomcat 的main方法,一个拼图要什么 main 方法。至于怎么拼?反射!其实大部分框架都是如此,框架完整了大部分的工作,我们只需要实现其中的一小部分就好了,框架提高了遍历,也遮蔽了原理,甚至改变了我们编程的流程。
写了一个 Servlet 程序,很简单很方便,不需要我去和网络打交道了,简单的实现业务就可以在浏览器看到漂亮的页面了,很棒!但是,总有种奇怪的感觉。
在IDE中启动Tomcat
Tomcat 还提供了集成在IDEA启动的 tomcat-embed 包:
1 | <dependency> |
引入 Tomcat依赖 之后不必再引入 Servlet API 的依赖。在 IDE 中启动 Tomcat 可以很方面的进行调试,并且使用 Maven 打包之后, war 也可以放到独立的 Tomcat 服务器中。
JSP是什么
无论是在多线程实现 Web服务器 还是在 Servlet 中,我们都写了很多这样的代码:
1 | PrintWriter pw = resp.getWriter(); |
非常难受,因为不但要正确编写HTML,还需要插入各种变量。如果想用这种方式写一个复杂的HTML网页,那么是非常困难的。要解决这一问题,我们可以用 JSP。
JSP(全称JavaServer Pages)是由 Sun 公司主导创建的一种动态网页技术标准。它也是 Java EE 的一部分,可以根据请求动态的生成 HTML其他格式文档的 Web 网页。
文件必须放到/src/main/webapp
下,内容和 HTML 很像,需要插入变量的地方可以使用特殊指令<% ... %>
。
写一个hello.jsp
例子,内容如下:
1 | <html> |
整个JSP的内容实际上是一个HTML,但是稍有不同:
- 包含在
<%--
和--%>
之间的是JSP的注释,它们会被完全忽略; - 包含在
<%
和%>
之间的是Java代码,可以编写任意Java代码; - 如果使用
<%= xxx %>
则可以快捷输出一个变量的值。
访问 JSP 页面时,直接指定完整路径。例如,http://localhost:8080/hello.jsp
。
JSP 和 Servlet 本质上没有区别,因为JSP在执行前首先被编译成一个Servlet。在Tomcat的临时目录下,可以找到一个hello_jsp.java
的源文件,这个文件就是Tomcat把JSP自动转换成的Servlet源码。
JSP现在用得很少了,因为现在的Web开发一般是前后端分离的,JSP是不分离的,前后端分离可以解耦合,也让专人做专事。
对于前端而言,追求的是页面,兼容,用户体验等。对于后端而言,追求的是高并发,高可用,高性能,存储,安全等。
MVC开发
JSP 使编写 HTML 变得容易了起来,并且也支持在其中写Java代码,但是如果在 JSP 中写一大堆业务逻辑,包括 SQL 啊之类的,感觉也不是很好,而 MVC 的模式就能很好的结合二者。
MVC是Web开发中比较常用的一种模式,MVC全称是Model-View-Controller,其中 V 是JSP,C 是 Servlet,而 M 是传递在 Controller 和 View 传递的对象,这样做的好处是可以同时用 Servlet 来处理复杂的业务逻辑,用 JSP 来编写 HTML。
整个MVC架构如下:
1 | ┌───────────────────────┐ |
使用MVC模式的好处是,Controller专注于业务处理,它的处理结果就是Model。Model可以是一个JavaBean,也可以是一个包含多个对象的Map,Controller只负责把Model传递给View,View只负责把Model给“渲染”出来,这样,三者职责明确,且开发更简单,因为开发Controller时无需关注页面,开发View时无需关心如何创建Model。
上面例子中的 Controller是继承自 Servlet,Controller 还可以不继承自 Servlet,用一个接收所有请求的 Servlet 映射到 / ,然后传递给不同的 Controller 处理,Controller再进一步传递给 View。这样的MVC的架构如下:
1 | HTTP Request ┌─────────────────┐ |
具体的实现参考廖雪峰的官网网站。评论区的雪莉胡远超1982同学总结得不错:
- 核心思想是根据提出的简化原来用servlet、jsp的写法,构造满足需求(比如简单使用注解表示路径)的简易MVC框架;
- 构造以url为key,dispatch为value的map,其中dispatch是封装了对req、response操作的method;
- 初始化init函数就是通过扫描controller包中的controller对象并逐一遍历对象中的带有GetMapping注解的方法,构造上述的map;
- 当请求来时,对一个url通过map找到对应的method,从而通过反射调用此方法,完成对请求的处理;
JavaEE 的 Servlet 规范还提供了 Filter 组件和 Listener 组件。Filter 可以封装 Servlet 的一些共有逻辑,比如登陆检查,编码转换等等,多个Filter会组成一个链,但没有固定的顺序。通过 Listener 可以监听 Web应用程序 的生命周期,获取HttpSession
等创建和销毁的事件。
Spring MVC
Spring是目前最为流行的 Java EE 框架,它提供了一系列的基础设施,并且很容易和其它开源框架集成。Spring最核心的概念是IoC和AOP。
IoC 是控制反转,类似工厂模式。
AOP 是面向切面编程,指拓展功能而不修改源代码,类似装饰器模式。
在我们实现 MVC 框架的时候,最为核心的是 DispatcherServlet
,DispatcherServlet 接收到来自客户端的请求后,再将其进一步转发到不同的 Controller,Spring MVC 已经实现了这一部分,可以通过代码和 web.xml 中注册两种方式来实现:
1 | public class MyWebApplicationInitializer implements WebApplicationInitializer { |
以下是在 web.xml 中注册和初始化 DispatcherServlet 的方法:
1 |
|
初始化参数contextClass
指定使用注解配置的AnnotationConfigWebApplicationContext
,配置文件的位置参数contextConfigLocation
指向AppConfig
的完整类名,最后,把这个Servlet映射到/*
,即处理所有URL。
上述配置可以看作一个样板配置,有了这个配置,Servlet容器会首先初始化Spring MVC的DispatcherServlet
,在DispatcherServlet
启动时,它根据配置AppConfig
创建了一个类型是WebApplicationContext的IoC容器,完成所有Bean的初始化,并将容器绑到ServletContext上。
因为DispatcherServlet
持有IoC容器,能从IoC容器中获取所有@Controller
的Bean,因此,DispatcherServlet
接收到所有HTTP请求后,根据Controller方法配置的路径,就可以正确地把请求转发到指定方法,并根据返回的ModelAndView
决定如何渲染页面。
配置完 DispatcherServlet 之后,编写 Controller 就很容易了,Spring 可以用 @Controller
来进行注释。
1 |
|
使用Spring MVC时,整个Web应用程序按如下顺序启动:
- 启动Tomcat服务器;
- Tomcat读取web.xml并初始化DispatcherServlet;
- DispatcherServlet创建IoC容器并自动注册到ServletContext中。
启动后,浏览器发出的HTTP请求全部由DispatcherServlet接收,并根据配置转发到指定Controller的指定方法处理。
Spring Boot
Spring Boot 进一步简化了 Web 开发的过程,在写 Spring MVC 的时候需要进行的各种配置 Spring Boot 都已经做了,使开发人员使用管业务即可,如果有特殊需求,只需要通过修改配置或编写少量代码就能完成。
二者对比,知乎上这张图很形象:
Spring Boot只需要一个依赖项来启动和运行Web应用程序:
1 | <dependency> |
当我们引入spring-boot-starter-web
时,自动创建了:
ServletWebServerFactoryAutoConfiguration
:自动创建一个嵌入式Web服务器,默认是Tomcat;DispatcherServletAutoConfiguration
:自动创建一个DispatcherServlet
;HttpEncodingAutoConfiguration
:自动创建一个CharacterEncodingFilter
;WebMvcAutoConfiguration
:自动创建若干与MVC相关的Bean。- …
JSP 的 View 部分也可以直接在application
配置文件中配置几个属性来完成。
1 | spring.mvc.view.prefix=/WEB-INF/jsp/ |
更多 Spring 和 Spring Boot 的对比,可以参考 Spring 官网和这个博客。
Spring Boot给你使用20%的时间去解决80%的问题,剩下20%的问题需要你用200%的时间去弄懂。——by:沃·兹基硕德
值得思考的地方(拖稿)
上面内容是我们如何去实现一个 Web服务器,但这其中还有很多被忽视的地方,比如浏览器的渲染,HTTP协议,Cookie等部分。在真正实现一个线上业务的时候,这些都是需要考虑的。
HTTP协议的发展
回到一开始用多线程实现 Web服务器的地方~
IO模型
Tomcat是什么IO模型呢?
最后,感谢廖大,学到很多~