golang之网络开发

1. TCP/UDP Server/Client开发

net包提供network I/O开发接口,包括TCP/IPUDPDNSUnix domain sockets

常用开发一般仅需要最基础接口或函数:

服务器:net.Listen()net.Accept()

ln, err := net.Listen("tcp", ":8080")
if err != nil {
    // handle error
}
for {
    conn, err := ln.Accept()
    if err != nil {
        // handle error
    }
    go handleConnection(conn)
}

客户端:net.Dial()

conn, err := net.Dial("tcp", "golang.org:80")
if err != nil {
    // handle error
}
fmt.Fprintf(conn, "GET / HTTP/1.0\r\n\r\n")
status, err := bufio.NewReader(conn).ReadString('\n')
// ...

相关API定义:

func Listen(network, address string) (Listener, error)
func (l *TCPListener) Accept() (Conn, error)
func Dial(network, address string) (Conn, error)

2. web开发

net/http包为web开发提供支持,能很简单地对Web的路由,静态文件,模版,cookie等数据进行设置和操作。

主要是两步:设置访问路由,设置监听端口(转发路由:根据用户请求调用不同路由函数)。

type HandlerFunc func(w http.ResponseWriter, r *http.Request)
func (f HandlerFunc) ServeHTTP(w http.ResponseWriter, r *http.Request) // ServeHTTP calls f(w, r).
func HandleFunc(pattern string, handle func(http.ResponseWriter, *http.Request))
type Handler interface {
  ServeHTTP(http.ResponseWriter, *http.Request)
}
func Handle(pattern string, handler http.Handler)
func ListenAndServe(addr string, handler http.Handler) error

ListenAndServe starts an HTTP server with a given address and handler. The handler is usually nil, which means to use DefaultServeMux. 若此时给定Handler,则采用给定的Handler(给定的handler需实现ServeHTTP()函数的接口)。

Handle and HandleFunc add handlers to DefaultServeMux.

DefaultServeMux路由规则:

  • URL分为两种,末尾为/:表示一个子树,后面可以跟其他子路经;末尾不是/,表示一个叶子,固定路径。以/结尾的URL可以匹配它的任何子路经,比如/images会匹配/images/cute-cat.
  • 它采用最长匹配原则,如果有多个匹配,一定采用匹配路径最长的那个进行处理。
  • 如果没有任何匹配项,返回404错误。

Handle()的参数为地址匹配字符串和handler接口,handler需要实现ServeHTTP()函数的接口。

HandleFunc()的参数是地址匹配字符串和函数handle,函数handle等同http.Handler接口要实现的函数。

http.Handle("/foo", fooHandler)

http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
http.Handle("/zoo", http.HandlerFunc(zoo)) // 直接将函数zoo赋值为http.HandlerFunc log.Fatal(http.ListenAndServe(
":8080", nil))
package main

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

type fooHandler struct{
}

func (f fooHandler) ServeHTTP(w http.ResponseWriter, r *http.Request){
    fmt.Fprintf(w, "Hello, first!\n")
}

func main(){
    http.Handle("/foo", fooHandler{})

    http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request){
        fmt.Fprintf(w, "Hello, %q\n", r.URL.Path)
    })

    log.Fatal(http.ListenAndServe(":8088", nil))
}

# go build -o first first.go
# ./first &
$ curl localhost:8088/foo
Hello, first!
$ curl localhost:8088/bar?house=wang
Hello, "/bar"
Handler示例

简单应用

Get, Head, Post and PostForm make HTTP or HTTPS requests:

resp, err := http.Get("http://example.com/")
...
resp, err := http.Post("http://example.com/upload", "image/jpeg", &buf)
...
resp, err := http.PostForm("http://example.com/form", url.Values{"key": {"Value"}, "id": {"123"}})
...
defer resp.Body.Close()  // when finished
body, err := ioutil.ReadAll(resp.Body)

复杂应用

client := &http.Client{
    CheckRedirect: redirectPolicyFunc,
}

resp, err := client.Get("http://example.com")
// ...

req, err := http.NewRequest("GET", "http://example.com", nil)
// ...
req.Header.Add("If-None-Match", `W/"wyzzy"`)
resp, err := client.Do(req)
// ...
1)通过创建Client控制HTTP客户端client headers, redirect policy and other settings
tr := &http.Transport{
    MaxIdleConns:       10,
    IdleConnTimeout:    30 * time.Second,
    DisableCompression: true,
}
client := &http.Client{Transport: tr}
resp, err := client.Get("https://example.com")
2)通过创建Transport控制客户端三层四层配置,如TLS,keepalives,compression等

注:Client和Transport可并发运行。

