Golang-http

Http协议(Hyper Text Transfer Protocol,超文本传输协议)是一个简单的请求-响应协议,它通常运行在TCP之上。

Http协议是基于客户端(Cilent)/服务器(Server)模式,且面向连接的。简单的来说就是客户端(Cilent)向服务器(Server)发送http请求(Request),服务器(Server)接收到http服务请求(Request)后会在http响应(Response)中回送所请求的数据。
image.png
Go的标准库 net/http 则提供了对http协议支持的封装,提供了强大而又灵活的功能实现。系列文章将通过 Cilent 、Server 、 Request 、Response 四方面去解析http协议以及 net/http 包。

Request

Request 结构

Http Requset指的是客户端发送给服务器的一个请求,或者是服务器收到的一个请求。
在命令行下查看HTTP协议,可以使用 curl 或 http命令发起HTTP请求;而在浏览器端下也可以使用开发者工具查看。
下图为一个Http Requset的示例:

image.png

请求行

请求行中的信息包括三部分:请求方式(Get/Post)请求URL协议版本,他们之间使用空格隔开。
例如上述图示中请求行内容为:POST / HTTP/1.1,则:

  • 请求方式为 POST

在1.1版本中,一共支持8个请求方式方法:

请求方式描述
GET请求指定的页面信息,并返回实体主体
POST向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。
数据被包含在请求体中。post请求可能会导致新的资源的建立或资源的修改;一般不会被删除。
HEAD类似于get请求,但服务器不返回body部分,用于获取报头。
这一方法可以在不必传输整个响应内容的情况下,就可以获取包含在响应消息头中的元信息。可用于查询资源修改日期等。
DELETE请求服务器删除 Request-URI 所标识的资源
PUT从客户端向服务器传送的数据取代指定的文档的内容。一般用于修改。
OPTIONS返回服务器针对特定资源所支持的HTTP请求方法
CONNECThttp1.1协议中预留给能够将连接改为管道方式的代理服务器
TRACE回显服务器收到的请求,主用于测试或诊断;

一般情况下,GET 、POST使用较为多。

  • 请求URL为 /

请求URL就比较好理解了,就是需要请求的地址,也可以理解为需要请求的API地址。

  • 请求协议版本为 HTTP/1.1
请求头

HTTP请求头包含了客户端(如Web浏览器)向服务器发送的请求的附加信息。它可以包含多个键值对,用于描述请求的各种属性,例如请求的方法、内容类型、接受的语言、请求的来源、Cookie等。这些信息可以帮助服务器了解客户端的需求,从而提供更准确、更有效的响应。
HTTP请求头可以有多个,每个字段占一行,字段名和字段值之间用冒号和空格分隔,例如:

Accept: */*
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Length: 11

HTTP请求头提供了很多标准属性,常见的属性如下表:

请求头作用示例
Accept告知服务器客户端能够处理的MIME类型Accept: text/html, application/xhtml+xml, application/xml;q=0.9
Accept-Encoding告知服务器客户端支持的内容编码方式Accept-Encoding: gzip, deflate
Accept-Language告知服务器客户端支持的语言Accept-Language: en-US, en;q=0.5
Cache-Control控制缓存行为Cache-Control: no-cache
Connection控制连接的行为Connection: keep-alive
Content-Length指定请求体的长度Content-Length: 348
Content-Type指定请求体的MIME类型Content-Type: application/x-www-form-urlencoded
Host指定服务器的域名和端口号
该属性跟请求行中请求URL一起组成一个完整的地址Host: http://www.example.com
User-Agent提供客户端的应用程序名称和版本号User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36
Authorization提供访问受保护资源所需的凭证Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
Cookie用于在客户端和服务器之间传递会话信息Cookie: $Version=1; UserId=1234; $Path=/
Origin指示请求的来源,用于防止跨站点攻击Origin: http://www.example.com
Referer指示请求的来源URL,用于记录访问日志和防止跨站点攻击Referer: http://www.example.com/index.html
Accept-Charset浏览器可以接受的字符编码集Accept-Charset:iso-8859-5
If-Modified-Since用于缓存控制,指定一个日期,如果该日期之后资源没有发生变化,则返回304 Not ModifiedIf-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT
If-None-Match用于缓存控制,指定一个实体标签,如果资源的实体标签匹配,则返回304 Not ModifiedIf-None-Match: “737060cd8c284d8af7ad3082f209582d”
Range请求资源的某个字节范围,用于分段下载Range :byte=500-999
Upgrade向服务器指定某种传输协议以便服务器进行转换(如果支持)Upgrade:Http/2.0,SHTTP/1.3,IRC/
Accept-Datetime告知服务器客户端支持的日期时间格式Accept-Datetime: Thu, 31 May 2007 20:35:00 GMT
If-Match用于缓存控制,指定一个实体标签,如果资源的实体标签匹配,则返回资源,否则返回412 Precondition FailedIf-Match: “737060cd8c284d8af7ad3082f209582d”
Max-Forwards限制转发次数,用于TRACE和OPTIONS请求Max-Forwards: 10
Proxy-Authorization提供访问代理所需的凭证Proxy-Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
TE向服务器指定某种传输协议以便服务器进行转换(如果支持)Upgrade:Http/2.0,SHTTP/1.3,IRC/
Warning关于消息实体的警告信息warn:199 Miscellaneous warning
Via通知中间网关或代理服务器地址,通信协议Vis:1.1 fred,1.1 http://nowhere.com(Apache/1.1)
Transfer-Encoding描述了在HTTP报文中的实体主体(body)是编码传输方式Transfer-Encoding字段有两种常见的取值:chunked和identity
  1. chunked:表示使用分块传输编码方式。在这种方式下,实体主体被分成一系列的块,每个块都包含自己的长度和实体数据。每个块的长度是以十六进制的方式表示的。使用分块传输编码可以解决传输过程中的时延和带宽浪费问题。
  2. identity:表示不使用任何编码方式,实体主体以原始数据的形式传输。在这种方式下,实体主体的长度可以通过Content-Length字段指定。
    除了上述两种编码方式之外,Transfer-Encoding还可以包含其他编码方式的值,比如gzip、deflate等,表示使用相应的压缩方式对实体主体进行编码。
    需要注意的是,当Transfer-Encoding字段和Content-Length字段同时存在时,Transfer-Encoding会覆盖Content-Length,只有当Transfer-Encoding的值为identity时,Content-Length字段才有效。 |
    | Content-Encoding | 描述实体主体的内容编码方式 | 它通常用于标识HTTP请求和响应中的实体主体使用的编码方式,比如gzip、deflate等。这些编码方式的作用是对实体主体进行压缩,以减少传输数据的大小,提高网络传输效率。 |
    | Expect | 用于告知服务器期望响应的特定行为 | 常见的用法是在客户端请求中加入 Expect: 100-continue,表示客户端希望在请求体发送前先确认服务端是否接受请求体,这样可以避免因发送大量请求体而造成的资源浪费。 |
    | X-Forwarded-For(非标准) | 在通过代理的时候会加这样的一个头,这个头会携带以前的IP,所以这里面可能是一个,也可能是多个IP/域名,并用逗号隔开。
    其中第一个是最原始的客户机的IP,每过一个代理就会加上自身的IP,如果你需要判断取得最原始用户的IP,可以通过这里来判断。 | clientip,proxy1ip,proxy2ip |
    | X-Requested-With(非标准) | 用来标记AJAX的。如果你在浏览器中,一般请求都是通过Ajax发出的,都会带这个头。 | XML.HttpRequest |

除了上述列出的一些标准头以外,还有很多不常用的头,再次不一一列举。
除了标准的头以外,HTTP协议允许用户定义自己的请求头,这些请求头通常以X-开头,例如X-Custom-Header、X-Forwarded-For等。自定义请求头通常用于在HTTP请求中添加一些自定义的元数据,以便与特定的应用程序或服务进行交互。
另外,在使用自定义请求头时需要遵循一些规则,例如请求头的名称不能包含空格、冒号等特殊字符,长度不能太长,不建议使用敏感信息等。此外,建议在编写自定义请求头时,参考RFC6648的建议和规范,以确保与HTTP标准的兼容性和互操作性。
这边重点快速介绍下 100-continue 这个比较有意思的机制。
100 Continue 是一个 HTTP协议的状态码和机制,用于实现客户端与服务器之间的流控,防止请求数据发送过快而导致服务器无法处理。
当客户端向服务器发送带有 HTTP请求体的请求时,请求头中可以附加 Expect: 100-continue 字段。这个字段的作用是通知服务器,客户端期望收到一个 100 Continue 的响应,告诉客户端可以继续发送请求体。服务器在接收到带有 Expect: 100-continue 请求头的请求后,会发送一个 100 Continue 的响应,告诉客户端可以继续发送请求体。
客户端收到 100 Continue 的响应后,就可以继续发送请求体了。如果服务器在接收到 Expect: 100-continue 请求头的请求后,无法或者不愿意提供 100 Continue 的响应,它可以直接返回 417 (Expectation Failed)状态码,告诉客户端不支持该特性。
100 Continue 的机制可以有效地防止客户端发送请求体过快而导致服务器无法处理的情况。

请求体

在HTTP协议中,请求体是指在请求头后面的数据部分,通常用于向服务器传递数据。请求体的格式和内容取决于请求的类型和使用的数据格式。
在HTTP请求中:

  • GET请求通常不包含请求体,因为GET请求的主要目的是获取资源,不需要向服务器传递数据。
  • 而POST请求和PUT请求等通常需要在请求体中传递数据,例如表单数据、JSON数据、XML数据等。

例如Http Requset的图示上的 hello=World就是一个请求体内容。
在请求体中传递数据时,需要注意数据的编码方式和格式,以确保服务器能够正确解析和处理数据,编码和格式在请求头 Content-Type 中设置。
常用的编码数据格式包括 :

  • application/x-www-form-urlencoded:用于提交form表单数据,将表单数据编码为键值对形式,并使用等号和&符号进行分隔。
  • multipart/form-data:用于表单提交文件和二进制数据,将数据分解为多个部分,并使用boundary进行分隔。常用于表单文件上传提交。
  • application/json:用于提交JSON格式的数据。
  • application/xml:用于提交XML格式的数据。

在表单提交数据的时候,有type-file的时候一般采取multipart/form-data编码方式,然则用默认的application/x-www-form-urlencoded即可。
请求体的大小通常会受到服务器和客户端的限制,过大的请求体可能会导致请求失败或响应时间延长。因此,在传递大量数据时,建议采用分块传输编码或流式传输等方式,以提高传输效率和性能。

Request结构体

net/http中的 Request结构体表示一个HTTP请求,包含请求方法、URL、请求头、请求体等信息。信息主要在文件 net/http/request.go中。
Request结构体的定义:

type Request struct {
    Method           string                        //HTTP请求方法,如GET、POST等
    URL              *url.URL                      //HTTP请求的URL地址,是一个指向url.URL类型的指针。
    Proto            string                        //HTTP协议版本,如"HTTP/1.0"或者"HTTP/1.1"
    ProtoMajor       int                           //HTTP协议的主版本号,整数类型。如1
    ProtoMinor       int                           //HTTP协议的次版本号,整数类型。如0
    Header           Header                        //HTTP请求头信息,是一个http.Header类型的映射,用于存储HTTP请求头。
    Body             io.ReadCloser                 //HTTP请求体,是一个io.ReadCloser类型的接口,表示一个可读可关闭的数据流。
    GetBody          func() (io.ReadCloser, error) //HTTP请求体获取函数
    ContentLength    int64                         //HTTP请求体的长度,整数类型。
    TransferEncoding []string                      //HTTP传输编码,如"chunked"等。
    Close            bool                          //表示在请求结束后是否关闭连接。
    Host             string                        //HTTP请求的主机名或IP地址,字符串类型。
    Form             url.Values                    //HTTP请求的表单数据,是一个url.Values类型的映射,用于存储表单字段和对应的值。
    PostForm         url.Values                    //HTTP POST请求的表单数据,同样是一个url.Values类型的映射。
    MultipartForm    *multipart.Form               //HTTP请求的multipart表单数据,是一个multipart.Form类型的结构体。
    Trailer          Header                        //HTTP Trailer头信息,是一个http.Header类型的映射,用于存储Trailer头部字段和对应的值。
    RemoteAddr       string                        //请求客户端的地址。
    RequestURI       string                        //请求的URI,包括查询字符串。
    TLS              *tls.ConnectionState          //如果请求是使用TLS加密的,则该字段存储TLS连接的状态信息。
    Cancel           <-chan struct{}               //一个只读通道,用于在请求被取消时发送信号。
    Response         *Response                     //一个指向http.Response类型的指针,表示HTTP响应信息。
    ctx              context.Context               //一个context.Context类型的上下文,用于控制请求的超时和取消。
}

上述的字段定义表示一个HTTP请求,包含了HTTP请求的各种元信息和数据。下面就说一说常见的函数

NewRequest函数用于创建一个新的Request类型,并将请求的方法、URL、请求体设置为传入的参数:

func NewRequest(method, url string, body io.Reader) (*Request, error)

ReadRequest函数作用是从bufio.Reader类型的参数b中读取HTTP请求,并解析请求行、请求头和请求体,返回一个*http.Request类型的指针。

func ReadRequest(b *bufio.Reader) (*Request, error)

Request.write() 方法用于将 HTTP请求写入一个 io.Writer 对象中,以便将其发送到服务器。

func (r *Request) write(w io.Writer, usingProxy bool, extraHeaders Header, waitForContinue func() bool) (err error)

该方法接收四个参数:

  • w:要写入的目标 io.Writer 对象;
  • usingProxy:一个布尔值,表示是否使用代理服务器;
  • extraHeaders:一个额外的请求头部,以 Header 对象的形式提供;
  • waitForContinue:一个函数,用于处理 100 Continue 情况,返回一个布尔值,表示是否需要等待服务器的确认继续发送请求数据

Response

HTTP Response(HTTP响应)是在客户端(Client)向服务器(Server)发送HTTP请求(Request)之后,服务器返回给客户端的数据。
同样可以使用 curl 或 http命令发起HTTP请求或者在浏览器端下也可以使用开发者工具查看。
下图为一个Http Response的示例:
image.png

Response 结构

响应行

响应行包括协议版本、状态码和状态码原因短语。他们之间使用空格隔开。
例如上述图示中请求行内容为:HTTP/1.1 302 Found,则:

  • 协议版本为 HTTP/1.1

响应协议版本通常与请求(request)的协议版本相同。常用的HTTP协议版本包括HTTP/1.0和HTTP/1.1,其中HTTP/1.1是目前使用最广泛的HTTP协议版本。

  • 状态码为 302

响应状态码表示服务器对请求的响应结果。在浏览器场景下,浏览器会根据状态码做出相应的处理。常见的状态码如下表:

状态码含义
200请求已经被成功处理
201请求已经被成功处理,并且在服务器上创建了新的资源
204请求已经被成功处理,但响应报文中不包含实体的主体部分
301请求的资源已经被永久移动到了新的URI
302请求的资源被临时移动到了新的URI
304请求的资源未被修改,客户端可以使用本地缓存的版本
400请求的语法错误或无法被服务器理解
401请求需要用户验证,该响应必须包含WWW-Authenticate头部
403服务器拒绝执行请求,客户端没有权限访问请求的资源
404请求的资源不存在
500服务器在执行请求时发生了错误
502服务器作为网关或代理时从上游服务器接收到无效的响应
503服务器暂时无法处理请求,通常由于服务器过载或维护

完整的状态码可以参考: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status
同上, 状态码原因是跟状态码一一对应的,可以参考: https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status

响应头

HTTP 响应头(response header)是一种 HTTP标头,其可以用于 HTTP响应,且与响应消息主体无关。它们被用于描述服务器的基本信息,以及数据的描述,服务器通过这些数据的描述信息,可以通知客户端如何处理等一会儿它回送的数据。
HTTP响应头部包含一组属性-值对,用于描述HTTP响应的各种属性,例如响应的内容类型、长度、缓存控制等。这些属性通常以键值对的形式出现,其中键表示属性名,值表示属性的值,以冒号(:)分隔。HTTP响应头部以一个空行作为结束标志。例如:

Bdpagetype: 3
Connection: keep-alive
Content-Length: 154
Content-Type: text/html
Date: Wed, 15 Feb 2023 07:31:16 GMT
Location: https://www.baidu.com/search/error.html

下面来罗列下常见的HTTP 响应头:

头部名称描述
Content-Type指定响应主体的MIME类型
Content-Length指定响应主体的长度
Cache-Control指定缓存控制策略
Server指定HTTP服务器的名称和版本号
Date指定响应产生的时间
Last-Modified指定资源的最后修改时间
ETag指定资源的实体标识
Expires指定响应的过期时间
Location重定向的URL地址
Set-Cookie指定一个或多个HTTP cookie
Access-Control-Allow-Origin指定跨域请求的允许域名
Content-Encoding指定响应主体的压缩方式
Transfer-Encoding指定传输编码方式
Connection指定是否保持连接
Vary指定缓存的变化因素

除了标准的HTTP响应头外,其实响应头部中的属性可以根据需要进行添加、修改或删除,以便客户端和服务器之间进行更加灵活的交互。

响应体

HTTP响应体(Response Body)是HTTP响应的一部分,包含服务器返回给客户端的数据。它跟随在HTTP响应头之后,并以空行分隔。HTTP响应体中的数据通常是HTML、XML、JSON或其他格式的文本、图像、音频或视频等多媒体内容。
HTTP响应体的格式通常由Content-Type响应头部指定,例如,Content-Type: text/html表示响应体是HTML格式的文本数据,Content-Type: image/jpeg表示响应体是JPEG格式的图像数据。常见的响应体Content-Type如下:

  • text/html:HTML格式的文本数据,用于显示网页。
  • text/plain:纯文本格式的数据,适用于显示纯文本文档。
  • application/json:JSON格式的数据,用于Web应用程序之间传递数据。
  • application/xml:XML格式的数据,适用于Web应用程序之间传递数据或存储结构化文档。
  • image/png、image/jpeg:PNG或JPEG格式的图像数据,用于显示图像。
  • audio/mpeg、video/mp4:MP3或MP4格式的音频或视频数据,用于播放音频或视频。
  • application/octet-stream:二进制数据,适用于传输未知的二进制文件类型。

除了以上列出的Content-Type类型,还有很多其他的Content-Type类型,可以根据实际需求进行指定。另外,Content-Length响应头部指定了响应体的字节数。
HTTP响应体的内容可以是静态的文件,也可以是动态生成的内容,例如通过CGI、ASP、PHP等技术生成的网页内容。在一些特殊情况下,HTTP响应体可以为空,例如HTTP 204 No Content状态码。
HTTP响应体还可以用来传递服务器端的状态信息、错误信息和调试信息等,方便开发者调试和维护Web应用程序。由于HTTP响应体可能包含大量数据,因此在传输过程中需要采用压缩和分块传输等技术,以提高传输效率和降低带宽消耗。

Response结构体

Response结构体定义了HTTP响应的基本属性,如状态码、HTTP头部、响应体等。
Response结构体也提供了一些便捷的方法,如Write、WriteHeader等,用于设置响应体和HTTP头部等属性。
Response结构体定义源码如下:

type Response struct {
    Status           string               //表示HTTP响应的状态行中的状态码和原因短语,例如"200 OK"
    StatusCode       int                  //表示HTTP响应的状态码,例如200
    Proto            string               //表示使用的HTTP协议版本,例如"HTTP/1.1"
    ProtoMajor       int                  //表示使用的HTTP协议版本的主版本号,例如1
    ProtoMinor       int                  //表示使用的HTTP协议版本的副版本号,例如1
    Header           Header               //表示HTTP响应的头部信息,是一个Header类型的映射
    Body             io.ReadCloser        //表示HTTP响应的主体,是一个io.ReadCloser类型的接口,可以读取响应的数据
    ContentLength    int64                //表示HTTP响应的主体的长度,如果长度未知则为-1
    TransferEncoding []string             //表示HTTP响应的传输编码,例如"chunked",如果未设置则为nil
    Close            bool                 //表示HTTP响应是否需要关闭连接
    Uncompressed     bool                 //表示HTTP响应是否已经解压缩
    Trailer          Header               //表示HTTP响应的头部信息中未确定的部分
    Request          *Request             //表示发送HTTP请求的Request结构体,如果未设置则为nil
    TLS              *tls.ConnectionState //表示HTTP响应的TLS连接信息,如果未使用TLS则为nil
}

Response结构体的字段表示了HTTP响应的各种属性,包括状态码、HTTP头部、响应体、HTTP协议版本、传输编码等。这些字段是HTTP服务器处理HTTP请求并生成HTTP响应时必不可少的。下面说说常见的函数

ReadResponse函数用于解析HTTP响应,将响应字节流转换为Response结构体类型。该函数会从给定的io.Reader对象中读取HTTP响应的各个部分,包括状态行、响应头部、响应体等,并将其组合成Response类型的结构体,以便后续处理。

/**
参数说明:
    r  指向bufio.Reader类型的指针r,表示HTTP响应的字节流数据来源
    req 指向http.Request类型的指针req,表示与HTTP响应相关的HTTP请求信息
*/
func ReadResponse(r *bufio.Reader, req *Request) (*Response, error) 

