Go Web基础和Http演进

0 阅读13分钟

1).Go语言接收HTTP请求:

服务器创建:

Go语言通过net/http包来处理Http请求,并通过HandleFunc()和http.ListenAndServe("net address",nil)两个函数即可轻松的创建一个Go语言服务器.

示例:

package main

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

func hiWorld(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello World")
}

func main() {
    http.HandleFunc("/hi", hiWorld)

    err := http.ListenAndServe(":8080", nil)
    if err != nil {
       log.Fatal(err)
    
}

在浏览器中输入localhost:8080/hi.会默认显示出"Hello World"字符串.表示服务器创建成功.可以通过Server结构体对服务器进行更详细的配置.包括为请求读取操作设置超时时间等.请求和响应流程如图所示.

流程如下:

1).客户端发送请求.

2).服务器中的多路复用器收到请求.

3).多路复用器根据Url找到注册的处理器.将请求交由处理器处理.

4).处理器执行程序逻辑.如果必要.则与数据库进行交互.得到处理结果.

5).处理器调用模板引擎将指定模板和上一步得到的结果渲染成客户端可识别的数据格式.

6).服务端将数据通过Http响应返回给客户端.

7).客户端拿到数据.执行对应的操作.

多路复用器:

DefaultServeMux是net/http包中默认提供的一个多路复用器.其实质是ServeMux的一个实例.多路复用器的任务是根据请求的url将请求重定向到不同的处理器.当用户没有为server对象指定处理器时.服务器默认用DefaultServeMux作为ServerMux结构体的实例.

type ServeMux struct {
    mu     sync.RWMutex
    tree   routingNode
    index  routingIndex
    mux121 serveMux121 // used only when GODEBUG=httpmuxgo121=1
}

type serveMux121 struct {
	mu    sync.RWMutex
	m     map[string]muxEntry
	es    []muxEntry // slice of entries sorted from longest to shortest.
	hosts bool       // whether any patterns contain hostnames
}

同时.ServeMux也是一个处理器.可以在需要时对其实例实施处理器串联.默认的多路复用处理器定义如下.

// DefaultServeMux is the default [ServeMux] used by [Serve].
var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

HandleFunc()函数用于为指定的Url注册一个处理器.处理器的HandleFunc()函数会在内部调用DefaultServeMux的对应方法.实现如下.

// HandleFunc registers the handler function for the given pattern in [DefaultServeMux].
// The documentation for [ServeMux] explains how patterns are matched.
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if use121 {
       DefaultServeMux.mux121.handleFunc(pattern, handler)
    } else {
       DefaultServeMux.register(pattern, HandlerFunc(handler))
    }
}

通过上面方法可以看出.http.HandleFunc()函数是将处理器注册到多路复用器中的.

处理器和处理器函数:

处理器:

服务器在接收到请求后.会根据url将请求交给相应的多路复用器.然后.多路复用器将请求转发给处理器处理.处理器是实现Handler接口的实现.Handler接口被定义在net/http包中.

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

Handler接口中只有一个ServeHTTP处理函数.任何实现了Handler接口中ServeHTTP函数的对象.都可以被注册到多路复用器中.可以定义一个结构体来实现该接口的方法.以注册到这个结构类型的对象到多路复用器中.

处理器函数:

注册一个处理器函数.

http.HandleFunc("/",func_name)

第一个参数表示匹配的路由地址.第二个参数表示一个名为func_name的方法.

定义一个函数:

func hiWorld(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "Hello World")
}

2).Go语言处理Http请求:

Request结构体:

net/http包中的Request结构体用于返回Http请求报文.定义如下.