s := &http.Server{
    Addr:           ":8080",
    Handler:        myHandler,
    ReadTimeout:    10 * time.Second,
    WriteTimeout:   10 * time.Second,
    MaxHeaderBytes: 1 << 20,
}
log.Fatal(s.ListenAndServe())
3)通过创建Server控制服务器端配置

常用量和函数

const (
    MethodGet     = "GET"
    MethodHead    = "HEAD"
    MethodPost    = "POST"
    MethodPut     = "PUT"
    MethodPatch   = "PATCH" // RFC 5789
    MethodDelete  = "DELETE"
    MethodConnect = "CONNECT"
    MethodOptions = "OPTIONS"
    MethodTrace   = "TRACE"
)const DefaultMaxHeaderBytes = 1 << 20 // 1 MB
const DefaultMaxIdleConnsPerHost = 2
var DefaultServeMux = &defaultServeMux
var ErrHandlerTimeout = errors.New("http: Handler timeout")
const (
    StatusContinue           = 100 // RFC 7231, 6.2.1
    StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2
    StatusProcessing         = 102 // RFC 2518, 10.1
    StatusEarlyHints         = 103 // RFC 8297

    StatusOK                   = 200 // RFC 7231, 6.3.1
    StatusCreated              = 201 // RFC 7231, 6.3.2
    StatusAccepted             = 202 // RFC 7231, 6.3.3
    StatusNonAuthoritativeInfo = 203 // RFC 7231, 6.3.4
    StatusNoContent            = 204 // RFC 7231, 6.3.5
    StatusResetContent         = 205 // RFC 7231, 6.3.6
    StatusPartialContent       = 206 // RFC 7233, 4.1
    StatusMultiStatus          = 207 // RFC 4918, 11.1
    StatusAlreadyReported      = 208 // RFC 5842, 7.1
    StatusIMUsed               = 226 // RFC 3229, 10.4.1

    StatusMultipleChoices  = 300 // RFC 7231, 6.4.1
    StatusMovedPermanently = 301 // RFC 7231, 6.4.2
    StatusFound            = 302 // RFC 7231, 6.4.3
    StatusSeeOther         = 303 // RFC 7231, 6.4.4
    StatusNotModified      = 304 // RFC 7232, 4.1
    StatusUseProxy         = 305 // RFC 7231, 6.4.5

    StatusTemporaryRedirect = 307 // RFC 7231, 6.4.7
    StatusPermanentRedirect = 308 // RFC 7538, 3

    StatusBadRequest                   = 400 // RFC 7231, 6.5.1
    StatusUnauthorized                 = 401 // RFC 7235, 3.1
    StatusPaymentRequired              = 402 // RFC 7231, 6.5.2
    StatusForbidden                    = 403 // RFC 7231, 6.5.3
    StatusNotFound                     = 404 // RFC 7231, 6.5.4
    StatusMethodNotAllowed             = 405 // RFC 7231, 6.5.5
    StatusNotAcceptable                = 406 // RFC 7231, 6.5.6
    StatusProxyAuthRequired            = 407 // RFC 7235, 3.2
    StatusRequestTimeout               = 408 // RFC 7231, 6.5.7
    StatusConflict                     = 409 // RFC 7231, 6.5.8
    StatusGone                         = 410 // RFC 7231, 6.5.9
    StatusLengthRequired               = 411 // RFC 7231, 6.5.10
    StatusPreconditionFailed           = 412 // RFC 7232, 4.2
    StatusRequestEntityTooLarge        = 413 // RFC 7231, 6.5.11
    StatusRequestURITooLong            = 414 // RFC 7231, 6.5.12
    StatusUnsupportedMediaType         = 415 // RFC 7231, 6.5.13
    StatusRequestedRangeNotSatisfiable = 416 // RFC 7233, 4.4
    StatusExpectationFailed            = 417 // RFC 7231, 6.5.14
    StatusTeapot                       = 418 // RFC 7168, 2.3.3
    StatusMisdirectedRequest           = 421 // RFC 7540, 9.1.2
    StatusUnprocessableEntity          = 422 // RFC 4918, 11.2
    StatusLocked                       = 423 // RFC 4918, 11.3
    StatusFailedDependency             = 424 // RFC 4918, 11.4
    StatusTooEarly                     = 425 // RFC 8470, 5.2.
    StatusUpgradeRequired              = 426 // RFC 7231, 6.5.15
    StatusPreconditionRequired         = 428 // RFC 6585, 3
    StatusTooManyRequests              = 429 // RFC 6585, 4
    StatusRequestHeaderFieldsTooLarge  = 431 // RFC 6585, 5
    StatusUnavailableForLegalReasons   = 451 // RFC 7725, 3

    StatusInternalServerError           = 500 // RFC 7231, 6.6.1
    StatusNotImplemented                = 501 // RFC 7231, 6.6.2
    StatusBadGateway                    = 502 // RFC 7231, 6.6.3
    StatusServiceUnavailable            = 503 // RFC 7231, 6.6.4
    StatusGatewayTimeout                = 504 // RFC 7231, 6.6.5
    StatusHTTPVersionNotSupported       = 505 // RFC 7231, 6.6.6
    StatusVariantAlsoNegotiates         = 506 // RFC 2295, 8.1
    StatusInsufficientStorage           = 507 // RFC 4918, 11.5
    StatusLoopDetected                  = 508 // RFC 5842, 7.2
    StatusNotExtended                   = 510 // RFC 2774, 7
    StatusNetworkAuthenticationRequired = 511 // RFC 6585, 6
)
StatusOK = 200 // RFC 7231, 6.3.1
func Error(w ResponseWriter, error string, code int)
Error replies to the request with the specified error message and HTTP code. 
It does not otherwise end the request; the caller should ensure no further writes are done to w. 
The error message should be plain text.