Response.Write方法用于将HTTP响应数据写入到连接的客户端,其具体功能如下:

  1. 根据HTTP响应头部中的Content-Type字段确定HTTP响应数据的类型,设置HTTP响应数据的Content-Type字段;
  2. 将HTTP响应头部和HTTP响应数据写入到连接的客户端。

此方法的函数签名如下:

func (r *Response) Write(w io.Writer) error

其中,r表示要写入的HTTP响应数据,w表示连接的客户端。
此方法会先将HTTP响应头部写入到连接的客户端,然后再将HTTP响应数据写入到连接的客户端。在写入HTTP响应数据之前,会先写入Content-Length字段,表示HTTP响应数据的长度,这样客户端才能正确地读取到HTTP响应数据。
写入过程中可能会出现各种错误,因此Write方法的返回值为error类型,表示写入过程中出现的任何错误。

Client(客户端)

前面分别介绍了 Request与 Respone,了解知道了HTTP请求与响应的大致结构以及所需的信息。那 Request与 Respone之间是如何通过网络进行交互的呢,这时候就需要Client与Server来协助与处理了,此篇文章重点介绍Client部分。
Client这里顾名思义就是HTTP客户端,用于发送HTTP请求( Request) 并获得响应Respone。
下面我们来详细介绍下在Go语言的net/http包中,Client是如何被定义以及使用的。

Client结构体

Go语言中的http.Client结构体是用于发送HTTP请求并返回响应的组件。它的定义如下:

type Client struct {
    Transport     RoundTripper
    CheckRedirect func(req *Request, via []*Request) error
    Jar           CookieJar
    Timeout       time.Duration
}

下面对各个字段进行分别说明:

Transport

