net/http

http 包实现了http客户端与服务端的实现

  • 1.创建http客户端
  • 2.客户端发起get,post,postForm请求演示
  • 3.自定义客户端请求
  • 4.创建服务端
  • 5.自定义服务端配置
  • 6.解析HTTP版本号
  • 7.将不同格式的时间字符串,转换成标准time结构
  • 8.获取http状态码Status 对应的文本信息
  • 9.监测服务器与客户端的连接状态
  • 10.获取请求头里面的数据
  • 11.将请求头的数据写入文件中
  • 12.文件服务器创建
  • 13.请求重定向演示
  • 14.将文件的内容返回给请求
  • 15.建立https服务
  • 16.接管服务器和客户端连接

1.创建http客户端

客户端请求连接过程

1 创建http客户端对象
2 按照指定方式请求数据
3 获取数据
4 关闭连接

type Client struct {
      
         // 传输机制
    Transport RoundTripper

    // CheckRedirect 指定了重定向策略.
    CheckRedirect func(req *Request, via []*Request) error

    // Jar 指定了缓存
    Jar CookieJar

       // 设定了超时时间
    Timeout time.Duration
}

var DefaultClient = &Client{}
DefaultClient提供了Get、Head、Post 的默认Client.

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

func main() {
  // 创建客户端发get请求
   resp,error := http.DefaultClient.Get("http://example.com/")
 // 相应结束后,请及时关闭会话
   defer resp.Body.Close()
   if error != nil {
       log.Fatal(error)
   }
   // 读取相应的数据
   data,error := ioutil.ReadAll(resp.Body)
   fmt.Println(string(data))
 }

我们看一下响应数据结构体都包含了那些数据

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 //是否未压缩
    Trailer Header
    Request *Request // 原始请求相关
    TLS *tls.ConnectionState // https 加密相关
}

2.客户端发起get,post,postForm请求演示

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"}})

3.自定义客户端请求

  • 定义请求方式
  • 定义请求重定向策略
  • 自定义请求头
  • 自定义Transport
  • TLS配置

我们就针对上面的5点进行演示

定义请求方式:除了系统提供默认的创建客户端的方式,我们也可以手动创建客户端,比较简单,直接看下面的例子

package main

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

func main() {
   // 创建一个客户端
   client := &http.Client{}

   // 创建一个自定义请求
   req,_:= http.NewRequest("Get","http://www.baidu.com",nil)

   // 让客户端执行请求
   resp,error := client.Do(req)
  // 关闭请求
  defer resp.Body.Close()
   if error != nil {
       log.Fatal(error)
   }
   data,error := ioutil.ReadAll(resp.Body)
   fmt.Println(string(data))
 }
请求数据的截图

定义请求策略:如果我们不设置请求重定向策略,系统会使用默认的重定向策略

package main

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

func main() {
   // 创建一个客户端
   client := &http.Client{
       CheckRedirect:CheckRedirect,
   }
   // 创建一个自定义请求
   req,_:= http.NewRequest("Get","http://www.baidu.com",nil)

   // 让客户端执行请求
   resp,error := client.Do(req)
   defer resp.Body.Close()
   if error != nil {
       log.Fatal(error)
   }
   fmt.Println(resp.StatusCode)
   data,error := ioutil.ReadAll(resp.Body)
   fmt.Println(string(data))

 }

func CheckRedirect(req *http.Request, via []*http.Request) error{
    fmt.Println(req.URL.Host)
    if req.URL.Host == "www.baidu.com" {
        // 返回http.ErrUseLastResponse 可以禁止重定向
        return  http.ErrUseLastResponse
    }
    return nil
}
image.png

自定义请求头

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"`)

自定义Transport 要管理代理、TLS配置、keep-alive、压缩和其他设置,创建一个Transport

tr := &http.Transport{
    TLSClientConfig:    &tls.Config{RootCAs: pool},
    DisableCompression: true,
}
client := &http.Client{Transport: tr}

4.创建服务器

简单的请求

func ListenAndServe(addr string, handler Handler) error

参数 Handler 是一个接口类型,需要我们实现下面的接口

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

下面我们看一个完整的例子

package main
import (
    "net/http"
    "fmt"
    )

// 第一步 Handle 实现接口
type  Controller struct {
}

func (c Controller)ServeHTTP(resp http.ResponseWriter, req *http.Request){
    fmt.Println(req.Host)
    resp.Write([]byte("我是服务器返回的数据"))
    req.Body.Close()
}

func main() {
   // 第二步 监听服务
   http.ListenAndServe(":8081",&Controller{})
 }

我们在http请求的时候,往往需要处理不同的请求路径,比如/login 和/register ,go为我们提供了如下的方法进行处理

func HandleFunc(pattern string, handler func(ResponseWriter, *Request))
func Handle(pattern string, handler Handler)

路由匹配

package main

import (
    "net/http"
    "fmt"
    )

func main() {
  // 第一步 注册请求匹配函数
  http.HandleFunc("/login", func(writer http.ResponseWriter, request *http.Request) {
      fmt.Println(request.Host)
      fmt.Println(request.URL)
      fmt.Println(request.URL.RawQuery)
      fmt.Println(request.Body)
      fmt.Println(request.Header)
      // 服务器相应数据
      writer.Write([]byte(request.UserAgent()))
      request.Body.Close()
  })
  // 监听服务
  http.ListenAndServe(":8081",nil)
 }

func Handle(pattern string, handler Handler) 需要自己创建变量实现 handle接口,实现的过程我们已经演示过了,就不在赘述了

5.自定义服务端配置

上面我们使用 http.ListenAndServe 启动了一个监听服务,但是我们在实际使用的过程中,有时需要配置一些信息比如

  • 读请求的最大时间
  • 写入的组大时间
  • 最大的头字节数
    ...

下面我们就演示一下如何实现自定义服务端配置的过程

package main

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

type  Controller struct {
}

func (c Controller)ServeHTTP(resp http.ResponseWriter, req *http.Request){
    fmt.Println(req.Host)
    resp.Write([]byte("我是服务器返回的数据"))
    req.Body.Close()
}

func main() {
  //// 监听服务
  server := http.Server{
      Addr:           ":8080",
      Handler:        &Controller{},
      ReadTimeout:    10 * time.Second,
      WriteTimeout:   10 * time.Second,
      MaxHeaderBytes: 1 << 20,
  }
  server.ListenAndServe()
 }

浏览器输入 http://localhost:8080

服务器日志
浏览器接受数据

6.解析HTTP 版本号

func ParseHTTPVersion(vers string) (major, minor int, ok bool)

    fmt.Println(http.ParseHTTPVersion("HTTP/1.0"))
image.png

7.将不同格式的时间字符串,转换成标准time结构

func ParseTime(text string) (t time.Time, err error)

    t,_ := http.ParseTime("Monday, 02-Jan-06 15:04:05 MST") // time.RFC850
    fmt.Println(t)
    t,_ = http.ParseTime("Mon Jan 2 15:04:05 2006")  //time.ANSIC 不能使用字符_
    fmt.Println(t)

日志:

2006-01-02 15:04:05 +0000 MST
2006-01-02 15:04:05 +0000 UTC

8.获取http状态码Status 对应的文本信息

func StatusText(code int) string

  fmt.Println(http.StatusText(200))
  fmt.Println(http.StatusText(301))
    fmt.Println(http.StatusText(500))

OK
Moved Permanently
Internal Server Error


9.监测服务器与客户端的连接状态

type ConnState int

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
)
package main

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

type  Controller struct {
}

func (c Controller)ServeHTTP(resp http.ResponseWriter, req *http.Request){
    fmt.Println(req.Host)
    resp.Write([]byte("我是服务器返回的数据"))
    req.Body.Close()
}

func main() {
  //// 监听服务
  server := http.Server{
      Addr:           ":8080",
      Handler:        &Controller{},
      ReadTimeout:    10 * time.Second,
      WriteTimeout:   10 * time.Second,
      MaxHeaderBytes: 1 << 1,
  }

  // 监测服务器的连接状态
  server.ConnState = func(conn net.Conn, state http.ConnState) {
      fmt.Println(state)
  }

  server.ListenAndServe()

 }
image.png

我们发现浏览器想服务器发送请求两次请求,但是只有一个closed状态,这个是因为一旦服务器与浏览器完成数据交互后,连接状态还是激活状态,没有关闭,如果在最大连接时间未到时,客户端继续发送请求,就不会要重新建立连接了。如果我们想要每次数据结束后立即关闭连接,使用下面的方法即可