func DetectContentType(data []byte) string
DetectContentType always returns a valid MIME type: 
if it cannot determine a more specific one, it returns "application/octet-stream".

func Handle(pattern string, handler Handler)
Handle registers the handler for the given pattern in the DefaultServeMux. 
The documentation for ServeMux explains how patterns are matched.

func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
HandleFunc registers the handler function for the given pattern in the DefaultServeMux. 
The documentation for ServeMux explains how patterns are matched.

func ListenAndServe(addr string, handler Handler) error
The handler is typically nil, in which case the DefaultServeMux is used.
ListenAndServe always returns a non-nil error.

func ListenAndServeTLS(addr, certFile, keyFile string, handler Handler) error

func NotFound(w ResponseWriter, r *Request)
NotFound replies to the request with an HTTP 404 not found error.
ParseHTTPVersion parses a HTTP version string. "HTTP/1.0" returns (1, 0, true).

func Redirect(w ResponseWriter, r *Request, url string, code int)
The provided code should be in the 3xx range and is usually StatusMovedPermanently, StatusFound or StatusSeeOther.
If the Content-Type header has not been set, Redirect sets it to "text/html; charset=utf-8" and writes a small HTML body. 
Setting the Content-Type header to any value, including nil, disables that behavior. func Serve(l net.Listener, handler Handler) error Serve accepts incoming HTTP connections on the listener l, creating a new service goroutine for each.
The service goroutines read requests and then call handler to reply to them. The handler
is typically nil, in which case the DefaultServeMux is used. Serve always returns a non-nil error. func ServeContent(w ResponseWriter, req *Request, name string, modtime time.Time, content io.ReadSeeker) ServeContent replies to the request using the content in the provided ReadSeeker.
The main benefit of ServeContent over io.Copy is that it handles Range requests properly, sets the MIME type, and handles If-Match,
If-Unmodified-Since, If-None-Match, If-Modified-Since, and If-Range requests. func ServeFile(w ResponseWriter, r *Request, name string) ServeFile replies to the request with the contents of the named file or directory. func StatusText(code int) string StatusText returns a text for the HTTP status code. It returns the empty string if the code is unknown.
func Error(w ResponseWriter, error string, code int)
Error replies to the request with the specified error message and HTTP code. 
It does not otherwise end the request; the caller should ensure no further writes are done to w. 
The error message should be plain text.