一个http.RoundTripper接口类型的对象,只包含一个方法RoundTrip,它接受一个http.Request类型的参数,表示HTTP请求,返回一个http.Response类型的响应和一个错误对象,该方法的作用是发送HTTP请求并返回响应,同时处理可能出现的传输错误,如超时、连接错误、重定向等。
http.RoundTripper 的默认实现是http.Transport,该实现使用TCP连接池,支持HTTP/1.1、HTTP/2协议,同时还支持HTTPS、代理、压缩和连接复用等特性。如果需要更灵活地控制HTTP请求的传输过程,可以自定义实现http.RoundTripper接口,并将其传递给http.Client的Transport字段。

CheckRedirect

一个函数类型,用于控制HTTP重定向。默认情况下,http.DefaultCheckRedirect允许自动跟随HTTP重定向。

Jar

一个http.CookieJar接口类型的对象,用于管理HTTP cookie。默认情况下,http.DefaultCookieJar使用net/http/cookiejar包中的默认cookie实现。

Timeout

一个time.Duration类型的对象,用于控制HTTP请求的超时时间。默认情况下,如果该字段没有设置超时时间,即无限期等待响应。

创建一个 Client也很简单,最简单的创建如下:

Client := &http.Client{}

一行代码搞定,当然也可以带上你自己所需要的参数来创建Client,比如使用http.Client的 Timeout字段创建一个有超时时间的客户端:

Client := &http.Client{
    Timeout: 15 * time.Second,
}

有一些更细粒度的超时控制:

Client := &http.Client{  
    Transport: &Transport{
        Dial: (&net.Dialer{
                Timeout:   30 * time.Second,
                KeepAlive: 30 * time.Second,
        }).Dial,
        TLSHandshakeTimeout:   10 * time.Second,
        ResponseHeaderTimeout: 10 * time.Second,
        ExpectContinueTimeout: 1 * time.Second,
    }
}

代码当中一些参数,下面列出解释以便理解:

  • net.Dialer.Timeout 限制建立TCP连接的时间
  • http.Transport.TLSHandshakeTimeout 限制 TLS握手的时间
  • http.Transport.ResponseHeaderTimeout 限制读取response header的时间
  • http.Transport.ExpectContinueTimeout 限制client在发送包含 Expect: 100-continue的header到收到继续发送body的response之间的时间等待

Client 使用

标准请求

使用 http.Client进行发送HTTP请求以及返回响应,基本流程如下:

  1. 创建http.Client对象。首先,我们需要创建一个http.Client对象。可以通过http.DefaultClient使用默认的HTTP客户端,也可以通过手动创建一个新的http.Client对象,以便自定义其参数。
  2. 创建HTTP请求。有了http.Client对象后,我们需要创建一个HTTP请求。在Request章节中,我们讲述到http.NewRequest函数,我们可以通过该函数创建一个新的请求对象,并设置请求URL、方法、请求体等参数。
  3. 发送HTTP请求。有了请求对象后,将请求对象传递给http.Client的Do方法,以便发送HTTP请求。Do方法返回一个响应对象,其中包含服务器的响应信息,如状态码、响应头和响应体等。
  4. 处理HTTP响应。我们可以使用响应对象中的方法和属性,如resp.StatusCode、resp.Header、resp.Body等,处理服务器的响应。通常,我们需要读取响应体的内容,并将其解析为合适的数据类型,如JSON或XML。
  5. 关闭HTTP响应。获取响应后,我们需要确保关闭HTTP响应的主体。可以使用defer resp.Body.Close()语句在函数退出时自动关闭响应体,以避免内存泄漏。

以下是一个简单的示例代码,演示了使用http.Client发送HTTP GET请求的基本流程:

package main

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

func main() {
    // 创建http.Client对象
    client := &http.Client{}

    // 创建HTTP请求
    req, err := http.NewRequest("GET", "http://example.com", nil)
    if err != nil {
        panic(err)
    }

    // 发送HTTP请求
    resp, err := client.Do(req)
    if err != nil {
        panic(err)
    }

    // 处理HTTP响应
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(body))
}
自定义Client

一般情况下,我们并不需要自定义http.Client来控制控制HTTP请求的行为和配置,使用net/http包中默认的 http.DefaultClient 即可,http.DefaultClient 在 client.go 文件中是这样定义的:

var DefaultClient = &Client{}

可以看出默认的http.DefaultClient并没有设置Client任何属性值,但是如果我们需要设置HTTP请求的超时时间、代理、连接池等选项,可能就需要我们自己去定义和创建http.Client了。
根据Client结构体内容,我们知道http.Client拥有 Transport、CheckRedirect、 Jar、Timeout四个属性字段,详细介绍如下:

Timeout

它用于设置 HTTP客户端的超时时间,是一个 time.Duration 类型的值,表示客户端在发送请求后等待服务器响应的最大时间。如果在这个时间内服务器没有响应,客户端会放弃请求并返回一个错误。这个超时时间是一个全局的设置,对于所有的 HTTP请求都生效。
默认情况下,http.Client.Timeout 的值是零,表示没有超时限制。下面是创建一个10秒超时时间的客户端示例:

client := &http.Client{
       Timeout: 10 * time.Second,   
} 

Jar

它是一个 http.CookieJar 接口类型的值,表示 HTTP客户端使用的 cookie容器。这个容器会自动存储服务器发送给客户端的 cookie,并在后续的 HTTP 请求中自动发送这些 cookie给服务器。
默认情况下,http.Client.Jar 是空的,这意味着 HTTP客户端不会发送任何 cookie给服务器。
可以创建一个自定义的 cookie容器,并将其赋值给 http.Client.Jar 属性,例如:

jar, err := cookiejar.New(nil)   
if err != nil {       
// handle error   
}   
client := &http.Client{       
    Jar: jar,   
}

CheckRedirect

它是一个可选的回调函数,用于在 HTTP客户端进行重定向时决定是否要遵循该重定向。
默认情况下,如果不设置 CheckRedirect 函数,HTTP客户端会遵循所有的重定向。对于 http.Get 和 http.Head 等高级别的 HTTP 请求函数,它们默认使用一个简单的 CheckRedirect 函数,该函数会在重定向次数超过 10次时返回一个 http.ErrUseLastResponse 错误。
可以通过自定义 http.Client 的 CheckRedirect 函数来控制 HTTP客户端的重定向行为。下面是一个简单的例子:

func CheckRedirect(eq http.Request, via []http.Request) error {
     if len(via) >= 2 {
         return fmt.Errorf("too many redirects")
     }     return nil   
}

func main() {
     client := &http.Client{
         CheckRedirect: CheckRedirect,     
    }   
}

上述代码重新定义了重定向函数,设定了只允许重定向2次以免出现重定向循环。

Transport

它表示了 http.Client 使用的网络传输层。默认情况下,http.Client 使用的是 http.DefaultTransport,它是一个基于 TCP的传输层。
http.Client.Transport 是一个接口类型,它定义了如下两个方法:

  • RoundTrip(req *Request) (*Response, error):执行一个 HTTP 请求并返回响应结果或者错误。
  • CancelRequest(req *Request):取消一个正在执行的请求

如果你需要创建自己的传输层,你需要实现 http.RoundTripper 接口。这个接口只有一个方法:

  • RoundTrip(req *Request) (*Response, error):执行一个 HTTP 请求并返回响应结果或者错误。

解释完 http.Client 得几个属性后,我们通过下面几个例子来了解和理解如何进行自定义 http.Client :
(1) 通过 Transport 字段自定义传输层

transport := &http.Transport{
      Proxy: http.ProxyFromEnvironment,
      TLSClientConfig: &tls.Config{
          InsecureSkipVerify: true,
      },
  }
 client := &http.Client{
      Transport: transport,
  }
 resp, err := client.Get("https://example.com")

上述代码自定义一个使用了代理服务器和自签名证书的的传输层。

(2) 通过自定义函数来进行重新制定重定向规则

func userCheckRedirect(req *http.Request, via []*http.Request) error {
     //只能执行3次重定向
     if len(via) >= 3 {
         return errors.New("stopped after 3 redirects")
     }
     return nil
  }
  client := &http.Client{
      CheckRedirect: userCheckRedirect,
  }

