目录

文章目录

  • 目录
  • net/http
  • HTTP 客户端
    • 实现原理
      • 构建请求
      • 开启事务
      • 等待响应
  • 参考文档

net/http

HTTP 是典型的 C/S 架构,客户端构建请求并等待响应,服务端处理请求并返回响应。Golang 的 net/http 标准库就同时封装了 HTTP 客户端和服务端的实现,是生产级别的 HTTP 实现。

官方文档:

  • 英文:https://golang.org/pkg/net/http/
  • 中文:https://cloud.tencent.com/developer/section/1143633

源码:

  • https://github.com/golang/go/tree/master/src/net/http

为了让 net/http 的客户端和服务端具有更好的可扩展性,net/http 实现了两个关键接口 http.RoundTripper(客户端发起请求)和 http.Handler(服务端处理请求):

  • http.RoundTripper:用于发出一个 HTTP 请求,客户端将 request 对象作为参数传入,就会发出该 request 并从服务端获取对应的 response 或 error。故称之为:“往返者”。
type RoundTripper interface {RoundTrip(*Request) (*Response, error)
}
  • http.Handler:用于响应客户端发出的 request,实现了处理 request 的实际业务逻辑,最后还会调用 http.ResponseWriter 接口的方法来构造一个相应的 response 或 error。故称之为:“处理器”。
type Handler interface {ServeHTTP(ResponseWriter, *Request)
}
  • http.ResponseWriter:提供了三个方法 Header、Write 和 WriteHeader 分别用于获取 HTTP 响应头、响应主体和设置 Status Code。
type ResponseWriter interface {Header() HeaderWrite([]byte) (int, error)WriteHeader(statusCode int)
}

HTTP 客户端

net/http 的 Client 提供了 Get、Head、Post 和 PostForm 函数来发起 HTTP/HTTPS 请求。下面先看几个示例来感受。

  • 发出一个简单的 http.Get 请求:
package mainimport ("bytes""fmt""log""net/http""reflect"
)func main() {resp, err := http.Get("http://www.baidu.com")if err != nil {log.Println(err)return}defer resp.Body.Close()headers := resp.Headerfor k, v := range headers {fmt.Printf("k=%v, v=%v\n", k, v)}fmt.Printf("resp status %s,statusCode %d\n", resp.Status, resp.StatusCode)fmt.Printf("resp Proto %s\n", resp.Proto)fmt.Printf("resp content length %d\n", resp.ContentLength)fmt.Printf("resp transfer encoding %v\n", resp.TransferEncoding)fmt.Printf("resp Uncompressed %t\n", resp.Uncompressed)fmt.Println(reflect.TypeOf(resp.Body)) // *http.gzipReaderbuf := bytes.NewBuffer(make([]byte, 0, 512))length, _ := buf.ReadFrom(resp.Body)fmt.Println(len(buf.Bytes()))fmt.Println(length)fmt.Println(string(buf.Bytes()))
}
  • 使用 http.NewRequest 来创建 request 对象,再通过 http.Client 执行这个 request 对象:
package mainimport ("fmt""io/ioutil""net/http""net/url""strings"
)func main() {v := url.Values{}v.Set("username", "xxxx")v.Set("password", "xxxx")body := ioutil.NopCloser(strings.NewReader(v.Encode()))req, err := http.NewRequest("POST", "http://xxx.com/logindo", body)if err != nil {fmt.Println("Fatal error ", err.Error())}req.Header.Set("Content-Type", "application/x-www-form-urlencoded;param=value")client := &http.Client{}resp, err := client.Do(req)defer resp.Body.Close()content, err := ioutil.ReadAll(resp.Body)if err != nil {fmt.Println("Fatal error ", err.Error())}fmt.Println(string(content))
}
  • 忽略 TLS 证书认证的请求:
package mainimport ("crypto/tls""fmt""io/ioutil""net/http"
)func main() {tr := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true},}client := &http.Client{Transport: tr}seedUrl := "https://www.douban.com/"resp, err := client.Get(seedUrl)if err != nil {fmt.Errorf("get https://www.douban.com/ error")panic(err)}defer resp.Body.Close()body, err := ioutil.ReadAll(resp.Body)if err != nil {fmt.Errorf("get https://www.douban.com/ error")panic(err)}fmt.Printf("%s\n", body)
}

实现原理

首先,我们可以设想:作为客户端发出一个请求需要经历几个步骤?

  1. 发出什么?
  2. 怎么发?
  3. 结果是什么?

基于这个原始的思维逻辑,net/http 客户端发出 HTTP 请求时,会执行以下 3 个步骤:

  1. 构建请求:调用 http.NewRequest 或 http.NewRequestWithContext 函数根据传入的 Context(可选)、Method、URL 和 Request Body 等实参构建一个请求。
  2. 开始事务:调用 http.Transport.RoundTrip 开启 HTTP 事务、获取连接并发送请求。
  3. 等待响应:在 HTTP 持久连接(默认是长连接)的 http.persistConn.writeLoop 中等待响应。

相应的,net/http 客户端实现了几个重要的结构体:

  • http.Client:表示 HTTP 客户端,实现了包括 Cookies 和重定向等协议内容。默认使用 http.DefaultTransport,开发者也可以自定义一个 Client,但并不常见。
  • http.Transport:实现了 http.RoundTripper 接口,包含:连接重用、构建请求、发送请求和 HTTP Proxy 等功能。
  • http.persistConn:是 TCP 协议长连接功能的封装,作为客户端与服务端交换 HTTP Message(消息)的句柄。

从下面分别对上述 3 个过程进行暂开。

构建请求

http.NewRequestWithContext 函数根据传入的 Context、Method、URL 和 Request Body 等实参组成一个 http.Request 结构体:

func NewRequestWithContext(ctx context.Context, method, url string, body io.Reader) (*Request, error) {if method == "" {method = "GET"}if !validMethod(method) {return nil, fmt.Errorf("net/http: invalid method %q", method)}u, err := urlpkg.Parse(url)if err != nil {return nil, err}rc, ok := body.(io.ReadCloser)if !ok && body != nil {rc = ioutil.NopCloser(body)}u.Host = removeEmptyPort(u.Host)req := &Request{ctx:        ctx,Method:     method,URL:        u,Proto:      "HTTP/1.1",ProtoMajor: 1,ProtoMinor: 1,Header:     make(Header),Body:       rc,Host:       u.Host,}if body != nil {...}return req, nil
}

http.Request 表示服务端接收到的请求或者是客户端发出的请求,包含了作为一个 HTTP 协议定义的 Request 所应该具有的全部内容,包括:请求行、请求体(Request Header)、报文主体(Request Body)。其中,请求行又包括了:Request Method、URL、协议版本。它还会额外的持有一个指向 HTTP 响应的引用,这是 net/http 的私有设计,用于索引一个 Response 实例:

type Request struct {Method stringURL *url.URLProto      string // "HTTP/1.0"ProtoMajor int    // 1ProtoMinor int    // 0Header HeaderBody io.ReadCloser...Response *Response
}

开启事务

构建了一个 http.Request 之后,就要开启一个 HTTP 事务来发送这个请求并等待响应,经过了下面一连串的函数或方法调用:

  1. http.Client.Do
  2. http.Client.do
  3. http.Client.send
  4. http.send
  5. http.Transport.RoundTrip

上文也提到,最后的 http.Transport 结构体实现了 http.RoundTripper 的接口,是整个请求过程中最重要且最复杂的结构体。

http.Transport 结构体会在 http.Transport.roundTrip 方法中发送一个 Request 并等待 Response,我们可以将该方法的执行过程分成两个部分:

  1. 根据 URL 的协议(http/https)查找并执行默认的、或自定义的 http.RoundTripper 函数。这里就体现了 net/http 客户端的可扩展性,开发者可自定义 http.RoundTripper,并调用 http.Transport.RegisterProtocol 方法为不同的协议注册预期的 http.RoundTripper 实现。

  2. 从连接池中获取、或初始化新的持久连接(默认为长连接,术语:Keep-Alive 或 Persistent Connection),并调用连接实例的 http.persistConn.roundTrip 方法发出请求。

默认的,会使用 http.persistConn 方法来处理 HTTP 请求,该方法首先会获取用于发送请求的连接。所谓 “连接” 实际上是一个 TCP 连接,因为 HTTP 协议建立在 TCP 协议之上,所以首先需要完成 TCP 三次握手新建一个连接。随后,http.persistConn 再调用 http.persistConn.roundTrip 方法使用这个连接发送请求:

func (t *Transport) roundTrip(req *Request) (*Response, error) {...for {select {case <-ctx.Done():return nil, ctx.Err()default:}treq := &transportRequest{Request: req, trace: trace}cm, err := t.connectMethodForRequest(treq)if err != nil {return nil, err}pconn, err := t.getConn(treq, cm)if err != nil {return nil, err}resp, err := pconn.roundTrip(treq)if err == nil {return resp, nil}}
}

其中,http.Transport.getConn 是获取连接的方法,具有两种方式:

  1. 调用 http.Transport.queueForIdleConn 在队列中等待闲置的连接。
  2. 调用 http.Transport.queueForDial 在队列中等待建立新的连接。
func (t *Transport) getConn(treq *transportRequest, cm connectMethod) (pc *persistConn, err error) {req := treq.Requestctx := req.Context()w := &wantConn{cm:         cm,key:        cm.key(),ctx:        ctx,ready:      make(chan struct{}, 1),}if delivered := t.queueForIdleConn(w); delivered {return w.pc, nil}t.queueForDial(w)select {case <-w.ready:...return w.pc, w.err...}
}

需要注意的是,连接对于操作系统而言,是一种昂贵的资源。如果对每个请求都新建一个连接,会带来很大的开销。所以,理想的方式是通过连接池机制来实现资源的分配和复用,继而有效地提高网络通信的整体性能。这是大多数的网络库客户端都会采取的设计。

非理想的情况,就调用 http.Transport.queueForDial(拨号连接)方法来新建一个连接。首先会在内部启动新的 Goroutine 并执行 http.Transport.dialConnFor 方法,在 http.Transport.dialConn 方法中可以看到到 TCP 连接和 net 库的身影:

func (t *Transport) dialConn(ctx context.Context, cm connectMethod) (pconn *persistConn, err error) {pconn = &persistConn{t:             t,cacheKey:      cm.key(),reqch:         make(chan requestAndChan, 1),writech:       make(chan writeRequest, 1),closech:       make(chan struct{}),writeErrCh:    make(chan error, 1),writeLoopDone: make(chan struct{}),}conn, err := t.dial(ctx, "tcp", cm.addr())if err != nil {return nil, err}pconn.conn = connpconn.br = bufio.NewReaderSize(pconn, t.readBufferSize())pconn.bw = bufio.NewWriterSize(persistConnWriter{pconn}, t.writeBufferSize())go pconn.readLoop()go pconn.writeLoop()return pconn, nil
}

如上所示,在新建 TCP 连接时,会在后台创建两个绑定的 Goroutine,分别用于从 TCP 连接中读取(Read)数据或者向 TCP 连接写入(Write)数据。可见,连接是一种宝贵的资源。作为网络开发者,因为对连接池抱有强烈的意识。

等待响应

TCP 持久连接的封装 http.persistConn 的 roundTrip 在发送 HTTP 请求之后会在 select 语句中等待响应的返回:

