Skip to content

Latest commit

 

History

History
908 lines (676 loc) · 27.1 KB

File metadata and controls

908 lines (676 loc) · 27.1 KB

七、连接网络

本章包含以下配方:

  • 解析本地 IP 地址
  • 连接到远程服务器
  • 按 IP 地址解析域,反之亦然
  • 连接到 HTTP 服务器
  • 解析和构建 URL
  • 创建 HTTP 请求
  • 读取和写入 HTTP 头
  • 处理 HTTP 重定向
  • 使用 RESTful API
  • 发送简单的电子邮件
  • 调用 JSON-RPC 服务

介绍

这一章是关于网络的。本章中的大多数食谱都集中在客户端。我们将介绍如何解析计算机上网络的基本信息、域名和 IP 解析,以及如何通过 HTTP 和 SMTP 等 TCP 相关协议进行连接。最后,我们将使用标准库通过 JSON-RCP1.0 进行远程过程调用。

检查 Go 是否正确安装。从第一章与环境互动检索咕噜版本配方的准备部分将帮助您。验证是否有任何其他应用程序阻塞了7070端口。

解析本地 IP 地址

此配方说明如何从可用的本地接口检索 IP 地址。

怎么做。。。

  1. 打开控制台,创建文件夹chapter07/recipe01
  2. 导航到该目录。
  3. 创建具有以下内容的interfaces.go文件:
        package main

        import (
          "fmt"
          "net"
        )

        func main() {

          // Get all network interfaces
          interfaces, err := net.Interfaces()
          if err != nil {
            panic(err)
          }

          for _, interf := range interfaces {
            // Resolve addresses
            // for each interface
            addrs, err := interf.Addrs()
            if err != nil {
              panic(err)
            }
            fmt.Println(interf.Name)
            for _, add := range addrs {
              if ip, ok := add.(*net.IPNet); ok {
                fmt.Printf("\t%v\n", ip)
              }
            }

          }
        }
  1. 在主终端运行go run interfaces.go执行代码。
  2. 您将看到以下输出:

它是如何工作的。。。

net 包包含Interfaces函数,该函数将网络接口列为Interface结构的一部分。Interface结构具有Addrs方法,该方法列出了可用的网络地址。通过这种方式,您可以按其接口列出地址。

另一个选项是使用net包的InterfaceAddrs函数,它提供实现Addr接口的结构片。这为您提供了获取所需信息的方法。

连接到远程服务器

基于 TCP 的协议是网络通信中最重要的协议。提醒一下,HTTP、FTP、SMTP 和其他协议都是这个组的一部分。