通过自定义函数 userCheckRedirect来控制重定向次数最多为3次,防止无限循环。如果不进行自定义函数,则默认重定向次数为 10次。

自定义Request

相当于 http.Client 来说,http.Request的自定义使用频率更高,更为普遍。
http.Request它包含了请求的方法、URL、Header以及 Body等信息。我们可以创建一个 http.Request 对象并设置它的http.Request.Header 、 http.Request.Body 、 http.Request.URL等属性。
在 net/http(一)–Request章节中讲到过,创建一个新的 http.Request, 使用的是 http.NewRequest 函数,下面将以代码示例形式讲述各种场景的自定义Request创建:
(1)自定义请求 Header:

req, err := http.NewRequest("GET", "https://www.example.com", nil)    
if err != nil {
        log.Fatal(err)    
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer my-token")

(2) 自定义参数:

query := url.Values{}     //通过 对url.Values类型数据的添加
query.Add("q", "golang")    
query.Add("page", "1")
url := &url.URL{
        Scheme:   "https",
        Host:     "www.example.com",
        Path:     "/search",
        RawQuery: query.Encode(),
}
req, err := http.NewRequest("GET", url.String(), nil)    
if err != nil {
        log.Fatal(err)
}

(3) 自定义 Basic认证:

eq, err := http.NewRequest("GET", "https://www.example.com", nil)    
if err != nil {
        log.Fatal(err)    
}
username := "my-username"    
password := "my-password"    
auth := username + ":" + password
base64Encoded := base64.StdEncoding.EncodeToString([]byte(auth))
req.Header.Set("Authorization", "Basic "+base64Encoded)

Basic 认证是比较常见的API调用认证,此处是通过设置请求头的 Authorization完成设置。
(4) 使用 Cookie :

cookie := &http.Cookie{        
    Name:  "session_id",        
    Value: "my-session-id",    
}
// Create the request    
url := "https://www.example.com/api/v1"    
req, err := http.NewRequest("GET", url, nil)    
if err != nil {        
    log.Fatal(err)    
}    
req.AddCookie(cookie)

Cookie 在 HTTP 请求中属实常见,代码中通过 Request.AddCookie方法来设置cookie。

(5) 发送POST表单数据:

url := "https://www.example.com/api/v1/posts"    
params := []string{"xjx", "zzz"}    
req, err := http.NewRequest("POST", url, nil)    
if err != nil {
        log.Fatal(err)
}    
data := url.Values{}    
data.Set("title", title)    
data.Set("content", content)    
for _, tag := range params {
        data.Add("tags", tag)
}    
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")    
req.Body = ioutil.NopCloser(strings.NewReader(data.Encode()))

POST表单数据一般通过设置请求体来发送,而请求体(Request Body)的数据设置一般跟 请求头的 Content-Type值相关。
下面列举常见的 Content-Type构造请求体例子:
[1] application/x-www-form-urlencoded
该类型Request Body数据一般跟 自定义参数类型,通过将需要发送的表单数据通过url.Values格式添加并Encode进行加编码,不同的是最后需要转化为*Reader对象。示例:

url := "http://www.example.com"  
contentType := "application/x-www-form-urlencoded"  
data := "key1=value1&key2=value2"  
requestBody := strings.NewReader(data)  
req, _ := http.NewRequest("POST", url, requestBody)  
req.Header.Set("Content-Type", contentType)

还可以后期在添加body

req, _ := http.NewRequest("POST", url, nil)  
req.Header.Set("Content-Type", contentType)  
req.Body = io.NopCloser(requestBody) //添加请求体

[2]application/json
该类型Request Body数据是json格式,非url.Values类型,其他操作方式差不多,直接看示例:

url := "http://www.example.com"  
contentType := "application/json"  
data := `{"key1":"xjx","key2":18}`  
requestBody := strings.NewReader(data)  
req, _ := http.NewRequest("POST", url, requestBody)  
req.Header.Set("Content-Type", contentType)

[3] application/xml
该类型Request Body数据是xml格式或者[]byte格式,如果是字符串形式跟JSON处理一致,[]byte类型如下:

url := "http://www.example.com"  
contentType := "application/xml"  
data := []byte(`<?xml version="1.0" encoding="UTF-8"?>      <root>          <name>John Doe</name>          <email>john.doe@example.com</email>      </root>`)  
requestBody := bytes.NewBuffer(data)  
req, _ := http.NewRequest("POST", url, requestBody)  
req.Header.Set("Content-Type", contentType)

[4] multipart/form-data
该类型一般用于上传文件,该类型表单数据一般通过二进制传输,我们转为 []byte或者string类型即可,示例如下:

url := "http://www.example.com"  
contentType := "multipart/form-data"  
data, _ := ioutil.ReadFile("post.txt")  
requestBody := bytes.NewReader(data)  
req, _ := http.NewRequest("POST", url, requestBody)  
req.Header.Set("Content-Type", contentType)

这些例子涵盖了 http.Request 的一些实际场景自定义使用,包括添加请求头、发送 GET请求参数和发送 POST表单数据等。你可以根据自己的需求进行更多自定义组合。
下面我们介绍一些使用 http.Client自带的函数(比如http.Client.Get/http.Get、http.Client.Post/http.Post、http.Client.PostForm/http.PostForm等)发送HTTP请求操作,这些自带函数基本都是对标请求准流程进行了封装以适应不同场景简便使用。

自带函数请求

net/http包的client.go 文件提供了自带函数来简便的调用Client通过GET、POST等方式请求HTTP,下面来简单举例说明。

http.Get/http.Client.Get

使用net/http包编写一个简单的发送HTTP请求的Client端,可以使用 http.Get 或者 http.Client.Get函数,这两函数本质是一样的,如下:

func Get(url string) (resp *Response, err error) 
func (c *Client) Get(url string) (resp *Response, err error)

下面看一个 http.Get 函数无参数请求HTTP示例代码如下:

package main

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

func main() {
    //Get方式获取URL的展示信息
    resp, err := http.Get("https://www.qq.com")
    if err != nil {
        fmt.Println("get url failed, err:", err)
    } else {
        defer resp.Body.Close()
        //读取body
        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            fmt.Println("read from resp.Body failed,err:", err)
        } else {
            fmt.Println(string(body))
        }
    }
}

再来一个关于有参数的HTTP请求示例代码:

package main

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

func main() {
    apiUrl := "http://www.example.com"
    //解析URl字符串获取URL对象
    u, err := url.ParseRequestURI(apiUrl)
    if err != nil {
        fmt.Printf("parse url requestUrl failed,err:%v\n", err)
    }
    //添加参数
    params := url.Values{}
    params.Add("age", "10")
    params.Add("name", "xjx")
    //将参数URL化,生成结果类似:bar=baz&foo=quux格式并赋值给URL.RawQuery
    u.RawQuery = params.Encode()
    resp, err := http.Get(u.String())
    if err != nil {
        fmt.Println("get url failed, err:", err)
    } else {
        defer resp.Body.Close()
        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
            fmt.Println("read from resp.Body failed,err:", err)
        } else {
            fmt.Println(string(body))
        }
    }
}

http.Post/http.Client.Post

上面演示了使用net/http包发送GET请求的示例,而发送POST请求则可以使用 http.Post 或者 http.Client.Post函数,如下:

func Post(url, contentType string, body io.Reader) (resp *Response, err error) 
func (c *Client) Post(url, contentType string, body io.Reader) (resp *Response, err error)

下面来看下net/http包发送Post请求的示例:

package main

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