server.SetKeepAlivesEnabled(false)

image.png

10.获取请求头里面的数据

    for key,value := range req.Header{
        fmt.Println(key,":",value)
    }

Accept-Encoding : [gzip, deflate]
Connection : [keep-alive]
Upgrade-Insecure-Requests : [1]
Cookie : [pgv_pvi=4726842368; Webstorm-7b1b8ce4=a417cfae-a0df-4fa4-96a0-bec5a8aa3f22; _ga=GA1.1.1767641313.1490064708; Phpstorm-854b20c0=cce12517-63b4-48f0-8a05-1f6156ee2339]
User-Agent : [Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Safari/605.1.15]
Accept-Language : [zh-cn]
Accept : [text/html,application/xhtml+xml,application/xml;q=0.9,/;q=0.8]

获取指定key的对应的值

req.Header.Get("key")

设置添加值到header中,如键已存在则会用只有新值一个元素的切片取代旧值切片

req.Header.Set("key","value")

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

func (h Header) Add(key, value string)

删除请求头数据

func (h Header) Del(key string)

11.将请求头的数据写入文件中

// Write以有线格式将头域写入w
func (h Header) Write(w io.Writer) error 
//WriteSubset以有线格式将头域写入w。当exclude不为nil时,如果h的键值对的键在exclude中存在且其对应值为真,该键值对就不会被写入w
func (h Header) WriteSubset(w io.Writer, exclude map[string]bool) error

示例 1

    file,_ := os.Create("/Users/xujie/go/src/awesomeProject/main/header.txt")
    req.Header.Write(file)
image.png

示例 2

    file,_ := os.Create("/Users/xujie/go/src/awesomeProject/main/header.txt")
    req.Header.WriteSubset(file,map[string]bool{"Accept":true})
image.png

通过方法 WriteSubset我们将header写入文件时,过滤了 Accept


12.文件服务器创建

func FileServer(root FileSystem) Handler

    http.Handle("/login",&Controller{})
    http.Handle("/",http.FileServer(http.Dir("/usr/local")))
    http.ListenAndServe(":8080",nil)

访问 http://localhost:8080 即可查看

image.png

这里注意一下 我们的注册 http.Handle("/",http.FileServer(http.Dir("/usr/local"))),如果我们想要给我们可访问目录设置一个别名,不让用户直接访问该怎么处理呢?

 http.Handle("/public",http.StripPrefix("/public",http.FileServer(http.Dir("/usr/local"))))
 http.ListenAndServe(":8080",nil)

接下来就可以通过 http://localhost:8080/public 进行访问了


13.请求重定向演示

如果我们访问一个页面出现了错误,我们让用户重定向到一个其他页面上,类似于这个过程就是重定向

func Redirect(w ResponseWriter, r *Request, urlStr string, code int)

示例

package main

import (
    "net/http"
          )

type  Controller struct {
}
func (c Controller)ServeHTTP(resp http.ResponseWriter, req *http.Request){
    // 请求重定向
    http.Redirect(resp,req,"/error",http.StatusMovedPermanently)
}
func main() {
    //// 监听服务
    http.Handle("/login",&Controller{})
    http.HandleFunc("/error", func(writer http.ResponseWriter, request *http.Request) {
        writer.Write([]byte("重定向的错误页面"))
    })
    http.ListenAndServe(":8080",nil)
 }

当我们浏览器访问http://localhost/login ,服务器会重定向到/error 对应的服务上,这个时候浏览器的输入栏的地址也会自动切换到http://localhost/error

image.png

如果想直接返回未找到资源的信息提示,可以使用

func NotFound(w ResponseWriter, r *Request)

将文件的内容返回给请求

  • 全部返回
package main
import (
    "net/http"
)
type  Controller struct {
}
func (c Controller)ServeHTTP(resp http.ResponseWriter, req *http.Request){
    http.ServeFile(resp,req,"/Users/xujie/go/src/awesomeProject/main/private.pem")
}
func main() {
  //// 监听服务
    http.Handle("/private",&Controller{})
    http.ListenAndServe(":8080",nil)
 }
image.png

15.建立https服务

