网络、模块和网页应用

内容纲要

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)。接下来分割后的输入通过 connectionWrite 方法被发送到服务器。

当然,服务器必须先启动好,如果服务器并未开始监听,客户端是无法成功连接的。

如果在服务器没有开始监听的情况下运行客户端程序,客户端会停止并打印出以下错误信息:对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 或者 IPv6TCP 或者 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 worldFormServer 会处理 /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) 去调用远程的方法,其中 TypeMethod 是调用的远程服务器端被定义的类型和方法, 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 秒)

© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片

    暂无评论内容