Skip to content

Files

Latest commit

8d041bb · Oct 24, 2021

History

History
1022 lines (785 loc) · 29.4 KB

File metadata and controls

1022 lines (785 loc) · 29.4 KB

五、网络编程

Go 标准库为网络操作提供了大量支持。它包括允许您使用 HTTP 管理 TCP/IP、UDP、DNS、邮件和 RPC 的包。第三方软件包也可以填补标准库中包含的空白,包括gorilla/websocketshttps://github.com/gorilla/websocket/ ),用于可在正常 HTTP 处理程序中使用的 WebSocket 实现。本章将探讨这些库,并演示如何使用它们的一些简单方法。这些方法将帮助无法使用更高级别抽象(如 REST 或 GRPC)但需要网络连接的开发人员。它对于需要执行 DNS 查找或处理原始电子邮件的 DevOps 应用也很有用。阅读本章后,您应该已经掌握了一些基本的网络编程,并准备深入研究。

本章将介绍以下配方:

  • 编写 TCP/IP 回送服务器和客户端
  • 编写 UDP 服务器和客户端
  • 使用域名解析
  • 使用 WebSocket
  • 使用 net/rpc 调用远程方法
  • 使用 net/mail 解析电子邮件

技术要求

为了继续本章中的所有配方,请按照以下步骤配置您的环境:

  1. 下载 Go 1.12.6 或更高版本并安装到您的操作系统上 https://golang.org/doc/install

  2. 打开终端或控制台应用,然后创建并导航到项目目录,如~/projects/go-programming-cookbook,所有代码都将从此目录运行和修改。

  3. 将最新代码克隆到~/projects/go-programming-cookbook-original中,您可以选择从该目录工作,而不是手动键入示例:

$ git clone git@github.com:PacktPublishing/Go-Programming-Cookbook-Second-Edition.git go-programming-cookbook-original

编写 TCP/IP 回送服务器和客户端

TCP/IP 是一种常见的网络协议,HTTP 协议就是建立在它之上的。TCP 要求客户端连接到服务器以发送和接收数据。此方法将使用net包在客户端和服务器之间建立 TCP 连接。客户端将向服务器发送用户输入,服务器将使用输入的相同字符串进行响应,但使用strings.ToUpper()的结果转换为大写。客户端将打印从服务器收到的任何消息,以便输出输入的大写版本。

怎么做。。。

这些步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter5/tcp的新目录并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/tcp 

您应该看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/tcp    
  1. 复制~/projects/go-programming-cookbook-original/chapter5/tcp中的测试,或者将其用作编写自己代码的练习!

  2. 创建一个名为server的新目录并导航到它。

  3. 创建一个名为main.go的文件,其内容如下:

package main

import (
  "bufio"
  "fmt"
  "net"
  "strings"
)

const addr = "localhost:8888"

func echoBackCapitalized(conn net.Conn) {
  // set up a reader on conn (an io.Reader)
  reader := bufio.NewReader(conn)

  // grab the first line of data encountered
  data, err := reader.ReadString('\n')
  if err != nil {
    fmt.Printf("error reading data: %s\n", err.Error())
    return
  }
  // print then send back the data
  fmt.Printf("Received: %s", data)
  conn.Write([]byte(strings.ToUpper(data)))
  // close up the finished connection
  conn.Close()
}

func main() {
  ln, err := net.Listen("tcp", addr)
  if err != nil {
    panic(err)
  }
  defer ln.Close()
  fmt.Printf("listening on: %s\n", addr)
  for {
    conn, err := ln.Accept()
    if err != nil {
      fmt.Printf("encountered an error accepting connection: %s\n", 
                  err.Error())
      // if there's an error try again
      continue
    }
    // handle this asynchronously
    // potentially a good use-case
    // for a worker pool
    go echoBackCapitalized(conn)
  }
}
  1. 导航到上一个目录。
  2. 创建一个名为client的新目录并导航到它。
  3. 创建一个名为main.go的文件,其内容如下:
