golang net/http 用法

ohgenlong · · 2474 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

1. 前言

http包包含http客户端和服务端的实现,利用Get,Head,Post,以及PostForm实现HTTP或者HTTPS的请求.

2. 本文分析内容安排

  • 函数

  • 结构

3. 函数

3.1 服务端函数

func Handle(pattern string, handler Handler)将handler按照指定的格式注册到DefaultServeMux,ServeMux解释了模式匹配规则聽
func HandleFunc(pattern string, handler func(ResponseWriter, *Request))同上,主要用来实现动态文件内容的展示,这点与ServerFile()不同的地方。聽
func ListenAndServe(addr string, handler Handler) error监听TCP网络地址addr然后调用具有handler的Serve去处理连接请求.通常情况下Handler是nil,使用默认的DefaultServeMux聽
func ListenAndServeTLS(addr string, certFile string, keyFile string, handler Handler) error该函数与ListenAndServe功能基本相同,二者不同之处是该函数需要HTTPS连接.也就是说,必须给该服务Serve提供一个包含整数的秘钥的文件,如果证书是由证书机构签署的,那么证书文件必须是服务证书之后跟着CA证书.聽
func ServeFile(w ResponseWriter, r *Request, name string)利用指定的文件或者目录的内容来响应相应的请求.聽
func SetCookie(w ResponseWriter, cookie *Cookie)给w设定cookie聽
func StatusText(code int) string对于http状态码返回文本表示,如果这个code未知,则返回空的字符串聽
func MaxBytesReader(w ResponseWriter, r io.ReadCloser, n int64) io.ReadCloser该函数类似于io.LimitReader但是该函数是用来限制请求体的大小.与io.LimitReader不同的是,该函数返回一个ReaderCloser,当读超过限制时,返回一个non-EOF,并且当Close方法调用时,关闭底层的reader.该函数组织客户端恶意发送大量请求,浪费服务器资源.聽
func ParseHTTPVersion(vers string) (major, minor int, ok bool)解析http字符串版本进行解析,”HTTP/1.0” 返回 (1, 0, true)聽
func ProxyURL(fixedURL *url.URL) func(*Request) (*url.URL, error)返回一个用于传输的代理函数,该函数总是返回相同的URL聽
func Redirect(w ResponseWriter, r *Request, urlStr string, code int)返回一个重定向的url给指定的请求,这个重定向url可能是一个相对请求路径的一个相对路径.聽
func Serve(l net.Listener, handler Handler) error该函数接受listener l的传入http连接,对于每一个连接创建一个新的服务协程,这个服务协程读取请求然后调用handler来给他们响应.handler一般为nil,这样默认的DefaultServeMux被使用.

3.2 客户端函数

Client具有Do,Get,Head,Post以及PostForm等方法。 其中Do方法可以对Request进行一系列的设定,而其他的对request设定较少。如果Client使用默认的Client,则其中的Get,Head,Post以及PostForm方法相当于默认的http.Get,http.Post,http.Head以及http.PostForm函数。聽
func (c *Client) Do(req *Request) (resp *Response, err error)Do发送http请求并且返回一个http响应,遵守client的策略,如重定向,cookies以及auth等.错误经常是由于策略引起的,当err是nil时,resp总会包含一个非nil的resp.body.当调用者读完resp.body之后应该关闭它,如果resp.body没有关闭,则Client底层RoundTripper将无法重用存在的TCP连接去服务接下来的请求,如果resp.body非nil,则必须对其进行关闭.通常来说,经常使用Get,Post,或者PostForm来替代Do.聽
func (c *Client) Get(url string) (resp *Response, err error)利用get方法请求指定的url.Get请求指定的页面信息,并返回实体主体。聽
func (c *Client) Head(url string) (resp *Response, err error)利用head方法请求指定的url,Head只返回页面的首部。聽
func (c *Client) Post(url string, bodyType string, body io.Reader) (resp *Response, err error)利用post方法请求指定的URl,如果body也是一个io.Closer,则在请求之后关闭它聽
func (c *Client) PostForm(url string, data url.Values) (resp *Response, err error)利用post方法请求指定的url,利用data的key和value作为请求体.聽
Do方法可以灵活的对request进行配置,然后进行请求。利用http.Client以及http.NewRequest来模拟请求。模拟request中带有cookie的请求。示例如下:

package聽mainimport聽(聽聽聽聽//聽"encoding/json"
聽聽聽聽"fmt"
聽聽聽聽"io/ioutil"
聽聽聽聽"net/http"
聽聽聽聽"strconv")func聽main()聽{
聽聽聽聽client聽:=聽&http.Client{}
聽聽聽聽request,聽err聽:=聽http.NewRequest("GET",聽"http://www.baidu.com",聽nil)聽聽聽聽if聽err聽!=聽nil聽{
聽聽聽聽聽聽聽聽fmt.Println(err)
聽聽聽聽}
聽聽聽聽cookie聽:=聽&http.Cookie{Name:聽"userId",聽Value:聽strconv.Itoa(12345)}
聽聽聽聽request.AddCookie(cookie)聽//request中添加cookie
聽聽聽聽//设置request的header
聽聽聽聽request.Header.Set("Accept",聽"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
聽聽聽聽request.Header.Set("Accept-Charset",聽"GBK,utf-8;q=0.7,*;q=0.3")
聽聽聽聽request.Header.Set("Accept-Encoding",聽"gzip,deflate,sdch")
聽聽聽聽request.Header.Set("Accept-Language",聽"zh-CN,zh;q=0.8")
聽聽聽聽request.Header.Set("Cache-Control",聽"max-age=0")
聽聽聽聽request.Header.Set("Connection",聽"keep-alive")
聽聽聽聽response,聽err聽:=聽client.Do(request)聽聽聽聽if聽err聽!=聽nil聽{
聽聽聽聽聽聽聽聽fmt.Println(err)聽聽聽聽聽聽聽聽return
聽聽聽聽}聽聽聽聽defer聽response.Body.Close()
聽聽聽聽fmt.Println(response.StatusCode)聽聽聽聽if聽response.StatusCode聽==聽200聽{
聽聽聽聽聽聽聽聽r,聽err聽:=聽ioutil.ReadAll(response.Body)聽聽聽聽聽聽聽聽if聽err聽!=聽nil聽{
聽聽聽聽聽聽聽聽聽聽聽聽fmt.Println(err)
聽聽聽聽聽聽聽聽}
聽聽聽聽聽聽聽聽fmt.Println(string(r))
聽聽聽聽}
}

4. 结构

4.1 Client

type ClientClient是一个http客户端,默认客户端(DefaultClient)将使用默认的发送机制的客户端.Client的Transport字段一般会含有内部状态(缓存TCP连接),因此Client类型值应尽量被重用而不是创建新的。多个协程并发使用Clients是安全的.

type聽Client聽struct聽{//聽Transport指定执行独立、单次HTTP请求的机制如果Transport为nil,则使用DefaultTransport。
聽聽聽聽Transport聽RoundTripper//聽CheckRedirect指定处理重定向的策略,如果CheckRedirect非nil,client将会在调用重定向之前调用它。参数req和via是将要执行的请求和已经执行的请求(时间越久的请求优先执行),如果CheckRedirect返回一个错误,client的GetGet方法不会发送请求req,而是回之前得到的响应和该错误。如果CheckRedirect为nil,会采用默认策略:在连续10次请求后停止。
聽聽聽聽CheckRedirect聽func(req聽*Request,聽via聽[]*Request)聽error//聽Jar指定cookie管理器,如果Jar为nil,在请求中不会发送cookie,在回复中cookie也会被忽略。
聽聽聽聽Jar聽CookieJar//聽Timeout指定Client请求的时间限制,该超时限制包括连接时间、重定向和读取response聽body时间。计时器会在Head,Get,Post或Do方法返回后开始计时并在读到response.body后停止计时。Timeout为零值表示不设置超时。Client的Transport字段必须支持CancelRequest方法,否则Client会在尝试用Head,Get,Post或Do方法执行请求时返回错误。Client的Transport字段默认值(DefaultTransport)支持CancelRequest方法。
聽聽聽聽Timeout聽time.Duration
}

type ConnState表示客户端连接服务端的状态,其中ConnState常用状态变量如下:

const聽(聽聽聽聽//聽StateNew代表一个新的连接,将要立刻发送请求。
聽聽聽聽//聽连接从这个状态开始,然后转变为StateAlive或StateClosed。
聽聽聽聽StateNew聽ConnState聽=聽iota
聽聽聽聽//聽StateActive代表一个已经读取了请求数据1到多个字节的连接。
聽聽聽聽//聽用于StateAlive的Server.ConnState回调函数在将连接交付给处理器之前被触发,
聽聽聽聽//聽等到请求被处理完后,Server.ConnState回调函数再次被触发。
聽聽聽聽//聽在请求被处理后,连接状态改变为StateClosed、StateHijacked或StateIdle。
聽聽聽聽StateActive聽聽聽聽//聽StateIdle代表一个已经处理完了请求、处在闲置状态、等待新请求的连接。
聽聽聽聽//聽连接状态可以从StateIdle改变为StateActive或StateClosed。
聽聽聽聽StateIdle聽聽聽聽//聽代表一个被劫持的连接。这是一个终止状态,不会转变为StateClosed。
聽聽聽聽StateHijacked聽聽聽聽//聽StateClosed代表一个关闭的连接。
聽聽聽聽//聽这是一个终止状态。被劫持的连接不会转变为StateClosed。
聽聽聽聽StateClosed
)

type Cookie常用SetCooker用来给http的请求或者http的response设置cookie

type聽Cookie聽struct聽{

聽聽聽聽聽聽聽聽Name聽聽聽聽聽聽聽string聽聽//名字
聽聽聽聽聽聽聽聽Value聽聽聽聽聽聽string聽聽//值
聽聽聽聽聽聽聽聽Path聽聽聽聽聽聽聽string聽聽聽//路径
聽聽聽聽聽聽聽聽Domain聽聽聽聽聽string聽聽聽
聽聽聽聽聽聽聽聽Expires聽聽聽聽time.Time聽//过期时间
聽聽聽聽聽聽聽聽RawExpires聽string

聽聽聽聽聽聽聽聽//聽MaxAge=0聽意味着聽没有'Max-Age'属性指定.
聽聽聽聽聽聽聽聽//聽MaxAge<0聽意味着聽立即删除cookie
聽聽聽聽聽聽聽聽//聽MaxAge>0聽意味着设定了MaxAge属性,并且其单位是秒
聽聽聽聽聽聽聽聽MaxAge聽聽聽int
聽聽聽聽聽聽聽聽Secure聽聽聽bool
聽聽聽聽聽聽聽聽HttpOnly聽bool
聽聽聽聽聽聽聽聽Raw聽聽聽聽聽聽string
聽聽聽聽聽聽聽聽Unparsed聽[]string聽//聽未解析的属性值对}

func (c *Cookie) String() string该函数返回cookie的序列化结果。如果只设置了Name和Value字段,序列化结果可用于HTTP请求的Cookie头或者HTTP回复的Set-Cookie头;如果设置了其他字段,序列化结果只能用于HTTP回复的Set-Cookie头。聽
type CookieJar在http请求中,CookieJar管理存储和使用cookies.Cookiejar的实现必须被多协程并发使用时是安全的.

type聽CookieJar聽interface聽{聽聽聽聽聽聽聽聽//聽SetCookies聽处理从url接收到的cookie,是否存储这个cookies取决于jar的策略和实现
聽聽聽聽聽聽聽聽SetCookies(u聽*url.URL,聽cookies聽[]*Cookie)聽聽聽聽聽聽聽聽//聽Cookies聽返回发送到指定url的cookies
聽聽聽聽聽聽聽聽Cookies(u聽*url.URL)聽[]*Cookie
}

type Dir使用一个局限于指定目录树的本地文件系统实现一个文件系统.一个空目录被当做当前目录聽
type Dir string聽
func (d Dir) Open(name string) (File, error)type FileFile是通过FileSystem的Open方法返回的,并且能够被FileServer实现.该方法与*os.File行为表现一样

type聽File聽interface聽{
聽聽聽聽聽聽聽聽io.Closer
聽聽聽聽聽聽聽聽io.Reader
聽聽聽聽聽聽聽聽Readdir(count聽int)聽([]os.FileInfo,聽error)
聽聽聽聽聽聽聽聽Seek(offset聽int64,聽whence聽int)聽(int64,聽error)
聽聽聽聽聽聽聽聽Stat()聽(os.FileInfo,聽error)
}

type FileSystem实现了对一系列指定文件的访问,其中文件路径之间通过分隔符进行分割

type聽FileSystem聽interface聽{
聽聽聽聽聽聽聽聽Open(name聽string)聽(File,聽error)
}

type Flusherresponsewriters允许http控制器将缓存数据刷新入client.然而如果client是通过http代理连接服务器,这个缓存数据也可能是在整个response结束后才能到达客户端

type聽Flusher聽interface聽{聽聽聽聽聽聽聽聽//聽Flush将任何缓存数据发送到client
聽聽聽聽聽聽聽聽Flush()
}

4.2 Server

type Handler实现Handler接口的对象可以注册到HTTP服务端,为指定的路径或者子树提供服务。ServeHTTP应该将回复的header和数据写入ResponseWriter接口然后返回。返回意味着该请求已经结束,HTTP服务端可以转移向该连接上的下一个请求。如果ServeHTTP崩溃panic,那么ServeHTTP的调用者假定这个panic的影响与活动请求是隔离的,二者互不影响.调用者恢复panic,将stack trace记录到错误日志中,然后挂起这个连接.

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

func FileServer(root FileSystem) HandlerFileServer返回一个使用FileSystem接口提供文件访问服务的HTTP处理器。可以使用httpDir来使用操作系统的FileSystem接口实现。其主要用来实现静态文件的展示。聽
func NotFoundHandler() Handler返回一个简单的请求处理器,该处理器对任何请求都会返回”404 page not found”聽
func RedirectHandler(url string, code int) Handler使用给定的状态码将它接受到的任何请求都重定向到给定的url聽
func StripPrefix(prefix string, h Handler) Handler将请求url.path中移出指定的前缀,然后将省下的请求交给handler h来处理,对于那些不是以指定前缀开始的路径请求,该函数返回一个http 404 not found 的错误.聽
func TimeoutHandler(h Handler, dt time.Duration, msg string) Handler具有超时限制的handler,该函数返回的新Handler调用h中的ServerHTTP来处理每次请求,但是如果一次调用超出时间限制,那么就会返回给请求者一个503服务请求不可达的消息,并且在ResponseWriter返回超时错误.聽
其中FileServer经常和StripPrefix一起连用,用来实现静态文件展示,举例如下:

package聽mainimport聽(聽聽聽聽"fmt"
聽聽聽聽"net/http")func聽main()聽{
聽聽聽聽http.Handle("/test/",聽http.FileServer(http.Dir("/home/work/")))聽///home/work/test/中必须有内容
聽聽聽聽http.Handle("/download/",聽http.StripPrefix("/download/",聽http.FileServer(http.Dir("/home/work/"))))
聽聽聽聽http.Handle("/tmpfiles/",聽http.StripPrefix("/tmpfiles/",聽http.FileServer(http.Dir("/tmp"))))聽//127.0.0.1:9999/tmpfiles/访问的本地文件/tmp中的内容
聽聽聽聽http.ListenAndServe(":9999",聽nil)
}

type HandlerFuncHandlerFunc type是一个适配器,通过类型转换我们可以将普通的函数作为HTTP处理器使用。如果f是一个具有适当签名的函数,HandlerFunc(f)通过调用f实现了Handler接口。

type聽Hijacker聽interface聽{聽聽聽聽聽聽聽聽//聽Hijack让调用者接管连接,在调用Hijack()后,http聽server库将不再对该连接进行处理,对于该连接的管理和关闭责任将由调用者接管.
聽聽聽聽聽聽聽聽Hijack()聽(net.Conn,聽*bufio.ReadWriter,聽error)聽//conn表示连接对象,bufrw代表该连接的读写缓存对象。}

Hijacker用法如下所示:

package聽mainimport聽(聽聽聽聽"fmt"
聽聽聽聽"net/http")func聽HiJack(w聽http.ResponseWriter,聽r聽*http.Request)聽{
聽聽聽聽hj,聽ok聽:=聽w.(http.Hijacker)聽聽聽聽if聽!ok聽{
聽聽聽聽聽聽聽聽http.Error(w,聽"webserver聽doesn't聽support聽hijacking",聽http.StatusInternalServerError)聽聽聽聽聽聽聽聽return
聽聽聽聽}
聽聽聽聽conn,聽bufrw,聽err聽:=聽hj.Hijack()聽聽聽聽if聽err聽!=聽nil聽{
聽聽聽聽聽聽聽聽http.Error(w,聽err.Error(),聽http.StatusInternalServerError)聽聽聽聽聽聽聽聽return
聽聽聽聽}聽聽聽聽defer聽conn.Close()
聽聽聽聽bufrw.WriteString("Now聽we're聽speaking聽raw聽TCP.聽Say聽hi:聽\n")
聽聽聽聽bufrw.Flush()

聽聽聽聽fmt.Fprintf(bufrw,聽"You聽said:聽%s聽Bye.\n",聽"Good")
聽聽聽聽bufrw.Flush()
}func聽main()聽{
聽聽聽聽http.HandleFunc("/hijack",聽HiJack)
聽聽聽聽err聽:=聽http.ListenAndServe(":9999",聽nil)聽聽聽聽if聽err聽!=聽nil聽{
聽聽聽聽聽聽聽聽fmt.Println(err)
聽聽聽聽}
}

type Request代表客户端给服务器端发送的一个请求.该字段在服务器端和客户端使用时区别很大

type聽Request聽struct聽{聽聽聽聽//聽Method指定HTTP方法(GET,POST,PUT等)默认使用GET方法。
聽聽聽聽Method聽string
聽聽聽聽//聽URL在服务端表示被请求的URI(uniform聽resource聽identifier,统一资源标识符),在客户端表示要访问的URL。
聽聽聽聽//聽在服务端,URL字段是解析请求行的URI(保存在RequestURI字段)得到的,对大多数请求来说,除了Path和RawQuery之外的字段都是空字符串。
聽聽聽聽//聽在客户端,URL的Host字段指定了要连接的服务器,而Request的Host字段指定要发送的HTTP请求的Host头的值。
聽聽聽聽URL聽*url.URL聽聽聽聽//聽接收到的请求的协议版本。client的Request总是使用HTTP/1.1
聽聽聽聽Proto聽聽聽聽聽聽string聽//聽"HTTP/1.0"
聽聽聽聽ProtoMajor聽int聽聽聽聽//聽1
聽聽聽聽ProtoMinor聽int聽聽聽聽//聽0
聽聽聽聽//聽Header字段用来表示HTTP请求的头域。如果header(多行键值对格式)为:
聽聽聽聽//聽聽聽聽accept-encoding:聽gzip,聽deflate
聽聽聽聽//聽聽聽聽Accept-Language:聽en-us
聽聽聽聽//聽聽聽聽Connection:聽keep-alive
聽聽聽聽//聽则:
聽聽聽聽//聽聽聽聽Header聽=聽map[string][]string{
聽聽聽聽//聽聽聽聽聽聽聽聽"Accept-Encoding":聽{"gzip,聽deflate"},
聽聽聽聽//聽聽聽聽聽聽聽聽"Accept-Language":聽{"en-us"},
聽聽聽聽//聽聽聽聽聽聽聽聽"Connection":聽{"keep-alive"},
聽聽聽聽//聽聽聽聽}
聽聽聽聽//聽HTTP规定header的键名(头名)是区分大小写的,请求的解析器通过规范化头域的键名来实现这点,即首字母大写,其他字母小写,通过"-"进行分割。
聽聽聽聽//聽在客户端的请求,可能会被自动添加或重写Header中的特定的头,参见Request.Write方法。
聽聽聽聽Header聽Header聽聽聽聽//聽Body是请求的主体.对于客户端请求来说,一个nil聽body意味着没有body,http聽Client的Transport字段负责调用Body的Close方法。
聽聽聽聽//聽在服务端,Body字段总是非nil的;但在没有主体时,读取Body会立刻返回EOF.Server会关闭请求主体,而ServeHTTP处理器不需要关闭Body字段。
聽聽聽聽Body聽io.ReadCloser聽聽聽聽//聽ContentLength记录相关内容的长度.如果为-1,表示长度未知,如果values>=0,表示可以从Body字段读取ContentLength字节数据。
聽聽聽聽//聽在客户端,如果Body非nil而该字段为0,表示不知道Body的长度。
聽聽聽聽ContentLength聽int64
聽聽聽聽//聽TransferEncoding按从最外到最里的顺序列出传输编码,空切片表示"identity"编码。
聽聽聽聽//聽本字段一般会被忽略。当发送或接受请求时,会自动添加或移除"chunked"传输编码。
聽聽聽聽TransferEncoding聽[]string
聽聽聽聽//聽Close在服务端指定是否在回复请求后关闭连接,在客户端指定是否在发送请求后关闭连接。
聽聽聽聽Close聽bool
聽聽聽聽//聽对于服务器端请求,Host指定URL指向的主机,可能的格式是host:port.对于客户请求,Host用来重写请求的Host头,如过该字段为"",Request.Write方法会使用URL.Host来进行赋值。
聽聽聽聽Host聽string
聽聽聽聽//聽Form是解析好的表单数据,包括URL字段的query参数和POST或PUT的表单数据.本字段只有在调用ParseForm后才有效。在客户端,会忽略请求中的本字段而使用Body替代。
聽聽聽聽Form聽url.Values聽聽聽聽//聽PostForm是解析好的POST或PUT的表单数据.本字段只有在调用ParseForm后才有效。在客户端,会忽略请求中的本字段而使用Body替代。
聽聽聽聽PostForm聽url.Values聽聽聽聽//聽MultipartForm是解析好的多部件表单,包括上传的文件.本字段只有在调用ParseMultipartForm后才有效。http客户端中会忽略MultipartForm并且使用Body替代
聽聽聽聽MultipartForm聽*multipart.Form聽聽聽聽//聽Trailer指定了在发送请求体之后额外的headers,在服务端,Trailer字段必须初始化为只有trailer键,所有键都对应nil值。
聽聽聽聽//聽(客户端会声明哪些trailer会发送)在处理器从Body读取时,不能使用本字段.在从Body的读取返回EOF后,Trailer字段会被更新完毕并包含非nil的值。
聽聽聽聽//聽(如果客户端发送了这些键值对),此时才可以访问本字段。
聽聽聽聽//聽在客户端,Trail必须初始化为一个包含将要发送的键值对的映射.(值可以是nil或其终值),ContentLength字段必须是0或-1,以启用"chunked"传输编码发送请求。
聽聽聽聽//聽在开始发送请求后,Trailer可以在读取请求主体期间被修改,一旦请求主体返回EOF,调用者就不可再修改Trailer。
聽聽聽聽//聽几乎没有HTTP客户端、服务端或代理支持HTTP聽trailer。
聽聽聽聽Trailer聽Header聽聽聽聽//聽RemoteAddr允许HTTP服务器和其他软件记录该请求的来源地址,该字段经常用于日志.本字段不是ReadRequest函数填写的,也没有定义格式。
聽聽聽聽//聽本包的HTTP服务器会在调用处理器之前设置RemoteAddr为"IP:port"格式的地址.客户端会忽略请求中的RemoteAddr字段。
聽聽聽聽RemoteAddr聽string
聽聽聽聽//聽RequestURI是客户端发送到服务端的请求中未修改的URI(参见RFC聽2616,Section聽5.1),如果在http请求中设置该字段便会报错.
聽聽聽聽RequestURI聽string
聽聽聽聽//聽TLS字段允许HTTP服务器和其他软件记录接收到该请求的TLS连接的信息,本字段不是ReadRequest函数填写的。
聽聽聽聽//聽对启用了TLS的连接,本包的HTTP服务器会在调用处理器之前设置TLS字段,否则将设TLS为nil。
聽聽聽聽//聽客户端会忽略请求中的TLS字段。
聽聽聽聽TLS聽*tls.ConnectionState
}

func NewRequest(method, urlStr string, body io.Reader) (*Request, error)利用指定的method,url以及可选的body返回一个新的请求.如果body参数实现了io.Closer接口,Request返回值的Body 字段会被设置为body,并会被Client类型的Do、Post和PostForm方法以及Transport.RoundTrip方法关闭。聽
func ReadRequest(b *bufio.Reader) (req *Request, err error)从b中读取和解析一个请求.聽
func (r *Request) AddCookie(c *Cookie)给request添加cookie,AddCookie向请求中添加一个cookie.按照RFC 6265 section 5.4的规则,AddCookie不会添加超过一个Cookie头字段.这表示所有的cookie都写在同一行,用分号分隔(cookie内部用逗号分隔属性)聽
func (r *Request) Cookie(name string) (*Cookie, error)返回request中指定名name的cookie,如果没有发现,返回ErrNoCookie聽
func (r *Request) Cookies() []*Cookie返回该请求的所有cookies聽
func (r *Request) SetBasicAuth(username, password string)利用提供的用户名和密码给http基本权限提供具有一定权限的header。当使用http基本授权时,用户名和密码是不加密的聽
func (r *Request) UserAgent() string如果在request中发送,该函数返回客户端的user-Agent

package聽mainimport聽(聽聽聽聽"fmt"
聽聽聽聽"io/ioutil"
聽聽聽聽"net/http")func聽Test()聽{
聽聽聽聽req,聽err聽:=聽http.NewRequest("GET",聽"http://www.baidu.com",聽nil)聽聽聽聽if聽err聽!=聽nil聽{
聽聽聽聽聽聽聽聽fmt.Println(err)聽聽聽聽聽聽聽聽return
聽聽聽聽}
聽聽聽聽req.SetBasicAuth("test",聽"123456")
聽聽聽聽fmt.Println(req.Proto)
聽聽聽聽cookie聽:=聽&http.Cookie{
聽聽聽聽聽聽聽聽Name:聽聽"test",
聽聽聽聽聽聽聽聽Value:聽"12",
聽聽聽聽}
聽聽聽聽req.AddCookie(cookie)聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽//添加cookie
聽聽聽聽fmt.Println(req.Cookie("test"))聽聽聽聽聽聽聽聽聽聽聽//获取cookie
聽聽聽聽fmt.Println(req.Cookies())聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽聽//获取cookies
聽聽聽聽req.Header.Set("User-Agent",聽"useragent")聽//设定ua
聽聽聽聽fmt.Println(req.UserAgent())
聽聽聽聽client聽:=聽&http.Client{}
聽聽聽聽resp,聽err聽:=聽client.Do(req)聽聽聽聽if聽err聽!=聽nil聽{
聽聽聽聽聽聽聽聽fmt.Println(err)聽聽聽聽聽聽聽聽return
聽聽聽聽}聽聽聽聽defer聽resp.Body.Close()聽聽聽聽if聽resp.StatusCode聽==聽200聽{
聽聽聽聽聽聽聽聽content,聽_聽:=聽ioutil.ReadAll(resp.Body)
聽聽聽聽聽聽聽聽fmt.Println(string(content))
聽聽聽聽}
}func聽main()聽{
聽聽聽聽Test()
}

func (r *Request) FormFile(key string) (multipart.File, *multipart.FileHeader, error)对于指定格式的key,FormFile返回符合条件的第一个文件,如果有必要的话,该函数会调用ParseMultipartForm和ParseForm。聽
func (r *Request) FormValue(key string) string返回key获取的队列中第一个值。在查询过程中post和put中的主题参数优先级高于url中的value。为了访问相同key的多个值,调用ParseForm然后直接检查RequestForm。聽
func (r *Request) MultipartReader() (*multipart.Reader, error)如果这是一个有多部分组成的post请求,该函数将会返回一个MIME 多部分reader,否则的话将会返回一个nil和error。使用本函数代替ParseMultipartForm可以将请求body当做流stream来处理。聽
func (r *Request) ParseForm() error解析URL中的查询字符串,并将解析结果更新到r.Form字段。对于POST或PUT请求,ParseForm还会将body当作表单解析,并将结果既更新到r.PostForm也更新到r.Form。解析结果中,POST或PUT请求主体要优先于URL查询字符串(同名变量,主体的值在查询字符串的值前面)。如果请求的主体的大小没有被MaxBytesReader函数设定限制,其大小默认限制为开头10MB。ParseMultipartForm会自动调用ParseForm。重复调用本方法是无意义的。聽
func (r *Request) ParseMultipartForm(maxMemory int64) errorParseMultipartForm将请求的主体作为multipart/form-data解析。请求的整个主体都会被解析,得到的文件记录最多 maxMemery字节保存在内存,其余部分保存在硬盘的temp文件里。如果必要,ParseMultipartForm会自行调用 ParseForm。重复调用本方法是无意义的。聽
func (r *Request) PostFormValue(key string) string返回post或者put请求body指定元素的第一个值,其中url中的参数被忽略。聽
func (r *Request) ProtoAtLeast(major, minor int) bool检测在request中使用的http协议是否至少是major.minor聽
func (r *Request) Referer() string如果request中有refer,那么refer返回相应的url。Referer在request中是拼错的,这个错误从http初期就已经存在了。该值也可以从Headermap中利用Header[“Referer”]获取;在使用过程中利用Referer这个方法而不是map的形式的好处是在编译过程中可以检查方法的错误,而无法检查map中key的错误。聽
func (r *Request) Write(w io.Writer) errorWrite方法以有线格式将HTTP/1.1请求写入w(用于将请求写入下层TCPConn等)。本方法会考虑请求的如下字段:Host URL Method (defaults to “GET”) Header ContentLength TransferEncoding Body如果存在Body,ContentLength字段<= 0且TransferEncoding字段未显式设置为[“identity”],Write方法会显式添加”Transfer-Encoding: chunked”到请求的头域。Body字段会在发送完请求后关闭。聽
func (r *Request) WriteProxy(w io.Writer) error该函数与Write方法类似,但是该方法写的request是按照http代理的格式去写。尤其是,按照RFC 2616 Section 5.1.2,WriteProxy会使用绝对URI(包括协议和主机名)来初始化请求的第1行(Request-URI行)。无论何种情况,WriteProxy都会使用r.Host或r.URL.Host设置Host头。聽
type Response指对于一个http请求的响应response

type聽Response聽struct聽{
聽聽聽聽Status聽聽聽聽聽string聽//聽例如"200聽OK"
聽聽聽聽StatusCode聽int聽聽聽聽//聽例如200
聽聽聽聽Proto聽聽聽聽聽聽string聽//聽例如"HTTP/1.0"
聽聽聽聽ProtoMajor聽int聽聽聽聽//聽主协议号:例如1
聽聽聽聽ProtoMinor聽int聽聽聽聽//聽副协议号:例如0
聽聽聽聽//聽Header保管header的key聽values,如果response中有多个header中具有相同的key,那么Header中保存为该键对应用逗号分隔串联起来的这些头的值//聽被本结构体中的其他字段复制保管的头(如ContentLength)会从Header中删掉。Header中的键都是规范化的,参见CanonicalHeaderKey函数
聽聽聽聽Header聽Header聽聽聽聽//聽Body代表response的主体。http的client和Transport确保这个body永远非nil,即使response没有body或body长度为0。调用者也需要关闭这个body//聽如果服务端采用"chunked"传输编码发送的回复,Body字段会自动进行解码。
聽聽聽聽Body聽io.ReadCloser聽聽聽聽//聽ContentLength记录相关内容的长度。
聽聽聽聽//聽其值为-1表示长度未知(采用chunked传输编码)
聽聽聽聽//聽除非对应的Request.Method是"HEAD",其值>=0表示可以从Body读取的字节数
聽聽聽聽ContentLength聽int64
聽聽聽聽//聽TransferEncoding按从最外到最里的顺序列出传输编码,空切片表示"identity"编码。
聽聽聽聽TransferEncoding聽[]string
聽聽聽聽//聽Close记录头域是否指定应在读取完主体后关闭连接。(即Connection头)
聽聽聽聽//聽该值是给客户端的建议,Response.Write方法的ReadResponse函数都不会关闭连接。
聽聽聽聽Close聽bool
聽聽聽聽//聽Trailer字段保存和头域相同格式的trailer键值对,和Header字段相同类型
聽聽聽聽Trailer聽Header聽聽聽聽//聽Request是用来获取此回复的请求,Request的Body字段是nil(因为已经被用掉了)这个字段是被Client类型发出请求并获得回复后填充的
聽聽聽聽Request聽*Request聽聽聽聽//聽TLS包含接收到该回复的TLS连接的信息。聽对未加密的回复,本字段为nil。返回的指针是被(同一TLS连接接收到的)回复共享的,不应被修改。
聽聽聽聽TLS聽*tls.ConnectionState
}

func Get(url string) (resp *Response, err error)利用GET方法对一个指定的URL进行请求,如果response是如下重定向中的一个代码,则Get之后将会调用重定向内容,最多10次重定向。聽
301 (永久重定向,告诉客户端以后应该从新地址访问)聽
302 (暂时性重定向,作为HTTP1.0的标准,以前叫做Moved Temporarily,现在叫做Found。现在使用只是为了兼容性处理,包括PHP的默认Location重定向用到也是302),注:303和307其实是对302的细化。聽
303 (对于Post请求,它表示请求已经被处理,客户端可以接着使用GET方法去请求Location里的URl)聽
307 (临时重定向,对于Post请求,表示请求还没有被处理,客户端应该向Location里的URL重新发起Post请求)聽
如果有太多次重定向或者有一个http协议错误将会导致错误。当err为nil时,resp总是包含一个非nil的resp.body,Get是对DefaultClient.Get的一个包装。聽
func Head(url string) (resp *Response, err error)该函数功能见net中Head方法功能。该方法与默认的defaultClient中Head方法一致。聽
func Post(url string, bodyType string, body io.Reader) (resp *Response, err error)该方法与默认的defaultClient中Post方法一致。聽
func PostForm(url string, data url.Values) (resp *Response, err error)该方法与默认的defaultClient中PostForm方法一致。聽
func ReadResponse(r *bufio.Reader, req *Request) (*Response, error)ReadResponse从r读取并返回一个HTTP 回复。req参数是可选的,指定该回复对应的请求(即是对该请求的回复)。如果是nil,将假设请 求是GET请求。客户端必须在结束resp.Body的读取后关闭它。读取完毕并关闭后,客户端可以检查resp.Trailer字段获取回复的 trailer的键值对。(本函数主要用在客户端从下层获取回复)聽
func (r *Response) Cookies() []*Cookie解析cookie并返回在header中利用set-Cookie设定的cookie值。聽
func (r *Response) Location() (*url.URL, error)返回response中Location的header值的url。如果该值存在的话,则对于请求问题可以解决相对重定向的问题,如果该值为nil,则返回ErrNOLocation的错误。聽
func (r *Response) ProtoAtLeast(major, minor int) bool判定在response中使用的http协议是否至少是major.minor的形式。聽
func (r *Response) Write(w io.Writer) error将response中信息按照线性格式写入w中。聽
type ResponseWriter该接口被http handler用来构建一个http response

type聽ResponseWriter聽interface聽{聽聽聽聽//聽Header返回一个Header类型值,该值会被WriteHeader方法发送.在调用WriteHeader或Write方法后再改变header值是不起作用的。
聽聽聽聽Header()聽Header聽聽聽聽//聽WriteHeader该方法发送HTTP回复的头域和状态码。如果没有被显式调用,第一次调用Write时会触发隐式调用WriteHeader(http.StatusOK)
聽聽聽聽//聽因此,显示调用WriterHeader主要用于发送错误状态码。
聽聽聽聽WriteHeader(int)聽聽聽聽//聽Write向连接中写入数据,该数据作为HTTP聽response的一部分。如果被调用时还没有调用WriteHeader,本方法会先调用WriteHeader(http.StatusOK)
聽聽聽聽//聽如果Header中没有"Content-Type"键,本方法会使用包函数DetectContentType检查数据的前512字节,将返回值作为该键的值。
聽聽聽聽Write([]byte)聽(int,聽error)
}

type RoundTripper该函数是一个执行简单http事务的接口,该接口在被多协程并发使用时必须是安全的。

type聽RoundTripper聽interface聽{聽聽聽聽//聽RoundTrip执行单次HTTP事务,返回request的response,RoundTrip不应试图解析该回复。
聽聽聽聽//聽尤其要注意,只要RoundTrip获得了一个回复,不管该回复的HTTP状态码如何,它必须将返回值err设置为nil。
聽聽聽聽//聽非nil的返回值err应该留给获取回复失败的情况。类似的,RoundTrip不能试图管理高层协议,如重定向、认证或者cookie。
聽聽聽聽//聽RoundTrip除了从请求的主体读取并关闭主体之外,不能够对请求做任何修改,包括(请求的)错误。
聽聽聽聽//聽RoundTrip函数接收的请求的URL和Header字段必须保证是初始化了的。
聽聽聽聽RoundTrip(*Request)聽(*Response,聽error)
}

func NewFileTransport(fs FileSystem) RoundTripper该函数返回一个RoundTripper接口,服务指定的文件系统。 返回的RoundTripper接口会忽略接收的请求中的URL主机及其他绝大多数属性。该函数的典型应用是给Transport类型的值注册”file”协议。如下所示:

t聽:=聽&http.Transport{}
t.RegisterProtocol("file",聽http.NewFileTransport(http.Dir("/")))
c聽:=聽&http.Client{Transport:聽t}
res,聽err聽:=聽c.Get("file:///etc/passwd")

type ServeMux该函数是一个http请求多路复用器,它将每一个请求的URL和一个注册模式的列表进行匹配,然后调用和URL最匹配的模式的处理器进行后续操作。模式是固定的、由根开始的路径,如”/favicon.ico”,或由根开始的子树,如”/images/” (注意结尾的斜杠)。较长的模式优先于较短的模式,因此如果模式”/images/”和”/images/thumbnails/”都注册了处理器,后一 个处理器会用于路径以”/images/thumbnails/”开始的请求,前一个处理器会接收到其余的路径在”/images/”子树下的请求。聽
注意,因为以斜杠结尾的模式代表一个由根开始的子树,模式”/”会匹配所有的未被其他注册的模式匹配的路径,而不仅仅是路径”/”。聽
模式也能(可选地)以主机名开始,表示只匹配该主机上的路径。指定主机的模式优先于一般的模式,因此一个注册了两个模式”/codesearch”和”codesearch.google.com/”的处理器不会接管目标为”http://www.google.com/“的请求。聽
ServeMux还会负责对URL路径的过滤,将任何路径中包含”.”或”..”元素的请求重定向到等价的没有这两种元素的URL。(参见path.Clean函数)聽
func NewServeMux() *ServeMux初始化一个新的ServeMux聽
func (mux *ServeMux) Handle(pattern string, handler Handler)将handler注册为指定的模式,如果该模式已经有了handler,则会出错panic。聽
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request))将handler注册为指定的模式聽
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)根据指定的r.Method,r.Host以及r.RUL.Path返回一个用来处理给定请求的handler。该函数总是返回一个非nil的 handler,如果path不是一个规范格式,则handler会重定向到其规范path。Handler总是返回匹配该请求的的已注册模式;在内建重 定向处理器的情况下,pattern会在重定向后进行匹配。如果没有已注册模式可以应用于该请求,本方法将返回一个内建的”404 page not found”处理器和一个空字符串模式。聽
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)该函数用于将最接近请求url模式的handler分配给指定的请求。聽
举例说明servemux的用法:

