标题无意冒犯,就是觉得这个广告挺好玩的
上面这张思维导图喜欢就拿走
目录
文章目录
使用net/http进行简单的面向网络编程
相信在经过了前面的摸索和学习之后, 读者已经可以进行简单的Go语言程序的开发了, 那么从第五天开始将进行对于Go语言内置包的运用.
在本节中, 我们主要使用的包为 net/http
包.
9.1 HTTP基础知识:
- 客户端和服务端
在网络编程中, 最基本的概念是客户端: client和服务端: server的概念. 客户端相当于是和用户进行交互的一端, 比如说浏览器, 而服务端是用于提供服务的一端, 负责和客户端进行交互, 还有和其他服务端进行交互, 比如说运行在服务器上的一个服务.
Client Server
客户端 <=====> 服务端
- 请求-响应 循环
相信读者应该听说过请求: request和响应: response的概念, 请求是由客户端或者服务端向服务端发送的想要获取信息的数据流. 在这里简单介绍一下HTTP请求:
HTTP请求是遵循HTTP协议的请求方式, 一般拥有的属性有: 请求方法: method, 请求头: header 和 请求数据域: payload
请求方法指出了一个HTTP请求对于资源操作的具体方式:
GET
,POST
,PUT
,DELETE
,PATCH
,OPTION
等等… 一般我们接触最多的就是GET
,POST
,PUT
和DELETE
这四种.
- GET: 表示对于资源的获取操作
- POST: 表示对于资源的创建的操作和需要发送数据的操作
- PUT: 表示对于资源的修改的操作
- DELETE: 表示对于资源的删除操作
方法终究只是用于标识请求意义的字段, 如果你想要在GET里塞很多的数据当然也是可以的…
请求头包含了一个请求的基本信息, 其中比较重要的有
Content-Type
和Cookie
和User-Agent
字段
当然所有的Header
都很重要! 关于每一个Header字段的意义可以参考一下MDN的资料
请求中的数据域包含了几个来源, 其实
header
也可以算其中的一个, 除此之外还有path
,query-string
和data
- path是指包含在路由中的参数, 比如:
/user/1/info/
这里的1
就是所谓的路径参数 - query-string, 字面意思, 就是用于查询操作的字符串, 一般位于整个URL的最后, 由
?
和&
拼接而成, 比如:/comments?length=12
, 这里的query-string便包括了length:12
这个键值对 - data就是数据包, 现在常用的编码方式有:
form-data
和json
许多的应用场景中,对于数据的传输都使用特定的方法,比如:
- 在许多的搜索API中,往往将数据放在query-string中,并且使用GET方法去调用接口。
- 在一般的POST方法中,传输数据的方式往往是以data,也就是数据载荷的方式来传输的。
- 而对于唯一确定资源的主键,比如id或者hash-key,则往往放在path当中
注意:对于上面提到的一部分情况,不是完全规定死的,而是可以随意变更的,使用什么方法,怎么去传输数据都可以随着个人的喜好而改变,只是很多时候大家达成了共识而已。
- 前后端分离的开发方法
在很多人开始写自己的第一个项目的时候,往往都是撰写一个前后端合并的项目:比如一个个人博客网站啦,一个所谓的宠物网店啦(致敬flasky)等等这个时候往往是程序员自己又写前端又写后端,使用HTML模板的方式来调用自己撰写的后端逻辑。
前后端合并的方式是比较初级的开发方式:由一个人独立开发整个应用,这使得对于程序员的技术要求更高,因为需要同时熟悉使用前端和后端的技术栈,这对于个人能力的考验是很大的。并且由个人维护的应用往往会出现难以让他人参与开发,难上手等各种问题。
因此在现在普遍使用的是前后端分离的开发方法,前端和后端约定一套应用编程接口,然后分离开发,这样既可以降低对于程序员个人能力的要求,对于整个应用的维护也是很好的。
9.2 使用net/http搭建最简单的接口
首先,使用net/http
需要在代码中使用以下语句:
import "net/http"
然后一起来写一个最简单的示例:
package main
import (
"fmt"
"net/http"
"time"
)
func showTimeNow(w http.ResponseWriter, r *http.Request) {
t := time.Now()
str := fmt.Sprintf(t.Format(time.RFC3339))
w.Write([]byte(str))
return
}
func main() {
http.HandleFunc("/time/now/", showTimeNow)
http.ListenAndServe(":8080" ,nil)
}
这个示例的作用非常的清晰:主函数内我们首先使用http.HandleFunc
方法来建立处理函数和路由的映射关系,然后使用http.ListenAndServe
方法来进行对于某个端口的监听。
在我们运行了这个简单的示例之后,就可以在本机的localhost:8080/time/now/建立起一个API,可以使用浏览器直接打开,你会看到格式化输出的当前的时间。
使用net/http撰写简单的爬虫
众所周知,我们所有能使用浏览器打开的网页,或者说能够在互联网上获取到的资源,全部都可以使用脚本模仿相同的方式去访问的到。而这种使用脚本获取互联网上资源的方式的程序,叫做爬虫程序。
爬虫这个名字大家都是耳熟能详的,这个小节就给大家揭开一点点爬虫的面纱。
一般简单的爬虫的目的就是模拟人的行为,从而获取到自己想要的数据。有点像那个电脑上的按键精灵的软件,由于程序处理数据的能力远远大于我们人类的肉眼和大脑,所以使用计算机程序来进行对于从互联网山获取的海量的数据中来挑取对于自己有用的信息。
以获取一个网页上的信息为例,这个最简单的爬虫的流程如下:
- 获取整个页面
- 从页面中筛选自己想要的信息
至于如何循环自动获取不同的页面,怎么去筛选自己想要的信息,那是需要另写一个爬虫教程的内容了,我们这里就只放一个最简单的,获取我们刚刚撰写的接口内容并且打印的程序。
package main
import (
"fmt"
"io/ioutil"
"net/http"
)
func main() {
resp, err := http.Get("http://127.0.0.1:8080/time/now/")
if err != nil {
fmt.Println("Get content failed.")
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
fmt.Println(string(body))
}
这个示例非常的简单:首先使用Get方法从我们本地的接口获取一个响应,然后对于响应的Body进行读取,由于网络中数据的传输是通过字节流的形式进行的,因此都会被转换为Reader和Writer来进行读取和写入,因此需要Close。
拿到内容之后我们打印出来就可以了,我的命令行会打印出:2019-12-12T00:27:27+08:00
。
9.3 net/http中的重要类型与接口
作为一个集成了HTTP中内容的包,net/http
包中包含了大量的类型和接口,以便向用户提供更友好的方法。
在net/http官方文档中可以看到全部的类型的定义,以及所有的方法以及作用,本小节主要介绍以下几个类型:
- Client
- Cookie
- Header
- Request
- Response
- Client
Client类型的实例是一个HTTP协议的客户端,它的默认值(零值)是使用DefaultTransport的可用客户端。
客户端一般会在内部维护一个传输的状态,比如带有缓存的TCP链接,因此在必要时,应该重新使用客户端,而不是重复的创建客户端。一个Client可以被多个goroutine安全的并发使用。
Client比起RoundTripper更加的高级,还附加了对于HTTP中详细信息的处理机制,比如Cookie和重定向。
在重定向请求时,Client将转发在初始请求中设置的全部请求头,但是以下情况不会:
- 当带有敏感信息的头部被转发到不被信任的目标时:比如"Authorization", "WWW-Authenticate"和Cookie。当重定向到不匹配的子域或者不是初始域时,以上头部会被忽略。比如,请求从"foo.com"重定向到"foo.com"或"sub.foo.com"时,带有敏感信息的头部会被一起转发,但是转发到"bar.com"时是不会的。
- 当使用内部CookieJar为nil的Client转发Cookie时,由于每一次的重定向都可能改变CookieJar的状态,因此一次重定向可能会改变初始请求中的CookieJar。所以当转发Cookie头部时,所有的突变都会被无视,面对这种情况CookieJar会把这些突变的值更新到Jar中。如果CookieJar为nil,则原封不动的转发初始请求的Cookie。
Client类型的实例拥有六个方法:CloseIdleConnections
, Do
, Get
, Head
, Post
, PostForm
. 可以很明显的看出Get
, Head
, Post
, PostForm
,都是一类方法:它们代表具体的请求方法,而PostForm
则是以特定的方式:表单,去提交数据。
而在源码中,以上这些特殊方法:PostForm
是调用Post
方法来实现的。而Get
, Head
, Post
都是通过调用Do
方法来实现的,比如让我们看一下Get
方法的源码:
func (c *Client) Get(url string) (resp *Response, err error) {
req, err := NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
return c.Do(req)
}
在该方法中,首先根据传入的参数(url)构建了一个GET方法的请求(请求类型Request),然后紧接着就调用了我们没有解释过的Do
方法。
关于Do
Do
方法的意义非常的“字面化”,就是客户端去执行(Do)一个特定的请求,然后返回该请求的响应。net/http封装了特定的请求方法,但是实际上这些方法都是依托Do
方法来实现的。因此所有人在需要时都可以自己封装类似于Delete
, Option
之类的方法,或者PostJSON
这种方法。或者直接手动构建一个请求然后去调用Do
方法,这样更加直截了当。
最后一个没有提到的方法是CloseIdleConnections
,字面意思是关闭所有的空闲连接,实际上也确实如此。
- Cookie
HTTP Cookie是服务器发送到用户浏览器并且保存在本地的一小部分数据,它会在浏览器下次向同一个服务器再次发起请求时被一起携带。通常来讲,它用于告知服务端两个请求是否来自同一个浏览器(用户),比如保持用户的登录状态,Cookie使得基于无状态的HTTP协议记录稳定的状态信息成为了可能。(摘自Mozilla-Cookie)
而http包中的Cookie类型,代表从HTTP响应中Set-Cookie
头部接受,或者在请求的Cookie头部中发送的信息。
Cookie类型是内容向类型,内部结构:
type Cookie struct {
Name string
Value string
Path string // optional
Domain string // optional
Expires time.Time // optional
RawExpires string // for reading cookies only
// MaxAge=0 means no 'Max-Age' attribute specified.
// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'
// MaxAge>0 means Max-Age attribute present and given in seconds
MaxAge int
Secure bool
HttpOnly bool
SameSite SameSite // Go 1.11
Raw string
Unparsed []string // Raw text of unparsed attribute-value pairs
}
其中的字段都和RFC-6265中描述的一致,作用可以自己去查看。
Cookie类型的实例拥有一个名为String
的方法,用于返回一个Cookie字符串。
- Header
Header代表HTTP中的头部信息,以键值对的方式来进行存储。
type Header map[string][]string
很是简单,对不对?因为Header就是键值对,而一个键可以有多个值,因此对应的是一个string的切片。
Header类型的实例拥有Add
, Clone
, Del
, Get
, Set
, Write
, WriteSubset
。看上去就是基本的CURD的操作,事实上也确实如此。
Add
方法传入一个键值对,将其添加到Header中,并且是大小写不敏感的。
Clone
方法返回一个实例的副本,当实例为nil时返回nil。(这里需要注意,Header所有方法的接受者是值接受者,因此允许为nil进行方法调用)
Del
方法用于删除一个键值对,传入一个键。
Get
方法传入一个键的string,然后返回一个格式化后的字符串。
Set
方法也是传入键值对的方式,但是会把该键对应的目标值直接替换为传入的值,而不是追加。同样的,大小写不敏感。
Write
方法将Header的内容写进传入的Writer,写入的格式是有线的。
WriteSubset
方法除了传入一个Writer之外,还将传入一个Map,用于标志写入哪些Header中的键值对。当传入的Map为nil时全部写入,否则,将Map中值为true的键不进行写入。
- Request
请求和响应,是每个网络相关包的大头,内容过长警告⚠️。
一个Request类型的对象表示一个从服务端发向客户端的HTTP请求。对于客户端和服务端而言Request中某些字段的语义略有不同。
Request结构体的内部结构:(注释太多都删掉了,想看的可以去这里看)
type Request struct {
Method string
URL *url.URL
Proto string // "HTTP/1.0"
ProtoMajor int // 1
ProtoMinor int // 0
Header Header
Body io.ReadCloser
GetBody func() (io.ReadCloser, error) // Go 1.8
ContentLength int64
TransferEncoding []string
Close bool
Host string
Form url.Values
PostForm url.Values // Go 1.1
MultipartForm *multipart.Form
Trailer Header
RemoteAddr string
RequestURI string
TLS *tls.ConnectionState
Cancel <-chan struct{} // Go 1.5
Response *Response // Go 1.7
}
接下来将一一解释其中的意义:
Method
字段表示一个确切的HTTP请求方法,不支持CONNECT方法。当字段为空串时,为GET方法。
URL
字段为*url.URL
类型,表示了一个确切的要被请求的URI(对于服务器的请求)或URL(对于客户端的请求),对于大多数的请求而言,除了Path和Query之外的字段都是空。
附:url.URL(真的越写越多,越写越长)
type URL struct {
Scheme string
Opaque string // encoded opaque data
User *Userinfo // username and password information
Host string // host or host:port
Path string // path (relative paths may omit leading slash)
RawPath string // encoded path hint (see EscapedPath method); added in Go 1.5
ForceQuery bool // append a query ('?') even if RawQuery is empty; added in Go 1.7
RawQuery string // encoded query values, without '?'
Fragment string // fragment for references, without '#'
}
Proto
字段标识使用的HTTP协议。默认是HTTP/1.0
ProtoMajor
字段标识HTTP协议版本号的整数部分,默认为1.
ProtoMinor
字段标识HTTP协议版本号的小数部分,默认为0.
Header
字段存储着请求的头部信息,之前介绍过了。
Body
字段为io.ReadCloser
类型,内容就是请求的主体(Body)信息,对于客户端的请求,本字段为nil表示请求没有主体内容,比如一个GET请求。而对于服务端的请求而言,本字段将不会为nil,但是主体不为空的时候,将直接返回一个EOF,然后服务端就会立即关闭这个Reader。
GetBody
字段为一个函数,直接返回一个io.ReadCloser
和error
。本字段定义了一个可选的能够返回Body的副本的函数。通常用于一个需要多次读取Body的重定向请求。在调用这个函数之前要确保Body的值被设置了。而对于服务端的请求而言,这个字段是没有意义的。
ContentLength
字段为一个int64
类型的整数,用于记录携带内容的长度,如果是-1则代表长度是未知的,大于零则代表可能从Body中读取的字节序列的长度。对于客户端的请求,本字段为0且Body为nil讲同等为长度未知对待。
TransferEncoding
字段为一个字符串切片类型,列出了一个请求从最外层到最内层的编码序列,空列表表示“Identity”编码。本字段通常可以被忽略,分块编码会在发送和接受时视需求而自动增删。
Close
字段为bool
类型,对于服务器请求而言,表示是否在响应一个请求之后关闭连接,而对于客户端请求而言,表示是否在发送这个请求并接收到响应之后关闭连接。对于服务器端的请求,HTTP服务器会自动处理,因此不需要特殊处理。而对于客户端请求而言,设置本字段会在发起每个请求时创建连接,就设置了DisableKeepAlives
一样。
Host
字段为字符串类型,对于服务器请求而言,为其请求URl中的Host部分。对于国际域名,本字段可能为Punycode
或者Unicode
结构,可以使用golang.org/x/net/idna来自由进行转换。为了遏制DNS的重绑定攻击,服务器上的处理函数应该验证本字段是否是被认证的的。
Form
字段为url.Values类型,包含了解析后的表单数据,既有URl
中的Query
参数,也有PATCH
,POST
和PUT
方法提交的表单数据。本字段只有在调用了ParseForm()
方法之后才是可用的。HTTP客户端无视表单,转而使用Body
传输数据。
PostForm
字段也为url.Values类型,包含的是单纯的由PATCH
,POST
和PUT
方法提交的表单数据。也是要先行调用ParseForm()
方法。
MultipartForm
字段为*multipart.Form类型,其内容是Multipart的表单数据,比如说上传的文件。同样要在调用ParseMultipartForm()
方法之后才可用。
Trailer
字段是Header
类型的,对于服务端请求,本字段将仅仅包含在Trailer
头部中的键,值全部为nil
。当处理程序从Body
中读取数据时,必须不能引用本字段,只有在Body
返回EOF
时,本字段才能再次被读取,并且会包含有非空数据(如果客户端发送了的话)。对于客户端请求而言,本字段必须被初始化为接下来可能要发送的键的map
,值可以是nil
或者其他零值,在分块发送时ContentLength
字段必须为0
或者-1
。在请求发送之后,当请求发送的数据被读取时,本字段的值可以被更新。一旦在请求的数据被读出EOF
后,本字段不可被更改。只有极少的HTTP客户端,服务器和代理支持本功能。
RemoteAddr
字段为字符串类型,本字段允许HTTP服务器或者其他软件记录下发送请求的机器的网络地址,通常用于输出日志。本字段不会被ReadRequest()
方法自动填充,并且没有固定的格式。HTTP客户端可以忽略本字段。
RequestURI
字段为字符串类型,本字段是从客户端到服务器,未经更改的原请求目标URI
,通常使用URL
。
TLS
字段为*tls.ConnectionState类型,本字段允许HTTP服务器或者其他的软件记录接收到请求的TLS连接的信息,同样,本字段不会因为调用ReadRequest()
方法而变得可用。本包中的HTTP服务器在调用处理程序之前会设置TLS-enabled
字段,否则字段将为零值。HTTP客户端会忽略本字段。
Cancel
字段为<-chan struct{}
类型,用于取消客户端发起的请求,对于服务端请求则不适用。本字段是可选的,并不是所有的RoundTripper
都支持取消请求。
Response
字段为*Response
类型,是导致本请求创建的重定向响应,本字段仅在客户端重定向期间会被使用。
- Response
一个Response类型的对象代表对于一个HTTP请求的响应。当HTTP客户端和Transport接收到了来自服务器的响应头时,会立即返回Response的实例。在读取Response的Body字段时,将以按需的方式从字节流中进行读取。
Response的内部构成:
type Response struct {
Status string // e.g. "200 OK"
StatusCode int // e.g. 200
Proto string // e.g. "HTTP/1.0"
ProtoMajor int // e.g. 1
ProtoMinor int // e.g. 0
Header Header
Body io.ReadCloser
ContentLength int64
TransferEncoding []string
Close bool
Uncompressed bool // Go 1.7
Trailer Header
Request *Request
TLS *tls.ConnectionState // Go 1.3
}
Status
字段为字符串类型,和注释中写的一样,是由状态码和描述一起拼接而成的。
StatusCode
字段为int
类型,用于表示响应的状态码,比如2xx, 3xx, 4xx, 5xx等等,至于具体的状态码是什么意思,自己去查。
Proto
字段为字符串类型,用于标识HTTP协议。
ProtoMajor
字段为int
类型,代表当前的HTTP协议的版本号的整数部分。
ProtoMinor
字段为int
类型,代表当前HTTP协议的版本号的小数部分。
Header
字段为Header
类型,代表响应头。
Body
字段为io.ReadCloser
类型,代表响应的正文内容。本字段是以按需的方式读取,以字节流的方式进行传输的。如果网络连接失败或者服务器终止响应,则读取本字段时将会抛出异常。HTTP客户端和Transport保证了即便响应没有包含正文,或者正文长度为0时,本字段也不为nil
。调用者一定要关闭对于正文的读取,否则之后的HTTP客户端不会重用以前的TCP连接。
ContentLength
字段为int64
类型,记录了相关联内容的长度,如果值为-1则代表长度是未知的。除非请求的方法是HEAD,否则本字段代表能从Body
字段中读出的字节数。
TransferEncoding
字段为字符串切片类型,包含从最外部到最内部的传输编码,如果值为nil
,则表示使用Identity
编码。
Close
字段为布尔类型,本字段记录了在读取正文之后,是否关闭连接。
Uncompressed
字段为布尔类型,本字段标识响应是否被压缩发送但是又被http
包解压缩了。如果本字段为真,则从Body
中读取解压后的内容而非服务端实际发送的被压缩的内容,ContentLength
字段的值被设置为-1,并且Content-Length
和Content-Encoding
两个字段将在响应头中被删除。
Trailer
字段是Header类型的,本字段将trailer中的键值对映射为Header的格式。本字段初始化时仅包含nil
,所有的值都来源于服务端发送的Trailer
头部,这些被解析出的值不会被添加到响应头中。在读取响应正文时,本字段不允许被调用。只有当Body
返回io.EOF
后,本字段的所有值才就绪(可以被使用且是正确的)。
Request
字段为Request的指针类型,是本响应所对应的请求。
TLS
字段为*tls.ConnectionState类型,包含了TLS连接到具体接收响应的链路信息,对于没有加密的响应,本字段的值应该为nil
。