HTTP 协议简介

什么是HTTP协议

HTTPHypertext Transfer Protocol 的缩写,也就是超文本标记协议,多数的 HTTP 协议运行在 TCP 上,新型的 HTTP3 协议运行在 UDP 上。

简单来说,HTTP 协议就是一个 客户端与服务端 通信的协议,客户端发送数据给服务端,那么服务端要认识这些数据表示什么,服务端返回给客户端的数据,客户端也要认识这些数据

HTTP发展

HTTP其实诞生也不过30多年,但是对人类的影响是巨大的,随着技术的不断发展进步,HTTP协议也在不断地完善和改进

HTTP/0.9

最古老的 HTTP 协议并不是 HTTP/1.0,而是 HTTP/0.9,但是 HTTP/0.9 并没有成为标准,也没有进行大规模的使用。
HTTP/0.9 的组成极其简单,对于客户端,仅仅支持 GET 方法,服务端也只能返回 HTML 文本内容

HTTP/0.9 协议不能满足实际需求,也仅仅在实验室中使用

HTTP/1.0

1996年, HTTP/1.0 公布,HTTP/1.0 在 HTTP/0.9 的基础上进行了多方面的扩展,完善了请求行信息,添加了请求头部,响应内容也支持头部信息,和目前的协议格式基本一致

并且开始支持三个请求方法:

  • POST: 向服务端发送数据
  • GET: 用来请求对应的资源
  • HEAD: 用于获取响应头部信息,与 GET 十分类似,但是没有响应主体

HTTP/1.0 默认使用的是短连接,也就是说,每一次向服务端请求一次资源,便需要重新创建一个新的 socket,向服务端请求数据,然后关闭 socket。

HTTP/1.1

在 HTTP/1.0 发布没几个月,1997年,HTTP/1.1 也正式公布了,并且成为第一个标准化的 HTTP 协议。

HTTP/1.1 中默认开启长连接,也就是指定头部的 Connection:keep-alive ,使用长连接的好处就是可以减少 socket 创建的消耗,因为创建一个 socket 需要进行系统调用,这个需要消耗比较多的资源。

此外HTTP/1.1中增加了一系列其他的请求方法,比如 PUT,DELETE,OPTIONS 等等,并且可以使用头部的 Cache-Control 控制资源的缓存。

HTTP/1.1 在互联网中广泛使用,现在仍然有很多网站在使用 HTTP/1.1,我们可以使用 wireshark 进行抓包分析

HTTP/2

HTTP/1.1 得到广泛使用,但是随着使用时间的越来越长,也暴露出了不少了的问题,比如说发送的内容都是明文字符,容易被第三方截获造成消息的泄露,在 HTTP/2 中采用了 SSL/TLS 协议,对数据进行加密,提高数据传输过程中的安全性,不过数据的加密需要较多的CPU资源和时间。

HTTP/1.1 在 socket 利用率方面还是有所缺陷,HTTP/2 中提出了 多路复用 机制,能够同时发送多个 HTTP 请求,然后服务端返回多个响应,如下图:

由于HTTP头部信息内容比较大,并且在很多时候发送的HTTP请求头部都有一部分的重复,HTTP/2中采用头部压缩技术 HPACK,使用字典对头部信息进行保存,这样下一次发送的时候就可以只发送对应的键就好了,大大压缩的头部消息。

HTTP/2 还支持 服务端推送,服务端在客户端请求资源的时候可以推断下一次客户端可能需要的资源,发送给客户端缓存到本地,下一次使用的时候可以直接使用缓存中的资源,当然客户端也可以拒绝服务端推送的资源。

实现简单的HTTP客户端和服务端

其实服务端和客户端的沟通就是通过 socket 进行连接,然后通过发送的数据和解析数据,客户端和服务端便可以互相理解,从而做出对应的动作,这里为了简单,使用 HTTP/1.1 协议作为讲解,请求网站 http://httpbin.org

客户端

根据我们上面的理解,客户端请求数据的时候有一个请求行和一个请求头部,我们只需要满足这个条件,便可以模拟浏览器发送一个 HTTP 请求了。

首先我们先与服务端建立连接

1
2
3
4
5
6
7
// 使用host:port形式
conn, err := net.Dial("tcp", "httpbin.org:80")
if err != nil {
fmt.Println("Dial tcp err: ", err)
return
}
defer conn.Close()

然后构建HTTP请求数据

1
2
3
4
5
6
msg := strings.Builder{}
msg.WriteString("GET /get HTTP/1.1\r\n")
msg.WriteString("Host: httpbin.org\r\n")
msg.WriteString("Accept: application/json\r\n")
msg.WriteString("Connection: close\r\n")
msg.WriteString("\r\n")

接着将数据发送到服务端

1
2
3
4
5
_, err = conn.Write([]byte(msg.String()))
if err != nil {
fmt.Println("Send msg err: ", err)
return
}

最后读取服务端返回的数据即可

1
2
3
4
5
6
7
8
9
10
11
12
buf := bufio.NewReader(conn)
for {
msg, err := buf.ReadString('\n')
if err != nil {
if err == io.EOF{
break
}
fmt.Println("Read err: ", err)
return
}
fmt.Print("Receive: ", msg)
}

服务端

对于服务端来说,我们需要解析客户端请求的数据,然后进一步做出响应,这里为了简单,仅仅向客户端显示一个 Hello World 字符串。

首先我们需要监听一个端口

1
2
3
4
5
listener, err := net.Listen("tcp", ":8888")
if err != nil {
fmt.Println("Listen err: ", err)
return
}

然后接收客户端的请求,得到一个与客户端的连接

1
2
3
4
5
6
7
8
9
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Accept err: ", err)
return
}
// 对于每一个连接,起一个goroutine进行处理
go handle(conn)
}

然后向连接中写入HTTP响应格式的数据即可

1
2
3
4
5
6
7
8
9
func handle(conn net.Conn){
defer conn.Close()
msg := strings.Builder{}
msg.WriteString("HTTP/1.1 200 OK\r\n")
msg.WriteString("\r\n")
msg.WriteString("Hello World\r\n")
msg.WriteString("\r\n")
conn.Write([]byte(msg.String()))
}

使用curl命令请求便可以获取到对应的响应信息

1
2
$ curl http://localhost:8888
Hello World

生活杂笔,学习杂记,偶尔随便写写东西。

作者

Edgar

发布于

2021-07-03

更新于

2021-12-21

许可协议

评论