package聽mainimport聽(聽聽聽聽"fmt"
聽聽聽聽"net/http")func聽Test(w聽http.ResponseWriter,聽r聽*http.Request)聽{
聽聽聽聽fmt.Fprintln(w,聽"just聽for聽test!")
}func聽main()聽{
聽聽聽聽mux聽:=聽http.NewServeMux()
聽聽聽聽mux.Handle("/",聽http.FileServer(http.Dir("/home")))
聽聽聽聽mux.HandleFunc("/test",聽Test)
聽聽聽聽err聽:=聽http.ListenAndServe(":9999",聽mux)聽聽聽聽if聽err聽!=聽nil聽{
聽聽聽聽聽聽聽聽fmt.Println(err)
聽聽聽聽}
}

type Server该结构体定义一些用来运行HTTP Server的参数,如果Server默认为0的话,那么这也是一个有效的配置。

type聽Server聽struct聽{
聽聽聽聽Addr聽聽聽聽聽聽聽聽聽聽聽string聽聽聽聽聽聽聽聽//聽监听的TCP地址,如果为空字符串会使用":http"
聽聽聽聽Handler聽聽聽聽聽聽聽聽Handler聽聽聽聽聽聽聽//聽调用的处理器,如为nil会调用http.DefaultServeMux
聽聽聽聽ReadTimeout聽聽聽聽time.Duration聽//聽请求的读取操作在超时前的最大持续时间
聽聽聽聽WriteTimeout聽聽聽time.Duration聽//聽回复的写入操作在超时前的最大持续时间
聽聽聽聽MaxHeaderBytes聽int聽聽聽聽聽聽聽聽聽聽聽//聽请求的头域最大长度,如为0则用DefaultMaxHeaderBytes
聽聽聽聽TLSConfig聽聽聽聽聽聽*tls.Config聽聽聽//聽可选的TLS配置,用于ListenAndServeTLS方法
聽聽聽聽//聽TLSNextProto(可选地)指定一个函数来在一个NPN型协议升级出现时接管TLS连接的所有权。
聽聽聽聽//聽映射的键为商谈的协议名;映射的值为函数,该函数的Handler参数应处理HTTP请求,
聽聽聽聽//聽并且初始化Handler.ServeHTTP的*Request参数的TLS和RemoteAddr字段(如果未设置)。
聽聽聽聽//聽连接在函数返回时会自动关闭。
聽聽聽聽TLSNextProto聽map[string]func(*Server,聽*tls.Conn,聽Handler)聽聽聽聽//聽ConnState字段指定一个可选的回调函数,该函数会在一个与客户端的连接改变状态时被调用。
聽聽聽聽//聽参见ConnState类型和相关常数获取细节。
聽聽聽聽ConnState聽func(net.Conn,聽ConnState)聽聽聽聽//聽ErrorLog指定一个可选的日志记录器,用于记录接收连接时的错误和处理器不正常的行为。
聽聽聽聽//聽如果本字段为nil,日志会通过log包的标准日志记录器写入os.Stderr。
聽聽聽聽ErrorLog聽*log.Logger聽聽聽聽//聽内含隐藏或非导出字段}

func (srv *Server) ListenAndServe() error监听TCP网络地址srv.Addr然后调用Serve来处理接下来连接的请求。如果srv.Addr是空的话,则使用“:http”。聽
func (srv *Server) ListenAndServeTLS(certFile, keyFile string) errorListenAndServeTLS监听srv.Addr确定的TCP地址,并且会调用Serve方法处理接收到的连接。必须提供证书文件和对应的私钥文 件。如果证书是由权威机构签发的,certFile参数必须是顺序串联的服务端证书和CA证书。如果srv.Addr为空字符串,会使 用”:https”。聽
func (srv *Server) Serve(l net.Listener) error接受Listener l的连接,创建一个新的服务协程。该服务协程读取请求然后调用srv.Handler来应答。实际上就是实现了对某个端口进行监听,然后创建相应的连接。聽
func (s *Server) SetKeepAlivesEnabled(v bool)该函数控制是否http的keep-alives能够使用,默认情况下,keep-alives总是可用的。只有资源非常紧张的环境或者服务端在关闭进程中时,才应该关闭该功能。聽
举例说明Server的用法:

package聽mainimport聽(聽聽聽聽"fmt"
聽聽聽聽"net/http")func聽Test(w聽http.ResponseWriter,聽r聽*http.Request)聽{
聽聽聽聽fmt.Fprintln(w,聽"just聽for聽test!")
}func聽main()聽{
聽聽聽聽newserver聽:=聽http.Server{
聽聽聽聽聽聽聽聽Addr:聽聽聽聽聽聽聽聽聽":9992",
聽聽聽聽聽聽聽聽ReadTimeout:聽聽0,
聽聽聽聽聽聽聽聽WriteTimeout:聽0,
聽聽聽聽}
聽聽聽聽mux聽:=聽http.NewServeMux()
聽聽聽聽mux.Handle("/",聽http.FileServer(http.Dir("/home")))
聽聽聽聽mux.HandleFunc("/test",聽Test)
聽聽聽聽newserver.Handler聽=聽mux
聽聽聽聽err聽:=聽newserver.ListenAndServe()聽聽聽聽if聽err聽!=聽nil聽{
聽聽聽聽聽聽聽聽fmt.Println(err)
聽聽聽聽}
聽聽聽聽fmt.Println(err)聽聽聽聽//聽err聽:=聽http.ListenAndServe(":9999",聽mux)
聽聽聽聽//聽if聽err聽!=聽nil聽{
聽聽聽聽//聽聽聽聽聽fmt.Println(err)
聽聽聽聽//聽}}

type Transport该结构体实现了RoundTripper接口,支持HTTP,HTTPS以及HTTP代理,TranSport也能缓存连接供将来使用。

type聽Transport聽struct聽{聽聽聽聽//聽Proxy指定一个对给定请求返回代理的函数。如果该函数返回了非nil的错误值,请求的执行就会中断并返回该错误。
聽聽聽聽//聽如果Proxy为nil或返回nil的*URL值,将不使用代理。
聽聽聽聽Proxy聽func(*Request)聽(*url.URL,聽error)聽聽聽聽//聽Dial指定创建未加密TCP连接的dial函数。如果Dial为nil,会使用net.Dial。
聽聽聽聽Dial聽func(network,聽addr聽string)聽(net.Conn,聽error)
  //聽DialTls利用一个可选的dial函数来为非代理的https请求创建一个TLS连接。如果DialTLS为nil的话,那么使用Dial和TLSClientConfig。  //如果DialTLS被设定,那么Dial钩子不被用于HTTPS请求和TLSClientConfig并且TLSHandshakeTimeout被忽略。返回的net.conn默认已经经过了TLS握手协议。  DialTLS聽func(network,聽addr聽string)聽(net.Conn,聽error)聽
聽聽聽聽//聽TLSClientConfig指定用于tls.Client的TLS配置信息。如果该字段为nil,会使用默认的配置信息。
聽聽聽聽TLSClientConfig聽*tls.Config聽聽聽聽//聽TLSHandshakeTimeout指定等待TLS握手完成的最长时间。零值表示不设置超时。
聽聽聽聽TLSHandshakeTimeout聽time.Duration聽聽聽聽//聽如果DisableKeepAlives为真,不同HTTP请求之间TCP连接的重用将被阻止。
聽聽聽聽DisableKeepAlives聽bool
聽聽聽聽//聽如果DisableCompression为真,会禁止Transport在请求中没有Accept-Encoding头时,
聽聽聽聽//聽主动添加"Accept-Encoding:聽gzip"头,以获取压缩数据。
聽聽聽聽//聽如果Transport自己请求gzip并得到了压缩后的回复,它会主动解压缩回复的主体。
聽聽聽聽//聽但如果用户显式的请求gzip压缩数据,Transport是不会主动解压缩的。
聽聽聽聽DisableCompression聽bool
聽聽聽聽//聽如果MaxIdleConnsPerHost不为0,会控制每个主机下的最大闲置连接数目。
聽聽聽聽//聽如果MaxIdleConnsPerHost为0,会使用DefaultMaxIdleConnsPerHost。
聽聽聽聽MaxIdleConnsPerHost聽int
聽聽聽聽//聽ResponseHeaderTimeout指定在发送完请求(包括其可能的主体)之后,
聽聽聽聽//聽等待接收服务端的回复的头域的最大时间。零值表示不设置超时。
聽聽聽聽//聽该时间不包括读取回复主体的时间。
聽聽聽聽ResponseHeaderTimeout聽time.Duration
}

func (t *Transport) CancelRequest(req *Request)通过关闭连接来取消传送中的请求。聽
func (t *Transport) CloseIdleConnections()关闭所有之前请求但目前处于空闲状态的连接。该方法并不中断任何正在使用的连接。聽
func (t *Transport) RegisterProtocol(scheme string, rt RoundTripper)RegisterProtocol注册一个新的名为scheme的协议。t会将使用scheme协议的请求转交给rt。rt有责任模拟HTTP请求的语义。RegisterProtocol可以被其他包用于提供”ftp”或”file”等协议的实现。聽
func (t *Transport) RoundTrip(req *Request) (resp *Response, err error)该函数实现了RoundTripper接口,对于高层http客户端支持,例如处理cookies以及重定向,查看Get,Post以及Client类型。

5. 总结

本文笔者在读docker和distribution交互pull数据的源码时,对其用到的源码不能完全理解,而搜到的一篇文章改版而来的,借鉴参考文献。1



有疑问加站长微信联系(非本文作者)

本文来自:51CTO博客

感谢作者:ohgenlong

查看原文:golang net/http 用法

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

2474 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传