func DetectContentType(data []byte) string
DetectContentType always returns a valid MIME type: 
if it cannot determine a more specific one, it returns "application/octet-stream".
// ResponseWriter接口被HTTP处理器用于构造HTTP回复。
type ResponseWriter interface { // Header返回一个Header类型值,该值会被WriteHeader方法发送。 // 在调用WriteHeader或Write方法后再改变该对象是没有意义的。 Header() Header // WriteHeader该方法发送HTTP回复的头域和状态码。 // 如果没有被显式调用,第一次调用Write时会触发隐式调用WriteHeader(http.StatusOK) // WriterHeader的显式调用主要用于发送错误码。 WriteHeader(int) // Write向连接中写入作为HTTP的一部分回复的数据。 // 如果被调用时还未调用WriteHeader,本方法会先调用WriteHeader(http.StatusOK) // 如果Header中没有"Content-Type"键, // 本方法会使用包函数DetectContentType检查数据的前512字节,将返回值作为该键的值。 Write([]byte) (int, error) }
type Request struct {
    // Method指定HTTP方法(GET、POST、PUT等)。对客户端,""代表GET。
    Method string
    // URL在服务端表示被请求的URI,在客户端表示要访问的URL。
    //
    // 在服务端,URL字段是解析请求行的URI(保存在RequestURI字段)得到的,
    // 对大多数请求来说,除了Path和RawQuery之外的字段都是空字符串。
    // (参见RFC 2616, Section 5.1.2)
    //
    // 在客户端,URL的Host字段指定了要连接的服务器,
    // 而Request的Host字段(可选地)指定要发送的HTTP请求的Host头的值。
    URL *url.URL
    // 接收到的请求的协议版本。本包生产的Request总是使用HTTP/1.1
    Proto      string // "HTTP/1.0"
    ProtoMajor int    // 1
    ProtoMinor int    // 0
    // Header字段用来表示HTTP请求的头域。如果头域(多行键值对格式)为:
    //    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中的特定的头,参见Request.Write方法。
    Header Header
    // Body是请求的主体。
    //
    // 在客户端,如果Body是nil表示该请求没有主体买入GET请求。
    // Client的Transport字段会负责调用Body的Close方法。
    //
    // 在服务端,Body字段总是非nil的;但在没有主体时,读取Body会立刻返回EOF。
    // Server会关闭请求的主体,ServeHTTP处理器不需要关闭Body字段。
    Body io.ReadCloser
    // ContentLength记录相关内容的长度。
    // 如果为-1,表示长度未知,如果>=0,表示可以从Body字段读取ContentLength字节数据。
    // 在客户端,如果Body非nil而该字段为0,表示不知道Body的长度。
    ContentLength int64
    // TransferEncoding按从最外到最里的顺序列出传输编码,空切片表示"identity"编码。
    // 本字段一般会被忽略。当发送或接受请求时,会自动添加或移除"chunked"传输编码。
    TransferEncoding []string
    // Close在服务端指定是否在回复请求后关闭连接,在客户端指定是否在发送请求后关闭连接。
    Close bool
    // 在服务端,Host指定URL会在其上寻找资源的主机。
    // 根据RFC 2616,该值可以是Host头的值,或者URL自身提供的主机名。
    // Host的格式可以是"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后才有效。
    // 在客户端,会忽略请求中的本字段而使用Body替代。
    MultipartForm *multipart.Form
    // Trailer指定了会在请求主体之后发送的额外的头域。
    //
    // 在服务端,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)
    // 一般应使用URI字段,在客户端设置请求的本字段会导致错误。
    RequestURI string
    // TLS字段允许HTTP服务器和其他软件记录接收到该请求的TLS连接的信息
    // 本字段不是ReadRequest函数填写的。
    // 对启用了TLS的连接,本包的HTTP服务器会在调用处理器之前设置TLS字段,否则将设TLS为nil。
    // 客户端会忽略请求中的TLS字段。
    TLS *tls.ConnectionState
} 
type Response struct {
    Status     string // 例如"200 OK"
    StatusCode int    // 例如200
    Proto      string // 例如"HTTP/1.0"
    ProtoMajor int    // 例如1
    ProtoMinor int    // 例如0
    // Header保管头域的键值对。
    // 如果回复中有多个头的键相同,Header中保存为该键对应用逗号分隔串联起来的这些头的值
    // (参见RFC 2616 Section 4.2)
    // 被本结构体中的其他字段复制保管的头(如ContentLength)会从Header中删掉。
    //
    // Header中的键都是规范化的,参见CanonicalHeaderKey函数
    Header Header
    // Body代表回复的主体。
    // Client类型和Transport类型会保证Body字段总是非nil的,即使回复没有主体或主体长度为0。
    // 关闭主体是调用者的责任。
    // 如果服务端采用"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
}

Request Header

Header代表HTTP头域的键值对

type Header map[string][]string

func (h Header) Get(key string) 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
func (h Header) WriteSubset(w io.Writer, exclude map[string]bool) error

Set添加键值对到h,如键已存在则会用只有新值一个元素的切片取代旧值切片。

Add添加键值对到h,如键已存在则会将新的值附加到旧值切片后面。

header大体上是两类,一类是http预定义的;一类是自己定义的。Go会自动将header名字转为标准名字--其实就是大小写调整(首字母大写)。一般用X开头来表明是自己定义的,比如说X-mycompany-your=header。

Request Body

body只能读取一次(ioutil.ReadAll(r.Body)),意味着你读了别人就不能读了;别人读了你就不能读了。

GetBody原则上是可以多次读取,但是在原生的http.Request里面是nil。

Request Query

除了body,可以通过query传递参数:http://xxx.com/path?id=123&name=wang

所有的值都被解释成字符串,所以若为数字需要字节解析为数字等。

Request URL

包含路径方面的所有信息和一些有用操作。

URL里面Host不一定有值,r.Host一般都有值,是Header的Host值。

RawPath不一定有值,Path肯定有。实际中要输出看一下,确认有没有。

r.URL参考net/url包,定义如下:

type URL struct {
    Scheme   string
    Opaque   string    // 编码后的不透明数据
    User     *Userinfo // 用户名和密码信息
    Host     string    // host或host:port
    Path     string
    RawQuery string // 编码后的查询字符串,没有'?'
    Fragment string // 引用的片段(文档位置),没有'#'
}
resp, err := http.PostForm("http://example.com/form", url.Values{"key": {"Value"}, "id": {"123"}})

URL类型代表一个解析后的URL(或者说,一个URL参照)。URL基本格式如下 

scheme://[userinfo@]host/path[?query][#fragment]

scheme后不是冒号加双斜线的URL被解释为如下格式:

scheme:opaque[?query][#fragment]

示例应用如下:

http://127.0.0.1:9090/house/wang?qing=hua&ding=dang&qing=wang&house=horse
------------------
url.Values{"ding":[]string{"dang"}, "house":[]string{"horse"}, "qing":[]string{"hua", "wang"}}
&url.URL{Scheme:"", Opaque:"", User:(*url.Userinfo)(nil), Host:"", Path:"/house/wang", RawPath:"", ForceQuery:false, RawQuery:"qing=hua&ding=dang&qing=wang&house=horse", Fragment:"", RawFragment:""}

Request Form

在处理form前调用r.ParseForm(),建议设置Content-Type: application/x-www-form-urlencoded.

表单form可由两种方式提交GET或POST,GET方式form表单键值对直接跟在url后(?分割,&链接),POST方式Form以请求body方式发送。

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

func main(){
    http.HandleFunc("/", sayhelloName)  // 设置访问路由
    err := http.ListenAndServe(":9090", nil) // 设置监听端口
    if err != nil {
        log.Fatal("ListenAndServe: ", err)
    }
}

func sayhelloName(w http.ResponseWriter, r *http.Request){
    r.ParseForm()
    fmt.Println("---------------")
    fmt.Printf("values: %#v\n", r.Form)
fmt.Println("url: %#v\n", r.URL) fmt.Println("====") for k, v := range r.Form{ fmt.Println("key:", k) fmt.Println("val:", strings.Join(v, " ")) } fmt.Fprintf(w, "Hello wang!") }

http请求处理

package main

import (
        "fmt"
        "net/http"
        "net/url"
        "strings"
        "io"
        "io/ioutil"
        "bytes"
        "time"
        "mime/multipart"
        "os"
)

func httpGetPost(method, url, contentType string, rbody io.Reader) error{
        fmt.Println("---------------------------------------")
        req, err := http.NewRequest(method, url, rbody)
        if err != nil {
                fmt.Printf("NewRequest error: method: %v, url: %v\n", method, url)
                return err
        }
        req.Header.Set("Content-Type", contentType)
        req.Header.Set("Cookie", "name=any")

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

        resp, err := client.Do(req)
        if err != nil {
                fmt.Printf("Client Do error: %s\n", err)
                return err
        }
        defer resp.Body.Close()

        body, err := ioutil.ReadAll(resp.Body)
        if err != nil {
                fmt.Printf("Failed reading response body: %s\n", err)
                return err
        }

        fmt.Printf("Response %d: %s\nURL: %#v\nbody: %s\n\n", resp.StatusCode, resp.Proto, resp.Request.URL, string(body))
        return nil
}

//var target = "http://127.0.0.1:8088/bar"
var target = "http://127.0.0.1:8088/bar?home=CHINA"

func main(){
        err := httpGetPost("GET", target, "", nil)
        if err != nil {
                fmt.Println("main http request error: ", err)
        }

        // application/x-www-form-urlencoded  strings
        data := url.Values{"app_id":{"238-148-abc"}, "mobile_tel": {"12345678901"}}
        body := strings.NewReader(data.Encode())
        httpGetPost("POST", target, "application/x-www-form-urlencoded", body)

        body = strings.NewReader("name=wang")
        httpGetPost("POST", target, "application/x-www-form-urlencoded", body)

        // multipart/form-data binary
        binbody := bytes.NewReader([]byte{0b001, 0b00010, 0b000011})
        httpGetPost("POST", target, "multipart/form-data", binbody)

        // application/json
        bytesbody := bytes.NewBuffer([]byte(`{"name": "wang"}`))
        httpGetPost("POST", target, "application/json", bytesbody)

        // multipart/form-data file
        file, err := os.Open("multipartfile")
        if err != nil {
                fmt.Printf("file multipartfile open error: %v\n", err)
                return
        }
        defer file.Close()

        multibody := &bytes.Buffer{}
        writer := multipart.NewWriter(multibody)
        part, err := writer.CreateFormFile("file", "multipartfile") // form-data header: key, value
        if err != nil {
                fmt.Printf("multipart writer CreateFormFile error: %v\n", err)
                return
        }
        _, err = io.Copy(part, file)

        writer.WriteField("name", "wang")
        err = writer.Close()
        if err != nil {
                fmt.Printf("multipart writer close error: %v\n", err)
                return
        }

        httpGetPost("POST", target, writer.FormDataContentType(), multibody)
}

godoc提供了一个web开发示例Writing Web Applications,用到了net/http,html/template, regexp,闭包等技术:

package main

import (
    _"fmt"
    "io/ioutil"
    "net/http"
    "log"
    "html/template"
    "regexp"
    _"errors"
)

type Page struct {
    Title string
    Body []byte
}

func (p *Page) save() error {
    filename := p.Title + ".txt"
    return ioutil.WriteFile(filename, p.Body, 0600)
}

func loadPage(title string) (*Page, error){
    filename := title + ".txt"
    body, err := ioutil.ReadFile(filename)
    if err != nil {
        return  nil, err
    }
    return &Page{Title: title, Body : body}, nil
}

var templates *template.Template
var validPath  *regexp.Regexp
func main(){
    /*
    p1 := &Page{Title: "TestPage", Body: []byte("This is a sample Page.")}
    p1.save()
    p2, _ := loadPage("TestPage")
    fmt.Println(string(p2.Body))
    */
    // import templates
    // panic when failed
    templates = template.Must(template.ParseFiles("edit.html", "view.html"))

    // validation
    validPath = regexp.MustCompile("^/(edit|save|view)/([a-zA-Z0-9]+)$")
/*
    http.HandleFunc("/view/", viewHandler)
    http.HandleFunc("/edit/", editHandler)
    http.HandleFunc("/save/", saveHandler)
*/
    http.HandleFunc("/view/", makeHandler(viewHandler))
    http.HandleFunc("/edit/", makeHandler(editHandler))
    http.HandleFunc("/save/", makeHandler(saveHandler))
    log.Fatal(http.ListenAndServe(":8090", nil))
}
/*
func getTitle(w http.ResponseWriter, r *http.Request)(string, error){
    m := validPath.FindStringSubmatch(r.URL.Path)
    if m == nil {
        http.NotFound(w, r)
        return "",  errors.New("Invalid Page Title")
    }
    return m[2], nil // The title is the second subexpression
}
*/
func makeHandler(fn func(http.ResponseWriter, *http.Request, string))  http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request){
        m := validPath.FindStringSubmatch(r.URL.Path)
        if m == nil {
            http.NotFound(w, r)
            return 
        }
        fn(w, r, m[2])
    }
}

