# 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 可以支持浏览器同时发出多个请求,但每个请求需要唯一标识,服务器可以不按请求的顺序返回多个响应,由浏览器自己把收到的响应和请求对应起来。

# 小结

使用 B/S 架构时,总是通过 HTTP 协议实现通信;

HTTP 协议是一个请求-响应协议,它总是发送一个请求,然后接收一个响应。

Web 开发通常是指开发服务器端的 Web 应用程序。