func main() {
    apiUrl := "http://www.example.com"

    // 构造类型为 x-www-form-urlencoded 的请求体数据
    // 发送 POST 请求
    requestBody := bytes.NewBufferString("key1=value1&key2=value2")
    resp, err := http.Post(apiUrl, "application/x-www-form-urlencoded", requestBody)

    if err != nil {
        // 发生错误
        fmt.Println("Error occurred while sending request:", err)
        return
    }
    defer resp.Body.Close()

    // 读取响应结果
    response, err := io.ReadAll(resp.Body)
    if err != nil {
        fmt.Println("Error occurred while reading response:", err)
        return
    }

    // 输出响应结果
    fmt.Println(string(response))
}

在http.Post 中第二个函数是可以自己控制的,它的值可以是 Request中 Content-Type 字段的允许值范围内,不清楚的可以查看Content-Type取值范围。
net/http包发送application/x-www-form-urlencoded类型的Post请求还有一个更简便的函数 http.PostForm或者 http.Client.PostForm:

func PostForm(url string, data url.Values) (resp *Response, err error) 
func (c *Client) PostForm(url string, data url.Values) (resp *Response, err error)

代码中已经固定了contentType类型为application/x-www-form-urlencoded,所以只需要传入 url和 url.Values类型的data即可。

http.Head/http.Client.Head

net/http 包提供了一个名为 http.Head/http.Client.Head的函数,用于发送 HTTP的 HEAD请求并返回响应结果的头部信息。HEAD请求与 GET请求类似,但服务器将不会返回响应体,只会返回响应头部信息。

func Head(url string) (resp *Response, err error)

func (c *Client) Head(url string) (resp *Response, err error)

以下是一个简单的示例,演示如何使用 http.Head 函数发送 HTTP HEAD 请求并获取响应头部信息:

package main

import (
    "fmt"
    "net/http"
)

func main() {
    url := "https://www.example.com"
    resp, err := http.Head(url)
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    defer resp.Body.Close()

    fmt.Println("Status:", resp.Status)
    fmt.Println("Content-Length:", resp.Header.Get("Content-Length"))
    fmt.Println("Content-Type:", resp.Header.Get("Content-Type"))
}

在这个示例中,我们首先定义了一个目标 URL,然后调用 http.Head 函数向该 URL发送 HEAD请求并获取响应结果。如果函数执行成功,我们打印出响应状态、响应长度以及响应类型。需要注意的是,在获取响应头部信息后,我们需要手动调用 resp.Body.Close() 来关闭响应体,以便释放资源。