/*
func viewHandler(w http.ResponseWriter, r *http.Request){
    title := r.URL.Path[len("/view/"):]
    p, _ :=  loadPage(title)
    fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", p.Title, p.Body)
}
*/

//func viewHandler(w http.ResponseWriter, r *http.Request){
func viewHandler(w http.ResponseWriter, r *http.Request, title string){
//    title := r.URL.Path[len("/view/"):]
/*
    title, err := getTitle(w, r)
    if err != nil {
        return
    } 
*/
    p, err:= loadPage(title)
    if err != nil {
        http.Redirect(w, r, "/edit/"+title, http.StatusFound)
        return
    }
    //t, _:= template.ParseFiles("view.html")
    //t.Execute(w, p)
    renderTemplate(w, "view", p)
}

/*
func editHandler(w http.ResponseWriter, r *http.Request){
    title := r.URL.Path[len("/edit/"):]
    p, err := loadPage(title)
    if err != nil {
        p = &Page{Title: title}
    }
    fmt.Fprintf(w, "<h1>Editing %s</h1>" +
    "<form action=\"/save/%s\" method=\"POST\">" +
    "<textarea name=\"body\">%s</textarea><br>" +
    "<input type=\"submit\" value=\"Save\">" +
    "</form>",
    p.Title, p.Title, p.Body)
}
*/
//func editHandler(w http.ResponseWriter, r*http.Request){
func editHandler(w http.ResponseWriter, r*http.Request, title string){
//    title := r.URL.Path[len("/edit/"):]
/*
    title, err := getTitle(w, r) 
    if err != nil {
        return
    }
*/
    p, err := loadPage(title)
    if err != nil {
        p = &Page{Title: title}
    }
//    t, _:= template.ParseFiles(edit.html)
//    t.Execute(w, p)
    renderTemplate(w, "edit", p)
}

