Go36-47-基于HTTP协议的网络服务(net/http)

基于HTTP协议的网络服务

HTTP协议是基于TCP/IP协议栈的,并且是一个面向普通文本的协议。原则上,使用任何一个文本编辑器,都可以写出一个完整的HTTP请求报文。只要搞清楚了请求报文的头部(header、请求头)和主体(body、请求体)应该包含的内容。
如果只是访问基于HTTP协议的网络服务,那么使用net/http包中的程序实体会非常方便。

http.Get函数

调用http.Get函数,只需要传递给它一个URL即可:

package main

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

func main() {
    resp, err := http.Get("http://baidu.com")
    if err != nil {
        fmt.Fprintf(os.Stderr, "request sending error: %v\n", err)
        return
    }
    defer resp.Body.Close()
    line := resp.Proto + " " + resp.Status
    fmt.Println("返回的第一行的内容:", line)
}

http.Get函数会返回两个结果:

  • (resp *Response): 网络服务返回的响应内容的结构化表示
  • (err error): 创建和发送HTTP请求,以及接收和解析HTTP响应的过程中可能发生的错误

http.Get函数会在内部使用缺省的HTTP客户端,并且调用它的Get方法来完成功能。这个缺省的HTTP客户端就是net/http包中的公开变量DefaultClient,源码中是这样的:

// 源码中提供的缺省的客户端
var DefaultClient = &Client{}

// 使用缺省的客户端调用Get方法
func Get(url string) () {
    return DefaultClient.Get(url)
}

所以下面的这两行代码:

var httpClient http.Client
resp, err := httpClient.Get(utl)

与示例中的这一行代码:

resp, err := http.Get(url)

是等价的。这里只是不使用DefaultClient而是自己创建了一个客户端。

http.Client类型

http.Client是一个结构体,并且它包含的字段都是公开的:

type Client struct {
    Transport RoundTripper
    CheckRedirect func(req *Request, via []*Request) error
    Jar CookieJar
    Timeout time.Duration
}

该类型是开箱即用的,因为它的所有字段,要么存在相应的缺省值,要么其零值直接就可以使用,并且代表着特定的含义。

Transport字段

主要看下Transport字段,该字段向网络服务发送HTTP请求,并从网络服务接收HTTP响应。该字段的方法RoundTrip应该实现单次HTTP事务(或者说基于HTTP协议的单次交互)需要的所有步骤。这个字段是一个接口:

type RoundTripper interface {
    RoundTrip(*Request) (*Response, error)
}

并且该字段有一个由http.DefaultTransport变量的缺省值:

func (c *Client) transport() RoundTripper {
    if c.Transport != nil {
        return c.Transport
    }
    return DefaultTransport
}

在初始化http.Client类型的时候,如果没有显式的为该字段赋值,这个Client字段就会直接使用DefaultTransport。

Timeout字段

该字段是单次HTTP事务的超时时间,它是time.Duration类型。它的零值是可用的,用于表示没有设置超时时间。

http.Transport类型

http.Transport类型是一个结构体,该类型包含的字段很多。这里通过http.Client结构体中的Transport字段的缺省值DefaultTransport,来深入了解一下。DefaultTransport是一个*http.Transport的结构体,做了一些默认的设置:

var DefaultTransport RoundTripper = &Transport{
    Proxy: ProxyFromEnvironment,
    DialContext: (&net.Dialer{
        Timeout:   30 * time.Second,
        KeepAlive: 30 * time.Second,
        DualStack: true,
    }).DialContext,
    MaxIdleConns:          100,
    IdleConnTimeout:       90 * time.Second,
    TLSHandshakeTimeout:   10 * time.Second,
    ExpectContinueTimeout: 1 * time.Second,
}

这里Transport结构体的指针就是就是RoundTripper接口的默认实现:

func (t *Transport) RoundTrip(req *Request) (*Response, error) {
    return t.roundTrip(req)
}