package main

import (
  "bufio"
  "fmt"
  "net"
  "os"
)

const addr = "localhost:8888"

func main() {
  reader := bufio.NewReader(os.Stdin)
  for {
    // grab a string input from the clie
    fmt.Printf("Enter some text: ")
    data, err := reader.ReadString('\n')
    if err != nil {
      fmt.Printf("encountered an error reading input: %s\n", 
                  err.Error())
      continue
    }
    // connect to the addr
    conn, err := net.Dial("tcp", addr)
    if err != nil {
      fmt.Printf("encountered an error connecting: %s\n", 
                  err.Error())
    }

    // write the data to the connection
    fmt.Fprintf(conn, data)

    // read back the response
    status, err := bufio.NewReader(conn).ReadString('\n')
    if err != nil {
      fmt.Printf("encountered an error reading response: %s\n", 
                  err.Error())
    }
    fmt.Printf("Received back: %s", status)
    // close up the finished connection
    conn.Close()
  }
}
  1. 导航到上一个目录。
  2. 运行go run ./server将看到以下输出:
$ go run ./server
listening on: localhost:8888
  1. 在单独的终端中,从tcp目录运行go run ./client,您将看到以下输出:
$ go run ./client 
Enter some text:
  1. 输入this is a test并点击进入。您将看到以下内容:
$ go run ./client 
Enter some text: this is a test
Received back: THIS IS A TEST
Enter some text: 
  1. Ctrl+C退出。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

服务器正在侦听端口8888。每当收到请求时,服务器必须接收请求并管理客户端连接。在这个程序中,它发送一个 Goroutine,从客户端读取请求,将接收到的数据大写,发送回客户端,最后关闭连接。服务器立即再次循环,等待接收新的客户端连接,而以前的连接是单独处理的。

客户端从STDIN读取输入,通过 TCP 连接连接到地址,写入从输入读取的消息,然后从服务器打印回响应。之后,它关闭连接并再次从STDIN循环读取。您也可以重做这个示例,让客户端保持连接,直到程序退出,而不是每次请求都保持连接。

编写 UDP 服务器和客户端

UDP 协议通常用于游戏和速度比可靠性更重要的地方。UDP 服务器和客户端不需要彼此连接。此方法将创建一个 UDP 服务器,该服务器将侦听来自客户端的消息,将其 IP 添加到其列表中,并将消息广播到以前看到的每个客户端。

每当客户端连接时,服务器将向STDOUT写入一条消息,并向其所有客户端广播相同的消息。此消息的文本应为Sent <count>,其中<count>将在服务器每次向其所有客户端广播时递增。因此,count可能具有不同的值,这取决于您连接到客户端所需的时间,因为服务器将广播,而不管它向多少个客户端发送消息。

怎么做。。。

以下步骤涵盖了编写和运行应用的过程:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter5/udp的新目录并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/udp 

您应该看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/udp    
  1. 复制~/projects/go-programming-cookbook-original/chapter5/udp中的测试,或者将其用作编写自己代码的练习!

  2. 创建一个名为server的新目录并导航到它。

  3. 创建一个名为broadcast.go的文件,其内容如下:

package main

import (
  "fmt"
  "net"
  "sync"
  "time"
)

type connections struct {
  addrs map[string]*net.UDPAddr
  // lock for modifying the map
  mu sync.Mutex
}

func broadcast(conn *net.UDPConn, conns *connections) {
  count := 0
  for {
    count++
    conns.mu.Lock()
    // loop over known addresses
    for _, retAddr := range conns.addrs {

      // send a message to them all
      msg := fmt.Sprintf("Sent %d", count)
      if _, err := conn.WriteToUDP([]byte(msg), retAddr); err != nil {
        fmt.Printf("error encountered: %s", err.Error())
        continue
      }

    }
    conns.mu.Unlock()
    time.Sleep(1 * time.Second)
  }
}
  1. 创建一个名为main.go的文件,其内容如下:
