HTTP

HTTP(HyperText Transfer Protocol,HTTP)是实现Web的应用层协议,HTPP由两个程序实现:一个客户端程序和服务器程序。这两个程序运行在两个设备上,使用HTTP报文进行会话。在深入HTTP之前,先解释一些Web的术语。

Web相关概念

  • Web页面(Web page):Web页面是由对象组成的。一个对象(object)是一个文件,这个文件可以是HTML、JPRG图形、一个JAVA程序或一段视频这样的文件。

    多数Web页面会包含一个HTML基本文件以及引用的几个对象

  • Web浏览器(Web browser):Web浏览器是实现了HTTP协议的客户端(client)。

  • Web服务器(Web server):Web服务器是实现了HTTP协议的服务器,能够存储Web对象,每个对象由URL寻址获得。

HTTP概况

HTTP定义了Web浏览器向Web服务器请求Web页面的方式、服务器向Web浏览器发送页面的方式。基本的思路如下图:

Http_1

HTTP使用TCP作为运输层协议,在服务器和客户端建立起TCP连接后,浏览器和客户端进程就能通过套接字(socket)访问TCP。

Web服务器能够从套接字获得从Web客户端发送的HTTP请求报文,同样Web客户端能从套接字获得从Web服务器发送的HTTP响应报文。当HTTP报文通过套接字进入TCP,报文就会脱离程序的控制并进入TCP的控制。这就是划分网络协议层的原因,我们无需关注TCP的工作,只需要关注应用层(HTTP)怎样处理报文。

无状态协议

HTTP是一个无状态协议,因为HTTP服务器不会保存用户的任何信息。但这不代表Web服务器不会保存任何信息,HTTP提供了cookie以支持Web服务器跟踪用户内容。

用go创建一个Web服务器和客户端

在浏览器或者使用curl命令访问本地 http://127.0.0.1:8080 ,会返回一个”welcome to this Web Page”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package main

import (
"fmt"
"net/http"
)

func main() {
http.HandleFunc("/", myHandle)
http.ListenAndServe("127.0.0.1:8080", nil)
}

func myHandle(w http.ResponseWriter, r *http.Request) {
// 回复
w.Write([]byte("welcome to this Web Page"))
}

或者编写一个客户端发送HTTP请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import (
"fmt"
"io"
"net/http"
)

func main() {
resp, _ := http.Get("http://127.0.0.1:8080")
defer resp.Body.Close()

buf := make([]byte, 1024)
for {
// 接收服务端信息
n, err := resp.Body.Read(buf)
if err != nil && err != io.EOF {
fmt.Println(err)
return
} else {
fmt.Println("读取完毕")
res := string(buf[:n])
fmt.Println(res)
break
}
}
}

代码来源:http编程 · Go语言中文文档 (topgoer.com)

非持续连接和持续连接

在客户端和服务器进行通信前,必须先建立起连接,然后客户端就能够发送一系列请求。客户端-服务器的连接是通过TCP建立起的,应用程序的设计者就需要考虑一个问题,即每个请求/响应对是经过一个TCP连接发送,还是所有请求/响应对都经过一个TCP连接发送

这两种方式分别称为:

  • 非持续连接:每次发送一个HTTP请求都需要建立一个TCP连接,在客户端收到HTTP响应后关闭TCP连接。
  • 持续连接:发送一系列HTTP请求可以在一个TCP连接得到响应,服务器会在一段时间后关闭TCP连接。

下图中展示了三种HTTP连接的方式,也是HTTP模型发展的三个阶段:HTTP/1.0、HTTP/1.1、HTTP/2.0。前两者是我们介绍的非持续连接和持续连接的方式实现的,第三个采用了流水线的持续连接设计思路允许在同一TCP连接中并发的处理HTTP请求和回复HTTP响应。

connect

Source: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Connection_management_in_HTTP_1.x

可以看出这三种方式的优劣:

HTTP/1.0会频繁建立TCP连接,这会给服务器带来严重的负担。HTTP/1.1改善了这个问题,它使TCP连接保持打开,等待一个时间间隔(没有被使用)后关闭连接。HTTP/2.0能够比前两者减少更多的延迟,但不是所用请求都适合使用这种方式,这里不作过多介绍。

HTTP报文格式

HTTP报文有两种:请求报文和响应报文、

HTTP请求报文