做个总结,net/http包中不管 GET,POST还是 PostForm函数,观其代码都是通过封装了 创建Client、创建 NewRequest、最后通过`http.Client的Do发送流程,方便了用户的操作,但也失去了用户自己自定义特殊需求的灵活性,这时候就需要通过基本请求流程,自行去定义请求来解决灵活性问题。

如下图是 Client端发送的核心流程:
image.png

Server(服务端)

基于 HTTP 构建的网络应用包括两个端,即客户端 ( Client ) 和服务端 ( Server )。
两个端的交互行为包括从客户端发出 request、服务端接受 request 进行处理并返回 response 以及客户端处理 response。所以 http 服务器的工作就在于如何接受来自客户端的 request,并向客户端返回 response, 如下图所示:
image.png

HTTP Server简单实现

对于 golang 来说,利用 net/http 包实现一个Http Server非常简单,只需要简简单单几句代码就可以实现,先看看 Golang 的其中一种 http server简单的实现:

package main

import (
    "fmt"
    "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "hello World!")
}

func main() {
    //注册路由
    http.HandleFunc("/", handler)
    //创建服务且监听
    http.ListenAndServe(":8080", nil)
}

再来看看另外一种http server实现,代码如下:

package main

import (
    "fmt"
    "net/http"
)

type routeIndex struct {
    content string
}

func (route *routeIndex) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, route.content)
}

func main() {
    //注册路由
    http.Handle("/", &routeIndex{content: "Hello, World"})
    //创建服务且监听
    http.ListenAndServe(":8080", nil)
}

**注意: ** 路由不区分post get …请求只要url的路径匹配就行 ,具体客户端是什么请求类型需要自行在代码里判断

路由注册

ServerMux 是 net/http 包中的一个路由器(router),是一个 HTTP 请求多路复用器,用于将收到的 HTTP 请求路由到相应的处理程序(handlers)。
在 ServerMux 中,我们可以通过调用 HandleFunc 或者 Handle 方法来注册一个路由和对应的处理函数。当一个请求到达 ServerMux 时,路由器会根据请求的 URL 路径找到对应的处理函数,并将请求转发给该函数进行处理。
ServerMux 结构体定义如下:

type ServeMux struct {
    mu    sync.RWMutex        //用于保证并发安全性的互斥锁
    m     map[string]muxEntry //一个映射表,将URL模式映射到对应的处理程序。在处理HTTP请求时,ServeMux将使用此映射表来查找与请求URL路径匹配的处理程序
    es    []muxEntry          //一个按长度排序的URL模式条目的切片。这个切片是用来加速ServeMux的URL匹配操作的。在处理HTTP请求时,ServeMux会按照长度递减的顺序迭代这个切片,以便找到最长的匹配URL模式
    hosts bool                //标志位,表示ServeMux是否具有任何带有主机名的URL模式。如果是,则在处理HTTP请求时,ServeMux还需要匹配主机名。如果不是,则可以忽略主机名匹配
}

ServeMux 是一个非常重要的组件,用于将HTTP请求路由到正确的处理程序,并且在Go标准库中被广泛使用。在ServeMux 中,还有一个 muxEntry 结构,muxEntry是 ServeMux 内部维护的数据结构,用于将 URL 路径模式与处理程序相关联。定义代码如下:

type muxEntry struct {
    h       Handler //一个处理程序,它是用于处理与该URL模式匹配的HTTP请求的函数
    pattern string  //与该处理程序相关联的URL模式。在ServeMux中,pattern是映射到处理程序的关键字之一。在匹配请求路径时,ServeMux将使用pattern来判断请求是否与该条目匹配。
}

muxEntry结构体用于将URL模式与处理程序相关联,以便在处理HTTP请求时能够正确地路由请求。
再来看看 DefaultServeMux ,可以看到 http.Handle 和 http.HandleFunc 这两个函数最终都由 DefaultServeMux 调用 Handle 方法来完成路由的注册的,该变量定义如下:

var defaultServeMux ServeMux
var DefaultServeMux = &defaultServeMux

这里的 DefaultServeMux 表示一个默认的 ServeMux,当我们没有创建自定义的 ServeMux,则会自动使用一个默认的 ServeMux。

自定义 ServeMux

我们可以创建自定义的 ServeMux 取代默认的 DefaultServeMux,示例代码如下:

package main

import (
    "fmt"
    "net/http"
)

type routeIndex struct {
    content string
}

func (route *routeIndex) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, route.content)
}

func htmlHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html")
    html := `<!doctype  html>  
    <META  http-equiv="Content-Type"  content="text/html"  charset="utf-8">    
    <html  lang="zhCN">  
            <head>   
                    <title>Golang</title>  
                    <meta  name="viewport"  content="width=device-width,  initial-scale=1.0,  maximum-scale=1.0,  user-scalable=0;"  />   
            </head>     
            <body>        
            <div  id="app">Hello, HandleFunc World!</div>       
            </body>   
    </html>`
    fmt.Fprintf(w, html)
}

func main() {
    //自定义serveMux
  mux := http.NewServeMux()
    mux.Handle("/Handle", &routeIndex{content: "Hello, Handle World"})
    mux.HandleFunc("/HandleFunc", htmlHandler)
    //创建服务且监听
    http.ListenAndServe(":8080", mux)
}

http.NewServeMux 方法用于创建一个新的 ServeMux 实例,如果调用的是自定义 ServeMux 实例 mux,那么 Server 实例接收到的路由对象将不再是 DefaultServeMux 而是 mux。

Handler

了解完 ServeMux,再来看看 Handler对象:

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

Handler对象是个接口,Handler 接口中声明了名为 ServeHTTP 的函数,也就是说任何结构只要实现了这个 ServeHTTP 方法,那么这个结构体就是一个 Handler 对象。http.Handler.ServeHTTP 方法是用来是用以处理 request 并构建 response 的核心逻辑所在。
总结起来一句话,要完成完整的http server 服务,必须完成对Handler接口的实现,即对 http.Handler.ServeHTTP 方法的实现。

对 Handler对象有了大概了解后,回到 http.Handle 和 http.HandleFunc 函数,看看他们是怎么实现 Handler 接口的。
先看func Handle(pattern string, handler Handler)函数, 其第二个参数必须为Handler接口的实现,所以调用该函数则必须先自行完成对Handler接口的实现,方式具体参考示例2。
再看看func HandleFunc(pattern string, handler func(ResponseWriter, *Request))函数,发现第二个参数只需传入满足它参数的一个函数即可,并不需要用户自行去实现 Handler接口的,究其原因,我们一看源码就明白了:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
        panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}

注意一下这行代码:mux.Handle(pattern, HandlerFunc(handler)),这里 HandlerFunc 实际上是将 handler 函数做了一个类型转换,看一下 HandlerFunc 的定义:

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

看到这里,应该最终明白了把,原来这里 HandlerFunc 实际上已经默认实现了Handler接口,实现了它的ServeHTTP函数,用户只需要传入 handler 函数即可被HandlerFunc 强行转为一个Handler对象,这是一种巧妙的转换技巧,不需要定义一个结构体,再让这个结构实现 ServeHTTP 方法。

路由绑定

ServeMux内部维护一个map[string]muxEntry,该map作用将URL模式映射到对应的muxEntry结构体,而muxEntry结构体则将处理程序与URL模式相关联。
那ServeMux是如何将将URL模式映射到对应的muxEntry结构体的呢?
通过调用 http.Handle 和 http.HandleFunc 函数完成映射的。而这两个函数,最终调用的方法的是: ServeMux.Handle, 其代码如下:

func (mux *ServeMux) Handle(pattern string, handler Handler) {
    //互斥锁,解决 多个goroutine 并发访问时的线程安全
    mux.mu.Lock()
    defer mux.mu.Unlock()

    //检查参数的合法性,如果有不合法的参数,则会抛出 panic 异常
    if pattern == "" {
        panic("http: invalid pattern")
    }
    if handler == nil {
        panic("http: nil handler")
    }
    if _, exist := mux.m[pattern]; exist {
        panic("http: multiple registrations for " + pattern)
    }
    //初始化ServeMux内部保存URL路径模式和处理程序之间映射关系的map(ServeMux.m),如果该 map 还未被初始化,则会在此处进行初始化
    if mux.m == nil {
        mux.m = make(map[string]muxEntry)
    }
    //将URL路径模式和处理程序建立映射关系
    //首先,创建一个 muxEntry 结构体,保存处理程序和 URL 路径模式
    //然后,将该 muxEntry 对象加入到 ServeMux 内部维护的 map 中
    e := muxEntry{h: handler, pattern: pattern}
    mux.m[pattern] = e
    //如果URL路径模式的最后一个字符是斜杠(即该 URL 路径模式对应的处理程序是一个目录),则将该 muxEntry 对象插入到 ServeMux.es中
    if pattern[len(pattern)-1] == '/' {
        mux.es = appendSorted(mux.es, e)
    }
    //如果URL路径模式的第一个字符不是斜杠(即该 URL 路径模式对应的处理程序是一个主机名),则将 ServeMux 的 hosts 字段设置为 true
    if pattern[0] != '/' {
        mux.hosts = true
    }
}

上述函数主要作用是:

  • 将 URL 路径模式和处理程序建立映射关系,并将映射关系保存到 ServeMux 的内部数据结构中
  • 同时,该函数还会对传入的参数进行一些合法性检查,如 URL 路径模式和处理程序不能为空,URL 路径模式不能重复等
  • 最后,该函数还会将 URL 路径模式按照长度从长到短排序,并标记 ServeMux 是否支持主机名路由。

最后用一张图来总结整个注册路由流程:

image.png

请求处理

处理完路由相关信息注册,就要进行TCP监听服务启动以及TCP 连接并处理请求了。标准库提供的 net/http.ListenAndServe可以用来监听 TCP 连接并处理请求,该函数会使用传入的监听地址和处理器初始化一个 HTTP 服务器 net/http.Server,调用该服务器的 net/http.Server.ListenAndServe方法:

func ListenAndServe(addr string, handler Handler) error {
    server := &Server{Addr: addr, Handler: handler}
    return server.ListenAndServe()
}

上述代码先创建了一个 Server 对象,传入了地址和 handler 参数后调用 Server 对象 ListenAndServe() 方法。
特别需要注意的一点是:该函数的第二个参数是 Handler 类型,不管是一个新的 ServeMux 对象mux,还是默认的 DefaultServeMux , ServeMux其本身自己也实现了 Handler 接口,也实现了ServeHTTP方法,也是一个 Handler 对象,但 ServeMux 的 ServeHTTP() 方法主要作用是匹配当前路由对应的 handler 方法,与自定义的http.Handler.ServeHTTP 方法用以处理 request 并构建 response 功能区别不同

server结构体

net/http.Server是 HTTP 服务器的主要结构体,用于控制 HTTP 服务器的行为。
其结构体定义为:

type Server struct {
  //服务器监听的地址和端口号,格式为 "host:port",例如 "127.0.0.1:8080"
    Addr                         string
  //HTTP 请求的处理器。对于每个收到的请求,服务器会将其路由到对应的处理器进行处理。通常使用 http.NewServeMux() 方法创建一个默认的多路复用器,并将其作为处理器。如果没有设置该字段,则使用 DefaultServeMux
    Handler                      Handler
  //一个布尔值,用于指示是否禁用 OPTIONS 方法的默认实现。如果该值为 true,则在收到 OPTIONS 请求时,服务器不会自动返回 Allow 头部,而是交给用户自行处理。默认为 false,即启用 OPTIONS 方法的默认实现
    DisableGeneralOptionsHandler bool
  //HTTPS 服务器的 TLS 配置,用于控制 HTTPS 服务器的加密方式、证书、密钥等安全相关的参数
    TLSConfig                    *tls.Config
  //HTTP 请求的读取超时时间。如果服务器在该时间内没有读取到完整的请求,就会关闭连接。该字段为 time.Duration 类型,默认为 0,表示没有超时限制
    ReadTimeout                  time.Duration
  //HTTP 请求头部读取超时时间。如果服务器在该时间内没有完成头部读取,就会关闭连接。该字段为 time.Duration 类型,默认为 0,表示没有超时限制
    ReadHeaderTimeout            time.Duration
  //HTTP 响应的写入超时时间。如果服务器在该时间内没有完成对响应的写入操作,就会关闭连接。该字段为 time.Duration 类型,默认为 0,表示没有超时限制
    WriteTimeout                 time.Duration
  //HTTP 连接的空闲超时时间。如果服务器在该时间内没有收到客户端的请求,就会关闭连接。该字段为 time.Duration 类型,默认为 0,表示没有超时限制
    IdleTimeout                  time.Duration
  //HTTP 请求头部的最大大小。如果请求头部的大小超过该值,服务器就会关闭连接。该字段为 int 类型,默认为 1 << 20(1MB)
    MaxHeaderBytes               int
    TLSNextProto                 map[string]func(*Server, *tls.Conn, Handler)
  //连接状态变化的回调函数,用于处理连接的打开、关闭等事件
    ConnState                    func(net.Conn, ConnState) 
   //错误日志的输出目标。如果该字段为 nil,则使用 log.New(os.Stderr, "", log.LstdFlags) 创建一个默认的日志输出目标
    ErrorLog                     *log.Logger
  //所有 HTTP 请求的基础上下文。当处理器函数被调用时,会将请求的上下文从基础上下文派生出来。默认为 context.Background()。
    BaseContext                  func(net.Listener) context.Context 
  //连接上下文的回调函数,用于创建连接上下文。每个连接上下文都与一个客户端连接相关联。如果未设置该字段,则每个连接的上下文都是 BaseContext 的副本
    ConnContext                  func(ctx context.Context, c net.Conn) context.Context
  //标志变量,用于表示服务器是否正在关闭。该变量在执行 Shutdown 方法时被设置为 true,用于避免新的连接被接受
    inShutdown                   atomic.Bool 
  //标志变量,用于控制服务器是否支持 HTTP keep-alive。如果该变量为 true,则服务器在每次响应完成后都会关闭连接,即不支持 keep-alive。如果该变量为 false,则服务器会根据请求头部中的 Connection 字段来决定是否支持 keep-alive。该变量在执行 Shutdown 方法时被设置为 true,用于关闭正在进行的
    disableKeepAlives            atomic.Bool
  // 一个 sync.Once 类型的值,用于确保在多线程环境下,NextProtoOnce 方法只被调用一次。NextProtoOnce 方法用于设置 Server.NextProto 字段
    nextProtoOnce                sync.Once 
  // error 类型的值,用于记录 NextProto 方法的调用结果。该值在多个 goroutine 之间共享,用于检测 NextProto 方法是否成功
    nextProtoErr                 error 
  //互斥锁,用于保护 Server 结构体的字段。因为 Server 结构体可能被多个 goroutine 并发访问,所以需要使用互斥锁来确保它们的安全性
    mu                           sync.Mutex    
   //存储 HTTP 或 HTTPS 监听器的列表。每个监听器都是一个 net.Listener 接口类型的实例,用于接收客户端请求。当调用 Server.ListenAndServe() 或 Server.ListenAndServeTLS() 方法时,会为每个监听地址创建一个对应的监听器,并将其添加到该列表中
    listeners                    map[*net.Listener]struct{}
  //表示当前处于活动状态的客户端连接的数量。该字段只是一个计数器,并不保证一定准确。该字段用于判断服务器是否处于繁忙状态,以及是否需要动态调整服务器的工作负载等
    activeConn                   map[*conn]struct{}                                    
  //在服务器关闭时执行的回调函数列表。当服务器调用 Server.Shutdown() 方法时,会依次执行该列表中的每个回调函数,并等待它们全部执行完毕。该字段可以用于在服务器关闭时释放资源、保存数据等操作
    onShutdown                   []func()                                              
  //表示所有监听器的组。该字段包含一个读写互斥锁 sync.RWMutex 和一个映射表 map[interface{}]struct{}。在监听器启动时,会将监听器地址作为键添加到映射表中。该字段主要用于实现优雅地关闭服务器。在服务器关闭时,会遍历所有监听器,逐个关闭它们,并等待所有连接关闭。如果在等待连接关闭时,有新的连接进来,服务器会先将新连接添加到 activeConn 字段中,并等待所有连接关闭后再退出。这样可以保证服务器在关闭过程中,不会丢失任何连接
    listenerGroup                sync.WaitGroup                                        
}

*Server.ListenAndServer

当创建完 一个 Server 对象后,调用 Server 对象 ListenAndServe() 方法会使用网络库提供的 net.Listen监听对应地址上的 TCP 连接并通过 net/http.Server.Serve处理客户端的请求:

func (srv *Server) ListenAndServe() error {
    //判断服务器是否正在关闭,如果是,则返回
    if srv.shuttingDown() {
        return ErrServerClosed
    }
    //获取服务器监听的地址
    addr := srv.Addr
    //如果服务器监听的地址为空,将其设置为 ":http"
    if addr == "" {
        addr = ":http"
    }
    //创建一个 TCP 监听器,监听指定的地址,如果创建监听器时出现错误,返回错误
    ln, err := net.Listen("tcp", addr)
    if err != nil {
        return err
    }
    //使用 Serve() 方法开始监听并处理连接,返回处理连接时可能出现的错误
    return srv.Serve(ln)
}

通过以上介绍我们可以自定义Server了

s := &http.Server{
	Addr:           ":8080",
	Handler:        myHandler,
	ReadTimeout:    10 * time.Second,
	WriteTimeout:   10 * time.Second,
	MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()

后面的我们就不在看了,有时间自己看看,下面展示请求处理的整个流程逻辑
image.png

简单的web服务器

Go语言里面提供了一个完善的 net/http 包,通过 net/http 包我们可以很方便的搭建一个可以运行的 Web 服务器。同时使用 net/http 包能很简单地对 Web 的路由,静态文件,模版,cookie 等数据进行设置和操作。

Web服务器的工作方式

我们平时浏览网页的时候,会打开浏览器,然后输入网址后就可以显示出想要浏览的内容。这个看似简单的过程背后却隐藏了非常复杂的操作。

对于普通的上网过程,系统其实是这样做的:

  • 浏览器本身是一个客户端,当在浏览器中输入 URL (网址)的时候,首先浏览器会去请求 DNS 服务器,通过 DNS 获取相应的域名对应的 IP,然后通过 IP 地址找到对应的服务器后,要求建立 TCP 连接;
  • 与服务器建立连接后,浏览器会向服务器发送 HTTP Request (请求)包;
  • 服务器接收到请求包之后开始处理请求包,并调用自身服务,返回 HTTP Response(响应)包;
  • 客户端收到来自服务器的响应后开始渲染这个 Response 包里的主体(body),等收到全部的内容后断开与该服务器之间的 TCP 连接。

image.png

通过上图可以将 Web 服务器的工作原理简单地归纳为:

  • 客户机通过 TCP/IP 协议与服务器建立 TCP 连接;
  • 客户端向服务器发送 HTTP 协议请求包,请求服务器里的资源文档;
  • 服务器向客户机发送 HTTP 协议应答包,如果请求的资源包含有动态语言的内容,那么服务器会调用动态语言的解释引擎负责处理“动态内容”,并将处理得到的数据返回给客户端;
  • 客户机与服务器断开,由客户端解释 HTML 文档,在客户端屏幕上渲染图形结果。

前面简单介绍了 Web 服务器的工作原理,那么如何用Go语言搭建一个 Web 服务器呢?示例代码如下:

package main
import (
    "fmt"
    "log"
    "net/http"
)
func main() {
    http.HandleFunc("/", index) // index 为向 url发送请求时,调用的函数
    log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
func index(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "C语言中文网")
}

运行之后并没有什么提示信息,但是命令行窗口会被占用(不能再输入其它命令)。这时我们在浏览器中输入 localhost:8000 可以看到下图所示的内容,则说明我们的服务器成功运行了。
image.png
上面的代码只是展示了 Web 服务器的简单应用,下面我们来完善一下,为这个服务器添加一个页面并设置访问的路由。
首先我们准备一个 html 文件,并命名为 index.html,代码如下所示:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <title>C语言中文网</title>
  </head>
  <body>
    <h1>C语言中文网</h1>
  </body>
</html>

然后将我们上面写的 Web 服务器的代码简单修改一下,如下所示:

package main

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

func main() {
	// 在/后面加上 index ,来指定访问路径
	http.HandleFunc("/index", index)
	log.Fatal(http.ListenAndServe("localhost:8000", nil))
}
func index(w http.ResponseWriter, r *http.Request) {
	content, _ := os.ReadFile("./index.html")
	w.Write(content)
}

运行成功后,在浏览器中输入 localhost:8000/index 就可以看到我们所添加的页面了,如下图所示:
image.png

https://blog.csdn.net/weixin_46120107/article/details/123555434

Request解析

image.png
image.png

Response解析

image.png
从上图来看,响应就三个方法

  • Header设置响应的体配置
  • Write 响应内容
  • WriteHeader响应状态码
点赞 -收藏 -关注
有问题在评论区或者私信我-收到会在第一时间回复
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

胡安民

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值