package main

import (
  "fmt"
  "net"
)

const addr = "localhost:8888"

func main() {
  conns := &connections{
    addrs: make(map[string]*net.UDPAddr),
  }

  fmt.Printf("serving on %s\n", addr)

  // construct a udp addr
  addr, err := net.ResolveUDPAddr("udp", addr)
  if err != nil {
    panic(err)
  }

  // listen on our specified addr
  conn, err := net.ListenUDP("udp", addr)
  if err != nil {
    panic(err)
  }
  // cleanup
  defer conn.Close()

  // async send messages to all known clients
  go broadcast(conn, conns)

  msg := make([]byte, 1024)
  for {
    // receive a message to gather the ip address
    // and port to send back to
    _, retAddr, err := conn.ReadFromUDP(msg)
    if err != nil {
      continue
    }

    //store it in a map
    conns.mu.Lock()
    conns.addrs[retAddr.String()] = retAddr
    conns.mu.Unlock()
    fmt.Printf("%s connected\n", retAddr)
  }
}
  1. 导航到上一个目录。

  2. 创建一个名为client的新目录并导航到它。

  3. 创建一个名为main.go的文件,其内容如下:

package main

import (
  "fmt"
  "net"
)

const addr = "localhost:8888"

func main() {
  fmt.Printf("client for server url: %s\n", addr)

  addr, err := net.ResolveUDPAddr("udp", addr)
  if err != nil {
    panic(err)
  }

  conn, err := net.DialUDP("udp", nil, addr)
  if err != nil {
    panic(err)
  }
  defer conn.Close()

  msg := make([]byte, 512)
  n, err := conn.Write([]byte("connected"))
  if err != nil {
    panic(err)
  }
  for {
    n, err = conn.Read(msg)
    if err != nil {
      continue
    }
    fmt.Printf("%s\n", string(msg[:n]))
  }
}
  1. 导航到上一个目录。
  2. 运行go run ./server将看到以下输出:
$ go run ./server
serving on localhost:8888
  1. 在单独的终端中,从udp目录运行go run ./client,您将看到以下输出,尽管计数可能不同:
$ go run ./client 
client for server url: localhost:8888
Sent 3
Sent 4
Sent 5
  1. 导航到运行服务器的终端,您将看到类似以下内容:
$ go run ./server 
serving on localhost:8888
127.0.0.1:64242 connected
  1. Ctrl+C退出服务器和客户端。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

服务器正在侦听端口8888,就像前面的方法一样。如果客户机启动,它会向服务器发送一条消息,服务器会将其地址添加到地址列表中。因为客户端可以异步连接,所以服务器在修改或读取列表之前必须使用互斥体。

一个单独的广播 Goroutine 单独运行,并将相同的消息发送到以前发送过它消息的所有客户端地址。假设他们仍在侦听,他们将在大致相同的时间从服务器接收相同的消息。您还可以与更多的客户端连接,以了解这一点的效果。

使用域名解析

net包提供了许多有关 DNS 查找的有用功能。此信息与使用 Unixdig命令可能获得的信息相当。这些信息对于您实现需要动态确定 IP 地址的任何类型的网络编程都非常有用。

本食谱将探讨如何收集这些数据。为了演示这一点,我们将实现一个简化的dig命令。我们将设法将 URL 映射到其所有 IPv4 和 IPv6 地址。通过修改GODEBUG=netdns=设置为gocgo,它将使用纯 Go DNS 解析器或cgo解析器。默认情况下,使用纯 Go DNS 解析程序。

怎么做。。。

这些步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter5/dns的新目录并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/dns 

您应该看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/dns
  1. 复制~/projects/go-programming-cookbook-original/chapter5/dns中的测试,或者将其用作编写自己代码的练习!
  2. 创建一个包含以下内容的dns.go文件:
package dns

import (
  "fmt"
  "net"

  "github.com/pkg/errors"
)

// Lookup holds the DNS information we care about
type Lookup struct {
  cname string
  hosts []string
}

// We can use this to print the lookup object
func (d *Lookup) String() string {
  result := ""
  for _, host := range d.hosts {
    result += fmt.Sprintf("%s IN A %s\n", d.cname, host)
  }
  return result
}

// LookupAddress returns a DNSLookup consisting of a cname and host
// for a given address
func LookupAddress(address string) (*Lookup, error) {
  cname, err := net.LookupCNAME(address)
  if err != nil {
    return nil, errors.Wrap(err, "error looking up CNAME")
  }
  hosts, err := net.LookupHost(address)
  if err != nil {
    return nil, errors.Wrap(err, "error looking up HOST")
  }

  return &Lookup{cname: cname, hosts: hosts}, nil
}
  1. 创建一个名为example的新目录并导航到它。
  2. 创建一个包含以下内容的main.go文件:
package main

import (
  "fmt"
  "log"
  "os"

  "github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/dns"
)

func main() {
  if len(os.Args) < 2 {
    fmt.Printf("Usage: %s <address>\n", os.Args[0])
    os.Exit(1)
  }
  address := os.Args[1]
  lookup, err := dns.LookupAddress(address)
  if err != nil {
    log.Panicf("failed to lookup: %s", err.Error())
  }
  fmt.Println(lookup)
}
  1. 运行go run main.go golang.org命令。
  2. 您还可以运行以下操作:
$ go build $ ./example golang.org

您应该看到以下输出:

$ go run main.go golang.org
golang.org. IN A 172.217.5.17
golang.org. IN A 2607:f8b0:4009:809::2011
  1. go.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

该配方对提供的地址执行了CNAME和主机查找。在我们的案例中,我们使用了golang.org。我们将结果存储在查找结构中,该结构使用String()方法打印输出结果。当我们将对象打印为字符串时,将自动调用该方法,或者我们可以直接调用该方法。我们在main.go中实现了一些基本的参数检查,以确保在程序运行时提供地址。

使用 WebSocket

WebSocket 允许服务器应用连接到用 JavaScript 编写的基于 web 的客户端。这允许您创建具有双向通信的 web 应用,并创建诸如聊天室等更新。

本教程将探讨如何在 Go 中编写 WebSocket 服务器,并演示客户端使用 WebSocket 服务器并与之通信的过程。它使用github.com/gorilla/websocket将标准处理程序升级为 WebSocket 处理程序,并创建客户端应用。

怎么做。。。

这些步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter5/websocket的新目录并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/websocket 

您应该看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/websocket    
  1. 复制~/projects/go-programming-cookbook-original/chapter5/websocket中的测试,或者将其用作编写自己代码的练习!
  2. 创建一个名为server的新目录并导航到它。
  3. 创建一个名为handler.go的文件,其内容如下:
package main

import (
  "log"
  "net/http"

  "github.com/gorilla/websocket"
)

// upgrader takes an http connection and converts it
// to a websocket one, we're using some recommended
// basic buffer sizes
var upgrader = websocket.Upgrader{
  ReadBufferSize: 1024,
  WriteBufferSize: 1024,
}

func wsHandler(w http.ResponseWriter, r *http.Request) {
  // upgrade the connection
  conn, err := upgrader.Upgrade(w, r, nil)
  if err != nil {
    log.Println("failed to upgrade connection: ", err)
    return
  }
  for {
    // read and echo back messages in a loop
    messageType, p, err := conn.ReadMessage()
    if err != nil {
      log.Println("failed to read message: ", err)
      return
    }
    log.Printf("received from client: %#v", string(p))
    if err := conn.WriteMessage(messageType, p); err != nil {
      log.Println("failed to write message: ", err)
      return
    }
  }
}
  1. 创建一个名为main.go的文件,其内容如下:
package main

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

func main() {
  fmt.Println("Listening on port :8000")
 // we mount our single handler on port localhost:8000 to handle all
  // requests
  log.Panic(http.ListenAndServe("localhost:8000", http.HandlerFunc(wsHandler)))
}
  1. 导航到上一个目录。
  2. 创建一个名为client的新目录并导航到它。
  3. 创建一个名为process.go的文件,其内容如下:
package main

import (
  "bufio"
  "fmt"
  "log"
  "os"
  "strings"

  "github.com/gorilla/websocket"
)

func process(c *websocket.Conn) {
  reader := bufio.NewReader(os.Stdin)
  for {
    fmt.Printf("Enter some text: ")
    // this will block ctrl-c, to exit press it then hit enter
    // or kill from another location
    data, err := reader.ReadString('\n')
    if err != nil {
      log.Println("failed to read stdin", err)
    }

    // trim off the space from reading the string
    data = strings.TrimSpace(data)

    // write the message as a byte across the websocket
    err = c.WriteMessage(websocket.TextMessage, []byte(data))
    if err != nil {
      log.Println("failed to write message:", err)
      return
    }

    // this is an echo server, so we can always read after the write
    _, message, err := c.ReadMessage()
    if err != nil {
      log.Println("failed to read:", err)
      return
    }
    log.Printf("received back from server: %#v\n", string(message))
  }
}
  1. 创建一个名为main.go的文件,其内容如下:
package main

import (
  "log"
  "os"
  "os/signal"

  "github.com/gorilla/websocket"
)

// catchSig cleans up our websocket conenction if we kill the program
// with a ctrl-c
func catchSig(ch chan os.Signal, c *websocket.Conn) {
  // block on waiting for a signal
  <-ch
  err := c.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
  if err != nil {
    log.Println("write close:", err)
  }
  return
}

func main() {
  // connect the os signal to our channel
  interrupt := make(chan os.Signal, 1)
  signal.Notify(interrupt, os.Interrupt)

  // use the ws:// Scheme to connect to the websocket
  u := "ws://localhost:8000/"
  log.Printf("connecting to %s", u)

  c, _, err := websocket.DefaultDialer.Dial(u, nil)
  if err != nil {
    log.Fatal("dial:", err)
  }
  defer c.Close()

  // dispatch our signal catcher
  go catchSig(interrupt, c)

  process(c)
}
  1. 导航到上一个目录。
  2. 运行go run ./server将看到以下输出:
$ go run ./server
Listening on port :8000
  1. 在单独的终端中,从websocket目录运行go run ./client,您将看到以下输出:
$ go run ./client
2019/05/26 11:53:20 connecting to ws://localhost:8000/
Enter some text: 
  1. 输入test字符串,您将看到以下内容:
$ go run ./client
2019/05/26 11:53:20 connecting to ws://localhost:8000/
Enter some text: test
2019/05/26 11:53:22 received back from server: "test"
Enter some text: 
  1. 导航到运行服务器的终端,您将看到类似以下内容:
$ go run ./server
Listening on port :8000
2019/05/26 11:53:22 received from client: "test"
  1. Ctrl+C退出服务器和客户端。在客户端上按Ctrl+C后,您可能还需要点击回车
  2. go.mod文件可能会被更新,go.sum文件现在应该存在于顶级配方目录中。
  3. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

服务器正在端口8000上侦听 WebSocket 连接。当请求传入时,github.com/gorilla/websocket包用于将请求升级到 WebSocket 连接。与前面的 echo 服务器示例类似,服务器等待 WebSocket 连接上的消息,并将相同的消息返回给客户端。因为它是一个处理程序,所以它可以异步处理许多 WebSocket 连接,并且它们将保持连接,直到客户端终止。

