四:HTTP 协议——浏览器和服务器怎么对话

一、回顾与本篇目标

前面几篇我们用 Node.js 启动了一个 HTTP 服务,浏览器访问之后能返回文字和 HTML 页面。但有一个问题我们一直没解决:不管浏览器请求什么路径,服务器都返回同样的内容。访问 //user/article/随便乱写,结果全都一样。

这显然不合理。一个真正的后端服务,应该能根据不同的 URL 返回不同的内容——访问 /user/123 返回用户信息,访问 /article/456 返回文章内容,访问不存在的路径返回 404。

要实现这个能力,首先要理解浏览器到底给服务器发了什么,以及服务器应该返回什么。这就是 HTTP 协议的核心内容。

本篇的目标:

  1. 看懂一个 HTTP 请求到底长什么样——请求行、请求头、请求体
  2. 看懂一个 HTTP 响应到底长什么样——状态行、响应头、响应体
  3. 掌握最常用的请求方法 GET 和 POST 的区别
  4. 记住最常用的状态码及其含义
  5. 在 Node.js 代码中获取请求路径和请求方法,根据不同的路径和方法返回不同的内容

二、HTTP 是什么

HTTP 的全称是 HyperText Transfer Protocol(超文本传输协议)。它规定了浏览器和服务器之间通信的格式

你可以把 HTTP 理解为两个人通信时约定的写信格式。如果一个人用英文写信,另一个人只会读中文,那就无法沟通。同样,浏览器和服务器都需要按照 HTTP 协议的格式来组织要发送的信息,对方才能正确理解。

一次完整的 HTTP 通信包含两个部分:

  • 请求:浏览器发给服务器,说“我要什么”。
  • 响应:服务器返回给浏览器,说“给你什么”。

这两个部分各自由若干行文本组成,格式严格,但内容完全可以由人阅读。

三、请求报文:浏览器给服务器发的东西

当你在浏览器地址栏输入 http://localhost:3000/user?name=张三 并回车,浏览器会组装一个请求报文,通过互联网(或本机网络)发送给服务器。这个报文的原始格式大致是这样的:

GET /user?name=张三 HTTP/1.1
Host: localhost:3000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0
Accept: text/html,application/json
Accept-Language: zh-CN,zh;q=0.9

别看它好像一堆乱码,拆开之后每一行都有明确的含义。一个 HTTP 请求报文分为三部分:请求行、请求头、请求体。

第一部分:请求行(第一行)

GET /user?name=张三 HTTP/1.1

这一行包含三个信息,用空格隔开:

  • GET:请求方法。告诉服务器“我要做什么”。GET 的意思是“我要获取数据”,就像你对图书馆管理员说“我要借这本书”。
  • /user?name=张三:请求路径和查询参数。/user 是路径,?name=张三 是查询参数。查询参数就是附加在路径后面的键值对,用来传递额外的信息(比如搜索关键词、分页页码)。
  • HTTP/1.1:协议版本。告诉服务器“我用的是 HTTP 1.1 版本的协议”。目前主流是 HTTP/1.1 和 HTTP/2,HTTP/3 也在逐渐推广。

第二部分:请求头(中间若干行)

Host: localhost:3000
User-Agent: Mozilla/5.0 ...
Accept: text/html,application/json
Accept-Language: zh-CN,zh;q=0.9

请求头是若干行“键: 值”格式的文本,用来告诉服务器附加信息

  • Host:我要访问的是哪个域名(或 IP)的哪个端口。一台服务器上可能跑着多个网站,通过 Host 来区分。
  • User-Agent:我是什么浏览器。服务器可以根据这个返回不同版本的页面(比如给手机浏览器返回移动版)。
  • Accept:我能接收什么类型的内容。浏览器告诉服务器“我可以处理 HTML 和 JSON”。
  • Accept-Language:我偏好什么语言。服务器可以根据这个返回中文或英文版本的页面。

请求头有很多种,上面只是最常见的几个。后面遇到新的再讲。

第三部分:请求体(一个空行之后的内容)

在上面的请求报文中,请求头后面是一个空行,然后什么都没有。这是因为 GET 请求通常没有请求体。GET 的意思是“把数据给我”,要传递的信息都在 URL 的查询参数里了。

