Golang中基于HTTP协议的网络服务
作者:鲲鹏飞九万里
一、HTTP协议的网络服务
HTTP协议是基于TCP/IP协议栈的,并且它也是一个面向普通文本的协议。
只要搞清楚了HTTP请求的报文(报文的头部(header)和主体(body))应该包含的内容,使用任何一个文本编译器,就饿可以编写一个完整的HTTP请求报文。
在这种情况下,直接使用net.Dial
函数,就可以。
使用net/http
代码包中的程序实体,可以更便捷的访问基于HTTP协议的网络服务。其中最便捷的是使用http.Get
函数。
1.1 使用http.Get
函数访问HTTP协议的网络服务
package main import ( "fmt" "net/http" ) func main() { url1 := "http://www.google.cn/" fmt.Printf("Send request to %q with method GET ... \n", url1) response1, err := http.Get(url1) if err != nil { fmt.Printf("request sending error: %v\n", err) } defer response1.Body.Close() line1 := response1.Proto + " " + response1.Status fmt.Printf("The first line of response: \n %s \n", line1) }
http.Get
函数会返回两个结果值:
- 第一个结果值的类型是*http.Response,它是网络服务给我们传回来的响应内容的结构化表示。
- 第二个结果值是error类型。它代表了在创建和发送HTTP请求,以及接受和解析HTTP响应的过程中可能发生的错误。
在http.Get
函数内部会使用缺省的HTTP客户端,并调用它的Get方法以完成功能。缺省客户端类型是*http.Client
,由公开变量DefaultClient代表。
1.2 使用缺省客户端DefaultClient(类型为*http.Client
)
package main import ( "fmt" "net/http" ) func main() { url1 := "http://www.google.cn/" fmt.Printf("Send request to %q with method GET ... \n", url1) // response1, err := http.Get(url1) response1, err := http.DefaultClient.Get(url1) if err != nil { fmt.Printf("request sending error: %v\n", err) } defer response1.Body.Close() line1 := response1.Proto + " " + response1.Status fmt.Printf("The first line of response: \n %s \n", line1) }
它的基本类型(http.Client
)可以开箱即用。
1.3 使用http.Client
访问HTTP协议的网络服务
package main import ( "fmt" "net/http" ) func main() { url1 := "http://www.google.cn/" fmt.Printf("Send request to %q with method GET ... \n", url1) // response1, err := http.Get(url1) // response1, err := http.DefaultClient.Get(url1) var oneClient http.Client response1, err := oneClient.Get(url1) if err != nil { fmt.Printf("request sending error: %v\n", err) } defer response1.Body.Close() line1 := response1.Proto + " " + response1.Status fmt.Printf("The first line of response: \n %s \n", line1) }
http.Client
是一个结构体类型,并且它包含的字段是公开的。之所以该类型的零值仍然可以使用,是因为它的这些字段要么存在着响应的缺省值,要么其零值直接可以使用,且代表着特定的含义。
二、http.Client中的Transport字段
http.Client
类型中的Transport字段代表着:向网络服务发送HTTP请求,并从网络服务接收HTTP响应的操作过程。
Transport字段的RoundTrip方法实现单次HTTP事务(或者说基于HTTP协议的单词交互)需要的所有步骤。
Transport 字段是http.RoundTrip
接口类型,它有一个缺省值,这个缺省值的变量名为DefaultTransport。DefaultTransport的实际类型为*http.Transport
,*http.Transport
可以被复用,并且是线程安全的。
如果没有显式的为
http.Client
中的Transport字段赋值,这个Client就会直接使DefaultTransport。
http.Client
中的Timeout字段,代表前面所说的单词HTTP事务的超时时间,它time.Duration
类型,它的零值是可用的,用于表示没有设置超时时间。
(1)http.Transport
类型中的DialContext字段
http.Transport
类型,在内部使用一个net.Dialer
类型的值,并且会把该值的Timeout字段的值,设定为30秒。
也就是说,这个Dialer值如果在30秒内还没有建立好网络连接,那么就会被判定为操作超时。
在DefaultTransport的值被初始化的时候,这样的Dialer值的DialContext方法会被赋给前者DialContext字段:
var DefaultTransport RoundTripper = &Transport{ Proxy: ProxyFromEnvironment, DialContext: defaultTransportDialContext(&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }), ForceAttemptHTTP2: true, MaxIdleConns: 100, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, } func defaultTransportDialContext(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error) { return dialer.DialContext }
KeepAlive的背后是一种针对网络连接(更确切地说,是TCP连接)的存活探测机制。它的值用于表示每隔多长时间发送一次探测包。当该值不大于0时,则表示不开启这种机制。
DefaultTransport会把这个字段的值设定为30秒。
(2)http.Transport
类型中的其它字段
一些是关于超时操作
IdleConnTimeout
:含义是空闲的连接在多久之后就应该关闭。
DefaultTransport 会把该字段的值设定为90秒。
如果该值为0,那么就表示不关闭空闲连接。注意,这样可能会造成资源的泄露。
ResponseHeaderTimeout
:含义是,从客户端把请求完全递交给操作系统到从操作系统那里接收到响应报文头到最长时长。
DefaultTransport并没有设定该字段的值。
-
ExpectContinueTimeout
:含义是,在客户端提交了请求报文头之后,等待接收第一个响应报文头的最长时间。
DefaultTransport 把该字段的值设定为1秒。
在客户端想要使用HTTP的“POST”方法把一个很大的报文体发送给服务端的时候,它可以先通过发送一个包含了“Expect: 100-continue”的请求报文头,来询问服务端是否愿意接受这个大报文体。这个字段就是用于设定在这种情况下的超时时间的。
注意,如果该字段的值不大于0,那么无论多大的请求报文体都将会被立即发送出去。
TLSHandshakeTimeout
:TLS是Transport Layer Security 的缩写,可以被翻译为传输层安全。这个字段代表了基于TLS协议的连接在被建立时的握手阶段的超时时间。
DefaultTransport 把该字段的值设置为10秒。
若该值为0,则表示对这个值不设限。
一些与IdleConnTimeout
相关的字段值
MaxIdleConns
:用于控制访问所有主机的最大空闲连接。如果为0,不做限制。
DefaultTransport 把MaxIdleConns设定为100。
MaxIdleConns字段只会对空闲连接的总数做出限定。
MaxIdleConnsPerHost
: 控制Transport值访问每一个网络服务的最大空闲连接数。如果为0,将使用缺省值2, 这个缺省值由DefaultMaxIdleConnsPerHost
所代表。
也就是说,默认情况下,对于某一个Transport值访问的每一个网络服务,它的空闲连接数都最多只能由两个。
MaxConnsPerHost
:针对某一个Transport值访问的每一个网络服务的最大连接数,不论这些连接是否是空闲的。
该字段没有缺省值,零值表示不限定。
MaxIdleConns和MaxIdleConnsPerHost两个与空闲连接数有关的字段的值应该是联动的,所以,有时需要根据实际情况定制它们,可以参考DefaultTransport变量的声明。
三、为什么会出现空闲的连接
3.1 空闲连接的产生
HTTP协议有一个请求报文头,叫做“Connection”。在HTTP协议的1.1 版本中,这个报文头的值默认是“keep-alive”。
在这种情况下,网络连接都是持久连接,它们会在当前的HTTP事务完成后仍然保持着连通性,因此是可以被复用的。
连接的可复用,带来两种可能:
- 一种可能是,针对同一个网络服务,有新的HTTP请求被提交,该连接被再次使用。
- 另一种可能是,不再有对该网络服务的HTTP请求,该连接被闲置。(产生空闲的连接)
后一种情况就产生了空闲连接。另外,如果分配给某一个网络服务的连接过多的话,也可能会导致空闲连接的产生。因为每一个新递交的HTTP请求,都只会征用一个空闲的连接。所以,为空闲连接设定限制,在大多数情况下都是很有必要的,也是需要斟酌的。
3.2 杜绝空闲连接的产生
如果想彻底杜绝空闲连接的产生,那么可以在初始化的时候,把它的DisableKeepAlives字段的值设定为true。这时,HTTP请求的“Connection”报文头的值就会被设置为“close”。这会告诉网络服务,这个网络连接不必保持,当前的HTTP事务完成后就可以断开它。
如此一来,每当一个HTTP请求被递交时,就会产生一个新的网络连接。这样做会明显地加重网络服务以及客户端的负载。所以,在一般情况下,我们都不要去设置这个DisableKeepAlive字段。
在net.Dialer类型中,也有一个看起来很相似的字段KeepAlive。不过,它与前面所说的HTTP 持久连接不是一个概念,KeepAlive是直接作用在底层的socket上的。
KeepAlive的背后是一种针对网络连接(更确切地说,是TCP连接)的存活探测机制。它的值用于表示每隔多长时间发送一次探测包。当该值不大于0时,则表示不开启这种机制。DefaultTransport会把这个字段的值设定为30秒。
四、http.Server
http.Server类型与http.Client相对应。http.Server代表的是基于HTTP协议的服务端,或者网络服务。
4.1 http.Server类型的ListenAndServe方法
http.Server类型的ListenAndServe方法的功能是:监听一个基于TCP协议的网络地址,并对接收到的HTTP请求进行处理。
- 这个方法默认会开启针对网络连接的存活探测机制,以保证连接是持久的。
- 同时,该方法会一直执行,直到有严重的错误发生或被外界关掉。
当被外界关掉时,它会返回一个由http.ErrServerClosed
变量代表的错误值。
4.2 ListenAndServe方法主要做的事情
func (srv *Server) ListenAndServe() error { if srv.shuttingDown() { return ErrServerClosed } addr := srv.Addr if addr == "" { addr = ":http" } ln, err := net.Listen("tcp", addr) if err != nil { return err } return srv.Serve(ln) }
ListenAndServe
方法主要会做下面的事情:
- 检查当前的
http.Server
类型的值的Addr字段。
该字段的值代表了当前的网络服务需要使用的网络地址。即:IP地址和端口号。如果这个字段的值为空字符串,那么就用":http"代替。
也就是说,使用任何可以代表本机的域名和IP地址,并且端口号为80.
- 通过调用
net.Listen
函数在已确定的网络地址上启动基于TCP协议的监听。 - 检查
net.Listen
函数返回的错误值。
如果该错误值不为nil,那么就直接返回该值。否则,通过调用当前值的Serve方法准备接受和处理将要到来的HTTP请求。
4.3 (衍生问题)net.Listen 函数都做了哪些事情
net.Listen
函数做的事情:
- 解析参数值中包含的网络地址隐含的IP地址和端口号;
- 根据给定的网络协议,确定监听的方法,并开始进行监听;
这里还可以延伸到net.socket函数,以及socket相关的知识。
4.4 (衍生问题)http.Server类型的Serve方法是怎么接受和处理HTTP请求的
在一个for循环中,网络监听的Accept方法会被不断的调用,
for { rw, err := l.Accept() }
该方法会返回两个结果值:
- 第一个结果值是net.Conn 类型,代表包含了新到来的HTTP请求的网络连接;
- 第二个结果值是error类型值,代表可能发生的错误。
如果错误不为nil,除非它代表了一个暂时性的错误,否则循环都会被终止。如果是暂时性的错误,那么循环的下一次迭代将会在一段时间之后开始执行。
如果这里的Accept方法没有返回非nil的错误值,那么这里的程序将会把它的第一个结果值包装成一个*http.conn类型的值,然后通过在新的goroutine中调用这个*http.conn 类型值的serve方法,来对当前的HTTP请求进行处理。
HTTP请求相关的,更多的衍生问题:
- 这个*http.conn类型值的状态有几种,分别代表着处理的哪个阶段?
- 处理的过程中会用到哪些读取器和写入器,它们的作用分别是什么?
- 这里的程序是怎么调用我们自定义的处理函数的?
五、思考:怎么优雅地停止基于HTTP协议的网络服务程序?
srv.Shutdown(context.Background())
的方式停止服务,通过RegisterOnShutdown可添加服务停止时的调用。
以上就是Golang中基于HTTP协议的网络服务的详细内容,更多关于Golang HTTP协议的资料请关注脚本之家其它相关文章!