这个类型是可以被复用的,并且也推荐被复用。同时它也是并发安全的。所以http.Client类型也是一样,推荐复用,并且并发安全。
看上面的默认设置,http.Transport类型,内部的DialContext字段会使用net.Dialer类型的值,并且把Timeout设置为30秒。仔细看,该值是一个方法,这里把Dialer值的DialContext方法赋值给了DefaultTransport里的同名字段,并且已经设置好了调用该方法时的结构体。

操作超时相关字段

http.Transport类型还包含了很多其他的字段,其中有一些字段是关于操作超时的:

  • IdleConnTimeout: 空闲的连接在多久之后就应该被关闭。DefaultTransport把该值设置为90秒。如果是0,表示不关闭空闲的连接,注意,这样很可能会造成资源的泄露。
  • ResponseHeaderTimeout: 从客户端把请求完全递交给操作系统到从操作系统那里接收到响应报文的最长时长。DefaultTransport没有设定该字段的值。
  • ExpectContinueTimeout: 在客户端递交了请求报文头之后,等待接收第一个响应报文头的最长时间。在客户端想要使用POST发送一个很大的请求给服务端的时候,可以通过发送一个包含了“Expect: 100-continue”的请求头,来询问服务端是否愿意接收这个大请求体。这个字段就是用于设定在这种情况下的超时时间的。如果该字段的值不大于0,那么就不询问了,直接把请求体一并发出,无论多大。这样可能会造成网络资源的浪费。DefaultTransport把该值设置为1秒。
  • TLSHandshakeTimeout: 表示基于TLS协议的连接在被建立时的握手阶段的超时时间。如果是0,则表示没有超时限制。DefaultTransport把该值设置为10秒。

TLS 是 Transport Layer Security 的缩写,可以被翻译为传输层安全。

连接数限制相关字段

此外,还有一些与IdleConnTimeout相关的字段值也值得关注:

  • MaxIdleConns
  • MaxIdleConnsPerHost
  • MaxConnsPerHost

MaxIdleConns
无论当前访问了多少个网络服务,MaxIdleConns字段只会对空闲连接的总数做限定。

MaxIdleConnsPerHost
而MaxIdleConnsPerHost字段限定的是,每一个网络服务的最大空闲连接数。每一个网络服务都有自己的网络地址,可能会使用不同的网络协议,对于一些HTTP请求也可能会用到代理。地址、协议、代理,通脱这三个方面的具体情况来鉴别不同的网络服务。
MaxIdleConnsPerHost是有缺省值的,由常量http.DefaultMaxIdleConnsPerHost表示,值为2:

const DefaultMaxIdleConnsPerHost = 2

func (t *Transport) maxIdleConnsPerHost() int {
    if v := t.MaxIdleConnsPerHost; v != 0 {
        return v
    }
    return DefaultMaxIdleConnsPerHost
}

在默认情况下,每一个网络服务,它的空闲连接数最多只能由2个。

MaxConnsPerHost
MaxConnsPerHost字段限制针对每一个网络服务的最大连接数,不论这些链接是否是空闲的。并且,该字段没有相应的缺省值,零值就是不做限制。

小结
不限制连接数,默认也不限制每一个网络服务的连接数。要限制整体的空闲连接数以及严格限制对每一个网络服务的空闲连接数。

空闲的连接

简单说明一下,为什么会出现空闲的连接。
HTTP协议的请求头里有一个Connection。在HTTP协议的1.1版本中,默认值是“keep-alive”。在这种情况下的网络连接是持久连接的,它们会在当前的HTTP事务完成后仍然保持着连通性,因此是可以被复用的。
既然连接可以被复用,就会有两种可能:

  1. 针对同一个网络服务,有新的HTTP请求被提交,该连接被再次使用。
  2. 不再对该网络服务提交HTTP请求,该连接被闲置。这样就产生了空闲的连接。

另外,如果分配给某一个网络服务的连接过多的话,也可能会导致空闲连接的产生。因为没一个HTTP请求只会使用一个空闲的连接。所以,在大多数情况下,都需要限制空闲连接数。