type Request struct {
	Method           string                      // HTTP 请求方法(GET、POST、PUT 等),客户端空字符串表示 GET
	URL              *url.URL                    // 请求的 URL,服务端为请求行 URI,客户端为完整 URL
	Proto            string                      // 协议版本字符串,如 "HTTP/1.0",服务端使用,客户端忽略
	ProtoMajor       int                         // 协议主版本号,服务端使用,客户端忽略
	ProtoMinor       int                         // 协议次版本号,服务端使用,客户端忽略
	Header           Header                      // HTTP 请求头,键已规范化,服务端接收或客户端发送
	Body             io.ReadCloser               // 请求体,客户端 GET 可为 nil,服务端总是非 nil
	GetBody          func() (io.ReadCloser, error) // 可选,返回 Body 的新副本,客户端重定向时使用,服务端未用
	ContentLength    int64                       // 请求体长度(字节),-1 表示未知,>=0 表示可读取字节数
	TransferEncoding []string                    // 传输编码列表(外到内),空为 identity,chunked 自动处理
	Close            bool                        // 是否在请求/响应后关闭连接,客户端设为 true 禁用连接复用
	Host             string                      // 主机名,服务端来自 Host 头或 URL,客户端可覆盖 URL.Host
	Form             url.Values                  // 解析后的表单数据(含 URL query 和 POST 表单),需先调用 ParseForm
	PostForm         url.Values                  // 解析后的 POST/PATCH/PUT 表单数据(不含 URL query),需先调用 ParseForm
	MultipartForm    *multipart.Form             // 解析后的 multipart 表单(含文件上传),需先调用 ParseMultipartForm
	Trailer          Header                      // 请求 trailer 头,服务端读完 body 后才有值,客户端发送前需初始化
	RemoteAddr       string                      // 客户端网络地址 IP:port,服务端设置用于日志,客户端忽略
	RequestURI       string                      // 请求行中未经修改的请求目标,仅服务端使用,客户端设置会报错
	TLS              *tls.ConnectionState        // TLS 连接信息,服务端 TLS 启用时设置,客户端忽略
	Cancel           <-chan struct{}             // 已废弃,表示请求取消的 channel,推荐使用 context
	Response         *Response                   // 引发此请求重定向的响应对象,仅在客户端重定向时填充
	Pattern          string                      // 匹配的 ServeMux 模式,未匹配时为空字符串
	ctx              context.Context             // 请求上下文,不可直接修改,应通过 Clone/WithContext 复制后修改
	pat              *pattern                    // 私有字段,匹配的 ServeMux 模式对象
	matches          []string                    // 私有字段,模式中通配符对应的值
	otherValues      map[string]string           // 私有字段,通过 SetPathValue 设置的路径值(不与通配符匹配时)
}

请求url:

url结构体的定义如下:

type URL struct {
	Scheme      string      // 协议方案,如 "http""https""ftp" 等
	Opaque      string      // 编码后的不透明数据(当 Scheme 后跟 "//" 时通常不使用该字段,如 "mailto:addr""addr" 部分)
	User        *Userinfo   // 用户名和密码信息(用于 HTTP Basic Auth 等场景)
	Host        string      // 主机名或 主机:端口(例如 "example.com""example.com:8080")
	Path        string      // URL 路径(相对路径允许省略开头的斜杠,如 "a/b")
	RawPath     string      // 编码后的路径提示(当 Path 包含转义字符时,RawPath 存储原始编码格式)
	OmitHost    bool        // 当为 true 时,如果 Host 为空,则在序列化时不输出空的 host(即不输出 "//" 部分)
	ForceQuery  bool        // 即使 RawQuery 为空,也强制在 URL 后追加一个问号 "?"
	RawQuery    string      // 编码后的查询参数(不包含 "?",例如 "id=1&name=go")
	Fragment    string      // 片段标识符(URL 中 "#" 之后的内容,不含 "#")
	RawFragment string      // 编码后的片段提示(当 Fragment 包含转义字符时,RawFragment 存储原始编码格式)
}

该结构体主要用来存储Url各部分的值.net/url包中的很多方法都是对url结构体进行相关操作的.其中Parse()方法的定义如下.

func Parse(rawurl string) (*URL,error)

该方法返回一个结构体.

请求首部:

请求和响应首部都使用Header类型表示.Header类型是一个映射(map)类型.表示Http首部中的多个键值对.定义如下.

type Header map[string][]string

通过请求对象的Header属性可以访问请求头信息.Header属性是映射结构.提供了Get()方法获取key对应的第一个值.定义如下.