//func saveHandler(w http.ResponseWriter, r *http.Request){
func saveHandler(w http.ResponseWriter, r *http.Request, title string){
//    title := r.URL.Path[len("/save/"):]
/*
    title , err := getTitle(w, r)
    if err != nil {
        return
    }
*/
    body := r.FormValue("body")
    p := &Page{Title: title, Body: []byte(body)}
    err := p.save()
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    http.Redirect(w, r, "/view/"+title, http.StatusFound)
}

func renderTemplate(w http.ResponseWriter, tmpl string, p *Page){
    /*
    t, err := template.ParseFiles(tmpl + ".html")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    err = t.Execute(w, p)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
    */
    err := templates.ExecuteTemplate(w, tmpl+".html", p)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}
View Code

2.2 HTTP/2

HTTP/2协议握手分2种方式,一种叫h2,一种叫h2c。

h2要求必须使用TLS加密,在TLS握手期间会顺带完成HTTPS/2协议的协商,如果协商失败(比如客户端不支持或者服务端不支持),则会使用HTTPS/1继续后续通讯。

h2c不使用TLS,而是多了一次基于HTTP协议的握手往返来完成向HTTP/2协议的升级,一般不建议使用。

Golang的http库在设计API时并没有支持用户使用h2c,而是鼓励使用h2。