关闭keep-alive
另外,请求头的Connection还可以设置为“close”,这样就彻彻底杜绝了空闲连接的生成。这会告诉网络服务,这个网络连接不必保持,当前的HTTP事务完成后就可以断开它了。做法是在初始化Transport值的时候,将DisableKeepAlives字段设置为true。
这么做的话,每次提交HTTP请求,就会产生一个新的网络连接。这样会明显的加重网络服务以及客户端的负载,并会让每个HTTP事务都耗费更多的时间。所以默认不设置这个DisableKeepAlives字段。

net.Dialer类型

http.Transport类型,内部的DialContext字段会使用net.Dialer类型的值。在net.Dialer类型中,也有一个KeepAlive字段。该字段是直接作用在底层的socket上的。
它的背后是一种针对网络连接(更确切的是说,是TCP连接)的存活探测机制。它的值用于表示每间隔多长时间发送一次探测包。当该值不大于0是,则表示不开启这种机制。
DefaultTransport会把这个字段设置为30秒。

Client示例

自定义Client和Transport使用的示例:

package main

import (
    "fmt"
    "io/ioutil"
    "net"
    "net/http"
    "strings"
    "sync"
    "time"
)

var domains = []string{
    "baidu.com",
    "sina.com.cn",
    "www.baidu.com",
    "www.sina.com.cn",
    "tieba.baidu.com",
    "news.baidu.com",
    "news.sina.com.cn",
}

func main() {
    myTransport := &http.Transport{
        Proxy: http.ProxyFromEnvironment,
        DialContext: (&net.Dialer{
            Timeout:   15 * time.Second,
            KeepAlive: 15 * time.Second,
            DualStack: true,
        }).DialContext,
        MaxConnsPerHost:       2,
        MaxIdleConns:          10,
        MaxIdleConnsPerHost:   2,
        IdleConnTimeout:       30 * time.Second,
        ResponseHeaderTimeout: 0,
        ExpectContinueTimeout: 1 * time.Second,
        TLSHandshakeTimeout:   10 * time.Second,
    }
    myClient := http.Client{
        Transport: myTransport,
        Timeout:   20 * time.Second,
    }

    var wg sync.WaitGroup
    for _, domain := range domains {
        wg.Add(1)
        go func(domain string) {
            var logBuf strings.Builder
            var diff time.Duration
            defer func() {
                logBuf.WriteString(fmt.Sprintf("持续时间: %s\n", diff))
                fmt.Println(logBuf.String())
                wg.Done()
            }()
            url := "https://" + domain
            logBuf.WriteString(fmt.Sprintf("发送请求: %s\n", url))
            tStart := time.Now()
            resp, err := myClient.Get(url)
            diff = time.Now().Sub(tStart)
            if err != nil {
                logBuf.WriteString(fmt.Sprintf("request get error: %v\n", err))
                return
            }
            defer resp.Body.Close()
            line := resp.Proto + " " + resp.Status
            logBuf.WriteString(fmt.Sprintf("response: %s\n", line))

            data, err := ioutil.ReadAll(resp.Body)
            if err != nil {
                logBuf.WriteString(fmt.Sprintf("get data error: %v\n", err))
                return
            }
            index1 := strings.Index(string(data), "<title>")
            index2 := strings.Index(string(data), "</title>")
            if index1 > 0 && index2 > 0 {
                logBuf.WriteString(fmt.Sprintf("title: %s\n", string(data)[index1+len("<title>"):index2]))
            }
        }(domain)
    }
    wg.Wait()
    fmt.Println("All Done")
}

http.Server类型

http.Server类型与http.Client是相对应的。http.Server代表的是基于HTTP协议的服务端,或者说网络服务。

ListenAndServe方法