HTTP请求报文主要由三部分组成:

  • 请求行(request line):包含三个字段
    • 方法字段:HTTP常见的方法包括GET、POST、HEAD、PUT和DELETE
    • URL:客户端请求的对象
    • HTTP版本:一般客户端默认版本使HTTP/1.1
  • 首部行(header line):首部行是向服务器或客户端提供一些必要的信息,比如:Host指明对象所在的主机,Connect指明发送完该对象后是否关闭(Keep-Alive/close),User-agent指明用户代理(向用户发送请求的浏览器类型)。
  • 实体体(entity body):实体体可以看作报文运输的货物。一般使用Post方法的报文会携带数据,这个数据可能来自网页的输入。

req

下面展示一个HTTP请求报文,第一行是请求行,其中使用方法是GET,请求对象 /,之后三行是首部行。GET方法一般没有实体体。

1
2
3
4
GET / HTTP/1.1 
Host: localhost:8080
User-Agent: Mozilla/5.0
Connection: keep-alive

可以在浏览器使用F12打开开发者工具查看网络中的HTTP请求,Edge、Google等主流浏览器都支持。

HTTP响应报文

HTTP响应报文与HTTP请求相似,但第一行与它不同,我们分析HTTP响应报文的状态行:

状态行分为三部分:

  • 版本:服务器使用HTTP版本,一般是HTTP/1.1
  • 状态码和短语:状态码是表明特定HTTP是否完成成功,短语与状态码相对应。常见响应如下
    • 200 OK:请求成功,信息返回响应报文中
    • 301 Move Permanently:请求的对象被永久转移,新的URL定义在响应报文首部行的 Localtion。客户软件会自动获取新的URL
    • 400 Bad Requset:一个通用差错代码,指示该请求不能被服务器理解

resp

下面展示一个HTTP响应报文,响应报文也能在浏览器中查看。

1
2
3
4
5
HTTP/1.1 200 OK
Content-Length:24
Content-Type:text/plain; charset=utf-8
Connection: keep-alive
Date:Fri, 19 Jul 2024 10:15:02 GMT

Cookie-解决HTTP无状态的设计

前面我们提到HTTP本身是一个无状态的协议,这能够简化服务器的设计,并且允许工程师去开发同时能处理上千个。

假如一个网站希望能够识别用户,可能是服务器用于限制用户的访问,或者是因为它想将内容与用户联系起来。这个时候,我们就能够使用cookie对用户进行跟踪,目前大多数Web站点都是用了cookie技术。

cookie实现需要实现四个组件

  • HTTP请求报文的首部行有cookie
  • HTTP响应报文的首部行有cookie
  • 用户端系统中有一个cookie文件,一般由用户的浏览器进行管理
  • Web站点有一个后端数据库

cookie工作过程

  1. PC端访问浏览器的Web对象,请求报文到达Web服务器后,服务器会创建一个识别码,以此作为索引在它的后端数据库中产生的表项。
  2. Web服务器会在HTTP响应报文的首部行添加Set-cookie:识别码 ,PC端接收后会根据Set-cookie在其特定的cookie文件中添加一行。
  3. PC之后向该服务器继续访问Web对象时会在HTTP请求报文首部行添加cookie字段,服务器可以根据请求报文中cookie跟踪用户动作或完成一些动作。
  4. PC端在一周后继续访问,加入服务器的cookie没有过期,服务器能够继续使用该cookie跟踪用户。

cookie

go代码模拟

我们使用go实现上述流程,我们假设一个场景:

  • 用户Alice是这个网站注册用户,首先Alice通过index网站登录,此时服务器发送HTTP响应报文时会携带一个Set-cookie发送给她的PC浏览器。
  • 然后Alice要在这个网站下获得一些需要的信息,Alice向该网站的另一个资源发出请求,发送的HTTP请求报文携带有cookie。
  • 网站服务器通过cookie确定Alice的身份,然后根据Alice在数据库中的信息推荐给她需要的信息。

服务器代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package main

import (
"net/http"
)

// 模拟后端数据库
var db map[string]string

func main() {
RUnDB()

http.HandleFunc("/", HandleIndex)
http.HandleFunc("/HandleCookie", HandleCookie)
http.ListenAndServe("127.0.0.1:8080", nil)
}

func RUnDB() {
db = make(map[string]string)
// 假设alice已经注册过了
db["username"] = "Alice"
}

func HandleIndex(writer http.ResponseWriter, request *http.Request) {
// 为客户端设置一个cookie id
cookie := http.Cookie{
Name: "username",
Value: "Alice",
}

// 响应报文添加set-cookie
http.SetCookie(writer, &cookie)

writer.Write([]byte("HandleIndex Page: 使用cookie收集了你的信息"))
}

func HandleCookie(writer http.ResponseWriter, request *http.Request) {
// 接受客户端传来的cookie
cookie := request.Cookies()[0]
// 使用cookie完成动作
if cookie.Value == db[cookie.Name] {
writer.Write([]byte("HandleCookie Page: 根据用户推荐相关信息"))
}

}

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package main