GO的http库默认支持HTTP/2协议,只要我们使用TLS则会默认启动HTTP/2特性,如果协商失败则蜕化为HTTPS/1。。

对http Client或者Server做一些更加定制化的配置时,就会覆盖掉http库的默认行为,从而导致无法启用HTTP/2协议。

只要server端开启TLS服务即可启用HTTP/2。

sever.go
package main

import (
    "log"
    "net/http"
)

func main(){
    srv := &http.Server{Addr: ":8088", Handler: http.HandlerFunc(handle)}

    log.Printf("Serving on https://server:8088")
    log.Fatal(srv.ListenAndServeTLS("cert/server.crt", "cert/server.key"))
}

func handle(w http.ResponseWriter, r *http.Request){
    w.Write([]byte("Hello"))
}

除了使用ServeTLS/ListenAndServeTLS来启动支持HTTPS/2特性的服务端之外,还可以通过http2.ConfigureServer来为http.Server启动HTTPS/2特性并直接使用Serve来启动服务。

package main

import (
    "fmt"
    "crypto/tls"
    "crypto/x509"
    "flag"
    "io/ioutil"
    "log"
    "net/http"
    "golang.org/x/net/http2"
)

var addr = flag.String("addr", "https://server:8088?numa=4&numb=6", "connect to")
var httpVer = flag.Int("httpVer", 2, "HTTP version")

func main(){
    flag.Parse()

    client := &http.Client{}

    caCert, err := ioutil.ReadFile("cert/ca.crt")
    if err != nil {
        log.Fatalf("Reading server certificate: %s", err)
    }   

    pool := x509.NewCertPool()
    pool.AppendCertsFromPEM(caCert)
    tlsConfig := &tls.Config{
        RootCAs: pool,
    }

    switch *httpVer {
        case 1:
            client.Transport = &http.Transport {
                TLSClientConfig: tlsConfig,
            }
        case 2:
            client.Transport = &http2.Transport {
                TLSClientConfig: tlsConfig,
            }
    }   

    resp, err := client.Get(*addr)
    if err != nil {
        log.Fatalf("Failed get: %s", err)
    }   
    defer resp.Body.Close()

    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        log.Fatalf("Failed reading response body: %s", err)
    }

    fmt.Printf("Response %d: %s\nbody: %s\n", resp.StatusCode, resp.Proto, string(body))
}

client定制后要开启HTTP/2,需要配置Transport(或使用http2.ConfigureTransport(transport))。

$ ./cli -httpVer=1
Response 200: HTTP/1.1
body: 10

$ ./cli -httpVer=20
Response 200: HTTP/2.0
body: 10 

2.3 cookie相关

A Cookie represents an HTTP cookie as sent in the Set-Cookie header of an HTTP response or the Cookie header of an HTTP request.

See https://tools.ietf.org/html/rfc6265 for details.

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
}

func (c *Cookie) String() string
String returns the serialization of the cookie for use in a Cookie header (if only Name and Value are set) or 
a Set-Cookie response header (if other fields are set). If c is nil or c.Name is invalid, the empty string is returned. func SetCookie(w ResponseWriter, cookie *Cookie) SetCookie adds a Set-Cookie header to the provided ResponseWriter's headers.
The provided cookie must have a valid Name. Invalid cookies may be silently dropped.

2.5 Values