但对于 POST 请求,情况不同。POST 的意思是“我要提交数据给你”,这些数据会放在请求体里。比如你注册网站时填了用户名和密码,浏览器就会把这些信息放在 POST 请求的请求体中发给服务器。

一个典型的 POST 请求报文长这样:

POST /user/register HTTP/1.1
Host: localhost:3000
Content-Type: application/json
Content-Length: 45

{"username":"张三","password":"123456"}

注意变化:

  • 请求方法从 GET 变成了 POST
  • 多了两个请求头:Content-Type(告诉服务器“我发给你的数据是 JSON 格式”)和 Content-Length(告诉服务器“我发给你的数据有多少字节”)。
  • 空行之后有实际的内容——请求体,这里是一段 JSON 字符串。

四、GET 和 POST 的区别(以及其他方法)

HTTP 协议定义了好几种请求方法,最常用的是 GET 和 POST。理解它们的区别,是设计 API 的基础。

GET:获取数据

  • 语义:“把某某资源给我”。
  • 数据位置:参数放在 URL 的查询参数里(?key=value)。
  • 有没有请求体:通常没有。
  • 安全性:URL 里的参数会暴露在地址栏、浏览器历史记录、服务器日志中。所以不能用 GET 传递密码等敏感信息
  • 重复请求:多次相同的 GET 请求,应该返回相同的结果(幂等)。刷新页面、点浏览器后退按钮,GET 请求会重复发送,但不会产生副作用。
  • 典型场景:搜索商品、查看文章、获取用户信息。

POST:提交数据

  • 语义:“我要新建一个资源”。
  • 数据位置:参数放在请求体里。
  • 有没有请求体:有。
  • 安全性:请求体里的数据不会出现在 URL 中,比 GET 稍微安全一点。但 HTTP 本身是明文传输的,真正敏感的信息还需要配合 HTTPS 加密。
  • 重复请求:多次相同的 POST 请求,可能会创建多个重复的资源(不幂等)。浏览器在刷新页面时通常会提示“是否重新提交表单”,就是为了防止意外重复提交。
  • 典型场景:注册用户、提交订单、发布文章。

其他方法(先了解,后面用到再深入)

  • PUT:更新某个资源的全部信息(比如修改用户的全部资料)。
  • PATCH:更新某个资源的部分信息(比如只修改用户名的昵称)。
  • DELETE:删除某个资源。

五、响应报文:服务器返回给浏览器的东西

服务器收到请求后,会处理并返回一个响应报文。它的格式和请求报文类似,也分为三部分:状态行、响应头、响应体

一个典型的响应报文长这样:

HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 128
Date: Sat, 15 Mar 2025 08:30:00 GMT

<!DOCTYPE html>
<html>
<head><title>示例</title></head>
<body><h1>你好</h1></body>
</html>

第一部分:状态行(第一行)

HTTP/1.1 200 OK

同样包含三个信息:

  • HTTP/1.1:协议版本。
  • 200:状态码。一个三位数字,表示服务器处理请求的结果。
  • OK:状态码的文字描述,方便人类阅读。

第二部分:响应头

Content-Type: text/html; charset=utf-8
Content-Length: 128
Date: Sat, 15 Mar 2025 08:30:00 GMT

和请求头一样,响应头也是一些“键: 值”格式的附加信息:

  • Content-Type:返回的内容是什么类型。这个我们上一篇已经用过了——text/html 表示 HTML 网页,text/plain 表示纯文本,application/json 表示 JSON 数据。
  • Content-Length:返回的内容有多少字节。
  • Date:服务器响应的时间。

第三部分:响应体(空行之后)

空行之后就是实际返回给浏览器的内容。可以是一段 HTML 代码、一段 JSON 数据、一张图片的二进制数据,等等。你在浏览器里看到的网页内容,就来自响应体。

六、状态码:三位数字的含义

状态码是服务器给浏览器的“一句话总结”。你不需要背下所有状态码,但需要知道常见的几个,以及它们的分类规律。

状态码的第一位数字表示大类

第一位 含义 常见状态码
1xx 信息,请求已收到,继续处理 很少见,不用记
2xx 成功,请求已被正常处理 200 OK(成功)、201 Created(创建成功,常用于 POST 之后)
3xx 重定向,需要进一步操作 301(永久重定向)、302(临时重定向)、304(内容未修改,用缓存)
4xx 客户端错误,请求有问题 400 Bad Request(请求格式错误)、401 Unauthorized(未登录)、403 Forbidden(无权限)、404 Not Found(资源不存在)
5xx 服务器错误,服务器内部出了问题 500 Internal Server Error(服务器代码出错)、502 Bad Gateway(网关错误)、503 Service Unavailable(服务器暂时不可用)

记忆口诀

  • 2 开头:成了。你要的东西在这里。
  • 3 开头:去别处。你要的东西搬到另一个地址了。
  • 4 开头:你搞错了。你请求的路径不对、没带身份证明、没有权限。
  • 5 开头:我炸了。服务器内部出 bug 了。

404 是你最熟悉的陌生人:访问一个不存在的页面,服务器找不到对应的资源,就返回 404。这不是服务器坏了,而是“你要的东西不在我这里”。

500 是后端开发者的噩梦:当你看到 500,说明服务器代码抛出了未捕获的异常。这时候需要去查看服务器日志,找到报错信息并修复代码。

七、在 Node.js 中读取请求信息

理论讲完了,现在回到代码。前面几篇里,我们 createServer 的回调函数有两个参数 requestresponse。现在我们知道 request 里面装的就是浏览器发来的请求报文的信息,response 是用来构建响应报文的工具。

让我们实际打印一下 request 里面有什么。

// inspect-request.js

const http = require('http');

const server = http.createServer(function (request, response) {
  // 打印请求方法和请求路径
  console.log('请求方法:' + request.method);
  console.log('请求路径:' + request.url);

  // 打印请求头
  console.log('请求头:');
  console.log(request.headers);

  response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
  response.end('请求信息已打印到终端,请看终端输出。');
});

server.listen(3000, function () {
  console.log('服务已启动,访问 http://localhost:3000/user?name=张三');
});

启动服务,在浏览器里访问 http://localhost:3000/user?name=张三。终端的输出类似:

请求方法:GET
请求路径:/user?name=张三
请求头:
{
  host: 'localhost:3000',
  'user-agent': 'Mozilla/5.0 ...',
  accept: 'text/html,...',
  ...
}

request.method 返回请求方法('GET''POST' 等)。

request.url 返回请求的完整路径和查询参数('/user?name=张三')。

request.headers 返回一个对象,包含所有请求头。

有了 request.url,我们就可以判断用户请求了哪个路径,然后返回不同的内容。这就是路由的雏形。

八、实战:根据请求路径返回不同内容

现在我们把前面几篇的知识整合起来,写一个能区分不同路径的 HTTP 服务。

需求

  • 访问 / → 返回“欢迎来到首页”
  • 访问 /about → 返回“这是关于页面”
  • 访问其他任何路径 → 返回 404 状态码和“页面未找到”

创建 simple-router.js

// simple-router.js —— 根据路径返回不同内容的 HTTP 服务

const http = require('http');

const server = http.createServer(function (request, response) {
  // 获取请求的路径
  let url = request.url;

  // 根据不同的路径返回不同的内容
  if (url === '/') {
    response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
    response.end('欢迎来到首页!');
  } else if (url === '/about') {
    response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
    response.end('这是关于页面。');
  } else {
    // 所有不匹配的路径,返回 404
    response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
    response.end('404 - 页面未找到');
  }
});

server.listen(3000, function () {
  console.log('服务已启动,试试访问以下地址:');
  console.log('  http://localhost:3000/');
  console.log('  http://localhost:3000/about');
  console.log('  http://localhost:3000/随便写');
});

运行 node simple-router.js,分别测试三个路径。你会看到:

  • / → 欢迎来到首页
  • /about → 这是关于页面
  • /xxx → 404 – 页面未找到(打开 F12 控制台,Network 标签里能看到状态码确实是 404)