http.Server类型的ListenAndServe方法的功能是:监听一个基于TCP协议的网络地址,并对接收到的HTTP请求进行处理。这个方法会默认开启针对网络连接的存活探测机制,以保证连接是持久的。同时,该方法会一直执行,直到有严重的错误发生或者被外界关掉。当被外界关掉时,它会返回一个由http.ErrServerClosed变量代表的错误值。
这个ListenAndServe方法主要会做以下几件事情:

  1. 检查当前的http.Server类型的值的Addr字段。Addr是当前的网络服务需要使用的网络地址,即:IP地址和端口号。如果这个字段的值为空字符串,那么就用":http"代替。也就是说,使用任何可以代表本机的域名和IP地址,并且端口号为80。
  2. 通过调用net.Listen函数在已确定的网络地址上启动基于TCP协议的监听。
  3. 检查net.Listen函数返回的错误值。如果该错误值不为nil,那么就直接返回该错误值。否则,通过调用当前http.Server值的Serve方法准备接受和处理将要到来的HTP请求。

这里又牵出两个问题:

  1. net.Listen函数
  2. http.Server类型的Serve方法

net.Listen函数

net.Listen函数的作用:

  • 解析参数值中包含的网络地址隐含的IP地址和端口号
  • 根据给定的网络协议,确定监听的方法,并开始进行监听

再往下深入的话,就会涉及到net.socket函数以及相关的socket知识。就此打住。

http.Server类型的Serve方法

在一个for循环中,网络监听器Accept方法会不断地调用,该方法的源码如下:

type tcpKeepAliveListener struct {
    *net.TCPListener
}

func (ln tcpKeepAliveListener) Accept() (net.Conn, error) {
    tc, err := ln.AcceptTCP()
    if err != nil {
        return nil, err
    }
    tc.SetKeepAlive(true)
    tc.SetKeepAlivePeriod(3 * time.Minute)
    return tc, nil
}

Accept方法会返回两个结果值:

  • net.Conn : 代表包含了新到来的HTTP请求的网络连接
  • error : 代表了可能发生的错误的error的类型值

当错误值不为nil时,如果此时是一个暂时性的错误,那么循环的下一次迭代将会在一段时间之后开始执行。否则,循环会被终止。
如果没有错误,返回的错误值就是nil。那么这里的程序将会把它的第一个结果值包装成一个*http.conn类型的值,然后通过在新的goroutine中调用这个conn值的serve方法,来对当前的HTTP请求进行处理。

上面最后说的处理的细节还是很多的:

  • conn值的各种状态,各状态代表的处理阶段
  • 处理过程中会用到的读取器和写入器,及其作用
  • 让程序调用自定义的处理函数

这些都没有一一说明,建议去看下源码。

Server示例

在下面的示例中,启动了3个Server。启动后,可以用浏览器访问进行验证:

package main

import (
    "fmt"
    "net/http"
    "os"
    "sync"
)

var wg sync.WaitGroup

// 一般没有这么用的,http.Server的Handler字段
// 要么是nil,就用包里的http.DefaultServeMux
// 要么用NewServeMux()来创建一个*http.ServeMux
// 我这里按照http.Handler接口的要求实现了一个,赋值给Handler字段
// 这个自定义的Handler不支持路由
func startServer1() {
    defer wg.Done()
    var httpServer http.Server
    httpServer.Addr = "127.0.0.1:8001"
    httpServer.Handler = http.HandlerFunc(
        func(w http.ResponseWriter, r *http.Request) {
            fmt.Println(*r)
            fmt.Fprint(w, "Hello World")
        },
    )
    fmt.Println("启动服务,访问: http://127.0.0.1:8001")
    if err := httpServer.ListenAndServe(); err != nil {
        if err == http.ErrServerClosed {
            fmt.Println("HTTP Server1 Closed.")
        } else {
            fmt.Fprintf(os.Stderr, "HTTP Server1 Error: %v\n", err)
        }
    }
}

