# Java Web 开发
# Web 基础
今天我们访问网站,使用 App 时,都是基于 Web 这种 Browser/Server 模式,简称 BS 架构,它的特点是,客户端只需要浏览器,应用程序的逻辑和数据都存储在服务器端。浏览器只需要请求服务器,获取 Web 页面,并把 Web 页面展示给用户即可。
# HTTP 协议
在 Web 应用中,浏览器请求一个 URL,服务器就把生成的 HTML 网页发送给浏览器,而浏览器和服务器之间的传输协议是 HTTP。
HTTP 协议是一个基于 TCP 协议之上的请求-响应协议,它非常简单
对于 Browser 来说,请求页面的流程如下:
- 与服务器建立 TCP 连接;
- 发送 HTTP 请求;
- 收取 HTTP 响应,然后把网页在浏览器中显示出来。
浏览器发送的 HTTP 请求如下:
GET / HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 xxx
Accept: */*
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8
其中,第一行表示使用 GET 请求获取路径为/的资源,并使用 HTTP/1.1 协议
服务器的响应如下:
HTTP/1.0 200 OK
Connection: close
Content-Type: text/html
Content-Length: 48
<html>...网页数据...
服务器响应的第一行总是版本号+空格+数字+空格+文本,数字表示响应代码。
HTTP 请求和响应都由 HTTP Header 和 HTTP Body 构成,其中 HTTP Header 每行都以\r\n 结束。如果遇到两个连续的\r\n,那么后面就是 HTTP Body。浏览器读取 HTTP Body,并根据 Header 信息中指示的 Content-Type、Content-Encoding 等解压后显示网页、图像或其他内容。
# 编写 HTTP Server
我们来看一下如何编写 HTTP Server。一个 HTTP Server 本质上是一个 TCP 服务器,我们先用 TCP 编程的多线程实现的服务器端框架:
服务入口
package com.example.web.lesson01;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
public class MyServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8080);
System.out.println("[debug] server is running");
for (; ; ) {
Socket sock = serverSocket.accept();
System.out.println("[debug] connected from" + sock.getRemoteSocketAddress());
Thread t = new Thread(new MyServerHandler(sock));
t.start();
}
}
}
处理每一个请求的线程代码
package com.example.web.lesson01;
import java.io.*;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class MyServerHandler implements Runnable {
Socket sock;
public MyServerHandler(Socket sock) {
this.sock = sock;
}
@Override
public void run() {
try (InputStream input = this.sock.getInputStream()) {
try (OutputStream output = this.sock.getOutputStream()) {
handle(input, output);
}
} catch (Exception e) {
} finally {
try {
this.sock.close();
} catch (IOException ioe) {
}
System.out.println("[debug] client disconnected.");
}
}
private void handle(InputStream input, OutputStream output) throws IOException {
System.out.println("[debug] Process new http request...");
BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(output, StandardCharsets.UTF_8));
boolean requestOk = false;
String first = reader.readLine();
System.out.println("```http");
System.out.println(first);
if (first.startsWith("GET / HTTP/1.")) {
requestOk = true;
}
for (; ; ) {
String header = reader.readLine();
if (header.isEmpty()) { // 读取到空行时, HTTP Header读取完毕
break;
}
System.out.println(header);
}
System.out.println("```");
System.out.println(requestOk ? "[debug] Response OK" : "[debug] Response Error");
String data;// 空行标识Header和Body的分隔
int length;
if (!requestOk) {
// 发送错误响应:
data = "<html><body><h1>404 Not Found</h1></body></html>";
writer.write("HTTP/1.0 404 Not Found\r\n");
} else {
// 发送成功响应:
data = "<html><body><h1>Hello, world!</h1></body></html>";
writer.write("HTTP/1.0 200 OK\r\n");
}
length = data.getBytes(StandardCharsets.UTF_8).length;
writer.write("Connection: close\r\n");
writer.write("Content-Type: text/html\r\n");
writer.write("Content-Length: " + length + "\r\n");
writer.write("\r\n");
writer.write(data);
writer.flush();
}
}
这里的核心代码是,先读取 HTTP 请求,这里我们只处理 GET /的请求。当读取到空行时,表示已读到连续两个\r\n,说明请求结束,可以发送响应。
发送响应的时候,首先发送响应代码 HTTP/1.0 200 OK 表示一个成功的 200 响应,使用 HTTP/1.0 协议,然后,依次发送 Header,发送完 Header 后,再发送一个空行标识 Header 结束,紧接着发送 HTTP Body,在浏览器输入 http://localhost:8080/
就可以看到响应页面:
Hello, world!
HTTP 目前有多个版本,1.0 是早期版本,浏览器每次建立 TCP 连接后,只发送一个 HTTP 请求并接收一个 HTTP 响应,然后就关闭 TCP 连接。
由于创建 TCP 连接本身就需要消耗一定的时间,因此,HTTP 1.1 允许浏览器和服务器在同一个 TCP 连接上反复发送、接收多个 HTTP 请求和响应,这样就大大提高了传输效率。
HTTP 2.0 可以支持浏览器同时发出多个请求,但每个请求需要唯一标识,服务器可以不按请求的顺序返回多个响应,由浏览器自己把收到的响应和请求对应起来。
# Web 基础 小结
使用 B/S 架构时,总是通过 HTTP 协议实现通信;
HTTP 协议是一个请求-响应协议,它总是发送一个请求,然后接收一个响应。
Web 开发通常是指开发服务器端的 Web 应用程序。
# Servlet 入门
编写 HTTP 服务器其实是非常简单的,只需要先编写基于多线程的 TCP 服务,然后在一个 TCP 连接中读取 HTTP 请求,发送 HTTP 响应即可。
但是,要编写一个完善的 HTTP 服务器,以 HTTP/1.1 为例,需要考虑的包括:
- 识别正确和错误的 HTTP 请求;
- 识别正确和错误的 HTTP 头;
- 复用 TCP 连接;
- 复用线程;
- IO 异常处理;
- ...
这些基础工作需要耗费大量的时间,并且经过长期测试才能稳定运行。如果我们只需要输出一个简单的 HTML 页面,就不得不编写上千行底层代码,那就根本无法做到高效而可靠地开发。
因此,在 JavaEE 平台上,处理 TCP 连接,解析 HTTP 协议这些底层工作统统扔给现成的 Web 服务器去做,我们只需要把自己的应用程序跑在 Web 服务器上。为了实现这一目的,JavaEE 提供了 Servlet API,我们使用 Servlet API 编写自己的 Servlet 来处理 HTTP 请求,Web 服务器实现 Servlet API 接口,实现底层功能:
┌───────────┐
│My Servlet │
├───────────┤
│Servlet API│
┌───────┐ HTTP ├───────────┤
│Browser│<──────>│Web Server │
└───────┘ └───────────┘
我们来实现一个最简单的 Servlet: