本章包含以下配方:
- 解析本地 IP 地址
- 连接到远程服务器
- 按 IP 地址解析域,反之亦然
- 连接到 HTTP 服务器
- 解析和构建 URL
- 创建 HTTP 请求
- 读取和写入 HTTP 头
- 处理 HTTP 重定向
- 使用 RESTful API
- 发送简单的电子邮件
- 调用 JSON-RPC 服务
这一章是关于网络的。本章中的大多数食谱都集中在客户端。我们将介绍如何解析计算机上网络的基本信息、域名和 IP 解析,以及如何通过 HTTP 和 SMTP 等 TCP 相关协议进行连接。最后,我们将使用标准库通过 JSON-RCP1.0 进行远程过程调用。
检查 Go 是否正确安装。从第一章与环境互动中检索咕噜版本配方的准备部分将帮助您。验证是否有任何其他应用程序阻塞了7070
端口。
此配方说明如何从可用的本地接口检索 IP 地址。
- 打开控制台,创建文件夹
chapter07/recipe01
。 - 导航到该目录。
- 创建具有以下内容的
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)
}
}
}
}
- 在主终端运行
go run interfaces.go
执行代码。 - 您将看到以下输出:
net 包包含Interfaces
函数,该函数将网络接口列为Interface
结构的一部分。Interface
结构具有Addrs
方法,该方法列出了可用的网络地址。通过这种方式,您可以按其接口列出地址。
另一个选项是使用net
包的InterfaceAddrs
函数,它提供实现Addr
接口的结构片。这为您提供了获取所需信息的方法。
基于 TCP 的协议是网络通信中最重要的协议。提醒一下,HTTP、FTP、SMTP 和其他协议都是这个组的一部分。
- 打开控制台,创建文件夹
chapter07/recipe02
。 - 导航到该目录。
- 创建具有以下内容的
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)
}
- 在主终端运行
go run tcpclient.go
执行代码。 - 您将看到以下输出:
网络包包含Dial
函数,该函数使用网络类型和地址。在上例中,网络为tcp
,地址为localhost:8080
。
一旦Dial
函数成功,返回Conn
类型,作为打开套接字的引用。Conn
接口还定义了Read
和Write
函数,因此它们可以用作Writer
和Reader
函数,用于从套接字进行写入和读取。最后,示例代码使用Scanner
获得响应。请注意,在这种情况下,Scanner
因制动管路而起作用。否则,应使用更通用的Read
方法。在本例中,通过SetReadDeadline
方法设置Read
截止日期。重要的是,最后期限不是一个持续时间,而是一个Time
。这意味着最后期限被设置为将来的一个时间点。如果您正在从循环中的套接字读取数据,并且需要将读取超时设置为 10 秒,则每次迭代都应包含类似于conn.SetReadDeadline(time.Now().Add(10*time.Second))
的代码。
为了启发整个代码示例,使用HTTP
标准包中的 HTTP 服务器作为客户机的对应物。这一部分包含在单独的配方中。
本食谱将向您介绍如何将 IP 地址转换为主机地址,反之亦然。
- 打开控制台,创建文件夹
chapter07/recipe03
。 - 导航到该目录。
- 创建具有以下内容的
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())
}
}
- 在主终端运行
go run lookup.go
执行代码。 - 您将看到以下输出:
从 IP 地址解析域名可以通过net
包中的LookupAddr
功能完成。为了从域名中找到IP
地址,应用了LookupIP
功能
前面的方法连接到远程服务器让我们了解了如何在较低级别连接 TCP 服务器。在此配方中,将显示与更高级别的 HTTP 服务器的通信。
- 打开控制台,创建文件夹
chapter07/recipe04
。 - 导航到该目录。
- 创建具有以下内容的
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))
}
- 在主终端运行
go run http.go
执行代码。 - 您将看到以下输出:
通过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 比尝试将其作为简单字符串处理要好。Go 标准库自然包含用于操作 URL 的实用程序。本食谱将介绍其中一些主要功能。
- 打开控制台,创建文件夹
chapter07/recipe05
。 - 导航到该目录。
- 创建具有以下内容的
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))
}
- 在主终端运行
go run url.go
执行代码。 - 您将看到以下输出:
net/url
包旨在帮助您操作和解析 URL。URL
结构包含将 URL 放在一起所需的字段。通过URL
结构的String
方法,可以轻松地转换为简单字符串。
当字符串表示可用且需要额外操作时,可以使用net/url
的Parse
功能。通过这种方式,可以将字符串转换为URL
结构,并修改底层 URL。
此配方将向您展示如何使用特定参数构造 HTTP 请求。
-
打开控制台,创建文件夹
chapter07/recipe06
。 -
导航到该目录。
-
创建具有以下内容的
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))
}
- 在主终端运行
go run request.go
执行代码。 - 您将看到以下输出:
示例代码中给出了构造请求的更复杂的方法。使用net/http
包的NewRequest
方法,返回指向Request
结构的指针。函数使用方法的请求、URL 和请求体。注意表单的构建方式。不使用普通字符串,而是使用url.Values
结构。最后,调用Encode
方法对给定的表单值进行编码。通过请求的http.Header
属性设置头。
前面的配方描述了如何创建 HTTP 请求。本食谱将详细介绍如何读取和写入请求头。
- 打开控制台,创建文件夹
chapter07/recipe07
。 - 导航到该目录。
- 创建具有以下内容的
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)
}
- 在主终端运行
go run headers.go
执行代码。 - 您将看到以下输出:
实际上,http
包中的头被表示为map[string][]string
,这样就必须处理Header
类型。前面的代码显示了如何设置和读取标题值。关于头的重要一点是头键的值是string
片。因此,头中的每个键都可以包含多个值。
Header
类型的Set
方法设置给定键下的一个项目切片。另一方面,Add
方法将值附加到切片。
使用Get
方法将从给定键下的切片中检索第一个值。如果需要整个切片,Header
需要作为地图处理。可以使用Del
方法移除整个头键。
服务器和客户端都使用Request
和Header
类型的http
包,因此服务器端和客户端的处理是相同的。
在某些情况下,您需要更多地控制重定向的处理方式。此配方将向您展示 Go 客户端实现的机制,以便您能够更好地控制 HTTP 重定向的处理。
- 打开控制台,创建文件夹
chapter07/recipe08
。 - 导航到该目录。
- 创建具有以下内容的
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)
}
}
- 在主终端运行
go run redirects.go
执行代码。 - 您将看到以下输出:
http
包的Client
包含CheckRedirect
字段。该字段是一个具有req
和via
参数的函数。req
是即将到来的请求,via
是之前的请求。这样,您可以在重定向后修改请求。在前面的示例中,Known-redirects
标题被修改。
如果CheckRedirect
函数返回错误,则返回最后一个带有封装错误的封闭体的响应。如果返回http.ErrUseLastResponse
,则返回最后一个响应,但主体未关闭,因此可以读取。
默认情况下,CheckRedirect
属性为零。在这种情况下,它的重定向限制为 10 次。在此计数之后,重定向将停止。
RESTful API 是应用程序和服务器提供对其服务的访问的最常见方式。此配方将向您展示如何在标准库的 HTTP 客户端的帮助下使用它。
- 打开控制台,创建文件夹
chapter07/recipe09
。 - 导航到该目录。
- 创建具有以下内容的
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,
}
}
- 在主终端运行
go run rest.go
执行代码。 - 您将看到以下输出:
前面的示例代码显示了 RESTAPI 的外观以及如何使用它。注意,decodeCity
和decodeCities
函数得益于请求的Body
实现了Reader
接口。结构的反序列化通过json.Decoder
完成。
本食谱将简要介绍如何使用标准库连接到 SMTP 服务器并发送电子邮件。
在此配方中,我们将使用谷歌 Gmail 帐户发送电子邮件。通过一些配置,此方法对于其他 SMTP 服务器也很有用。
- 打开控制台,创建文件夹
chapter07/recipe10
。 - 导航到该目录。
- 创建具有以下内容的
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)
}
}
- 在主终端运行
go run smtp.go
执行代码。 - 输入帐户的电子邮件(谷歌帐户)并点击输入。
- 输入帐号密码,点击输入。
- 在检查电子邮件框之前,您将看到以下输出:
smtp
包提供与 SMTP 服务器交互的基本功能。Dial
功能提供给客户端。客户端最重要的方法是Mail
,设置发件人邮件;Rcpt
设置收件人邮件;Data
提供可以写入邮件内容的Writer
。最后,Quit
方法发送 QUIT 并关闭与服务器的连接。
前面的示例使用到 SMTP 服务器的安全连接,因此使用客户端的Auth
方法设置身份验证,并调用StartTLS
方法启动到服务器的安全连接。
注意,Auth
结构是与smtp
包的PlainAuth
功能分开创建的。
此配方将说明如何使用标准库调用通过 JSON-RPC 协议的过程。
- 打开控制台,创建文件夹
chapter07/recipe11
。 - 导航到该目录。
- 创建具有以下内容的
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))
}
}
- 在主终端运行
go run jsonrpc.go
执行代码。 - 您将看到以下输出:
Go 的标准库将 JSON-RPC 1.0 作为其内置包的一部分实现。jsonrpc
包实现了Dial
功能,生成调用远程过程的客户端。客户机本身包含Call
方法,该方法接受过程调用、参数和存储结果的指针。
createServer
将创建一个示例服务器来测试客户端调用。
HTTP 协议可以用作 JSON-RPC 的传输层。net/rpc
包包含DialHTTP
函数,可以创建客户端并调用远程过程。