// 这个最简单,都是调用http包里的函数。本质上还是要调用方法的,都会用默认的或是零值
func startServer2() {
    defer wg.Done()
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprint(w, "Hello World\nThis is Server2")
    })
    fmt.Println("启动服务,访问: http://127.0.0.1:8002")
    // 第二个参数传nil,就是用包里的http.DefaultServeMux,或者也可以自己创建一个传给第二个参数
    if err := http.ListenAndServe("127.0.0.1:8002", nil); err != nil {
        if err == http.ErrServerClosed {
            fmt.Println("HTTP Server2 Closed.")
        } else {
            fmt.Fprintf(os.Stderr, "HTTP Server2 Error: %v\n", err)
        }
    }
}

// 这个例子里用到了解析Get请求的参数,并且还设置了2个路由
func startServer3() {
    defer wg.Done()
    mux := http.NewServeMux()
    mux.HandleFunc("/hi", func(w http.ResponseWriter, r *http.Request) {
        if r.URL.Path != "/hi" {
            // 这个分支应该是进不来的,因为要进入这个分支,路径应该必须是"/hi"
            fmt.Println("Server3 hi 404")
            http.NotFound(w, r)
            return
        }
        name := r.FormValue("name")
        if name == "" {
            fmt.Fprint(w, "Hi!")
        } else {
            fmt.Fprintf(w, "Hi, %s!", name)
        }
    })
    mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprint(w, "Hello World\nThis is Server3")
    })
    // 如果只是定义http.Server的下面2个字段,完全可以使用http.ListenAndServe函数来启动服务
    // 这样的用法可以对http.Server里更多的字段进行自定义
    httpServer := http.Server{
        Addr: "127.0.0.1:8003",
        Handler: mux,
    }
    fmt.Println("启动服务,访问: http://127.0.0.1:8003/hi?name=Adam")
    if err := httpServer.ListenAndServe(); err != nil {
        if err == http.ErrServerClosed {
            fmt.Println("HTTP Server3 Closed.")
        } else {
            fmt.Fprintf(os.Stderr, "HTTP Server3 Error: %v\n", err)
        }
    }
}

func main() {
    wg.Add(1)
    go startServer1()
    wg.Add(1)
    go startServer2()
    wg.Add(1)
    go startServer3()
    wg.Wait()
}

补充-优雅的停止HTTP服务

包里还提供了一个Shutdown方法,可以优雅的停止HTTP服务:

func (srv *Server) Shutdown(ctx context.Context) error {
    // 内容省略
}

我们要做的就是在需要的时候,可以调用该Shutdown方法。
这里的问题是,调用了ListenAndServe方法之后,就进入了无限循环的流程。这里最好是用一个goroutine来启动ListenAndServe方法,在goroutine外声明http.Server。然后在主线程里等待一个信号,比如是从通道接收值。这样就可以在主线程里调用这个Shutdown方法执行了。

原文地址:http://blog.51cto.com/steed/2349067

时间: 2024-10-12 12:19:47

Go36-47-基于HTTP协议的网络服务(net/http)的相关文章

实现基于NTP协议的网络校时功能

无论PC端还是移动端系统都自带时间同步功能,基于的都是NTP协议,这里使用C#来实现基于NTP协议的网络校时功能(也就是实现时间同步). 1.NTP原理 NTP[Network Time Protocol]是用来使计算机时间同步化的一种协议,它可以使计算机对其服务器或时钟源(如石英钟,GPS等等)做同步化,它可以提供高精准度的时间校正(LAN上与标准间差小于1毫秒,WAN上几十毫秒),且可介由加密确认的方式来防止恶毒的协议攻击. 先介绍下NTP数据包格式(其标准化文档为RFC2030,NTP版本

Java中的基于Tcp协议的网络编程

一:网络通信的三要素? IP地址     端口号     通信协议 IP地址:是网络中设备的通信地址.由于IP地址不易记忆,故可以使用主机名.本地环回地址,127.0.0.1   本地主机名localhost 端口号:发送端准备的数据要发送到指定的目的应用程序上,为了标识这些应用程序,所以用网络数字来标识这些不同的应用程序,这些数 字称为端口号.端口号是不同进程之间的标识.一般来说,有0~65535的端口可供使用,但是1~1024系统使用,或者称作保留端口. 通信协议:指定义的通信规则,这个规则