这就是最原始的路由实现——用 if...else 判断 request.url。真正的框架(比如下一篇要学的 Express)会把路由做得更优雅,但核心原理一模一样:拿到请求路径,匹配到对应的处理函数,返回对应的内容。

九、识别 GET 和 POST

除了区分路径,有时候同一个路径对 GET 和 POST 的处理也不一样。比如 /user/register

  • GET 请求 → 返回注册页面(展示一个表单)
  • POST 请求 → 接收表单数据,完成注册

在 Node.js 中,用 request.method 来区分:

// method-demo.js —— 区分 GET 和 POST

const http = require('http');

const server = http.createServer(function (request, response) {
  let url = request.url;
  let method = request.method;

  if (url === '/register') {
    if (method === 'GET') {
      // GET 请求:返回注册页面(HTML 表单)
      response.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
      response.end(`
        <h2>用户注册</h2>
        <form method="POST" action="/register">
          <input name="username" placeholder="用户名" /><br/>
          <input name="password" type="password" placeholder="密码" /><br/>
          <button type="submit">注册</button>
        </form>
      `);
    } else if (method === 'POST') {
      // POST 请求:处理注册逻辑(暂时先返回一个提示)
      response.writeHead(200, { 'Content-Type': 'text/plain; charset=utf-8' });
      response.end('注册成功!(数据处理功能稍后实现)');
    }
  } else {
    response.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
    response.end('404 - 页面未找到');
  }
});

server.listen(3000, function () {
  console.log('访问 http://localhost:3000/register');
});

访问 /register,你会看到一个注册表单。点击“注册”按钮,表单以 POST 方式提交到同一个路径,服务器识别到是 POST 请求,返回“注册成功”。

这个例子中的关键点:同一个路径 /register,GET 和 POST 两种方法,服务器返回了完全不同的内容。这就是后端 API 设计的常见模式。

十、本篇动手练习

练习 1:用浏览器开发者工具查看请求和响应

启动本篇第八节的 simple-router.js。打开浏览器,按 F12 打开开发者工具,切换到 Network 标签。访问 http://localhost:3000/,点击 Network 列表里出现的请求。你会看到完整的请求头、响应头、状态码。访问 /about/xxx,对比状态码的不同。

练习 2:增加更多路由

simple-router.js 里增加两个新路由:

  • 访问 /contact → 返回“联系方式:email@example.com”
  • 访问 /help → 返回“帮助中心正在建设中”

练习 3:处理查询参数

修改 simple-router.js,让 /hello 路径能够读取查询参数。例如访问 /hello?name=张三,返回“你好,张三!”。提示:request.url 包含了查询参数,你需要手动解析 ? 后面的部分。暂时可以用简单的字符串分割来处理。

十一、本篇小结

这一篇你系统学习了 HTTP 协议的基础:

  • HTTP 请求报文:请求行(方法 + 路径 + 协议版本)、请求头(键值对,附加信息)、请求体(POST 提交的数据)。
  • GET 和 POST 的区别:GET 用于获取数据,参数在 URL 中;POST 用于提交数据,参数在请求体中。
  • HTTP 响应报文:状态行(协议版本 + 状态码 + 文字描述)、响应头、响应体。
  • 状态码:2xx 成功,3xx 重定向,4xx 客户端错误,5xx 服务器错误。重点记住 200、201、301、302、400、401、403、404、500。
  • request.method 获取请求方法,request.url 获取请求路径和查询参数,request.headers 获取请求头。
  • 路由的基本原理:根据 request.urlrequest.methodif...else 分支处理,返回不同的内容。

HTTP 协议是前后端通信的“语言”。理解了它,你就知道前端发送了什么、后端接收到了什么、后端应该返回什么。下一篇,我们会在这个基础上正式学习路由的设计,并引入 Postman 这个专业工具来更方便地测试 API。

下一篇预告

下一篇——《API 与路由——让前后端正式对接》:我们会学会用 Postman 测试 API、解析查询参数和路径参数、设计符合 RESTful 风格的 API 接口,并写出一个能处理多种 URL 模式的完整路由模块。

后端零基础入门,每周更新。

© 版权声明
THE END
喜欢就支持一下吧
点赞11 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容