本章包含以下配方:
- 创建 TCP 服务器
- 创建 UDP 服务器
- 处理多个客户端
- 创建 HTTP 服务器
- 处理 HTTP 请求
- 创建 HTTP 中间件层
- 提供静态文件
- 为使用模板生成的内容提供服务
- 处理重定向
- 处理饼干
- 正常关闭 HTTP 服务器
- 提供安全的 HTTP 内容
- 解析表单变量
本章涵盖从实现简单的 TCP 和 UDP 服务器到旋转 HTTP 服务器的主题。这些方法将引导您从 HTTP 请求处理(为静态内容提供服务)到提供安全的 HTTP 内容。
检查 Go 是否正确安装。第 1 章**与环境交互的检索 Golang 版本配方中的准备部分将帮助您。
确保端口8080
和7070
未被其他应用程序使用。
在*连接网络一章中,*介绍了 TCP 连接的客户端。在此配方中,将描述服务器端。
- 打开控制台,创建文件夹
chapter09/recipe01
。 - 导航到该目录。
- 创建具有以下内容的
servertcp.go
文件:
package main
import (
"bufio"
"fmt"
"io"
"net"
)
func main() {
l, err := net.Listen("tcp", ":8080")
if err != nil {
panic(err)
}
for {
fmt.Println("Waiting for client...")
conn, err := l.Accept()
if err != nil {
panic(err)
}
msg, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
panic(err)
}
_, err = io.WriteString(conn, "Received: "+string(msg))
if err != nil {
fmt.Println(err)
}
conn.Close()
}
}
- 通过
go run servertcp.go
执行代码:
- 打开另一个终端并执行
nc localhost 8080
。 - 写任何文本,例如,
Hello
。 - 请参见输出:
可以使用net
包创建 TCP 服务器。网络包包含创建TCPListener
的Listen
函数,可以Accept
客户端连接。Accept
方法调用TCPListener
块,直到收到客户端连接。如果客户端连接出现,Accept
方法返回TCPConn
连接。TCPConn
是与客户端的连接,用于读取和写入数据。
TCPConn
实现Reader
和Writer
接口。所有写入和读取数据的方法都可以使用。请注意,有一个分隔符用于读取数据,否则,如果客户端强制关闭连接,将接收 EOF。
请注意,此实现一次只处理一个客户机。
用户数据报协议(UDP)是互联网的基本协议之一。此食谱将向您展示如何侦听 UDP 数据包并读取内容。
- 打开控制台,创建文件夹
chapter09/recipe02
。 - 导航到该目录。
- 创建具有以下内容的
serverudp.go
文件:
package main
import (
"fmt"
"log"
"net"
)
func main() {
pc, err := net.ListenPacket("udp", ":7070")
if err != nil {
log.Fatal(err)
}
defer pc.Close()
buffer := make([]byte, 2048)
fmt.Println("Waiting for client...")
for {
_, addr, err := pc.ReadFrom(buffer)
if err == nil {
rcvMsq := string(buffer)
fmt.Println("Received: " + rcvMsq)
if _, err := pc.WriteTo([]byte("Received: "+rcvMsq), addr);
err != nil {
fmt.Println("error on write: " + err.Error())
}
} else {
fmt.Println("error: " + err.Error())
}
}
}
- 通过
go run serverudp.go:
启动服务器
- 打开另一个终端并执行
nc -u localhost 7070
。 - 向终端写入任何消息,例如,
Hello
,点击输入。 - 请参见输出:
与 TCP 服务器一样,UDP 服务器可以在net
包的帮助下创建。通过使用ListenPacket
功能,创建PacketConn
PacketConn
没有像TCPConn
那样实现Reader
和Writer
接口。读取接收到的数据包时,应使用ReadFrom
方法。ReadFrom
方法阻塞,直到收到数据包。在此之后,返回客户端的Addr
(记住 UDP 不是基于连接的)。为了响应客户,可以使用PacketConn
的WriteTo
方法;在本例中,这将使用消息和Addr
,即客户端Addr
。
前面的方法说明了如何创建 UDP 和 TCP 服务器。示例代码未准备好同时处理多个客户端。在本食谱中,我们将介绍如何在任何给定时间处理更多客户机。
- 打开控制台,创建文件夹
chapter09/recipe03
。 - 导航到该目录。
- 创建具有以下内容的
multipletcp.go
文件:
package main
import (
"fmt"
"log"
"net"
)
func main() {
pc, err := net.ListenPacket("udp", ":7070")
if err != nil {
log.Fatal(err)
}
defer pc.Close()
buffer := make([]byte, 2048)
fmt.Println("Waiting for client...")
for {
_, addr, err := pc.ReadFrom(buffer)
if err == nil {
rcvMsq := string(buffer)
fmt.Println("Received: " + rcvMsq)
if _, err := pc.WriteTo([]byte("Received: "+rcvMsq), addr);
err != nil {
fmt.Println("error on write: " + err.Error())
}
} else {
fmt.Println("error: " + err.Error())
}
}
}
- 通过
go run multipletcp.go
执行代码。 - 打开另外两个终端,执行
nc localhost 8080
。 - 向两个打开的端子写入内容,然后查看输出。以下两个图像是连接的客户端。
服务器正在运行的终端中的输出:
TCP 服务器实现的工作原理与上一个配方相同,即本章中的创建 TCP 服务器。实现得到了增强,能够同时处理多个客户端。请注意,我们现在在单独的goroutine
中处理已接受的连接。这意味着服务器可以通过Accept
方法继续接受客户端连接
因为 UDP 协议不是有状态的,并且不保持任何连接,所以多个客户端的处理被转移到应用程序逻辑,您需要识别客户端和数据包序列。只有对客户端的写入响应才能与 goroutines 的使用并行。
在 Go 中创建 HTTP 服务器非常简单,标准库提供了更多的方法。让我们看看最基本的一个。
- 打开控制台,创建文件夹
chapter09/recipe04
。 - 导航到该目录。
- 创建具有以下内容的
httpserver.go
文件:
package main
import (
"fmt"
"net/http"
)
type SimpleHTTP struct{}
func (s SimpleHTTP) ServeHTTP(rw http.ResponseWriter,
r *http.Request) {
fmt.Fprintln(rw, "Hello world")
}
func main() {
fmt.Println("Starting HTTP server on port 8080")
// Eventually you can use
// http.ListenAndServe(":8080", SimpleHTTP{})
s := &http.Server{Addr: ":8080", Handler: SimpleHTTP{}}
s.ListenAndServe()
}
- 通过
go run httpserver.go
执行代码。 - 请参见输出:
- 在浏览器中访问 URL
http://localhost:8080
或使用curl
。Hello world
内容应显示为:
net/http
包包含几种创建 HTTP 服务器的方法。最简单的是从net/http
包中实现Handler
接口。Handler
接口需要该类型来实现ServeHTTP
方法。此方法处理请求和响应。
服务器本身是以来自net/http
包的Server
结构的形式创建的。Server
结构需要Handler
和Addr
字段。通过调用方法ListenAndServe
,服务器开始提供给定地址上的内容。
如果使用Server
的Serve
方法,则必须提供Listener
。
net/http
包还提供了默认服务器,如果ListenAndServe
作为net/http
包的函数调用,则可以使用该服务器。它使用Handler
和Addr
,与Server
结构相同。在内部,创建了Server
。
应用程序通常使用 URL 路径和 HTTP 方法来定义应用程序的行为。这个配方将说明如何利用标准库来处理不同的 URL 和方法。
- 打开控制台,创建文件夹
chapter09/recipe05
。 - 导航到该目录。
- 创建具有以下内容的
handle.go
文件:
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/user", func(w http.ResponseWriter,
r *http.Request) {
if r.Method == http.MethodGet {
fmt.Fprintln(w, "User GET")
}
if r.Method == http.MethodPost {
fmt.Fprintln(w, "User POST")
}
})
// separate handler
itemMux := http.NewServeMux()
itemMux.HandleFunc("/items/clothes", func(w http.ResponseWriter,
r *http.Request) {
fmt.Fprintln(w, "Clothes")
})
mux.Handle("/items/", itemMux)
// Admin handlers
adminMux := http.NewServeMux()
adminMux.HandleFunc("/ports", func(w http.ResponseWriter,
r *http.Request) {
fmt.Fprintln(w, "Ports")
})
mux.Handle("/admin/", http.StripPrefix("/admin",
adminMux))
// Default server
http.ListenAndServe(":8080", mux)
}
-
通过
go run handle.go
执行代码。 -
在浏览器中或通过
curl
检查以下 URL:http://localhost:8080/user
http://localhost:8080/items/clothes
http://localhost:8080/admin/ports
-
请参见输出:
net/http
包包含ServeMux
结构,它实现了Server
结构中要使用的Handler
接口,但也包含了如何定义不同路径处理的机制。ServeMux
指针包含接受路径的方法HandleFunc
和Handle
,并且HandlerFunc
函数处理给定路径的请求,或者其他处理程序也会这样做
有关如何使用这些功能,请参见前面的示例。Handler
接口和HandlerFunc
需要使用请求和响应参数实现函数。这样您就可以访问这两个结构。请求本身允许访问Headers
、HTTP 方法和其他请求参数。
具有 web UI 或 REST API 的现代应用程序通常使用中间件机制来记录活动,或保护给定接口的安全性。在此配方中,将介绍这种中间件层的实现。
- 打开控制台,创建文件夹
chapter09/recipe06
。 - 导航到该目录。
- 创建具有以下内容的
middleware.go
文件:
package main
import (
"io"
"net/http"
)
func main() {
// Secured API
mux := http.NewServeMux()
mux.HandleFunc("/api/users", Secure(func(w http.ResponseWriter,
r *http.Request) {
io.WriteString(w, `[{"id":"1","login":"ffghi"},
{"id":"2","login":"ffghj"}]`)
}))
http.ListenAndServe(":8080", mux)
}
func Secure(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
sec := r.Header.Get("X-Auth")
if sec != "authenticated" {
w.WriteHeader(http.StatusUnauthorized)
return
}
h(w, r) // use the handler
}
}
- 通过
go run middleware.go
执行代码。 - 通过执行这两个命令(第一个没有,第二个有
X-Auth
头),使用curl
检查 URLhttp://localhost:8080/api/users
:curl -X GET -I http://localhost:8080/api/users
curl -X GET -H "X-Auth: authenticated" -I http://localhost:8080/api/users
- 请参见输出:
- 使用
X-User
头测试 URLhttp://localhost:8080/api/profile
。 - 请参见输出:
上例中中间件的实现利用了 Golang 的功能作为一等公民特性。原始的HandlerFunc
被包裹在一个HandlerFunc
中,用于检查X-Auth
标题。然后使用Secure
功能来保护HandlerFunc
,在ServeMux
的HandleFunc
方法中使用。
请注意,这只是一个简单的示例,但通过这种方式,您可以实现更复杂的解决方案。例如,可以从Header
令牌中提取用户身份,随后,可以将新类型的处理程序定义为type AuthHandler func(u *User,w http.ResponseWriter, r *http.Request)
。然后,函数WithUser
为ServeMux
创建HandlerFunc
。
几乎任何 web 应用程序都需要提供静态文件。使用标准库可以轻松实现 JavaScript 文件、静态 HTML 页面或 CSS 样式表的服务。这道菜会告诉你怎么做。
- 打开控制台,创建文件夹
chapter09/recipe07
。 - 导航到该目录。
- 创建具有以下内容的文件
welcome.txt
:
Hi, Go is awesome!
- 创建文件夹
html
,导航到该文件夹并创建包含以下内容的文件page.html
:
<html>
<body>
Hi, I'm HTML body for index.html!
</body>
</html>
- 创建具有以下内容的
static.go
文件:
package main
import (
"net/http"
)
func main() {
fileSrv := http.FileServer(http.Dir("html"))
fileSrv = http.StripPrefix("/html", fileSrv)
http.HandleFunc("/welcome", serveWelcome)
http.Handle("/html/", fileSrv)
http.ListenAndServe(":8080", nil)
}
func serveWelcome(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, "welcome.txt")
}
- 通过
go run static.go
执行代码。 - 使用浏览器或
curl
实用程序检查以下 URL:http://localhost:8080/html/page.html
http://localhost:8080/welcome
- 请参见输出:
net/http
包提供了ServeFile
和FileServer
功能,这些功能是为静态文件服务而设计的。ServeFile
函数仅使用给定的文件路径参数使用ResponseWriter
和Request
,并将文件内容写入响应。
FileServer
函数创建使用FileSystem
参数的整个Handler
。前面的示例使用了实现FileSystem
接口的Dir
类型。FileSystem
接口需要实现Open
方法,该方法使用字符串并返回给定路径的实际File
。
出于某些目的,不需要使用所有 JavaScript 创建高度动态的 web UI,而使用生成内容的静态内容就足够了。Go 标准库提供了一种构建动态生成内容的方法。此配方为 Go 标准库模板提供了一条线索。
- 打开控制台,创建文件夹
chapter09/recipe08
。 - 导航到该目录。
- 创建具有以下内容的文件
template.tpl
:
<html>
<body>
Hi, I'm HTML body for index.html!
</body>
</html>
- 创建具有以下内容的文件
dynamic.go
:
package main
import "net/http"
import "html/template"
func main() {
tpl, err := template.ParseFiles("template.tpl")
if err != nil {
panic(err)
}
http.HandleFunc("/",func(w http.ResponseWriter, r *http.Request){
err := tpl.Execute(w, "John Doe")
if err != nil {
panic(err)
}
})
http.ListenAndServe(":8080", nil)
}
- 通过
go run dynamic.go
执行代码。 - 检查 URL
http://localhost:8080
并查看输出:
Go 标准库还包含用于模板化内容的包。包html/template
和text/template
提供解析模板并使用它们创建输出的函数。解析使用ParseXXX
函数或新创建的Template
结构指针的方法完成。前面的示例使用了html/template
包的ParseFiles
函数。
模板本身是基于文本的文档或包含动态变量的文本片段。模板的使用基于将模板文本与包含模板中变量值的结构合并。为了将模板与这样的结构合并,这里有Execute
和ExecuteTemplate
方法。请注意,这些方法使用编写器接口,在那里写入输出;本例中使用了ResponseWriter
。
文档中很好地解释了模板语法和功能
重定向是告知客户端内容已移动,或者需要查找其他位置以完成请求的常用方式。此配方描述了使用标准库实现重定向的方法。
- 打开控制台,创建文件夹
chapter09/recipe09
。 - 导航到该目录。
- 创建具有以下内容的文件
redirect.go
:
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
log.Println("Server is starting...")
http.Handle("/secured/handle",
http.RedirectHandler("/login",
http.StatusTemporaryRedirect))
http.HandleFunc("/secured/hadlefunc",
func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
})
http.HandleFunc("/login", func(w http.ResponseWriter,
r *http.Request) {
fmt.Fprintf(w, "Welcome user! Please login!\n")
})
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
- 通过
go run redirect.go
执行代码。 - 使用
curl -v -L http://localhost:8080/s
ecured/handle
查看重定向是否有效:
net/http
包包含执行重定向的简单方法。RedirectHandler
可以被利用。该函数使用将重定向请求的URL
和将发送到客户端的status code
。函数本身将结果发送到Handler
,可以在ServeMux
的Handle
方法中使用Handler
(本例直接使用包中的默认值)。
第二种方法是使用Redirect
函数,它为您执行重定向。该函数使用ResponseWriter
、请求指针以及与RequestHandler
相同的 URL 和状态码,发送给客户端。
也可以通过手动设置Location
标题和写入正确的状态代码来完成重定向。Go 库仅使开发人员易于使用。
Cookie 提供了一种在客户端轻松存储数据的方法。此配方说明了如何在标准库的帮助下设置、检索和删除 cookie。
- 打开控制台,创建文件夹
chapter09/recipe10
。 - 导航到该目录。
- 创建具有以下内容的文件
cookies.go
:
package main
import (
"fmt"
"log"
"net/http"
"time"
)
const cookieName = "X-Cookie"
func main() {
log.Println("Server is starting...")
http.HandleFunc("/set", func(w http.ResponseWriter,
r *http.Request) {
c := &http.Cookie{
Name: cookieName,
Value: "Go is awesome.",
Expires: time.Now().Add(time.Hour),
Domain: "localhost",
}
http.SetCookie(w, c)
fmt.Fprintln(w, "Cookie is set!")
})
http.HandleFunc("/get", func(w http.ResponseWriter,
r *http.Request) {
val, err := r.Cookie(cookieName)
if err != nil {
fmt.Fprintln(w, "Cookie err: "+err.Error())
return
}
fmt.Fprintf(w, "Cookie is: %s \n", val.Value)
fmt.Fprintf(w, "Other cookies")
for _, v := range r.Cookies() {
fmt.Fprintf(w, "%s => %s \n", v.Name, v.Value)
}
})
http.HandleFunc("/remove", func(w http.ResponseWriter,
r *http.Request) {
val, err := r.Cookie(cookieName)
if err != nil {
fmt.Fprintln(w, "Cookie err: "+err.Error())
return
}
val.MaxAge = -1
http.SetCookie(w, val)
fmt.Fprintln(w, "Cookie is removed!")
})
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
- 通过
go run cookies.go
执行代码。 - 按以下顺序访问 URL,请参见:
net/http
包还提供了对 cookie 进行操作的功能和机制。示例代码介绍了如何设置/获取和删除 cookie。SetCookie
函数接受表示 cookies 的Cookie
结构指针,当然也接受ResponseWriter
。直接在Cookie
结构中设置Name
、Value
、Domain
和到期。在幕后,SetCookie
函数写入头来设置 cookies。
cookie 值可以从Request
结构中检索。如果请求中存在 cookie,则带有 name 参数的方法Cookie
返回指向Cookie
的指针。
要列出请求中的所有 cookie,可以调用方法Cookies
。此方法返回Cookie
结构指针的切片。
为了让客户端知道应该删除 cookie,可以检索具有给定名称的Cookie
,并且MaxAge
字段应该设置为负值。请注意,这不是 Go 特性,而是客户端应该采用的工作方式
在第一章与环境的互动中,介绍了如何实现优雅关机的机制。在这个配方中,我们将描述如何关闭 HTTP 服务器并给它时间来处理现有的客户机。
- 打开控制台,创建文件夹
chapter09/recipe11
。 - 导航到该目录。
- 创建具有以下内容的文件
gracefully.go
:
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/",func(w http.ResponseWriter, r *http.Request){
fmt.Fprintln(w, "Hello world!")
})
srv := &http.Server{Addr: ":8080", Handler: mux}
go func() {
if err := srv.ListenAndServe(); err != nil {
log.Printf("Server error: %s\n", err)
}
}()
log.Println("Server listening on : " + srv.Addr)
stopChan := make(chan os.Signal)
signal.Notify(stopChan, os.Interrupt)
<-stopChan // wait for SIGINT
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(
context.Background(),
5*time.Second)
srv.Shutdown(ctx)
<-ctx.Done()
cancel()
log.Println("Server gracefully stopped")
}
- 通过
go run gracefully.go
执行代码。 - 等待服务器开始侦听:
-
将浏览器连接到
http://localhost:8080
;这将导致浏览器等待响应 10 秒钟。 -
在 10 秒内按Ctrl+C发送
SIGINT
信号。 -
尝试从其他选项卡再次连接(服务器应拒绝其他连接)。
-
请参见终端中的输出:
来自net/http
包的Server
提供了正常关闭连接的方法。前面的代码在单独的goroutine
中启动 HTTP 服务器,并在变量中保留对Server
结构的引用
通过调用Shutdown
方法,Server
开始拒绝新连接,并关闭打开的侦听器和空闲连接。然后它无限期地等待已经挂起的连接,直到这些连接变为空闲。关闭所有连接后,服务器将关闭。注意,Shutdown
方法消耗Context
。如果提供的Context
在停机前过期,则返回Context
错误,并且Shutdown
不再阻塞。
此配方描述了创建 HTTP 服务器的最简单方法,该服务器通过 TLS/SSL 层为内容提供服务。
准备私钥和自签名 X-509 证书。为此,可以使用 OpenSSL 实用程序。通过执行命令openssl genrsa -out server.key 2048
,使用 RSA 算法导出的私钥被生成到文件server.key
。基于此私钥,可以通过调用openssl req -new -x509 -sha256 -key server.key -out server.crt -days 365
生成 X-509 证书。创建了server.crt
文件。
- 打开控制台,创建文件夹
chapter09/recipe12
。 - 导航到该目录。
- 将创建的
server.key
和server.crt
文件放入其中。 - 创建具有以下内容的文件
servetls.go
:
package main
import (
"fmt"
"net/http"
)
type SimpleHTTP struct{}
func (s SimpleHTTP) ServeHTTP(rw http.ResponseWriter,
r *http.Request) {
fmt.Fprintln(rw, "Hello world")
}
func main() {
fmt.Println("Starting HTTP server on port 8080")
// Eventually you can use
// http.ListenAndServe(":8080", SimpleHTTP{})
s := &http.Server{Addr: ":8080", Handler: SimpleHTTP{}}
if err := s.ListenAndServeTLS("server.crt", "server.key");
err != nil {
panic(err)
}
}
- 通过
go run servetls.go
执行服务器。 - 访问 URL
https://localhost:8080
(使用 HTTPS 协议)。如果使用curl
实用程序,则必须使用--insecure
标志,因为我们的证书是自签名且不受信任的:
除了ListenAndServe
函数外,在net/http
包中还存在用于通过 SSL/TLS 服务 HTTP 的 TLS 变体。使用Server
的ListenAndServeTLS
方法,提供安全 HTTP。ListenAndServeTLS
使用私钥和 X-509 证书的路径。当然,可以使用直接来自net/http
包的功能ListenAndServeTLS
。
HTTPPOST
表单是以结构化方式将信息传递给服务器的一种非常常见的方式。这个食谱展示了如何在服务器端解析和访问这些内容。
- 打开控制台,创建文件夹
chapter09/recipe12
。 - 导航到该目录。
- 创建具有以下内容的文件
form.go
:
package main
import (
"fmt"
"net/http"
)
type StringServer string
func (s StringServer) ServeHTTP(rw http.ResponseWriter,
req *http.Request) {
fmt.Printf("Prior ParseForm: %v\n", req.Form)
req.ParseForm()
fmt.Printf("Post ParseForm: %v\n", req.Form)
fmt.Println("Param1 is : " + req.Form.Get("param1"))
rw.Write([]byte(string(s)))
}
func createServer(addr string) http.Server {
return http.Server{
Addr: addr,
Handler: StringServer("Hello world"),
}
}
func main() {
s := createServer(":8080")
fmt.Println("Server is starting...")
if err := s.ListenAndServe(); err != nil {
panic(err)
}
}
- 通过
go run form.go
执行代码。 - 打开第二个终端,使用
curl
执行POST
:
curl -X POST -H "Content-Type: app
lication/x-www-form-urlencoded" -d "param1=data1¶m2=data2" "localhost:8080?
param1=overriden¶m3=data3"
- 请参阅服务器运行的第一个终端中的输出:
net/http
包的Request
结构包含Form
字段,该字段包含POST
表单变量和合并的 URL 查询变量。前面代码中的重要步骤是对Request
指针调用ParseForm
方法。此方法调用导致将POST
表单值和查询值解析为Form
变量。请注意,如果使用了Form
字段上的Get
方法,则参数的POST
值优先。事实上,Form
和PostForm
字段属于url.Values
类型。
如果只需要访问POST
表单中的参数,则提供Request
的PostForm
字段。这只保留了POST
身体的一部分。