func (pc *persistConn) roundTrip(req *transportRequest) (resp *Response, err error) {writeErrCh := make(chan error, 1)pc.writech <- writeRequest{req, writeErrCh, continueCh}resc := make(chan responseAndError)pc.reqch <- requestAndChan{req:        req.Request,ch:         resc,}for {select {case re := <-resc:if re.err != nil {return nil, pc.mapRoundTripError(req, startBytesWritten, re.err)}return re.res, nil...}}
}

每个 HTTP 请求都由 http.persistConn.writeLoop 的 Goroutine 循环写入的,http.Request.write 方法会根据 http.Request 结构体中的成员按照 HTTP 协议组成 TCP 数据段:

func (pc *persistConn) writeLoop() {defer close(pc.writeLoopDone)for {select {case wr := <-pc.writech:startBytesWritten := pc.nwritewr.req.Request.write(pc.bw, pc.isProxy, wr.req.extra, pc.waitForContinue(wr.continueCh))...case <-pc.closech:return}}
}

当调用 http.Request.write 方法向 write channel 中的 Request 写入数据时,实际上就写入了 http.persistConnWriter 中的 TCP 连接,TCP 协议传输的是字节流([]byte 类型),所需要需要完成 HTTP 请求报文到 TCP 数据段的一次封装:

type persistConnWriter struct {pc *persistConn
}func (w persistConnWriter) Write(p []byte) (n int, err error) {n, err = w.pc.conn.Write(p)w.pc.nwrite += int64(n)return
}

Read/Write 两个 Goroutine 独立执行并通过 Channel 进行通信。作为持久连接中的另一个 Goroutine 则进行着读循环 http.persistConn.readLoop,负责从 TCP 连接中读取数据。在将数据最终呈现给客户端之前,肯定还需要进行 TCP 字节流到 HTTP 响应报文的一次解封装,完成这个的就是 http.ReadResponse 方法:

func ReadResponse(r *bufio.Reader, req *Request) (*Response, error) {tp := textproto.NewReader(r)resp := &Response{Request: req,}line, _ := tp.ReadLine()if i := strings.IndexByte(line, ' '); i == -1 {return nil, badStringError("malformed HTTP response", line)} else {resp.Proto = line[:i]resp.Status = strings.TrimLeft(line[i+1:], " ")}statusCode := resp.Statusif i := strings.IndexByte(resp.Status, ' '); i != -1 {statusCode = resp.Status[:i]}resp.StatusCode, err = strconv.Atoi(statusCode)resp.ProtoMajor, resp.ProtoMinor, _ = ParseHTTPVersion(resp.Proto)mimeHeader, _ := tp.ReadMIMEHeader()resp.Header = Header(mimeHeader)readTransfer(resp, r)return resp, nil
}

从上述方法可以看到 HTTP 响应报文的大致内容,表现为 http.Response 结构体,包含了响应行、响应头(Response Header)、报文主体(Response Body)。其中,响应行又包含:State Code、协议版本等内容。

参考文档

https://draveness.me/golang/docs/part4-advanced/ch09-stdlib/golang-net-http