ListenAndServeTLS函数和ListenAndServe函数的行为基本一致,除了它期望HTTPS连接之外。此外,必须提供证书文件和对应的私钥文件。如果证书是由权威机构签发的,certFile参数必须是顺序串联的服务端证书和CA证书。如果srv.Addr为空字符串,会使用":https"

我们先生成 cert.pem和key.pem

package main

import (
    "bytes"
    cryptorand "crypto/rand"
    "crypto/rsa"
    "crypto/x509"
    "crypto/x509/pkix"
    "encoding/pem"
    "fmt"
    "math/big"
    "net"
    "time"
)

func main() {
    ip := []byte("192.168.110.66")
    alternateDNS := []string{"kubernetes.default.svc", "kubernetes.default", "kubernetes", "localhost"}
    cert, key, _ := GenerateSelfSignedCertKey("10.10.10.10", []net.IP{ip}, alternateDNS)
    fmt.Println(string(cert), string(key))

}

func GenerateSelfSignedCertKey(host string, alternateIPs []net.IP, alternateDNS []string) ([]byte, []byte, error) {
    priv, err := rsa.GenerateKey(cryptorand.Reader, 2048)
    if err != nil {
        return nil, nil, err
    }

    template := x509.Certificate{
        SerialNumber: big.NewInt(1),
        Subject: pkix.Name{
            CommonName: fmt.Sprintf("%s@%d", host, time.Now().Unix()),
        },
        NotBefore: time.Now(),
        NotAfter:  time.Now().Add(time.Hour * 24 * 365),

        KeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
        ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
        BasicConstraintsValid: true,
        IsCA: true,
    }

    if ip := net.ParseIP(host); ip != nil {
        template.IPAddresses = append(template.IPAddresses, ip)
    } else {
        template.DNSNames = append(template.DNSNames, host)
    }

    template.IPAddresses = append(template.IPAddresses, alternateIPs...)
    template.DNSNames = append(template.DNSNames, alternateDNS...)

    derBytes, err := x509.CreateCertificate(cryptorand.Reader, &template, &template, &priv.PublicKey, priv)
    if err != nil {
        return nil, nil, err
    }

    // Generate cert
    certBuffer := bytes.Buffer{}
    if err := pem.Encode(&certBuffer, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
        return nil, nil, err
    }

    // Generate key
    keyBuffer := bytes.Buffer{}
    if err := pem.Encode(&keyBuffer, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil {
        return nil, nil, err
    }

    return certBuffer.Bytes(), keyBuffer.Bytes(), nil
}

命令行生成

openssl genrsa -out ca.key 2048 生成root密钥
openssl req -new -x509 -sha256 -key ca.key -days 3650 -out ca.crt -subj "/CN=dotcoo.com" 生成root签名
openssl genrsa -out server.key 2048 生成server密钥
openssl req -new -sha256 -key server.key -subj "/CN=server.com" -out server.csr 生成server签名请求
openssl x509 -req -sha256 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 3650 给server签名
openssl x509 -in ./server.crt -noout -text 查询状态
image.png

接下来我们就可以使用https访问我们的本地服务了

package main

import (
    "net/http"
    "log"
)

type  Controller struct {

}

func (c Controller)ServeHTTP(resp http.ResponseWriter, req *http.Request){
   resp.Write([]byte("返回数据"))
}

func main() {
  //// 监听服务
    http.Handle("/private",&Controller{})
    err := http.ListenAndServeTLS(":10443","/Users/xujie/go/src/awesomeProject/server.crt","/Users/xujie/go/src/awesomeProject/server.key",nil)
    if err != nil {
        log.Fatal(err)
    }
 }
image.png

如果你任然使用http访问就会发生错误

2018/09/27 17:28:30 http: TLS handshake error from [::1]:59181: tls: first record does not look like a TLS handshake

16.接管服务器和客户端连接

type Hijacker interface {
    // Hijack让调用者接管连接,返回连接和关联到该连接的一个缓冲读写器。
    // 调用本方法后,HTTP服务端将不再对连接进行任何操作,
    // 调用者有责任管理、关闭返回的连接。
    Hijack() (net.Conn, *bufio.ReadWriter, error)
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,172评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,346评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,788评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,299评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,409评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,467评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,476评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,262评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,699评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,994评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,167评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,499评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,149评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,387评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,028评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,055评论 2 352

推荐阅读更多精彩内容