在客户端,我们增加了一个catchsig函数来处理Ctrl+C事件。这允许我们在客户端退出时干净地终止与服务器的连接。否则,客户端只需在STDIN上获取用户输入并将其发送到服务器,记录响应,然后重复。

使用 net/rpc 调用远程方法

Go 通过net/rpc包为您的系统提供基本的 RPC 功能。这是在不依赖 GRPC 或其他更复杂的 RPC 包的情况下进行 RPC 调用的潜在替代方案。但是,它的功能相当有限,您可能希望导出的任何函数都必须符合非常特定的函数签名。

代码中的注释说明了可以远程调用的方法的一些限制。此配方演示了如何创建一个共享函数,该函数具有通过结构传入的多个参数,并且可以远程调用。

怎么做。。。

这些步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter5/rpc的新目录并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/rpc 

您应该看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/rpc    
  1. 复制~/projects/go-programming-cookbook-original/chapter5/rpc中的测试,或者将其用作编写自己代码的练习!
  2. 创建一个名为tweak的新目录并导航到它。
  3. 创建一个名为tweak.go的文件,其内容如下:
package tweak

import (
  "strings"
)

// StringTweaker is a type of string
// that can reverse itself
type StringTweaker struct{}

// Args are a list of options for how to tweak
// the string
type Args struct {
  String string
  ToUpper bool
  Reverse bool
}

// Tweak conforms to the RPC library which require:
// - the method's type is exported.
// - the method is exported.
// - the method has two arguments, both exported (or builtin) types.
// - the method's second argument is a pointer.
// - the method has return type error.
func (s StringTweaker) Tweak(args *Args, resp *string) error {

  result := string(args.String)
  if args.ToUpper {
    result = strings.ToUpper(result)
  }
  if args.Reverse {
    runes := []rune(result)
    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
      runes[i], runes[j] = runes[j], runes[i]
    }
    result = string(runes)

  }
  *resp = result
  return nil
}
  1. 导航到上一个目录。
  2. 创建一个名为server的新目录并导航到它。
  3. 创建一个名为main.go的文件,其内容如下:
package main

import (
  "fmt"
  "log"
  "net"
  "net/http"
  "net/rpc"

  "github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/rpc/tweak"
)

func main() {
  s := new(tweak.StringTweaker)
  if err := rpc.Register(s); err != nil {
    log.Fatal("failed to register:", err)
  }

  rpc.HandleHTTP()

  l, err := net.Listen("tcp", ":1234")
  if err != nil {
    log.Fatal("listen error:", err)
  }

  fmt.Println("listening on :1234")
  log.Panic(http.Serve(l, nil))
}
  1. 导航到上一个目录。
  2. 创建一个名为client的新目录并导航到它。
  3. 创建一个名为main.go的文件,其内容如下:
package main

import (
  "fmt"
  "log"
  "net/rpc"

  "github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/rpc/tweak"
)

func main() {
  client, err := rpc.DialHTTP("tcp", "localhost:1234")
  if err != nil {
    log.Fatal("error dialing:", err)
  }

  args := tweak.Args{
    String: "this string should be uppercase and reversed",
    ToUpper: true,
    Reverse: true,
  }
  var result string
  err = client.Call("StringTweaker.Tweak", args, &result)
  if err != nil {
    log.Fatal("client call with error:", err)
  }
  fmt.Printf("the result is: %s", result)
}
  1. 导航到上一个目录。
  2. 运行go run ./server将看到以下输出:
$ go run ./server
Listening on :1234
  1. 在单独的终端中,从rpc目录运行go run ./client,您将看到以下输出:
$ go run ./client
the result is: DESREVER DNA ESACREPPU EB DLUOHS GNIRTS SIHT
  1. Ctrl+C退出服务器。
  2. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

StringTweaker结构被放入一个单独的库中,以便客户端(设置参数)和服务器(注册 RPC 并启动服务器)可以访问其导出的类型。它还符合本配方开头提到的规则,以便与net/rpc配合使用