怎么做。。。

  1. 打开控制台,创建文件夹chapter07/recipe02
  2. 导航到该目录。
  3. 创建具有以下内容的tcpclient.go文件:
        package main

        import (
          "bufio"
          "context"
          "fmt"
          "io"
          "net"
          "net/http"
          "time"
        )

        type StringServer string

        func (s StringServer) ServeHTTP(rw http.ResponseWriter,
                                        req *http.Request) {
          rw.Write([]byte(string(s)))
        }

        func createServer(addr string) http.Server {
          return http.Server{
            Addr: addr,
            Handler: StringServer("HELLO GOPHER!\n"),
          }
       }

       const addr = "localhost:7070"

       func main() {
         s := createServer(addr)
         go s.ListenAndServe()

         // Connect with plain TCP
         conn, err := net.Dial("tcp", addr)
         if err != nil {
           panic(err)
         }
         defer conn.Close()

         _, err = io.WriteString(conn, "GET / HTTP/1.1\r\nHost:
                                 localhost:7070\r\n\r\n")
         if err != nil {
           panic(err)
         }

         scanner := bufio.NewScanner(conn)
         conn.SetReadDeadline(time.Now().Add(time.Second))
         for scanner.Scan() {
           fmt.Println(scanner.Text())
         }

         ctx, _ := context.WithTimeout(context.Background(),
                                       5*time.Second)
         s.Shutdown(ctx)

       }
  1. 在主终端运行go run tcpclient.go执行代码。
  2. 您将看到以下输出:

它是如何工作的。。。

网络包包含Dial函数,该函数使用网络类型和地址。在上例中,网络为tcp,地址为localhost:8080

一旦Dial函数成功,返回Conn类型,作为打开套接字的引用。Conn接口还定义了ReadWrite函数,因此它们可以用作WriterReader函数,用于从套接字进行写入和读取。最后,示例代码使用Scanner获得响应。请注意,在这种情况下,Scanner因制动管路而起作用。否则,应使用更通用的Read方法。在本例中,通过SetReadDeadline方法设置Read截止日期。重要的是,最后期限不是一个持续时间,而是一个Time。这意味着最后期限被设置为将来的一个时间点。如果您正在从循环中的套接字读取数据,并且需要将读取超时设置为 10 秒,则每次迭代都应包含类似于conn.SetReadDeadline(time.Now().Add(10*time.Second))的代码。

为了启发整个代码示例,使用HTTP标准包中的 HTTP 服务器作为客户机的对应物。这一部分包含在单独的配方中。

按 IP 地址解析域,反之亦然

本食谱将向您介绍如何将 IP 地址转换为主机地址,反之亦然。

怎么做。。。

  1. 打开控制台,创建文件夹chapter07/recipe03
  2. 导航到该目录。
  3. 创建具有以下内容的lookup.go文件:
        package main

        import (
          "fmt"
          "net"
        )

        func main() {

          // Resolve by IP
          addrs, err := net.LookupAddr("127.0.0.1")
          if err != nil {
            panic(err)
          }

          for _, addr := range addrs {
            fmt.Println(addr)
          }

          //Resolve by address
          ips, err := net.LookupIP("localhost")
          if err != nil {
            panic(err)
          }

          for _, ip := range ips {
            fmt.Println(ip.String())
          }
        }
  1. 在主终端运行go run lookup.go执行代码。
  2. 您将看到以下输出:

它是如何工作的。。。

从 IP 地址解析域名可以通过net包中的LookupAddr功能完成。为了从域名中找到IP地址,应用了LookupIP功能

连接到 HTTP 服务器

前面的方法连接到远程服务器让我们了解了如何在较低级别连接 TCP 服务器。在此配方中,将显示与更高级别的 HTTP 服务器的通信。

怎么做。。。

  1. 打开控制台,创建文件夹chapter07/recipe04
  2. 导航到该目录。
  3. 创建具有以下内容的http.go文件:
        package main

        import (
          "fmt"
          "io/ioutil"
          "net/http"
          "net/url"
          "strings"
        )

        type StringServer string

        func (s StringServer) ServeHTTP(rw http.ResponseWriter,
                                        req *http.Request) {
          req.ParseForm()
          fmt.Printf("Received form data: %v\n", req.Form)
          rw.Write([]byte(string(s)))
        } 

        func createServer(addr string) http.Server {
          return http.Server{
            Addr: addr,
            Handler: StringServer("Hello world"),
          }
        }

        const addr = "localhost:7070"

        func main() {
          s := createServer(addr)
          go s.ListenAndServe()

          useRequest()
          simplePost()

        }

        func simplePost() {
          res, err := http.Post("http://localhost:7070",
                          "application/x-www-form-urlencoded",
                          strings.NewReader("name=Radek&surname=Sohlich"))
          if err != nil {
            panic(err)
          }

          data, err := ioutil.ReadAll(res.Body)
          if err != nil {
            panic(err)
          }
          res.Body.Close()
          fmt.Println("Response from server:" + string(data))
        }

        func useRequest() {

          hc := http.Client{}
          form := url.Values{}
          form.Add("name", "Radek")
          form.Add("surname", "Sohlich")

          req, err := http.NewRequest("POST",
                        "http://localhost:7070",
                        strings.NewReader(form.Encode()))
                        req.Header.Add("Content-Type",
                        "application/x-www-form-urlencoded")

          res, err := hc.Do(req)

          if err != nil {
            panic(err)
          }

          data, err := ioutil.ReadAll(res.Body)
          if err != nil {
            panic(err)
          }
          res.Body.Close()
          fmt.Println("Response from server:" + string(data))
        }
  1. 在主终端运行go run http.go执行代码。
  2. 您将看到以下输出:

它是如何工作的。。。

通过net/http包可以连接到 HTTP 服务器。当然,有更多的方法可以实现这一点,但上面的代码说明了两种最常见的方法。第一个选项实现了simplePost功能,并说明了默认客户端的使用。这里选择 POST 方法,因为它比 GET 更复杂。Post方法接受Reader形式的 URL、内容类型和主体。调用Post函数会立即请求服务器并返回结果。

注意,Post方法只是包装了一个在实现中使用http.DefaultClient的函数。net/http包还包含Get功能。

useRequest函数实现了相同的功能,但使用了更可定制的 API 和它自己的Client实例。该实现利用NewRequest函数根据以下给定参数创建请求:方法、URL 和请求体。内容类型必须单独设置为Header属性。请求通过在Client上创建的Do方法执行。

另见

创建 HTTP 请求配方将帮助您详细组装请求。

解析和构建 URL

在许多情况下,使用方便的工具操作 URL 比尝试将其作为简单字符串处理要好。Go 标准库自然包含用于操作 URL 的实用程序。本食谱将介绍其中一些主要功能。

怎么做。。。

  1. 打开控制台,创建文件夹chapter07/recipe05
  2. 导航到该目录。
  3. 创建具有以下内容的url.go文件:
        package main

        import (
          "encoding/json"
          "fmt"
          "net/url"
        )

        func main() {

          u := &url.URL{}
          u.Scheme = "http"
          u.Host = "localhost"
          u.Path = "index.html"
          u.RawQuery = "id=1&name=John"
          u.User = url.UserPassword("admin", "1234")

          fmt.Printf("Assembled URL:\n%v\n\n\n", u)

          parsedURL, err := url.Parse(u.String())
          if err != nil {
            panic(err)
          }
          jsonURL, err := json.Marshal(parsedURL)
          if err != nil {
            panic(err)
          }
          fmt.Println("Parsed URL:")
          fmt.Println(string(jsonURL))

        }
  1. 在主终端运行go run url.go执行代码。
  2. 您将看到以下输出:

它是如何工作的。。。

net/url包旨在帮助您操作和解析 URL。URL结构包含将 URL 放在一起所需的字段。通过URL结构的String方法,可以轻松地转换为简单字符串。

当字符串表示可用且需要额外操作时,可以使用net/urlParse功能。通过这种方式,可以将字符串转换为URL结构,并修改底层 URL。

创建 HTTP 请求

此配方将向您展示如何使用特定参数构造 HTTP 请求。

怎么做。。。

  1. 打开控制台,创建文件夹chapter07/recipe06

  2. 导航到该目录。

  3. 创建具有以下内容的request.go文件:

        package main

        import (
          "fmt"
          "io/ioutil"
          "net/http"
          "net/url"
          "strings"
        )

        type StringServer string

        func (s StringServer) ServeHTTP(rw http.ResponseWriter,
                                        req *http.Request) {
          req.ParseForm()
          fmt.Printf("Received form data: %v\n", req.Form)
          fmt.Printf("Received header: %v\n", req.Header)
          rw.Write([]byte(string(s)))
        }

        func createServer(addr string) http.Server {
          return http.Server{
            Addr: addr,
            Handler: StringServer("Hello world"),
          }
        } 

        const addr = "localhost:7070"

        func main() {
          s := createServer(addr)
          go s.ListenAndServe()

          form := url.Values{}
          form.Set("id", "5")
          form.Set("name", "Wolfgang")

          req, err := http.NewRequest(http.MethodPost,
                              "http://localhost:7070",
                              strings.NewReader(form.Encode()))

          if err != nil {
            panic(err)
          }
          req.Header.Set("Content-Type",
                         "application/x-www-form-urlencoded")

          res, err := http.DefaultClient.Do(req)
          if err != nil {
            panic(err)
          }
          data, err := ioutil.ReadAll(res.Body)
          if err != nil {
            panic(err)
          }
          res.Body.Close()
          fmt.Println("Response from server:" + string(data))

        }
  1. 在主终端运行go run request.go执行代码。
  2. 您将看到以下输出:

它是如何工作的。。。

示例代码中给出了构造请求的更复杂的方法。使用net/http包的NewRequest方法,返回指向Request结构的指针。函数使用方法的请求、URL 和请求体。注意表单的构建方式。不使用普通字符串,而是使用url.Values结构。最后,调用Encode方法对给定的表单值进行编码。通过请求的http.Header属性设置头。

读取和写入 HTTP 头

前面的配方描述了如何创建 HTTP 请求。本食谱将详细介绍如何读取和写入请求头。

怎么做。。。

  1. 打开控制台,创建文件夹chapter07/recipe07
  2. 导航到该目录。
  3. 创建具有以下内容的headers.go文件:
        package main

        import (
          "fmt"
          "net/http"
        )

        func main() {

          header := http.Header{}

          // Using the header as slice
          header.Set("Auth-X", "abcdef1234")
          header.Add("Auth-X", "defghijkl")
          fmt.Println(header)

          // retrieving slice of values in header
          resSlice := header["Auth-X"]
          fmt.Println(resSlice)

          // get the first value
          resFirst := header.Get("Auth-X")
          fmt.Println(resFirst)

          // replace all existing values with
          // this one
          header.Set("Auth-X", "newvalue")
          fmt.Println(header)

          // Remove header
          header.Del("Auth-X")
          fmt.Println(header)

        }
  1. 在主终端运行go run headers.go执行代码。
  2. 您将看到以下输出:

它是如何工作的。。。

实际上,http包中的头被表示为map[string][]string,这样就必须处理Header类型。前面的代码显示了如何设置和读取标题值。关于头的重要一点是头键的值是string片。因此,头中的每个键都可以包含多个值。

Header类型的Set方法设置给定键下的一个项目切片。另一方面,Add方法将值附加到切片。

使用Get方法将从给定键下的切片中检索第一个值。如果需要整个切片,Header需要作为地图处理。可以使用Del方法移除整个头键。

服务器和客户端都使用RequestHeader类型的http包,因此服务器端和客户端的处理是相同的。

处理 HTTP 重定向

在某些情况下,您需要更多地控制重定向的处理方式。此配方将向您展示 Go 客户端实现的机制,以便您能够更好地控制 HTTP 重定向的处理。

怎么做。。。

  1. 打开控制台,创建文件夹chapter07/recipe08
  2. 导航到该目录。
  3. 创建具有以下内容的redirects.go文件:
        package main

        import (
          "fmt"
          "net/http"
        )

        const addr = "localhost:7070"

        type RedirecServer struct {
          redirectCount int
        }

        func (s *RedirecServer) ServeHTTP(rw http.ResponseWriter,
                                          req *http.Request) {
          s.redirectCount++
          fmt.Println("Received header: " + 
                      req.Header.Get("Known-redirects"))
          http.Redirect(rw, req, fmt.Sprintf("/redirect%d",
                        s.redirectCount), http.StatusTemporaryRedirect)
        }

        func main() {
          s := http.Server{
            Addr: addr,
            Handler: &RedirecServer{0},
          }
          go s.ListenAndServe()

          client := http.Client{}
          redirectCount := 0

          // If the count of redirects is reached
          // than return error.
          client.CheckRedirect = func(req *http.Request, 
                                 via []*http.Request) error {
            fmt.Println("Redirected")
            if redirectCount > 2 {
              return fmt.Errorf("Too many redirects")
            }
            req.Header.Set("Known-redirects", fmt.Sprintf("%d",
                           redirectCount))
            redirectCount++
            for _, prReq := range via {
              fmt.Printf("Previous request: %v\n", prReq.URL)
            }
            return nil
          }

          _, err := client.Get("http://" + addr)
          if err != nil {
            panic(err)
          }
        }
  1. 在主终端运行go run redirects.go执行代码。
  2. 您将看到以下输出:

它是如何工作的。。。

http包的Client包含CheckRedirect字段。该字段是一个具有reqvia参数的函数。req是即将到来的请求,via是之前的请求。这样,您可以在重定向后修改请求。在前面的示例中,Known-redirects标题被修改。

如果CheckRedirect函数返回错误,则返回最后一个带有封装错误的封闭体的响应。如果返回http.ErrUseLastResponse,则返回最后一个响应,但主体未关闭,因此可以读取。

默认情况下,CheckRedirect属性为零。在这种情况下,它的重定向限制为 10 次。在此计数之后,重定向将停止。

使用 RESTful API

RESTful API 是应用程序和服务器提供对其服务的访问的最常见方式。此配方将向您展示如何在标准库的 HTTP 客户端的帮助下使用它。

怎么做。。。

  1. 打开控制台,创建文件夹chapter07/recipe09
  2. 导航到该目录。
  3. 创建具有以下内容的rest.go文件:
        package main

        import (
          "encoding/json"
          "fmt"
          "io"
          "io/ioutil"
          "net/http"
          "strconv"
          "strings"
        )

        const addr = "localhost:7070"

        type City struct {
          ID string
          Name string `json:"name"`
          Location string `json:"location"`
        }

        func (c City) toJson() string {
          return fmt.Sprintf(`{"name":"%s","location":"%s"}`,
                             c.Name, c.Location)
        }

        func main() {
          s := createServer(addr)
          go s.ListenAndServe()

          cities, err := getCities()
          if err != nil {
            panic(err)
          }
          fmt.Printf("Retrived cities: %v\n", cities)

          city, err := saveCity(City{"", "Paris", "France"})
          if err != nil {
            panic(err)
          }
          fmt.Printf("Saved city: %v\n", city)

        }

        func saveCity(city City) (City, error) {
          r, err := http.Post("http://"+addr+"/cities",
                              "application/json",
                               strings.NewReader(city.toJson()))
          if err != nil {
            return City{}, err
          }
          defer r.Body.Close()
          return decodeCity(r.Body)
        }

        func getCities() ([]City, error) {
          r, err := http.Get("http://" + addr + "/cities")
          if err != nil {
            return nil, err
          }
          defer r.Body.Close()
          return decodeCities(r.Body)
        }

        func decodeCity(r io.Reader) (City, error) {
          city := City{}
          dec := json.NewDecoder(r)
          err := dec.Decode(&city)
          return city, err
        }

       func decodeCities(r io.Reader) ([]City, error) {
         cities := []City{}
         dec := json.NewDecoder(r)
         err := dec.Decode(&cities)
         return cities, err
       }

       func createServer(addr string) http.Server {
         cities := []City{City{"1", "Prague", "Czechia"},
                          City{"2", "Bratislava", "Slovakia"}}
         mux := http.NewServeMux()
         mux.HandleFunc("/cities", func(w http.ResponseWriter,
                                        r *http.Request) {
           enc := json.NewEncoder(w)
           if r.Method == http.MethodGet {
             enc.Encode(cities)
           } else if r.Method == http.MethodPost {
             data, err := ioutil.ReadAll(r.Body)
             if err != nil {
               http.Error(w, err.Error(), 500)
             }
             r.Body.Close()
             city := City{}
             json.Unmarshal(data, &city)
             city.ID = strconv.Itoa(len(cities) + 1)
             cities = append(cities, city)
             enc.Encode(city)
           }

         })
         return http.Server{
           Addr: addr,
           Handler: mux,
         }
       }
  1. 在主终端运行go run rest.go执行代码。
  2. 您将看到以下输出:

它是如何工作的。。。

前面的示例代码显示了 RESTAPI 的外观以及如何使用它。注意,decodeCitydecodeCities函数得益于请求的Body实现了Reader接口。结构的反序列化通过json.Decoder完成。

发送简单的电子邮件

本食谱将简要介绍如何使用标准库连接到 SMTP 服务器并发送电子邮件。

准备

在此配方中,我们将使用谷歌 Gmail 帐户发送电子邮件。通过一些配置,此方法对于其他 SMTP 服务器也很有用。

怎么做。。。

  1. 打开控制台,创建文件夹chapter07/recipe10
  2. 导航到该目录。
  3. 创建具有以下内容的smtp.go文件:
        package main

        import (
          "crypto/tls"
          "fmt"
          "net/smtp"
        )

        func main() {

          var email string
          fmt.Println("Enter username for smtp: ")
          fmt.Scanln(&email)

          var pass string
          fmt.Println("Enter password for smtp: ")
          fmt.Scanln(&pass)

          auth := smtp.PlainAuth("", email, pass, "smtp.gmail.com")

          c, err := smtp.Dial("smtp.gmail.com:587")
          if err != nil {
            panic(err)
          }
          defer c.Close()
          config := &tls.Config{ServerName: "smtp.gmail.com"}

          if err = c.StartTLS(config); err != nil {
            panic(err)
          }

          if err = c.Auth(auth); err != nil {
            panic(err)
          }

          if err = c.Mail(email); err != nil {
            panic(err)
          }
          if err = c.Rcpt(email); err != nil {
            panic(err)
          }

          w, err := c.Data()
          if err != nil {
            panic(err)
          }

          msg := []byte("Hello this is content")
          if _, err := w.Write(msg); err != nil {
            panic(err)
          }

          err = w.Close()
          if err != nil {
            panic(err)
          }
          err = c.Quit()

          if err != nil {
            panic(err)
          }

        }
  1. 在主终端运行go run smtp.go执行代码。
  2. 输入帐户的电子邮件(谷歌帐户)并点击输入
  3. 输入帐号密码,点击输入
  4. 在检查电子邮件框之前,您将看到以下输出:

它是如何工作的。。。

smtp包提供与 SMTP 服务器交互的基本功能。Dial功能提供给客户端。客户端最重要的方法是Mail,设置发件人邮件;Rcpt设置收件人邮件;Data提供可以写入邮件内容的Writer。最后,Quit方法发送 QUIT 并关闭与服务器的连接。

前面的示例使用到 SMTP 服务器的安全连接,因此使用客户端的Auth方法设置身份验证,并调用StartTLS方法启动到服务器的安全连接。

注意,Auth结构是与smtp包的PlainAuth功能分开创建的。

调用 JSON-RPC 服务

此配方将说明如何使用标准库调用通过 JSON-RPC 协议的过程。

怎么做。。。

  1. 打开控制台,创建文件夹chapter07/recipe11
  2. 导航到该目录。
  3. 创建具有以下内容的jsonrpc.go文件:
        package main

        import (
          "log"
          "net"
          "net/rpc"
          "net/rpc/jsonrpc"
        )

        type Args struct {
          A, B int
        }

        type Result int

        type RpcServer struct{}

        func (t RpcServer) Add(args *Args, result *Result) error {
          log.Printf("Adding %d to %d\n", args.A, args.B)
          *result = Result(args.A + args.B)
          return nil
        } 

        const addr = ":7070"

        func main() {
          go createServer(addr)
          client, err := jsonrpc.Dial("tcp", addr)
          if err != nil {
            panic(err)
          }
          defer client.Close()
          args := &Args{
            A: 2,
            B: 3,
          }
          var result Result
          err = client.Call("RpcServer.Add", args, &result)
          if err != nil {
            log.Fatalf("error in RpcServer", err)
          }
          log.Printf("%d+%d=%d\n", args.A, args.B, result)
        }

        func createServer(addr string) {
          server := rpc.NewServer()
          err := server.Register(RpcServer{})
          if err != nil {
            panic(err)
          }
          l, e := net.Listen("tcp", addr)
          if e != nil {
            log.Fatalf("Couldn't start listening on %s errors: %s",
                       addr, e)
          }
          for {
            conn, err := l.Accept()
            if err != nil {
              log.Fatal(err)
            }
            go server.ServeCodec(jsonrpc.NewServerCodec(conn))
          }
        }
  1. 在主终端运行go run jsonrpc.go执行代码。
  2. 您将看到以下输出:

它是如何工作的。。。

Go 的标准库将 JSON-RPC 1.0 作为其内置包的一部分实现。jsonrpc包实现了Dial功能,生成调用远程过程的客户端。客户机本身包含Call方法,该方法接受过程调用、参数和存储结果的指针。

createServer将创建一个示例服务器来测试客户端调用。

HTTP 协议可以用作 JSON-RPC 的传输层。net/rpc包包含DialHTTP函数,可以创建客户端并调用远程过程。