B/S 架构与 HTTP 响应基础 | JavaSE
B/S 架构与 HTTP 响应基础
一、学习目标
学完本章,你应该能够:
- 能够解释 B/S(Browser/Server)架构的基本工作模型。
- 能够说明浏览器本质上也是一个网络客户端。
- 能够描述浏览器输入 URL 后与 Java 服务端之间最基本的 TCP + HTTP 交互流程。
- 能够理解 HTTP 请求和 HTTP 响应的基本结构。
- 能够解释状态行、响应头、空行和响应正文分别有什么作用。
- 能够手写最小 HTTP/1.1 响应。
- 能够使用
ServerSocket + Socket + OutputStream返回 HTML 页面给浏览器。 - 能够理解
Content-Type、Content-Length和 UTF-8 编码之间的关系。 - 能够理解 JavaSE 手写 HTTP Server 与后续 Java Web / Tomcat 等服务器之间的关系。
二、核心知识
2.1 什么是 B/S 架构
B/S:
Browser / Server
浏览器 / 服务端
基本模型:
Browser
│
│ 网络请求
▼
Server
│
│ 网络响应
▼
Browser
例如:
Chrome
Edge
Firefox
Safari
都可以充当:
客户端。
所以:
B/S
并不是:
“没有客户端。”
更加准确的理解是:
不需要为普通用户单独开发一个专用客户端,而是利用浏览器作为通用客户端。
2.2 C/S 与 B/S 再对比
C/S:
自己开发 Client
↓
Server
例如:
桌面聊天客户端
游戏客户端
IDE 客户端
B/S:
Browser
↓
Server
例如:
个人博客
在线商城
后台管理系统
搜索网站
从开发者角度看,B/S 极大降低了客户端安装和更新成本。
2.3 浏览器和 Java Server 如何通信
假设启动:
Java Server
127.0.0.1:8080
浏览器访问:
http://127.0.0.1:8080/
最基础的过程:
浏览器
↓
找到 127.0.0.1
↓
连接 TCP 8080
↓
发送 HTTP Request
↓
Java Server 接收
↓
生成 HTTP Response
↓
通过 TCP 返回
↓
浏览器解析 HTTP
↓
解析 HTML
↓
显示网页
这里把整个网络编程阶段全部串起来了。
2.4 HTTP 是什么
HTTP:
Hypertext Transfer Protocol
超文本传输协议
HTTP 是:
应用层协议。
在本章演示的 HTTP/1.1 场景中,它工作在:
HTTP
↓
TCP
↓
IP
之上。
也就是说:
TCP
解决:
可靠传输字节。
而:
HTTP
解决:
浏览器和服务器如何理解这些字节分别代表请求、响应、状态码、头字段和网页正文。
2.5 HTTP 说明了“字节是什么意思”
上一章强调:
TCP
=
字节流
如果浏览器只是发送:
GET / HTTP/1.1
服务器必须知道:
GET
/
HTTP/1.1
分别是什么意思。
这些规则由:
HTTP 协议
规定。
因此:
TCP
负责传
HTTP
负责约定怎么说
这是理解网络分层的一个非常典型例子。
三、HTTP 请求基础
3.1 浏览器会主动发送 Request
浏览器连接服务器以后,并不是:
什么都不做
而是会发送 HTTP 请求。
例如一个极简请求:
GET / HTTP/1.1
Host: 127.0.0.1:8080
注意真正的 HTTP/1.1 文本协议行结束使用:
\r\n
因此更接近:
GET / HTTP/1.1\r\n
Host: 127.0.0.1:8080\r\n
\r\n
3.2 HTTP 请求的基本组成
初学阶段可以理解成:
请求行
请求头
请求头
请求头
空行
可选请求正文
例如:
GET /hello HTTP/1.1
Host: localhost:8080
User-Agent: ...
3.3 请求行
例如:
GET /hello HTTP/1.1
可以拆成:
GET
↓
请求方法
/hello
↓
请求目标
HTTP/1.1
↓
HTTP 协议版本
本章只需要认识:
GET
即可。
后续 Java Web 才系统学习:
- GET
- POST
- PUT
- DELETE
- HTTP Headers
- Query Parameters
- Request Body
等完整内容。
四、HTTP 响应基础
4.1 浏览器为什么不能直接接收一段 HTML
假设服务端只写:
<h1>Hello Java</h1>
浏览器虽然可能在某些实验环境中看到数据,但这并不是完整规范的 HTTP 响应。
浏览器期待的是:
HTTP Response。
基本结构:
状态行
响应头
响应头
响应头
空行
响应正文
4.2 最小 HTTP 响应模型
课程原始模型:
HTTP/1.1 200 OK\r\n
Content-Type: text/html; charset=UTF-8\r\n
\r\n
网页正文
展开:
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
<html>
...
</html>
三个核心部分:
状态行
+
响应头
+
响应正文
中间通过:
空行
分隔响应头和响应正文。
4.3 状态行
第一行:
HTTP/1.1 200 OK
包含:
HTTP/1.1
↓
协议版本
200
↓
状态码
OK
↓
原因短语
其中最重要的是:
状态码
4.4 HTTP 状态码分类
HTTP 状态码按第一位通常分成:
| 状态码 | 含义 |
| ------ | ------------------------- |
| 1xx | Informational,信息性响应 |
| 2xx | Successful,成功 |
| 3xx | Redirection,重定向 |
| 4xx | Client Error,客户端错误 |
| 5xx | Server Error,服务端错误 |
初学最常见:
200 OK
表示:
请求成功处理。
以后还会经常遇到:
404 Not Found
资源不存在。
500 Internal Server Error
服务器内部错误。
4.5 Content-Type
响应头:
Content-Type: text/html; charset=UTF-8
表示:
text/html
↓
响应正文是什么类型
以及:
charset=UTF-8
↓
文本使用什么字符编码
浏览器根据:
Content-Type
决定:
如何理解响应正文。
如果返回:
<html>...</html>
通常应该:
text/html
如果返回纯文本:
text/plain
如果以后返回 JSON:
application/json
4.6 为什么 charset 很重要
假设 HTML:
<h1>星雨笔录</h1>
服务端编码:
UTF-8
浏览器却按照其他字符集解释:
可能乱码
因此:
Content-Type: text/html; charset=UTF-8
就是在协议层告诉浏览器:
响应正文中的文本按照 UTF-8 解码。
4.7 HTTP 中的空行为什么不能漏
HTTP Header 与 Body 之间:
必须存在边界
简单 HTTP/1.1 文本协议中表现为:
Header\r\n
Header\r\n
\r\n
Body
连续:
\r\n\r\n
表示:
Header 部分结束。
如果漏掉这个空行:
浏览器可能继续把正文理解成 Header
协议结构就会出错。
4.8 Content-Length
一个更加明确的 HTTP 响应还可以加入:
Content-Length: 123
表示:
响应正文有多少个字节。
注意:
Content-Length
表示的是:
字节数。
不是:
html.length()
这种 Java 字符数量。
正确思路:
byte[] body =
html.getBytes(
StandardCharsets.UTF_8
);
int contentLength =
body.length;
所以:
Java String 字符数
≠
UTF-8 编码后的字节数
特别是出现中文时更加明显。
五、使用方法
5.1 创建最小 B/S 服务端
先建立:
ServerSocket serverSocket =
new ServerSocket(8080);
浏览器访问:
http://127.0.0.1:8080/
浏览器本质上会创建一个 TCP 客户端连接。
服务端:
Socket socket =
serverSocket.accept();
就可以获得浏览器的 TCP Socket。
5.2 HTTP 处理任务
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class HttpTask implements Runnable {
private final Socket socket;
public HttpTask(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try (
Socket client = socket;
BufferedReader reader =
new BufferedReader(
new InputStreamReader(
client.getInputStream(),
StandardCharsets.US_ASCII
)
);
OutputStream output =
client.getOutputStream()
) {
// 1. 读取请求行
String requestLine =
reader.readLine();
if (requestLine == null) {
return;
}
System.out.println(
"Request: "
+ requestLine
);
// 2. 读取剩余请求头
String line;
while (
(line = reader.readLine())
!= null
&&
!line.isEmpty()
) {
System.out.println(line);
}
// 3. 准备 HTML
String html = """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>JavaSE HTTP Server</title>
</head>
<body>
<h1>星雨笔录 · JavaSE</h1>
<p>Hello HTTP!</p>
<p>这个页面来自 ServerSocket。</p>
</body>
</html>
""";
byte[] body =
html.getBytes(
StandardCharsets.UTF_8
);
// 4. 准备 HTTP Response Header
String headers =
"HTTP/1.1 200 OK\r\n"
+
"Content-Type: text/html; "
+
"charset=UTF-8\r\n"
+
"Content-Length: "
+
body.length
+
"\r\n"
+
"Connection: close\r\n"
+
"\r\n";
// 5. 输出 Header
output.write(
headers.getBytes(
StandardCharsets.US_ASCII
)
);
// 6. 输出 Body
output.write(body);
output.flush();
} catch (Exception e) {
System.out.println(
"HTTP 请求处理失败:"
+ e.getMessage()
);
}
}
}
5.3 B/S 服务端主程序
结合上一章线程池:
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class MiniHttpServer {
public static void main(String[] args)
throws Exception {
ExecutorService pool =
new ThreadPoolExecutor(
3,
10,
10,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
try (
ServerSocket serverSocket =
new ServerSocket(8080)
) {
System.out.println(
"HTTP Server 已启动"
);
System.out.println(
"浏览器访问:"
+
"http://127.0.0.1:8080/"
);
while (true) {
Socket socket =
serverSocket.accept();
pool.execute(
new HttpTask(socket)
);
}
} finally {
pool.shutdown();
}
}
}
运行程序后,在浏览器输入:
http://127.0.0.1:8080/
如果请求与响应格式正确,浏览器即可显示:
星雨笔录 · JavaSE
Hello HTTP!
这个页面来自 ServerSocket。
至此,我们实际上已经:
用 JavaSE 手写出了一个最小 Web Server。
六、原理与进阶
6.1 一次访问完整发生了什么
浏览器输入:
http://127.0.0.1:8080/
完整知识链可以画成:
Browser
│
│ TCP connect
▼
127.0.0.1:8080
│
▼
ServerSocket.accept()
│
▼
Socket
│
▼
ThreadPool
│
▼
HttpTask
│
│ read HTTP Request
▼
Java 业务逻辑
│
▼
HTML String
│
▼
UTF-8 byte[]
│
▼
HTTP Response
│
▼
Socket OutputStream
│
▼
TCP
│
▼
Browser
│
▼
解析 HTML
│
▼
渲染页面
这一张图几乎串联了:
- 字符串
- 编码
- IO
- 网络
- TCP
- 多线程
- 线程池
- B/S
- HTTP
6.2 为什么响应头使用 ASCII,而 HTML 使用 UTF-8
HTTP 协议中的:
HTTP/1.1 200 OK
Content-Type
Content-Length
这些基础协议元素属于非常有限的文本字符范围。
因此示例中:
headers.getBytes(
StandardCharsets.US_ASCII
)
可以清晰表达:
这是 HTTP 协议头。
而真正网页正文包含:
中文
使用:
StandardCharsets.UTF_8
编码。
6.3 为什么原教学示例可以直接 println
课程原始案例使用:
PrintStream ps =
new PrintStream(
socket.getOutputStream()
);
ps.println(
"HTTP/1.1 200 OK"
);
ps.println(
"Content-Type:text/html;"
+
"charset=utf-8"
);
ps.println();
ps.println("<html>...");
它非常适合第一次理解:
状态行
+
响应头
+
空行
+
HTML
但是从协议精确性来说:
HTTP/1.1 的协议行结束符定义为
CRLF,即\r\n。
而:
println()
使用的是:
当前平台的行结束符。
因此新版主方案直接:
"HTTP/1.1 200 OK\r\n"
显式构造协议数据。
这样不依赖:
Windows
Linux
macOS
的平台换行差异。
6.4 为什么加入 Content-Length
原始最小案例没有:
Content-Length
而是在写完网页后:
socket.close();
对于没有其他正文长度声明的 HTTP 响应:
连接关闭
本身可以成为正文结束的边界。
但学习一个更加明确的 HTTP 响应时:
Content-Length
非常值得加入。
它告诉浏览器:
接下来应该读取 N 个字节作为 Body
例如:
Content-Length: 235
比:
一直读到服务器断开
拥有更明确的消息边界。
6.5 Connection: close
本章为了降低 HTTP 持久连接的复杂度,明确发送:
Connection: close
表示:
当前响应完成后关闭 TCP 连接。
因此模型非常简单:
连接
↓
一个请求
↓
一个响应
↓
关闭连接
真实 HTTP/1.1 还支持:
持久连接
即同一个 TCP Connection 可以承载多个 HTTP 请求/响应。
但这已经超出本章最小 HTTP Server 的目标。
6.6 浏览器为什么可能连接不止一次
你明明只在地址栏输入了一次:
http://127.0.0.1:8080/
服务器日志却可能看到:
多次连接
这是正常现象。
因为网页可能引用:
<img src="...">
<link rel="stylesheet" href="...">
<script src="..."></script>
浏览器还会继续请求:
- 图片
- CSS
- JavaScript
- favicon
- 其他资源
所以:
一个“网页”并不等于一个网络资源。
6.7 为什么浏览器访问图片会再次发 HTTP 请求
例如服务器返回:
<img src="/logo.png">
浏览器首先拿到:
index HTML
解析发现:
/logo.png
于是再发送:
GET /logo.png HTTP/1.1
所以未来真正 Web Server 必须根据:
Request Path
决定返回:
HTML
图片
CSS
JS
JSON
等不同内容。
这就是:
路由(Routing)
概念最早的雏形。
6.8 为什么我们没有实现真正完整的 HTTP Server
真正的 HTTP Server 需要处理:
- 请求方法
- URL
- Header
- Body
- Content-Length
- Chunked Encoding
- Persistent Connection
- 状态码
- MIME Type
- 错误响应
- 安全问题
- 并发
- TLS / HTTPS
- 超时
- 日志
显然不应该全部自己重新实现。
因此企业 Java Web 开发不会每天:
new ServerSocket(8080);
手搓 HTTP 协议。
而是使用成熟 Web Server / Servlet Container / Framework。
例如后面会接触:
Tomcat
Spring Boot
Spring MVC
它们帮助开发者处理大量底层网络与 HTTP 细节。
6.9 Java Web 到底从哪里开始
可以建立这条知识链:
JavaSE Socket
↓
TCP
↓
HTTP
↓
Web Server
↓
Servlet / Controller
↓
Java Web
所以 Java Web 并不是凭空出现。
以前你写:
@GetMapping("/hello")
public String hello() {
return "hello";
}
看起来只是一个 Controller 方法。
但它背后最终仍然要完成:
浏览器建立连接
↓
发送 HTTP Request
↓
服务器解析 Request
↓
调用业务代码
↓
生成 HTTP Response
↓
返回浏览器
JavaSE 网络编程就是这一切的底层认知基础。
6.10 HTTP/1.1 与所有 HTTP 是否完全等价
不是。
本章手写的是:
HTTP/1.1
+
TCP
用于建立最清晰的网络模型。
现代 HTTP 体系还有:
HTTP/2
HTTP/3
其中 HTTP/3 的底层网络机制已经与经典:
HTTP/1.1 over TCP
不同。
因此应该准确表达:
本章学习的是 B/S 架构与 HTTP/1.1 基础响应模型,目的是理解 Java Web 的网络底层,而不是完整覆盖现代 HTTP 协议族。
七、常见问题
7.1 B/S 是不是没有客户端
不是。
Browser 就是客户端。
区别主要是:
开发者不需要再为普通用户开发专门客户端。
7.2 HTTP 和 TCP 是同一个协议吗
不是。
TCP:
传输层
主要提供:
可靠、有序字节流
HTTP:
应用层
定义:
浏览器和服务器如何组织请求和响应
7.3 HTTP Request 是浏览器自动生成的吗
是。
当你访问:
http://127.0.0.1:8080/
浏览器会按照 HTTP 协议发送请求。
7.4 为什么服务器不能只写 HTML
浏览器需要知道:
这是 HTTP 响应
状态是什么
正文是什么类型
正文从哪里开始
因此应该按照 HTTP Response 格式返回。
7.5 HTTP 响应为什么需要空行
用来分隔:
Headers
与:
Message Body
7.6 Content-Type 有什么作用
告诉客户端:
正文是什么媒体类型、应该如何解释。
例如:
text/html
表示 HTML。
7.7 Content-Length 应该写 html.length() 吗
不应该简单这样写。
Content-Length 表示:
字节数
应该:
byte[] body =
html.getBytes(
StandardCharsets.UTF_8
);
body.length
7.8 为什么中文更能暴露 Content-Length 错误
因为:
一个 Java char / 字符
与:
UTF-8 编码后的字节数
不是一一等价。
中文通常需要多个 UTF-8 字节。
7.9 println 为什么不作为新版主方案
因为 HTTP/1.1 协议行终止符是:
\r\n
而 println() 使用平台行结束符。
教学演示容易理解,但精确实现推荐显式写:
"\r\n"
7.10 为什么这个服务器响应一次就关闭 Socket
因为本章为了聚焦:
请求
↓
响应
采用:
Connection: close
简化持久连接问题。
7.11 浏览器刷新为什么服务器又执行一次
因为刷新代表:
浏览器重新发起 HTTP Request。
服务器因此重新:
accept
↓
处理
↓
response
7.12 这个 MiniHttpServer 可以上线当网站服务器吗
不应该。
它只是教学程序。
没有完整实现:
- HTTP
- HTTPS
- 安全
- 路由
- 静态资源
- 异常处理
- 请求大小限制
- 超时
- 攻击防护
- 生产级连接管理
它的价值在于:
帮你看清 Java Web 底下究竟发生了什么。
七、练习与验收
7.1 知识问答
- B/S 的全称是什么?
- Browser 在 B/S 中是不是客户端?
- 浏览器输入 URL 后最基础的网络流程是什么?
- HTTP 属于哪一层协议?
- HTTP/1.1 与 TCP 是什么关系?
- HTTP Request 最基础由哪些部分组成?
- HTTP Response 最基础由哪些部分组成?
HTTP/1.1 200 OK属于响应哪一部分?Content-Type有什么作用?- Header 与 Body 之间为什么要有空行?
Content-Length表示字符数还是字节数?- 为什么一个网页可能触发多次 HTTP 请求?
- 为什么企业开发通常不直接使用
ServerSocket手写完整 HTTP Server?
7.2 代码阅读
阅读:
String html =
"<h1>你好 Java</h1>";
byte[] body =
html.getBytes(
StandardCharsets.UTF_8
);
String headers =
"HTTP/1.1 200 OK\r\n"
+
"Content-Type: text/html; "
+
"charset=UTF-8\r\n"
+
"Content-Length: "
+
body.length
+
"\r\n"
+
"\r\n";
回答:
200表示什么?- 浏览器为什么知道正文是 HTML?
- 为什么需要 UTF-8?
- 为什么使用
body.length? - 最后的
\r\n\r\n表示什么?
7.3 手写代码
关闭 AI 自动补全。
实现:
MiniHttpServer
要求:
- 监听 TCP 8080
- 浏览器访问
/ - 返回
HTTP/1.1 200 OK - 返回
text/html - 使用 UTF-8
- 正确写入
Content-Length - Header 和 Body 正确分隔
- 页面显示:
- 标题
- 一个 H1
- 一个段落
然后浏览器访问:
http://127.0.0.1:8080/
验证结果。
7.4 Debug
下面响应有什么问题?
String html =
"<h1>星雨笔录</h1>";
String response =
"HTTP/1.1 200 OK\r\n"
+
"Content-Type: text/html; "
+
"charset=UTF-8\r\n"
+
"Content-Length: "
+
html.length()
+
"\r\n"
+
"\r\n"
+
html;
回答:
html.length()为什么可能不是正确 Content-Length?- 中文字符为什么会暴露问题?
- 应该怎样计算正文长度?
7.5 综合训练
将服务器升级成:
GET /
→ 首页
GET /hello
→ Hello 页面
其他路径
→ 404
要求:
- 读取 Request Line。
- 从中分析请求路径。
/返回一个 HTML。/hello返回另一个 HTML。- 其他请求返回:
HTTP/1.1 404 Not Found
不要使用 Spring Boot。
只允许:
ServerSocket
Socket
IO
ThreadPool
String
完成。
这个任务就是一个:
极简路由 Web Server。
7.6 本章验收
如果你能够闭卷画出:
Browser
│
│ HTTP Request
▼
Socket
│
▼
ServerSocket
│
▼
ThreadPool
│
▼
Java Business Logic
│
▼
HTTP Response
│
▼
TCP
│
▼
Browser
│
▼
HTML Render
并能手写:
HTTP/1.1 200 OK\r\n
Content-Type: text/html; charset=UTF-8\r\n
Content-Length: ...\r\n
\r\n
<html>...</html>
同时能够回答:
HTTP 与 TCP 到底分别负责什么?
Content-Type、Content-Length、空行分别有什么意义?
Java Web 为什么最终仍然建立在网络通信之上?
那么 JavaSE 网络编程的主线已经完整打通。