基于 UDP 协议的网络编程

类 DatagramSocket 和 DatagramPacket 实现了基于 UDP 协议网络程序 UDP 数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证 UDP 数据报一定能够安全送到目的地,也不能确定什么时候可以抵达 DatagramPacket 对象封装了 UDP 数据报(<64k),在数据报中包含了发送端的 IP 地址和端口号以及接收端的 IP 地址和端口号 UDP 协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和接收方的连接 举例: publ

基于UDP协议的网络编程

UDP协议是一种不可靠的网络协议,它在通信实例的两端各建立一个Socket,但这两个Socket之间并没有虚拟链路,这两个Socket只是发送.接收数据报的对象. Java使用DatagramSocket代表基于UDP协议的Socket,DatagramSocket本身只是码头,不维护状态,不能产生IO流,它的唯一作用就是接收和发送数据报.Java使用DatagramPacket来代表数据报,DatagramSocket接收和发送数据都是通过DatagramPacket对象完成的. Datagr

基于UDP协议的网络程序

下图是基于UDP协议的客户端/服务器程序的一般流程: 图1.1 UDP协议通信流程 UDP套接口是无连接的.不可靠的数据报协议: 既然他不可靠为什么还要用呢?其一:当应用程序使用广播或多播时只能使用UDP协议:其二:由于他是无连接的,所以速度快.因为UDP套接口是无连接的,如果一方的数据报丢失,那另一方将无限等待,解决办法是设置一个超时. 建立UDP套接口时socket函数的第二个参数应该是SOCK_DGRAM,说明是建立一个UDP套接口:由于UDP是无连接的,所以服务器端并不需要listen或

学习笔记——网络编程3(基于TCP协议的网络编程)

TCP协议基础 IP协议是Internet上使用的一个关键协议,它的全称是Internet Protocol,即Internet协议,通常简称IP协议. 使用ServerSocket创建TCP服务器 在两个通信实体没有建立虚拟链路之前,必须有一个通信实体先做出“主动姿态”,主动接收来自其他通信实体的连接请求. Java中能接收其他通信实体连接请求的类是ServerSocket,ServerSocket对象用于监听来自客户端Socket连接,如果没有连接,它将一直处于等待状态. ServerSoc

第13章 TCP编程(3)_基于自定义协议的多进程模型

5. 自定义协议编程 (1)自定义协议:MSG //自定义的协议(TLV:Type length Value) typedef struct{ //协议头部 char head[10];//TLV中的T unsigned int checkNum; //校验码 unsigned int cbSizeContent; //协议体的长度 //协议体部 char buff[512]; //数据 }MSG; (2)自定义读写函数 ①extern int write_msg(int sockfd, cha

第13章 TCP编程(4)_基于自定义协议的多线程模型

7. 基于自定义协议的多线程模型 (1)服务端编程 ①主线程负责调用accept与客户端连接 ②当接受客户端连接后,创建子线程来服务客户端,以处理多客户端的并发访问. ③服务端接到的客户端信息后,回显给客户端 (2)客户端编程 ①从键盘输入信息,并发送给服务端 ②接收来自服务端的信息 //msg.h与前一节相同 #ifndef __MSG_H__ #define __MSG_H__ #include <sys/types.h> //求结构体中成员变量的偏移地址 #define OFFSET(T

基于TCP协议的网络通信

**重点内容**1.使用InetAddress(IP地址类) 这个类有点儿奇葩,没有提供构造方法.而是有两个静态方法来实例化. ·getByName(String host) 通过主机名获取对应的InetAddress对象 ·getByAddress(byte[] addr) 通过IP地址获取对应的InetAddress对象 getCononicalHostName() 获取IP地址的全限定域名 getHostAddress() 返回IP地址字符串 getHostName() 获取IP地址的主机