Go 语言编程 — net/http — HTTP 客户端相关推荐

  1. 如何在云计算平台使用R语言编程的快速入门指南

    前言 云计算正逐步成为适用于超出笔记本或台式机处理能力的问题或数据的一种自然延伸.然而,对于完全没有基础的初学者来说,学习使用云计算平台会显得比实际更难. 在本文中,我们用信息图的方式向大家介绍云计算 ...

  2. linux c语言 ppt,linux操作系统下c语言编程入门.ppt

    linux操作系统下c语言编程入门.ppt Linux操作系统下C语言编程入门 CNT Linux操作系统简介基础知识进程介绍文件操作时间概念消息管理线程操作网络编程Linux下C开发工具介绍 一 L ...

  3. c语言程序题是如何判分的,C语言编程题判分系统的研究与实现

    摘要: 随着计算机技术和因特网的发展,B/S模式的教学系统在高校教学中显得越来越重要.C语言程序设计是计算机专业的一门重要的基础课程,具有实践性强的特点,在教学中应加大实践教学比例.本文利用网络技术开 ...

  4. java语言就业方向_四大就业方向彻底解决你学习Java语言编程的后顾之忧-生活感悟与随笔 -亦是美网络...

    前面的文章中已经给大家介绍了关于Java编程语言的优势.特点以及就业前景等内容,大家之所以这么热衷于Java编程学习,不仅是因为Java是编程市场上最为广泛流行的语言,大家更关心的是学习了Java编程 ...

  5. Linux下安装MySQL数据库以及用C语言编程存取数据库

    ubuntu下安装软件相当简单,一条简单的 apt-get install 就可以解决,相比源码安装方式唯一的缺点就是,你无法自定义软件的安装目录.不过这也不是什么太大的缺点.下面我们就用 apt-g ...

  6. linux环境c语言编程 蔡晋,Linux环境C语言编程

    Linux环境C语言编程第1讲linux系统环境介绍 Linux环境C语言编程第2讲命令行解析+环境变量+gcc基本参数 Linux环境C语言编程第3讲共享库.gdb的使用 Linux环境C语言编程第 ...

  7. R语言编程艺术(4)R对数据、文件、字符串以及图形的处理

    本文对应<R语言编程艺术> 第8章:数学运算与模拟: 第10章:输入与输出: 第11章:字符串操作: 第12章:绘图 =================================== ...

  8. ROS系统下完成TCP通信 C语言编程

    ROS系统下完成TCP通信,服务端和客户端C语言编程 服务端(Server): #include <std_msgs/Bool.h> #include "ros/ros.h&qu ...

  9. linux c语言编程(转)

    linux操作系统下 c语言编程入门 整理编写:007xiong 原文:Hoyt等 (一)目录介绍 1)Linux程序设计入门--基础知识 2)Linux程序设计入门--进程介绍 3)Linux程序设 ...

最新文章

  1. 【Java小工匠聊密码学】--base58编码
  2. Python最简编码规范
  3. cocos2d笔记——解析HelloWorldScene
  4. Java的this和super关键字详解
  5. queue的常见用法详解
  6. Java中的垃圾回收
  7. 一些SAP德国总部的照片
  8. 引用类型和原始类型的对比(java)
  9. php生成随机不重复的数字_PHP生成不重复随机数的几种方法
  10. 站在BERT肩膀上的NLP新秀们(PART II)
  11. python代码写好了怎么运行-教你如何编写、保存与运行 Python 程序
  12. oracle函数 power(x,y)
  13. iOS 14.7 中的所有新功能
  14. asp是怎么获取header的?_什么是微服务架构?来看看从业10年的架构师是怎么回答的吧...
  15. 不依赖第三方环境和服务
  16. 用excel做logistic回归分析_用Excel做回归分析
  17. 可能是求质数最高效的算法
  18. 运营周期爆发期是什么?
  19. 我国IPTV研究目前已达到全球领先水平
  20. 分享一款开源堡垒机-jumpserver

热门文章

  1. 机器运算知识点计算机组成原理,计算机组成原理考研知识点非常全
  2. python课后题答案第五章_Python语言程序设计(美-梁勇)第5章习题解答
  3. html 控件坐标定位,利用JS改变html控件位置
  4. PHP支持模板,让ThinkPHP5支持模板主题(参照ThinkPHP3.2)
  5. 王者服务器维修2019年四月份,2019王者荣耀4周年庆版本更新时间介绍[图]
  6. 新冠轻症也会导致大脑退化,牛津大学最新研究登上Nature
  7. 全球最大中文单体模型来了!2600亿参数,AI产业规模化应用可期
  8. Linker加载so失败问题分析
  9. 在家也能做化学实验!VR教育机构MEL Science获250万美元融资
  10. 139.00.005 Git学习-分支管理