func (h header) Get(key string)

其他常用方法定义如下:

func (h header) Set(key,value  string)

func (h header) Add(key,value  string)

func (h header) Del(key string)

func (h header) Write(w io.Writer) error

请求主体:

请求和响应的主体都由Request结构中的Body字段表示.Body字段是一个io.ReadCloser接口.定义如下.

type ReadCloser interface {
    Reader
    Closer
}

Body字段是Reader接口和Closer接口的结合.Reader接口如下.

type Reader interface {
    Read(p []byte) (n int, err error)
}

通过Read方法可以看到.实现了ReadCloser接口.所以Body.Read可以读取请求的主体信息.

ResponseWriter接口原理:

Go语言中.客户端的请求信息都封装在了Request对象中.发送客户端的响应并不是Response对象.而是ResponseWriter接口.该接口是处理器用来创建Http响应的接口.定义如下.

type ResponseWriter interface {
	// Header 返回待发送的响应头映射。在 WriteHeader 或 Write 调用后修改 Header 无效(除非是 1xx 状态码或 trailer)。
	// 可通过预声明 "Trailer" 头或使用 TrailerPrefix 来设置 HTTP trailer。将头值设为 nil 可抑制自动响应头(如 "Date")。
	Header() Header

	// Write 向连接写入数据作为 HTTP 响应体。若未显式调用 WriteHeader,则自动写入 StatusOK。
	// 若 Header 中无 Content-Type,会根据写入数据的前 512 字节自动添加。数据量较小时会自动添加 Content-Length。
	// 对于 HTTP/1.x,建议在写入响应前读取完请求体;对于 HTTP/2,允许并发读写,但为兼容性最好先读取。
	Write([]byte) (int, error)

	// WriteHeader 发送带指定状态码的 HTTP 响应头。若未显式调用,第一次调用 Write 会隐式调用 WriteHeader(http.StatusOK)。
	// 主要用于发送错误码或 1xx 信息响应。可发送多个 1xx 头,随后最多一个 2xx-5xx 头。
	// 1xx 头立即发送,2xx-5xx 头可能被缓冲。调用 Flusher 可刷新缓冲数据。发送 2xx-5xx 头时会清空 Header 映射。
	WriteHeader(statusCode int)
}

实际上.在底层支撑ResponseWrite接口的是http.response结构体.在调用处理器处理Http请求时.会调用readRequest方法.readRequest方法会声明response结构体.并且其返回值是response指针.这也是为什么处理器方法声明时.Request是指针类型.而ResponseWrite不是指针类型的原因.

3).Http2:

Http1问题:

1.高延迟使页面加载速度降低.

2.无状态特性使Http头部信息量变大.

3.铭文传输的不安全性.

4.不支持服务器推送消息.

Spdy协议与Http2:

上面提到的缺陷.可以通过拼合图 将小图内联 使用多个域名等方式来提高性能.不过这些优化建议都绕开了协议.直到2009年.谷歌公开了自行研发的SPDY协议.主要解决HTTP1.1效率不高的问题.降低延迟 压缩header等.实践证明了这些优化的效果.最终带来了Http2的诞生.架构图如下所示.

由于背负这庞大的历史包袱.协议的修改兼容性是首要考虑的目标.如图所示.SPDY位于HTTP之下.位于TCP和SSL之上.这样可以轻松的兼容老版本的HTTP协议.同时可以使用已有的SSL功能.SPDY协议在Chrome浏览器上证明了可行后.就被当做HTTP2的基础.

Go Http2:

Https是基于Http的扩展.用于计算机网络安全的通信.在Https中.原有的Http协议会得到TLS(安全传输层协议)或其前辈SSL(安全套接层)的加密.在Go语言中使用Https的方法很简单.net/http包中提供了启动函数示例如下.

示例:

package main

import (
    "fmt"
    "log"
    "net/http"
    "time"
)