import (
"fmt"
"io"
"net/http"
"strings"
"time"
)

// 模拟存放cookie的管理器
var db map[string]string = make(map[string]string)

func main() {
RunDB()

Get("http://127.0.0.1:8080", "/", false)
Get("http://127.0.0.1:8080", "/HandleCookie", true)
time.Sleep(7 * time.Second)
fmt.Println("模拟一个周后")
Get("http://127.0.0.1:8080", "/HandleCookie", true)

}

func RunDB() {
db = make(map[string]string)
}

func Get(url string, path string, hasCookie bool) {
// 创建一个http.Client对象
client := &http.Client{}
// 创建一个http.Request对象
req, _ := http.NewRequest("GET", url+path, nil)

// 发送cookie
if hasCookie {
// 从数据库中获取键值
s := strings.Split(db[url], "=")
// 请求中添加cookie
cookie := http.Cookie{
Name: s[0],
Value: s[1],
}
req.AddCookie(&cookie)
}

// 3. 发送请求
resp, _ := client.Do(req)
defer resp.Body.Close()

// 请求中有Set-Cookie,则将Set-Cookie保存到数据库中
if resp.Header.Get("Set-Cookie") != "" {
db[url] = resp.Header.Get("Set-Cookie")
fmt.Println("Set-Cookie:", resp.Header.Get("Set-Cookie"))
}

Output(resp)
}

func Output(resp *http.Response) {
buf := make([]byte, 1024)
for {
// 接收服务端信息
n, err := resp.Body.Read(buf)
if err != nil && err != io.EOF {
fmt.Println(err)
return
} else {
res := string(buf[:n])
fmt.Println(res)
break
}
}
}

Web缓存

web缓存是能够通过暂时存储Web对象,以减少服务延迟的技术。如果客户端频繁访问某几个页面,那么使用Web缓存存储这些Web对象会减少大量时延。

web缓存分类:

  • 数据库缓存
  • CDN缓存
  • 浏览器缓存
  • 代理服务器缓存

代理服务器

web缓存器(Web cache)也叫代理服务器(proxy server),它是能够代表初始Web服务器来满足HTTP请求的网络实体。

Web缓存器的运行如下:

  1. PC端浏览器会向代理服务器创建TCP连接,并向代理服务器发送一个HTTP请求。
  2. 如果代理服务器有PC浏览器请求的对象,代理服务器就能直接返回该对象。
  3. 如果没有请求的对象,代理服务器就会和初始服务器建立TCP连接,并发送一个HTTP请求。然后初始服务器会发送HTTP响应给Web缓存器。
  4. 当代理服务器中没有该对象时,它会在本地存储一个该对象副本,并向PC浏览器使用HTTP响应报文发送该副本(通过现有的PC浏览器和代理服务器的TCP连接)。

代理

反向代理

代理服务器除了作为Web缓存器外它还具有其他功能,比如拦截和过滤HTTP请求和响应、隐藏网络内部IP等。上面使用的代理服务器是作为正向代理。

正向代理是指代理服务器在客户端和服务器之间。客户端发送请求到代理服务器,代理服务器将请求转发给目标服务器,并将目标服务器的响应转发回客户端(代理服务器常与客户端在同一局域网下)。

正向代理常用于加强安全、缓存内容以加速访问、访问受限资源等场景。

代理服务器还能用于反向代理,反向代理是指代理服务器在服务器和客户端之间。客户端发送请求到反向代理服务器,反向代理服务器将请求转发给真实服务器,并将真实服务器的响应转发回客户端(反向代理服务器常与服务器在同一局域网下)。

反向代理常用于负载均衡、高可用性、加强安全等场景。

反向代理

条件GET

缓存能够很好减少用户感受到的响应时间,但是也引入了一个新问题:如果缓存中引入的副本已经过时了,或者说服务器上文件已经更新了但缓存的副本还没更新。

对此,HTTP使用了条件GET解决这种问题,它的解决方式如下:

  1. 允许代理服务器向服务器发送使用GET方法的请求报文
  2. 这个报文的请求头包含一个if-Modified-Since

当代理服务器从客户端接受到一个请求对象,这个对象正好存储在代理服务器中。此时代理服务器发现缓存一段时间没有更新,它会向服务器发送一个条件GET请求。如果服务器没有修改过该对象,响应报文是不会包含对象副本。

参考

  1. 计算机网络(自顶向下方法)第七版
  2. 终于有人把正向代理和反向代理解释的明明白白了!-腾讯云开发者社区-腾讯云 (tencent.com)
  3. WEB缓存 - 掘金 (juejin.cn)