tcp 服务器
这部分我们将使用 TCP 协议和协程范式编写一个简单的客户端 - 服务器应用,一个(web)服务器应用需要响应众多客户端的并发请求:go 会为每一个客户端产生一个协程用来处理请求。我们需要使用 net 包中网络通信的功能。它包含了用于 TCP/IP 以及 UDP 协议、域名解析等方法。
服务端
package main
import (
"fmt"
"net"
)
func main() {
fmt.Println("Starting the server ...")
// 创建 listener
listener, err := net.Listen("tcp", "localhost:50000")
if err != nil {
fmt.Println("Error listening", err.Error())
return //终止程序
}
// 监听并接受来自客户端的连接
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println("Error accepting", err.Error())
return // 终止程序
}
go doServerStuff(conn)
}
}
func doServerStuff(conn net.Conn) {
for {
buf := make([]byte, 512)
len, err := conn.Read(buf)
if err != nil {
fmt.Println("Error reading", err.Error())
return //终止程序
}
fmt.Printf("Received data: %v", string(buf[:len]))
}
}
我们在 main()
创建了一个 net.Listener
的变量,他是一个服务器的基本函数:用来监听和接收来自客户端的请求(来自 localhost
即 IP 地址为 127.0.0.1 端口为 50000 基于 TCP 协议)。这个 Listen()
函数可以返回一个 error
类型的错误变量。用一个无限 for
循环的 listener.Accept()
来等待客户端的请求。客户端的请求将产生一个 net.Conn
类型的连接变量。然后一个独立的协程使用这个连接执行 doServerStuff()
,开始使用一个 512 字节的缓冲 data 来读取客户端发送来的数据并且把它们打印到服务器的终端,len
获取客户端发送的数据字节数;当客户端发送的所有数据都被读取完成时,协程就结束了。这段程序会为每一个客户端连接创建一个独立的协程。必须先运行服务器代码,再运行客户端代码。
客户端
package main
import (
"bufio"
"fmt"
"net"
"os"
"strings"
)
func main() {
//打开连接:
conn, err := net.Dial("tcp", "localhost:50000")
if err != nil {
//由于目标计算机积极拒绝而无法创建连接
fmt.Println("Error dialing", err.Error())
return // 终止程序
}
inputReader := bufio.NewReader(os.Stdin)
fmt.Println("First, what is your name?")
clientName, _ := inputReader.ReadString('\n')
// fmt.Printf("CLIENTNAME %s", clientName)
trimmedClient := strings.Trim(clientName, "\r\n") // Windows 平台下用 "\r\n",Linux平台下使用 "\n"
// 给服务器发送信息直到程序退出:
for {
fmt.Println("What to send to the server? Type Q to quit.")
input, _ := inputReader.ReadString('\n')
trimmedInput := strings.Trim(input, "\r\n")
// fmt.Printf("input:--s%--", input)
// fmt.Printf("trimmedInput:--s%--", trimmedInput)
if trimmedInput == "Q" {
return
}
_, err = conn.Write([]byte(trimmedClient + " says: " + trimmedInput))
}
}
户端通过 net.Dial
创建了一个和服务器之间的连接
它通过无限循环中的 os.Stdin
接收来自键盘的输入直到输入了 “Q”。注意使用 \r
和 \n
换行符分割字符串(在 windows 平台下使用 \r\n)。接下来分割后的输入通过 connection
的 Write
方法被发送到服务器。
当然,服务器必须先启动好,如果服务器并未开始监听,客户端是无法成功连接的。
如果在服务器没有开始监听的情况下运行客户端程序,客户端会停止并打印出以下错误信息:对tcp 127.0.0.1:50000发起连接时产生错误:由于目标计算机的积极拒绝而无法创建连接。
打开控制台并转到服务器和客户端可执行程序所在的目录,Windows 系统下输入 server.exe
(或者只输入 server),Linux 系统下输入 ./server。
接下来控制台出现以下信息:Starting the server ...
在 Windows 系统中,我们可以通过 CTRL/C
停止程序。
然后开启 2 个或者 3 个独立的控制台窗口,然后分别输入 client 回车启动客户端程序
网络编程中 net.Dial
函数是非常重要的,一旦你连接到远程系统,就会返回一个 Conn 类型接口,我们可以用它发送和接收数据。Dial 函数巧妙的抽象了网络结构及传输。所以 IPv4
或者 IPv6
,TCP
或者 UDP
都可以使用这个公用接口。
下边这个示例先使用 TCP 协议连接远程 80 端口,然后使用 UDP 协议连接,最后使用 TCP 协议连接 IPv6
类型的地址:
// make a connection with www.example.org:
package main
import (
"fmt"
"net"
"os"
)
func main() {
conn, err := net.Dial("tcp", "192.0.32.10:80") // tcp ipv4
checkConnection(conn, err)
conn, err = net.Dial("udp", "192.0.32.10:80") // udp
checkConnection(conn, err)
conn, err = net.Dial("tcp", "[2620:0:2d0:200::10]:80") // tcp ipv6
checkConnection(conn, err)
}
func checkConnection(conn net.Conn, err error) {
if err != nil {
fmt.Printf("error %v connecting!", err)
os.Exit(1)
}
fmt.Printf("Connection is made with %v\n", conn)
}
一个简单的网页服务器
package main
import (
"fmt"
"log"
"net/http"
)
func HelloServer(w http.ResponseWriter, req *http.Request) {
fmt.Println("Inside HelloServer handler")
fmt.Fprintf(w, "Hello,"+req.URL.Path[1:])
}
func main() {
http.HandleFunc("/", HelloServer)
err := http.ListenAndServe("localhost:8080", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err.Error())
}
}
发送网络请求
在下边这个程序中,数组中的 url 都将被访问:会发送一个简单的 http.Head()
请求查看返回值;它的声明如下:func Head(url string) (r *Response, err error)
package main
import (
"fmt"
"net/http"
)
var urls = []string{
"http://www.google.com/",
"http://golang.org/",
"http://blog.golang.org/",
}
func main() {
// Execute an HTTP HEAD request for all url's
// and returns the HTTP status string or an error string.
for _, url := range urls {
resp, err := http.Head(url)
if err != nil {
fmt.Println("Error:", url, err)
}
fmt.Println(url, ": ", resp.Status)
}
}
写一个简单的网页应用
边的程序在端口 8088 上启动了一个网页服务器;SimpleServer
会处理 /test1 url
使它在浏览器输出 hello world
。FormServer
会处理 /test2 url
:如果 url 最初由浏览器请求,那么它就是一个 GET 请求,并且返回一个 form 常量,包含了简单的 input 表单,这个表单里有一个文本框和一个提交按钮。当在文本框输入一些东西并点击提交按钮的时候,会发起一个 POST 请求。FormServer 中的代码用到了 switch 来区分两种情况。在 POST
情况下,使用 request.FormValue("inp")
通过文本框的 name 属性 inp 来获取内容,并写回浏览器页面。在控制台启动程序并在浏览器中打开 url http://localhost:8088/test2
来测试这个程序:
package main
import (
"io"
"net/http"
)
const form = `
<html><body>
<form action="#" method="post" name="bar">
<input type="text" name="in" />
<input type="submit" value="submit"/>
</form>
</body></html>
`
/* handle a simple get request */
func SimpleServer(w http.ResponseWriter, request *http.Request) {
io.WriteString(w, "<h1>hello, world</h1>")
}
func FormServer(w http.ResponseWriter, request *http.Request) {
w.Header().Set("Content-Type", "text/html")
switch request.Method {
case "GET":
/* display the form to the user */
io.WriteString(w, form)
case "POST":
/* handle the form data, note that ParseForm must
be called before we can extract form data */
//request.ParseForm();
//io.WriteString(w, request.Form["in"][0])
io.WriteString(w, request.FormValue("in"))
}
}
func main() {
http.HandleFunc("/test1", SimpleServer)
http.HandleFunc("/test2", FormServer)
if err := http.ListenAndServe(":8088", nil); err != nil {
panic(err)
}
}
web应用的异常处理
当 web 服务器发生一个恐慌( panic )时,我们的 web 服务器就会终止。这样非常的糟糕:一个 web 服务必须是一个健壮的程序,能够处理可能会出现的问题。
一个方法是可以在每一个处理函数( handler )中去使用 defer/recover
,但是这样会导致出现很多重复的代码。更加优雅的解决方法是使用中的闭包的方法处理错误。我们将这种机制应用到上一节中的 simple webserver
中,当然,它也可以很容易的应用于任何 web 服务器的程序中。
为了使代码更具可读性,我们为处理函数(HandleFunc)创建一个 type :
type HandleFnc func(http.ResponseWriter,*http.Request)
创建一个 logPanics 函数:
func logPanics(function HandleFnc) HandleFnc {
return func(writer http.ResponseWriter, request *http.Request) {
defer func() {
if x := recover(); x != nil {
log.Printf("[%v] caught panic: %v", request.RemoteAddr, x)
}
}()
function(writer, request)
}
}
然后我将处理函数作为回调包装进 logPanics:
http.HandleFunc("/test1", logPanics(SimpleServer))
http.HandleFunc("/test2", logPanics(FormServer))
下面是完整的代码示例:
package main
import (
"net/http"
"io"
"log"
)
type HandleFnc func(http.ResponseWriter,*http.Request)
// ... 其他函数
func main() {
http.HandleFunc("/test1", logPanics(SimpleServer))
http.HandleFunc("/test2", logPanics(FormServer))
if err := http.ListenAndServe(":8088", nil); err != nil {
panic(err)
}
}
func logPanics(function HandleFnc) HandleFnc {
return func(writer http.ResponseWriter, request *http.Request) {
defer func() {
if x := recover(); x != nil {
log.Printf("[%v] caught panic: %v", request.RemoteAddr, x)
// 下面一行代码是译者添加,默认出现 panic 只会记录日志,页面就是一个无任何输出的白页面,
// 可以给页面一个错误信息,如下面的示例返回了一个 500
http.Error(writer, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
}()
function(writer, request)
}
}
在 Web 应用中使用模板
下面的程序是一个用来运行 wiki 的 web 应用,它用了不到 100 行代码实现了一组页面的显示、编辑、和保存。它是一个 Go 网站的 codelab 中的一个 wiki 教程,我认为它是最好的 Go 教程 之一;可以通过 wiki 查看完整的代码,可以更好的理解程序是如何构建的。在这里我们将从上到下对整个程序进行补充说明。这个程序是一个 web 服务器,所以它必须在命令行启动(译者注:不要在 IDE 中启动,否则会找不到路径,必须在命令行启动),比如在 8080 端口。浏览器可以通过像这样的 url 来访问 wiki 页面的内容: localhost:8080/view/page1 。
然后会到和这个名字(page1)相同的文本文件中读取文件的内容展示在页面中;页面中包含了一个可以编辑 wiki 页面的超链接( localhost:8080/edit/page1 )。编辑页面用一个文本框显示内容,用户可以修改文本并通过 Save 按钮保存到文件中;然后会在相同的页面(view/page1)中查看到被修改的内容。如果想要查看的页面不存在(例如: localhost:8080/edit/page999 ),程序会将其跳转到一个编辑页面,这样就可以创建并保存一个新的 wiki 页面。
这个 wiki 页面需要一个标题和文本内容;它在程序中是由下面的结构体组成,内容是一个叫 Body 的字节切片。
type Page struct {
Title string
Body []byte
}
package main
import (
"net/http"
"io/ioutil"
"log"
"regexp"
"text/template"
)
const lenPath = len("/view/")
var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$")
var templates = make(map[string]*template.Template)
var err error
type Page struct {
Title string
Body []byte
}
func init() {
for _, tmpl := range []string{"edit", "view"} {
templates[tmpl] = template.Must(template.ParseFiles(tmpl + ".html"))
}
}
func main() {
http.HandleFunc("/view/", makeHandler(viewHandler))
http.HandleFunc("/edit/", makeHandler(editHandler))
http.HandleFunc("/save/", makeHandler(saveHandler))
err := http.ListenAndServe(":8080", nil)
if err != nil {
log.Fatal("ListenAndServe: ", err.Error())
}
}
func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.
HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
title := r.URL.Path[lenPath:]
if !titleValidator.MatchString(title) {
http.NotFound(w, r)
return
}
fn(w, r, title)
}
}
func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
p, err := load(title)
if err != nil {
// 找不到页面
http.Redirect(w, r, "/edit/" + title, http.StatusFound)
return
}
renderTemplate(w, "view", p)
}
func editHandler(w http.ResponseWriter, r *http.Request, title string) {
p, err := load(title)
if err != nil {
p = &Page{Title: title}
}
renderTemplate(w, "edit", p)
}
func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
body := r.FormValue("body")
p := &Page{Title: title, Body: []byte(body)}
err := p.save()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
http.Redirect(w, r, "/view/" + title, http.StatusFound)
}
func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
err := templates[tmpl].Execute(w, p)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
func (p *Page) save() error {
filename := p.Title + ".txt"
// 创建一个只有当前用户拥有读写权限的文件
return ioutil.WriteFile(filename, p.Body, 0600)
}
func load(title string) (*Page, error) {
filename := title + ".txt"
body, err := ioutil.ReadFile(filename)
if err != nil {
return nil, err
}
return &Page{Title: title, Body: body}, nil
}
Template 扩展的功能
在上一节中,我们使用了模板去合并结构体与 html 模板中的数据。这对于构建 web 应用程序确实非常有用,但是模板技术比这更通用:数据驱动模板可以用于生成文本输出,HTML 仅仅是其中一个特例。
通过执行 template 将模板与数据结构合并,在大多数情况下是一个结构体或者一个结构体的切片。它可以重写一段文本,通过传递给 templ.Execute ()
的数据项进行替换生成新的内容。只有能被导出的数据项才可以用于模板合并。操作可以是数据评估或控制结构,并通过 「{{
」和「}}
」定义。数据项可以是值或者指针;接口隐藏了间接引用(个人觉得应该是接口将传递的是值还是指针忽略了,因为实际使用中,无论是指针还是值都可以直接通过 .Title
来使用)。
字段替代: {{.FieldName}}
package main
import (
"os"
"text/template"
)
type Person struct {
Name string
nonExportedAgeField string
}
func main() {
t := template.New("hello")
t, _ = t.Parse("hello {{.Name}}!")
p := Person{Name:"Mary", nonExportedAgeField: "31"} // data
if err := t.Execute(os.Stdout, p); err != nil {
fmt.Println("There was an error:", err.Error())
}
}
// Output: hello Mary!
我们的结构体包含了一个不能导出的字段,并且当我们尝试通过一个定义字符串去合并他时,像下面这样:
t, _ = t.Parse("your age is {{.nonExportedAgeField}}!")
发生下面错误:There was an error: template:nonexported template hello:1: can’t evaluate field nonExportedAgeField in type main.Person.
注: 可能是新版本已经更新了这部分代码,现在(go1.10.1 )已经不会出现上面的错误,如果在结构体中没有定义
nonExportedAgeField
字段,在p := Person{Name:"Mary", nonExportedAgeField: "31"}
的时候就直接编译不过去了,如果定义了nonExportedAgeField
字段(小写开头不能导出),也不会报错,只是模板中定义的{{.nonExportedAgeField}}
不显示内容。但是直接使用{{ . }}
,不管字段是否可以导出,会将两个字段全部输出。
你可以直接在 Execute()
中使用 {{.}}
直接显示两个参数,结果就是: hello {Mary 31}
!
当模板应用在浏览器中时,要先用 html 过滤器去过滤输出的内容,像这样: {{html .}}
,或者使用一个 FieldName {{ .FieldName |html }}
|html
部分告诉 template 引擎在输出 FieldName
的值之前要通过 html
格式化它。他会转义特殊的 html
字符( 如:会将 >
替换成 >
), 这样可以防止用户的数据破坏 HTML 表单。
模板验证
检查模板的语法是否定义正确,对 Parse 的结果执行 Must 函数。在下面的示例中 tOK 是正确, tErr 的验证会出现错误并会导致一个运行时恐慌(panic)!
package main
import (
"text/template"
"fmt"
)
func main() {
tOk := template.New("ok")
// 一个有效的模板,所以 Must 时候不会出现恐慌(panic)
template.Must(tOk.Parse("/*这是一个注释 */ some static text: {{ .Name }}"))
fmt.Println("The first one parsed OK.")
fmt.Println("The next one ought to fail.")
tErr := template.New("error_template")
template.Must(tErr.Parse("some static text {{ .Name }"))
}
If-else
package main
import (
"os"
"text/template"
)
func main() {
tEmpty := template.New("template test")
// if 是一个空管道时的内容
tEmpty = template.Must(tEmpty.
Parse("Empty pipeline if demo: {{if ``}} Will not print. {{end}}\n"))
tEmpty.Execute(os.Stdout, nil)
tWithValue := template.New("template test")
// 如果条件满足,则为非空管道
tWithValue = template.Must(tWithValue.
Parse("Non empty pipeline if demo: {{if `anything`}} Will print. {{end}}\n"))
tWithValue.Execute(os.Stdout, nil)
tIfElse := template.New("template test")
// 如果条件满足,则为非空管道
tIfElse = template.Must(tIfElse.
Parse("if-else demo: {{if `anything`}} Print IF part. {{else}} Print ELSE part.{{end}}\n"))
tIfElse.Execute(os.Stdout, nil)
}
.
与 with-end
在 Go 模板中使用 (.)
: 他的值 {{.}}
被设置为当前管道的值。
with 语句将点的值设置为管道的值。如果管道是空的,就会跳过 with 到 end 之前的任何内容;当嵌套使用时,点会从最近的范围取值。在下面这个程序中会说明:
package main
import (
"os"
"text/template"
)
func main() {
t := template.New("test")
t, _ = t.Parse("{{with `hello`}}{{.}}{{end}}!\n")
t.Execute(os.Stdout, nil)
t, _ = t.Parse("{{with `hello`}}{{.}} {{with `Mary`}}{{.}}{{end}} {{end}}!\n")
t.Execute(os.Stdout, nil)
}
RPC 远程调用
Go 程序可以通过 net/rpc
包相互通讯,所以这是另一个客户端 - 服务器端模式的应用。它提供了通过网络连接进行函数调用的便捷方法。只有程序运行在不同的机器上它才有用。rpc
包建立在 gob 上,将其编码 / 解码,自动转换成可以通过网络调用的方法。
服务器注册一个对象,通过对象的类型名称暴露这个服务:注册后就可以通过网络或者其他远程客户端的 I/O
连接它的导出方法。这是关于通过网络暴露类型上的方法。
这个包使用了 http
协议、tcp
协议和用于数据传输的 gob
包。服务器可以注册多个不同类型的对象(服务),但是相同的类型注册多个对象的时候会出错。
这里我们讨论一个简单的示例: 我们定义一个 Args
类型,并且在它上面创建一个 Multiply
方法,最好封装在一个单独的包中;这个方法必须返回一个可能的错误。
// rpc_objects.go
package rpc_objects
import "net"
type Args struct {
N, M int
}
func (t *Args) Multiply(args *Args, reply *int) net.Error {
*reply = args.N * args.M
return nil
}
// rpc_server.go
package main
import (
"net/http"
"log"
"net"
"net/rpc"
"time"
"./rpc_objects"
)
func main() {
calc := new(rpc_objects.Args)
rpc.Register(calc)
rpc.HandleHTTP()
listener, e := net.Listen("tcp", "localhost:1234")
if e != nil {
log.Fatal("Starting RPC-server -listen error:", e)
}
go http.Serve(listener, nil)
time.Sleep(1000e9)
}
/* 输出:
启动程序 E:/Go/GoBoek/code_examples/chapter_14/rpc_server.exe ...
** after 5 s: **
End Process exit status 0
*/
客户端必须知道服务器端定义的对象的类型和它的方法。它调用 rpc.DialHTTP()
去创建连接的客户端,当客户端被创建时,它可以通过 client.Call("Type. Method", args, &reply)
去调用远程的方法,其中 Type
与 Method
是调用的远程服务器端被定义的类型和方法, args
是一个类型的初始化对象,reply
是一个变量,使用前必须要先声明它,它用来存储调用方法的返回结果。
// rpc_client.go
// 如果服务器端没有启动:
// 不能启动服务, 所以客户端会立刻停止并报错:
// 2011/08/01 16:08:05 Error dialing:dial tcp :1234:
// The requested address is not valid in its context.
// with serverAddress = localhost:
// 2011/08/01 16:09:23 Error dialing:dial tcp 127.0.0.1:1234:
// No connection could be made because the target machine actively refused it.
package main
import (
"fmt"
"log"
"net/rpc"
"./rpc_objects"
)
const serverAddress = "localhost"
func main() {
client, err := rpc.DialHTTP("tcp", serverAddress + ":1234")
if err != nil {
log.Fatal("Error dialing:", err)
}
// Synchronous call
args := &rpc_objects.Args{7, 8}
var reply int
err = client.Call("Args.Multiply", args, &reply)
if err != nil {
log.Fatal("Args error:", err)
}
fmt.Printf("Args: %d * %d = %d", args.N, args.M, reply)
}
/* 输出结果:
Starting Process E:/Go/GoBoek/code_examples/chapter_14/rpc_client.exe ...
Args: 7 * 8 = 56
End Process exit status 0
*/
服务器创建一个用于计算的对象,并且将它通过 rpc.Register(object)
注册,调用 HandleHTTP()
,并在一个地址上使用 net.Listen
开始监听。你也可以通过名称注册对象,如:rpc.RegisterName("Calculator", calc)
注:
rpc.Register
要求Multiply
方法的返回值要求是一个error
类型,所以示例的net.Error
执行会出错,因此要换成error
类型(有可能是版本更新造成的,测试使用的 Go 版本为: go1.10.1 )。
对每一个进入到 listener
的请求,都是由协程去启动一个 http.Serve(listener, nil)
,为每一个传入的 HTTP
连接创建一个新的服务线程。我们必须保证在一个特定的时间内服务器是唤醒状态,例如:time.Sleep(1000e9)
(1000 秒)
暂无评论内容