func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
       fmt.Fprintf(w, "Hello, you're using %s", r.Proto)
    })

    // 推荐:显式声明 Server,便于配置超时等
    srv := &http.Server{
       Addr:         ":8080",
       Handler:      mux,
       ReadTimeout:  5 * time.Second,
       WriteTimeout: 10 * time.Second,
       IdleTimeout:  120 * time.Second, // 关键,控制 Keep-Alive
    }

    log.Println("Starting HTTP/2 server on :8080")
    // 直接使用 ListenAndServeTLS,Go 会自动处理 HTTP/2 协商
    if err := srv.ListenAndServeTLS("server.crt", "server.key"); err != nil {
       log.Fatal(err)
    }
}

4).Http3:

Http3是即将到来的第三个主要版本的Http协议.与之前版本的1和2不同.3中将弃用TCP协议.改为基于UDP协议的QUIC协议.主要为了解决HTTP2中存在的队头阻塞问题.由于HTTP2在单个TCP连接上使用了多路复用.受到TCP拥塞控制的影响.少量丢包就可能导致整个TCP连接上的所有流被阻塞.

虽然Http2解决了很多之前的旧版本的问题.但它还是存在一个巨大的问题.主要是由底层支持的TCP协议造成的.HTTP2使用了多路复用.一般来说同一域名下只需要使用一个TCP连接.但当这个连接中出现了丢包的情况.就会导致Http2的表现情况反而不如Http1的情况.因为在出现了丢包的情况.整个Tcp都要开始等待重传.也就导致了后面的所有数据都会阻塞.对于Http1来说.可以开启多个连接.出现这种情况反而只会影响其中一个连接.其余的都可以正常传输数据.基于这个原因.谷歌另起炉灶设计了一个基于UDP协议的QUIC协议.并且使用在了HTTP3中.

QUIC新功能:

0-RTT:

通过使用类似TCP的快速打开技术.缓存当前会话的上下文.在下次恢复对话的时候.只需要将之前缓存传递给服务器验证通过就可以进行传输了.0-RTT建立连接可以说是QUIC相比HTTP2最大的性能优势了.

传输层0-RTT就能建立连接.

加密层0-RTT就能建立加密连接.

从上面的图可以看出.Https的一次完全握手建立连接过程.也需要3RTT.而QUIC呢.由于建立在UDP的基础上.同时又实现了0-RTT的安全握手.大部分情况下只需要0个RTT就能实现数据发送.

多路复用:

Http2虽然支持多路复用.但是TCP协议终究没有这个功能.QUIC原生就实现了这个功能.并且传输的单个数据流可以保证有序的且不含影响其它的数据流.

和Http2一样.同一条QUIC连接上可以创建多个stream来发送多个Http请求.但QUIC是基于UDP的.一个连接上的多个Stream是没有依赖.如图所示.

stream2丢失了一个UDP包.不会影响后面的stream3和stream4.不存在TCP的队头阻塞.虽然stream2需要重传.但是不影响后面的发送.

QUIC在移动端的表现也会比TCP表现好.因为TCP是基于IP和端口去识别连接的.这种方式在多变的移动端网络环境下是很脆弱的.但QUIC是通过ID的方式去识别一个连接的.不过网络环境如何变化.只要Id不变就能迅速重连上.

加密认证的报文:

TCP协议头部没有任何加密和认证.所以在传输过程中很容易被中间网络设备篡改.注入和窃听.比如修改序列号 滑动窗口等.但QUIC的包可以说武装到了牙齿.除了个别报文比如PUBLIC_RESET和CHLO.所有报文头部都是经过认证的.报文主体都是经过加密的.因此只要对QUIC报文做任何修改.接收端都能及时发现.

向前纠错机制:

QUIC协议有一个非常独特的特性.即向前纠错.每个数据除了它本身内容之外.还包括了部分其他数据包的数据.因此少量的丢包可以通过其他包的冗余数据直接组装而无需重传.向前纠错牺牲了每个数据包可以发送数据的上限.但是减少了因为丢包导致的数据重传.因为数据重传将会消耗更多地时间.

语雀地址www.yuque.com/itbosunmian…?

《Go.》 密码:xbkk 欢迎大家访问.提意见.