StringTweaker可用于获取输入字符串,也可根据传递的选项,将其中包含的所有字符反转为大写。此模式可以扩展以创建更复杂的函数,还可以使用额外的函数使代码在增长时更具可读性

使用 net/mail 解析电子邮件

net/mail软件包提供了许多有用的功能,可以帮助您处理电子邮件。如果您有电子邮件的原始文本,则可以将其解析为提取标题、发送日期等信息。此配方将通过解析硬编码为字符串的原始电子邮件来演示这些功能。

怎么做。。。

这些步骤包括编写和运行应用:

  1. 从终端或控制台应用中,创建一个名为~/projects/go-programming-cookbook/chapter5/mail的新目录并导航到此目录。
  2. 运行以下命令:
$ go mod init github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/mail 

您应该看到一个名为go.mod的文件,其中包含以下内容:

module github.com/PacktPublishing/Go-Programming-Cookbook-Second-Edition/chapter5/mail
  1. 复制~/projects/go-programming-cookbook-original/chapter5/mail中的测试,或者将其用作编写自己代码的练习!
  2. 创建一个包含以下内容的header.go文件:
package main

import (
  "fmt"
  "net/mail"
  "strings"
)

// extract header info and print it nicely
func printHeaderInfo(header mail.Header) {

  // this works because we know it's a single address
  // otherwise use ParseAddressList
  toAddress, err := mail.ParseAddress(header.Get("To"))
  if err == nil {
    fmt.Printf("To: %s <%s>\n", toAddress.Name, toAddress.Address)
  }
  fromAddress, err := mail.ParseAddress(header.Get("From"))
  if err == nil {
    fmt.Printf("From: %s <%s>\n", fromAddress.Name, 
                fromAddress.Address)
  }

  fmt.Println("Subject:", header.Get("Subject"))

  // this works for a valid RFC5322 date
  // it does a header.Get("Date"), then a
  // mail.ParseDate(that_result)
  if date, err := header.Date(); err == nil {
    fmt.Println("Date:", date)
  }

  fmt.Println(strings.Repeat("=", 40))
  fmt.Println()
}
  1. 创建一个包含以下内容的main.go文件:
package main

import (
  "io"
  "log"
  "net/mail"
  "os"
  "strings"
)

// an example email message
const msg string = `Date: Thu, 24 Jul 2019 08:00:00 -0700
From: Aaron <fake_sender@example.com>
To: Reader <fake_receiver@example.com>
Subject: Gophercon 2019 is going to be awesome!

Feel free to share my book with others if you're attending.
This recipe can be used to process and parse email information.
`

func main() {
  r := strings.NewReader(msg)
  m, err := mail.ReadMessage(r)
  if err != nil {
    log.Fatal(err)
  }

  printHeaderInfo(m.Header)

  // after printing the header, dump the body to stdout
  if _, err := io.Copy(os.Stdout, m.Body); err != nil {
    log.Fatal(err)
  }
}
  1. 运行go run .命令。
  2. 您还可以运行以下操作:
$ go build $ ./mail 

您应该看到以下输出:

$ go run .
To: Reader <fake_receiver@example.com>
From: Aaron <fake_sender@example.com>
Subject: Gophercon 2019 is going to be awesome!
Date: 2019-07-24 08:00:00 -0700 -0700
========================================

Feel free to share my book with others if you're attending.
This recipe can be used to process and parse email information. 
  1. 如果您复制或编写了自己的测试,请转到一个目录并运行go test。确保所有测试都通过。

它是如何工作的。。。

printHeaderInfo函数完成了此配方的大部分工作。它将头中的地址解析为*mail.Address结构,并将日期头解析为日期对象。然后,它获取消息中的所有信息并将其格式化为可读格式。主函数解析初始电子邮件并传递此标题。