Values将建映射到值的列表。它一般用于查询的参数和表单的属性。不同于http.Header这个字典类型,Values的键是大小写敏感的。

Values定义在net/url包:
type Values map[string][]string

func ParseQuery(query string) (m Values, err error)

func (v Values) Get(key string) string
func (v Values) Set(key, value string)
func (v Values) Add(key, value string)
func (v Values) Del(key string)
func (v Values) Encode() string

Encode方法将v编码为url编码格式("bar=baz&foo=quux"),编码时会以键进行排序。

2.6 优雅关闭

http.Server 内置的 Shutdown() 方法优雅地关机。

func (srv *Server) Shutdown(ctx context.Context) error

Shutdown gracefully shuts down the server without interrupting any active connections. Shutdown works by first closing all open listeners, then closing all idle connections, and then waiting indefinitely for connections to return to idle and then shut down. If the provided context expires before the shutdown is complete, Shutdown returns the context's error, otherwise it returns any error returned from closing the Server's underlying Listener(s).

package main

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

func main() {
    var srv http.Server

    idleConnsClosed := make(chan struct{})
    go func() {
        sigint := make(chan os.Signal, 1)
        signal.Notify(sigint, os.Interrupt)
        <-sigint

        // We received an interrupt signal, shut down.
        if err := srv.Shutdown(context.Background()); err != nil {
            // Error from closing listeners, or context timeout:
            log.Printf("HTTP server Shutdown: %v", err)
        }
        close(idleConnsClosed)
    }()

    if err := srv.ListenAndServe(); err != http.ErrServerClosed {
        // Error starting or closing listener:
        log.Fatalf("HTTP server ListenAndServe: %v", err)
    }

    <-idleConnsClosed
}

3. 文件服务 

库文件在包:net/http中。

A FileSystem implements access to a collection of named files. The elements in a file path are separated by slash ('/', U+002F) characters, regardless of host operating system convention. See the FileServer function to convert a FileSystem to a Handler.

type FileSystem interface {
    Open(name string) (File, error)
}
func FileServer(rot FileSystem) Handler
type Dir string

FileServer returns a handler that serves HTTP requests with the contents of the file system rooted at root.

As a special case, the returned file server redirects any request ending in "/index.html" to the same path, without the final "index.html".

To use the operating system's file system implementation, use http.Dir:

http.Handle("/", http.FileServer(http.Dir("/tmp")))
package main

import (
    "log"
    "net/http"
)

func main() {
    // Simple static webserver:
    log.Fatal(http.ListenAndServe(":8080", http.FileServer(http.Dir("/home/wang"))))
}

上述示例可以通过127.0.0.1:8080/访问到/home/wang/目录下的所有文件。

func StripPrefix(prefix string, h Handler) Handler
http.Handle("/tmpfiles/", http.StripPrefix("/tmpfiles/", http.FileServer(http.Dir("/tmp"))))

StripPrefix returns a handler that serves HTTP requests by removing the given prefix from the request URL's Path (and RawPath if set) and invoking the handler h

当访问127.0.0.1:8088/tmpfiles时,实际访问/tmp目录内容。若不加http.StripPrfix(),则访问/tmp/tmpfiles目录。

gin中服务静态文件:

func main() {
    router := gin.Default()
    router.Static("/assets", "./assets")
    router.StaticFS("/more_static", http.Dir("my_file_system"))
    router.StaticFile("/favicon.ico", "./resources/favicon.ico")

    // Listen and serve on 0.0.0.0:8080
    router.Run(":8080")
}
func (group *RouterGroup) Static(relativePath, root string) IRoutes {
    return group.StaticFS(relativePath, Dir(root, false))
}

Static()等同于StaticFS(),除了不显示目录文件外(必须指定具体访问的文件)。StaticFS()直接访问目录。StaticFile()仅可访问一个file。

 

参考:

  1. https://golang.google.cn/pkg/net/    https://studygolang.com/static/pkgdoc/pkg/net_http.htm#Server 中文

  2. https://www.kancloud.cn/kancloud/the-way-to-go/165098 go入门指南

  3. http://wiki.jikexueyuan.com/project/go-web-programming/03.3.html go web编程

  4. Writing Web Applications

  5. Go语言练习:网络编程实例——简易图片上传网站 photoweb
  6. Go 源码学习之--net/http

  7. Go语言的http/2服务器功能及客户端使用

  8. Golang爬虫全攻略  网络应用解析

  9. golang几种post请求方式

  10. golang之net/http包简单的get/post请求

  11. golang实现分布式缓存笔记(一)基于http的缓存服务

posted @ 2019-08-07 22:54  yuxi_o  阅读(1167)  评论(0编辑  收藏  举报