tcp服务器

这部分将使用TCP协议和协程范式编写一个简单的客户端-服务端应用,一个(web)服务器应用需要响应众多客户端的并发请求:Go会为每一个客户端产生一个协程用来处理请求。需要使用net包中网络通信的功能。它包含了处理TCP/IP以及UDP协议、域名解析等方法。

server.go:

  1. package main
  2. import (
  3. "fmt"
  4. "net"
  5. )
  6. func doServerStuff(conn net.Conn) {
  7. for {
  8. buf := make([]byte, 512)
  9. len, err := conn.Read(buf)
  10. if err != nil {
  11. fmt.Println("Error reading", err.Error())
  12. return
  13. }
  14. fmt.Printf("Received data: %v", string(buf[:len]))
  15. }
  16. }
  17. func main() {
  18. fmt.Println("Starting the server ...")
  19. listener, err := net.Listen("tcp", "localhost:50000")
  20. if err != nil {
  21. fmt.Println("Error listening", err.Error())
  22. return // 终止程序
  23. }
  24. for {
  25. conn, err := listener.Accept()
  26. if err != nil {
  27. fmt.Println("Error accepting", err.Error())
  28. return // 终止程序
  29. }
  30. go doServerStuff(conn)
  31. }
  32. }

在 main() 中创建了一个 net.Listener 类型的变量 listener,他实现了服务器的基本功能:用来监听和接收来自客户端的请求(在 localhost 即 IP 地址为 127.0.0.1 端口为 50000 基于 TCP 协议)。Listen() 函数可以返回一个 error 类型的错误变量。用一个无限 for 循环的 listener.Accept() 来等待客户端的请求。客户端的请求将产生一个 net.Conn 类型的连接变量。然后一个独立的协程使用这个连接执行 doServerStuff(),开始使用一个 512 字节的缓冲 data 来读取客户端发送来的数据,并且把它们打印到服务器的终端,len 获取客户端发送的数据字节数;当客户端发送的所有数据都被读取完成时,协程就结束了。这段程序会为每一个客户端连接创建一个独立的协程。必须先运行服务器代码,再运行客户端代码。

client.go:

  1. package main
  2. import (
  3. "bufio"
  4. "fmt"
  5. "net"
  6. "os"
  7. "strings"
  8. )
  9. func main() {
  10. conn, err := net.Dial("tcp", "localhost:50000")
  11. if err != nil {
  12. fmt.Println("Error dialing", err.Error())
  13. return
  14. }
  15. inputReader := bufio.NewReader(os.Stdin)
  16. fmt.Println("First, what is your name?")
  17. clientName, _ := inputReader.ReadString('\n')
  18. trimmedClient := strings.Trim(clientName, "\n")
  19. // 给服务器发送信息直到程序退出
  20. for {
  21. fmt.Println("What to send to the server? Type Q to quit")
  22. input, _ := inputReader.ReadString('\n')
  23. trimmedInput := strings.Trim(input, "\n")
  24. if trimmedInput == "Q" {
  25. return
  26. }
  27. _, err = conn.Write([]byte(trimmedClient + "says: " + trimmedInput))
  28. }
  29. }

客户端通过 net.Dial 创建了一个和服务器之间的连接。它通过无限循环从 os.Stdin 接收来自键盘的输入,直到输入了“Q”。注意裁剪 \r 和 \n 字符(仅 Windows 平台需要)。裁剪后的输入被 connection 的 Write 方法发送到服务器。当然,服务器必须先启动好,如果服务器并未开始监听,客户端是无法成功连接的。

在网络编程中 net.Dial 函数是非常重要的,一旦你连接到远程系统,函数就会返回一个 Conn 类型的接口,我们可以用它发送和接收数据。Dial 函数简洁地抽象了网络层和传输层。所以不管是 IPv4 还是 IPv6,TCP 或者 UDP 都可以使用这个公用接口。

以下示例先使用 TCP 协议连接远程 80 端口,然后使用 UDP 协议连接,最后使用 TCP 协议连接 IPv6 地址:

  1. // make a connection with www.example.org:
  2. package main
  3. import (
  4. "fmt"
  5. "net"
  6. "os"
  7. )
  8. func main() {
  9. conn, err := net.Dial("tcp", "192.0.32.10:80") // tcp ipv4
  10. checkConnection(conn, err)
  11. conn, err = net.Dial("udp", "192.0.32.10:80") // udp
  12. checkConnection(conn, err)
  13. conn, err = net.Dial("tcp", "[2620:0:2d0:200::10]:80") // tcp ipv6
  14. checkConnection(conn, err)
  15. }
  16. func checkConnection(conn net.Conn, err error) {
  17. if err != nil {
  18. fmt.Printf("error %v connecting!", err)
  19. os.Exit(1)
  20. }
  21. fmt.Printf("Connection is made with %v\n", conn)
  22. }

下班是一个使用net包从socket中打开,写入、读取数据的例子:

  1. package main
  2. import (
  3. "fmt"
  4. "io"
  5. "net"
  6. )
  7. func main() {
  8. var (
  9. host = "www.apache.org"
  10. port = "80"
  11. remote = host + ":" + port
  12. msg string = "GET / \n"
  13. data = make([]uint8, 4096)
  14. read = true
  15. count = 0
  16. )
  17. // 创建一个socket
  18. con, err := net.Dial("tcp", remote)
  19. // 发送消息,一个http Get请求
  20. io.WriteString(con, msg)
  21. // 读取服务器的响应
  22. for read {
  23. count, err = con.Read(data)
  24. read = (err == nil)
  25. fmt.Printf(string(data[0:count]))
  26. }
  27. con.Close()
  28. }

下边这个版本的 simple_tcp_server.go 从很多方面优化了第一个 tcp 服务器的示例 server.go 并且拥有更好的结构,它只用了 80 行代码!

simple_tcp_server.go:

  1. package main
  2. import (
  3. "flag"
  4. "fmt"
  5. "net"
  6. "syscall"
  7. )
  8. const maxRead = 25
  9. func initServer(hostAndPort string) net.Listener {
  10. serverAddr, err := net.ResolveTCPAddr("tcp", hostAndPort)
  11. checkError(err, "Resolving address:port failed: '"+hostAndPort+"'")
  12. listener, err := net.ListenTCP("tcp", serverAddr)
  13. checkError(err, "ListenTCP: ")
  14. println("Listening to: ", listener.Addr().String())
  15. return listener
  16. }
  17. func connectionHandler(conn net.Conn) {
  18. connFrom := conn.RemoteAddr().String()
  19. println("Connection from: ", connFrom)
  20. sayHello(conn)
  21. for {
  22. var ibuf []byte = make([]byte, maxRead+1)
  23. length, err := conn.Read(ibuf[0:maxRead])
  24. ibuf[maxRead] = 0
  25. switch err {
  26. case nil:
  27. handleMsg(length, err, ibuf)
  28. case syscall.EAGAIN: // try again
  29. continue
  30. default:
  31. goto DISCONNECT
  32. }
  33. }
  34. DISCONNECT:
  35. err := conn.Close()
  36. println("Closed connection: ", connFrom)
  37. checkError(err, "Close: ")
  38. }
  39. func sayHello(to net.Conn) {
  40. obuf := []byte{'L', 'e', 't', '\'', 's', ' ', 'G', 'O', '!', '\n'}
  41. wrote, err := to.Write(obuf)
  42. checkError(err, "Write: write "+string(wrote)+" bytes.")
  43. }
  44. func handleMsg(length int, err error, msg []byte) {
  45. if length > 0 {
  46. print("<", length, ":")
  47. for i := 0; ; i++ {
  48. if msg[i] == 0 {
  49. break
  50. }
  51. fmt.Printf("%c", msg[i])
  52. }
  53. print(">")
  54. }
  55. }
  56. func checkError(error error, info string) {
  57. if error != nil {
  58. panic("ERROR: " + info + " " + error.Error()) // terminate program
  59. }
  60. }
  61. func main() {
  62. flag.Parse()
  63. if flag.NArg() != 2 {
  64. panic("usage: host port")
  65. }
  66. hostAndPort := fmt.Sprintf("%s:%s", flag.Arg(0), flag.Arg(1))
  67. listener := initServer(hostAndPort)
  68. for {
  69. conn, err := listener.Accept()
  70. checkError(err, "Accept: ")
  71. go connectionHandler(conn)
  72. }
  73. }
  • 在 initServer 函数中通过 net.ResolveTCPAddr 得到了服务器地址和端口,这个函数最终返回了一个 *net.TCPListener
  • 每一个连接都会以协程的方式运行 connectionHandler 函数。函数首先通过 conn.RemoteAddr() 获取到客户端的地址并显示出来
  • 它使用 conn.Write 发送 Go 推广消息给客户端
  • 它使用一个 25 字节的缓冲读取客户端发送的数据并一一打印出来。如果读取的过程中出现错误,代码会进入 switch 语句 default 分支,退出无限循环并关闭连接。如果是操作系统的 EAGAIN 错误,它会重试。
  • 所有的错误检查都被重构在独立的函数 checkError 中,当错误产生时,利用错误上下文来触发 panic。

一个简单的web服务器

Go提供了net/http包,下边示例是是一个简单的网页服务器,它引入http包并启动了网页服务器,使用http.ListenAndServe("localhost:8080", nil)函数,如果成功则返回空,否则会返回一个错误(地址localhost部分可以省略,8080是指定的端口号)。

http.URL用于表示网页地址,其中字符串属性Path用于保存url的路径;http.Request描述了客户端请求,内含一个URL字段。

如果req是来自html表单的POST类型请求,”var1”是该表单中一个输入域的名称,那么用户输入的值就可以通过Go代码req.FormValue("var1")获取到。还有一种方法是先执行request.ParseForm(),然后再获取request.Form["var1"]的第一个返回参数:

  1. var1, found := request.Form["var1"]

第二个参数found为true。如果var1并未出现在表单中,found就是false。

表单属性实际上是map[string][]string类型。网页服务器发送一个http.Response响应,它是通过http.ResponseWriter对象输出的,后者组装了HTTP服务器响应,通过对其写入内容,将数据发送给了HTTP客户端。

现在仍然要编写程序,以实现服务器必须做的事,即如何处理请求。这是通过http.HandleFunc函数完成的。在这个例子中,当根路径”/“被请求的时候,HelloServer函数被执行了。这个函数是http.HandleFunc类型的,通常被命名为Prefhandler,和某个路径前缀Pref匹配。

http.HandleFunc注册了一个处理函数(这里是HelloServer)来处理对应/的请求。

/可以被替换为其他更特定的url,比如/create,/edit等;可以为每一个特定的url定义个单独的处理函数。这个函数需要两个参数:第一个是ReponseWriter类型的w;第二个是请求req。程序向w写入了Hellor.URL.Path[1:]组成的字符串:末尾的[]表示”创建一个从索引为1的字符到结尾的子切片“,用来丢弃路径开头的”/“,fmt.Fprint()函数完成了本次写入;另一种可行的写法是io.WriteString(w, "hello\n")

总结:第一个参数是请求的路径,第二个参数是当路径被请求时,需要调用的处理函数的引用。

webserver.go:

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net/http"
  6. )
  7. func HelloServer(w http.ResponseWriter, req *http.Request) {
  8. fmt.Println("Inside helloserver handler")
  9. fmt.Fprintf(w, "hello,"+req.URL.Path[1:])
  10. }
  11. func main() {
  12. http.HandleFunc("/", HelloServer)
  13. err := http.ListenAndServe("localhost:8080", nil)
  14. if err != nil {
  15. log.Fatal("ListenAndServer", err.Error())
  16. }
  17. }
  1. 前两行(没有错误处理代码)可以替换成以下写法:
  1. http.ListenAndServe(":8080", http.HandleFunc(HelloServer))
  1. fmt.Fprintfmt.Fprintf都是可以用来写入http.ResponseWriter的函数(他们实现了io.Writer
  1. fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", title, body)
  1. 如果你需要使用安全的https连接,使用http.ListenAndServeTLS()代替http.ListenAndServe()
  2. 除了http.HandleFunc("/", Hfunc),其中的Hfunc是一个处理函数,签名为:
  1. func Hfunc(w http.ResponseWriter, req *http.Request) {
  2. ...
  3. }

也可以使用这种方式:http.Handle("/",http.HandlerFunc(Hfunc))

HandlerFunc只是定义了上述HFunc签名的别名:

  1. type HandlerFunc func(ResponseWriter, *Request)

它是一个可以把普通的函数当做HTTP处理器(Handler)的适配器。如果函数f声明的合适,HandlerFunc(f)就是一个执行f函数的Handler对象。

http.Handle的第二个参数也可以是T类型的对象obj:http.Handle("/", obj),如果T有ServeHTTP方法,那就实现了http的Handler接口:

  1. func (obj *Typ) ServeHTTP( w http.ResponseWriter, req *http.Request ) {
  2. ...
  3. }

访问并读取页面数据

在下边这个程序中,数组中的url都将被访问:会发送一个简单的http.Head()请求查看返回值;它的声明如下:func Head(url string) (r *Response, err error),返回的响应Response其状态码会被打印出来

  1. package main
  2. import (
  3. "fmt"
  4. "net/http"
  5. )
  6. var urls = []string{
  7. "https://www.baidu.com",
  8. "https://golang.org",
  9. "https://www.google.com",
  10. }
  11. func main() {
  12. for _, url := range urls {
  13. reps, err := http.Head(url)
  14. if err != nil {
  15. fmt.Println("Error", url, err)
  16. }
  17. fmt.Println(url, ": ", reps.Status)
  18. }
  19. }

http包中其他重要的函数:

  • http.Redirect(w ResponseWriter, r *Request, url string, code int):这个函数会让浏览器重定向到url(可以是基于请求url的相对路径),同时指定状态码;
  • http.NotFound(w ResponseWriter, r *Request): 这个函数将返回网页没有找到,HTTP404错误;
  • http.Error(w ResponseWriter, error string, code int): 这个函数返回特定的错误信息和HTTP代码;
  • 另一个http.Request对象req的重要属性:req.Method,用来描述网页是以何种方式被请求的;
  • 可以使用w.header().Set("Content-Type", "../..")设置头信息

一个简单的网页应用

下边的程序在端口 8088 上启动了一个网页服务器;SimpleServer 会处理 url /test1 使它在浏览器输出 hello world。FormServer 会处理 url /test2:如果 url 最初由浏览器请求,那么它是一个 GET 请求,返回一个 form 常量,包含了简单的 input 表单,这个表单里有一个文本框和一个提交按钮。当在文本框输入一些东西并点击提交按钮的时候,会发起一个 POST 请求。FormServer 中的代码用到了 switch 来区分两种情况。请求为 POST 类型时,name 属性 为 inp 的文本框的内容可以这样获取:request.FormValue(“inp”)。然后将其写回浏览器页面中。在控制台启动程序,然后到浏览器中打开 url http://localhost:8088/test2 来测试这个程序:

  1. package main
  2. import (
  3. "io"
  4. "net/http"
  5. )
  6. const form = `
  7. <html> <body>
  8. <form action="#" method="post" name="bar">
  9. <input type="text" name="in" />
  10. <input type="submit" value="submit" />
  11. </form>
  12. </body></html>
  13. `
  14. /* handle a simple get request */
  15. func SimpleServer(w http.ResponseWriter, req *http.Request) {
  16. io.WriteString(w, "<h1> hello, world </h1>")
  17. }
  18. func FormServer(w http.ResponseWriter, req *http.Request) {
  19. w.Header().Set("Context-Type", "text/html")
  20. switch req.Method {
  21. case "GET":
  22. io.WriteString(w, form)
  23. case "POST":
  24. io.WriteString(w, req.FormValue("in"))
  25. }
  26. }
  27. func main() {
  28. http.HandleFunc("/test1", SimpleServer)
  29. http.HandleFunc("/test2", FormServer)
  30. if err := http.ListenAndServe(":8080", nil); err != nil {
  31. panic(err)
  32. }
  33. }

练习:编写一个网页程序,可以让用户输入一连串的数字,然后将它们打印出来,计算出这些数字的均值和中值

  1. // statistics.go
  2. package main
  3. import (
  4. "fmt"
  5. "log"
  6. "net/http"
  7. "sort"
  8. "strconv"
  9. "strings"
  10. )
  11. type statistics struct {
  12. numbers []float64
  13. mean float64
  14. median float64
  15. }
  16. const form = `<html><body><form action="/" method="POST">
  17. <label for="numbers">Numbers (comma or space-separated):</label><br>
  18. <input type="text" name="numbers" size="30"><br />
  19. <input type="submit" value="Calculate">
  20. </form></html></body>`
  21. const error = `<p class="error">%s</p>`
  22. var pageTop = ""
  23. var pageBottom = ""
  24. func main() {
  25. http.HandleFunc("/", homePage)
  26. if err := http.ListenAndServe(":9001", nil); err != nil {
  27. log.Fatal("failed to start server", err)
  28. }
  29. }
  30. func homePage(writer http.ResponseWriter, request *http.Request) {
  31. writer.Header().Set("Content-Type", "text/html")
  32. err := request.ParseForm() // Must be called before writing response
  33. fmt.Fprint(writer, pageTop, form)
  34. if err != nil {
  35. fmt.Fprintf(writer, error, err)
  36. } else {
  37. if numbers, message, ok := processRequest(request); ok {
  38. stats := getStats(numbers)
  39. fmt.Fprint(writer, formatStats(stats))
  40. } else if message != "" {
  41. fmt.Fprintf(writer, error, message)
  42. }
  43. }
  44. fmt.Fprint(writer, pageBottom)
  45. }
  46. func processRequest(request *http.Request) ([]float64, string, bool) {
  47. var numbers []float64
  48. var text string
  49. if slice, found := request.Form["numbers"]; found && len(slice) > 0 {
  50. //处理如果网页中输入的是中文逗号
  51. if strings.Contains(slice[0], "&#65292") {
  52. text = strings.Replace(slice[0], "&#65292;", " ", -1)
  53. } else {
  54. text = strings.Replace(slice[0], ",", " ", -1)
  55. }
  56. for _, field := range strings.Fields(text) {
  57. if x, err := strconv.ParseFloat(field, 64); err != nil {
  58. return numbers, "'" + field + "' is invalid", false
  59. } else {
  60. numbers = append(numbers, x)
  61. }
  62. }
  63. }
  64. if len(numbers) == 0 {
  65. return numbers, "", false // no data first time form is shown
  66. }
  67. return numbers, "", true
  68. }
  69. func getStats(numbers []float64) (stats statistics) {
  70. stats.numbers = numbers
  71. sort.Float64s(stats.numbers)
  72. stats.mean = sum(numbers) / float64(len(numbers))
  73. stats.median = median(numbers)
  74. return
  75. }
  76. func sum(numbers []float64) (total float64) {
  77. for _, x := range numbers {
  78. total += x
  79. }
  80. return
  81. }
  82. func median(numbers []float64) float64 {
  83. middle := len(numbers) / 2
  84. result := numbers[middle]
  85. if len(numbers)%2 == 0 {
  86. result = (result + numbers[middle-1]) / 2
  87. }
  88. return result
  89. }
  90. func formatStats(stats statistics) string {
  91. return fmt.Sprintf(`<table border="1">
  92. <tr><th colspan="2">Results</th></tr>
  93. <tr><td>Numbers</td><td>%v</td></tr>
  94. <tr><td>Count</td><td>%d</td></tr>
  95. <tr><td>Mean</td><td>%f</td></tr>
  96. <tr><td>Median</td><td>%f</td></tr>
  97. </table>`, stats.numbers, len(stats.numbers), stats.mean, stats.median)
  98. }

确保网页应用健壮

当网页应用的处理函数发生panic,服务器会简单地终止运行。网页服务器必须是足够健壮的程序,能够承受任何可能的突发问题。

首先想到的是在每个处理函数中使用defer/recover,但会产生太多的重复代码。使用闭包的错误处理模式是更优雅的方案,它可以被简单应用到任何网页服务器程序中。

为增强代码可读性,为网页处理函数创建一个类型:

  1. type HandleFnc func(http.ResponseWriter, *http.Request)

错误处理函数logPanics:

  1. func logPaincs(function HandleFnc) HandleFnc {
  2. return func(writer http.ResponseWriter, request *http.Request) {
  3. defer func() {
  4. if x:=recover();x != nil {
  5. log.Printf("[%v] caught panic: %v", request.RemoteAdder, x)
  6. }
  7. }()
  8. function(writer, request)
  9. }
  10. }

然后用logPanics来包装对处理函数的调用:

  1. http.HandleFunc("/test1", logPanics(SimpleServer))
  2. http.HandleFunc("/test2", logPanics(FormServer))

rebust_webserver.go:

  1. package main
  2. import (
  3. "io"
  4. "log"
  5. "net/http"
  6. )
  7. const form = `
  8. <html> <body>
  9. <form action="#" method="post" name="bar">
  10. <input type="text" name="in" />
  11. <input type="submit" value="submit" />
  12. </form>
  13. </body></html>
  14. `
  15. /* handle a simple get request */
  16. func SimpleServer(w http.ResponseWriter, req *http.Request) {
  17. io.WriteString(w, "<h1> hello, world </h1>")
  18. }
  19. type HandleFnc func(http.ResponseWriter, *http.Request)
  20. func FormServer(w http.ResponseWriter, req *http.Request) {
  21. w.Header().Set("Context-Type", "text/html")
  22. switch req.Method {
  23. case "GET":
  24. io.WriteString(w, form)
  25. case "POST":
  26. io.WriteString(w, req.FormValue("in"))
  27. }
  28. }
  29. func main() {
  30. http.HandleFunc("/test1", logPanics(SimpleServer))
  31. http.HandleFunc("/test2", logPanics(FormServer))
  32. if err := http.ListenAndServe(":8080", nil); err != nil {
  33. panic(err)
  34. }
  35. }
  36. func logPanics(function HandleFnc) HandleFnc {
  37. return func(writer http.ResponseWriter, req *http.Request) {
  38. defer func() {
  39. if x := recover(); x != nil {
  40. log.Printf("[%v] caught panic: %v", req.RemoteAddr, x)
  41. }
  42. }()
  43. function(writer, req)
  44. }
  45. }

用模板编写网页应用

以下程序是用 100 行以内代码实现可行的 wiki 网页应用,它由一组页面组成,用于阅读、编辑和保存。它是来自 Go 网站 codelab 的 wiki 制作教程,我所知的最好的 Go 教程之一,非常值得进行完整的实验,以见证并理解程序是如何被构建起来的https://golang.org/doc/articles/wiki/。这里,我们将以自顶向下的视角,从整体上给出程序的补充说明。程序是网页服务器,它必须从命令行启动,监听某个端口,例如 8080。浏览器可以通过请求 URL 阅读 wiki 页面的内容,例如:http://localhost:8080/view/page1。

接着,页面的文本内容从一个文件中读取,并显示在网页中。它包含一个超链接,指向编辑页面http://localhost:8080/edit/page1。编辑页面将内容显示在一个文本域中,用户可以更改文本,点击“保存”按钮保存到对应的文件中。然后回到阅读页面显示更改后的内容。如果某个被请求阅读的页面不存在,例如:http://localhost:8080/edit/page999,程序可以作出识别,立即重定向到编辑页面,如此新的 wiki 页面就可以被创建并保存。

wiki页面需要一个标题和文本内容,它在程序中被建模为如下结构体,Body字段存放内容,由字节切片组成:

  1. type Page struct {
  2. Title string
  3. Body []byte
  4. }

示例:wiki.go:

  1. package main
  2. import (
  3. "html/template"
  4. "io/ioutil"
  5. "log"
  6. "net/http"
  7. "regexp"
  8. "text/template"
  9. )
  10. const lenPath = len("/view")
  11. var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$")
  12. var templates = make(map[string]*template.Template)
  13. var err error
  14. type Page struct {
  15. Title string
  16. Body []byte
  17. }
  18. func init() {
  19. for _, tmpl := range []string{"edit", "view"} {
  20. templates[tmpl] = template.Must(template.ParseFiles(tmpl + ".html"))
  21. }
  22. }
  23. func main() {
  24. http.HandleFunc("/view/", makeHandler(viewHandler))
  25. http.HandleFunc("/edit/", makeHandler(editHandler))
  26. http.HandleFunc("/save/", makeHandler(saveHandler))
  27. err := http.ListenAndServe(":8080", nil)
  28. if err != nil {
  29. log.Fatal("ListenAndServer: ", err.Error())
  30. }
  31. }
  32. func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
  33. return func(w http.ResponseWriter, r *http.Request) {
  34. title := r.URL.Path[lenPath:]
  35. if !titleValidator.MatchString(title) {
  36. http.NotFound(w, r)
  37. return
  38. }
  39. fn(w, r, title)
  40. }
  41. }
  42. func viewHandler(w http.ResponseWriter, r *http.Request, title string) {
  43. p, err := load(title)
  44. if err != nil { //page not found
  45. http.Redirect(w, r, "/edit/"+title, http.StatusFound)
  46. return
  47. }
  48. renderTemplate(w, "view", p)
  49. }
  50. func editHandler(w http.ResponseWriter, r *http.Request, title string) {
  51. p, err := load(title)
  52. if err != nil {
  53. p = &Page{Title: tile}
  54. }
  55. renderTemplate(w, "edit", p)
  56. }
  57. func saveHandler(w http.ResponseWriter, r *http.Request, title string) {
  58. body := r.FormValue("body")
  59. p := &Page{Title: title, Body: []byte(body)}
  60. err := p.save()
  61. if err != nil {
  62. http.Error(w, err.Error(), http.StatusInternalServerError)
  63. return
  64. }
  65. http.Redirect(w, r, "/view/"+title, http.StatusNotFound)
  66. }
  67. func renderTemplate(w http.ResponseWriter, tmpl string, p *Page) {
  68. err := templates[tmpl].Execute(w, p)
  69. if err != nil {
  70. http.Error(w, err.Error(), http.StatusInternalServerError)
  71. }
  72. }
  73. func (p *Page) save() error {
  74. filename := p.Title + ".txt"
  75. return ioutil.WriteFile(filename, p.Body, 0600)
  76. }
  77. func load(title string) (*Page, error) {
  78. filename := title + ".txt"
  79. body, err := ioutil.ReadFile(filename)
  80. if err != nil {
  81. return nil, err
  82. }
  83. return &Page{Title: title, Body: body}, nil
  84. }
  • io/ioutil方便地读写文件,regexp用于验证输入标题,template来动态创建html文档;
  • 为避免黑客构造特殊输入攻击服务器,用如下正则表达式检查用户在浏览器输入的URL:
  1. var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$")

makeHandler会用它对请求管控。

  • 必须有一种机制把Page结构体数据插入到网页的标题和内容中,可以利用template包通过如下步骤完成:

    • 现在文本编辑器中创建html模板文件,例如view.html:
      1. <h1> {{.Title |html}}</h1>
      2. <p>[<a href="/edit/{{.Title |html}}">edit</a>]</p>
      3. <div>{{printf "%s" .Body |html}}</div>
      {{.Title |html}}``{{printf "%s" .Body |html}}
    • template.Must(template.ParseFiles(tmpl + ".html"))把模板文件转换为*template.Template类型的对象,为了高效,在程序运行时仅做一次解析,在init()函数中处理可以方便地达到目的。所有模板对象都被保持在内存中,存放在以html文件名作为索引的map中:

      1. templates = make(map[string] *template.Template)
    • 为了真正从模板和结构体构建出页面,必须使用:

      1. templates[tmpl].Exectu(w, p)

      renderTemplate

  • 在 main() 中网页服务器用 ListenAndServe 启动并监听 8080 端口。需要先为紧接在 URL localhost:8080/ 之后, 以view, edit 或 save 开头的 url 路径定义一些处理函数。

在此定义了 3 个处理函数,由于包含重复的启动代码,我们将其提取到单独的 makeHandler 函数中。这是一个值得研究的特殊高阶函数:其参数是一个函数,返回一个新的闭包函数:

  1. func makeHandler(fn func(http.ResponseWriter, *http.Request, string)) http.HandlerFunc {
  2. return func(w http.ResponseWriter, r *http.Request) {
  3. title := r.URL.Path[lenPath:]
  4. if !titleValidator.MatchString(title) {
  5. http.NotFound(w, r)
  6. return
  7. }
  8. fn(w, r, title)
  9. }
  10. }
  • 闭包封闭了函数变量fn来构造其返回值。但在此之前,先用titleValidator.MatchString(title)验证输入标题title的有效性。如果标题包含了字母和数字以外的字符,就触发NotFound错误(例如:localhost:8080/view/page++)。viewHandler,editHandler和saveHandler都是传入main()中makrHandler的参数,类型必须都与fn相同。
  • viewHander尝试按标题读取文本文件,这是通过调用load()函数完成的,它会构建文件名并用ioutil.ReadFile读取内容。如果文件存在,其内容会存入字符串中。一个指向Page结构体的指针按字面量被创建:&Page{Title: title, Body: body}

另外,该值和表示没有error的nil值一起返回给调用者。然后再renderTemplate中将该结构体与模板对象整合。

万一发生错误,wiki页面在磁盘上不存在,错误会被返回给viewHandler,此时会自动重定向,跳转请求对应标题的编辑页面。

  • 当在编辑页面点击“保存”按钮时,触发保存页面内容的动作。按钮须放在html表单中,它开头是这样的:
  1. <form action="/save/{{.Title |html}}" method="POST">

这意味着,当提交表单到类似http://localhost/save/{Title}这样的 URL 格式时,一个 POST 请求被发往网页服务器。针对这样的URL我们已经定义好了处理函数:saveHandler()。在 request 对象上调用FormValue()方法,可以提取名称为body的文本域内容,用这些信息构造一个 Page 对象,然后尝试通过调用save()方法保存其内容。万一运行失败,执行http.Error以将错误显示到浏览器。如果保存成功,重定向浏览器到该页的阅读页面。save()函数非常简单,利用ioutil.WriteFile(),写入Page结构体的Body字段到文件filename中,之后会被用于模板替换占位符 {{printf “%s” .Body |html}}。

template包

template对象把数据结构整合到HTML模板中,模板是一项更为通用的技术方案:数据驱动的模板被创建出来,以生成文本输出。

模板通过与数据结构的整合来生成,通常为结构体或其切片。当数据传递给tmpl.Execute(),它用其中的元素进行替换,动态地重写某一小段文本。只有被导出的数据项才可以被整合进模板中。可以在{{ }}中加入数据求值或控制结构。数据项可以是值或者指针,接口隐藏了它们的差异。

字段替换:{{.FieldName}}

要在模板中包含某个字段的内容,使用双花括号括起以点(.)开头的字段名。例如,假设 Name 是某个结构体的字段,其值要在被模板整合时替换,则在模板中使用文本 {{.Name}}。当 Name 是 map 的键时这么做也是可行的。要创建一个新的 Template 对象,调用 template.New,其字符串参数可以指定模板的名称。正如上边示例出现过的,Parse 方法通过解析模板定义字符串,生成模板的内部表示。当使用包含模板定义字符串的文件时,将文件路径传递给 ParseFiles 来解析。解析过程如产生错误,这两个函数第二个返回值 error != nil。最后通过 Execute 方法,数据结构中的内容与模板整合,并将结果写入方法的第一个参数中,其类型为 io.Writer。再一次地,可能会有 error 返回。以下程序演示了这些步骤,输出通过 os.Stdout 被写到控制台。

template_field.go:

  1. package main
  2. import (
  3. "fmt"
  4. "os"
  5. "text/template"
  6. )
  7. type Person struct {
  8. Name string
  9. nonExporteAgeField string
  10. }
  11. func main() {
  12. t := template.New("hello")
  13. t, _ = t.Parse("hello {{.Name}}")
  14. // t, _ = t.Parse("your age is {{.nonExporteAgeField}}")
  15. // t, _ = t.Parse("hello {{.}}")
  16. p := Person{Name: "test", nonExporteAgeField: "30"}
  17. if err := t.Execute(os.Stdout, p); err != nil {
  18. fmt.Println("There was an error:", err.Error())
  19. }
  20. }

数据结构中包含一个未导出的字段,当尝试把它整合到类似这样的定义字符串:

  1. t, _ = t.Parse("your age is {{.nonExporteAgeField}}")

会产生错误:nonExporteAgeField is an unexported field of struct type main.Person

如果只是想简单地把Execute()方法的第二个参数用于替换,使用{{.}}

当在浏览器环境中进行这些步骤,应首先使用html过滤器来过滤内容,例如{{html .}},或者对FieldName过滤:{{.FieldName |html}}。|html这部分代码,是请求模板引擎在输出FieldName的结果前把值传递给html格式化器,它会执行HTML字符转义。这可以避免用户输入数据破坏HTML文档结构。

验证模板格式

为了确保模板定义语法是正确的,使用Must函数处理Parse的返回结果。在下面的例子中tOK是正确的模板,tErr验证时发生错误,会导致运行panic。

template_validation.go:

  1. package main
  2. import (
  3. "fmt"
  4. "html/template"
  5. )
  6. func main() {
  7. tOk := template.New("ok")
  8. template.Must(tOk.Parse("/* and a comment */ some static text: {{.Name}}"))
  9. fmt.Println("The first one parsed OK.")
  10. fmt.Println("The next one ought to fail.")
  11. tErr := template.New("error_template")
  12. template.Must(tErr.Parse("some static text {{.Name}}"))
  13. }

if-else

运行Execute产生的结果来自模板的输出,它包含静态文本,以及被{{}}包裹的称之为管道的文本。

可以对管道数据的输出结果用if-else-end设置条件约束:如果管道是空的,类似于:

  1. {{if ``}} Will not print. {{end}}

或者

  1. {{if `anything`}} Print IF part. {{else}} Print ELSE part.{{end}}

template_ifelse.go:

  1. package main
  2. import (
  3. "os"
  4. "html/template"
  5. )
  6. func main() {
  7. tEmpty := template.New("template test")
  8. tEmpty = template.Must(tEmpty.Parse("Empty pipeline if demo: {{if ``}} Will not print. {{end}}\n")) //empty pipeline following if
  9. tEmpty.Execute(os.Stdout, nil)
  10. tWithValue := template.New("template test")
  11. tWithValue = template.Must(tWithValue.Parse("Non empty pipeline if demo: {{if `anything`}} Will print. {{end}}\n")) //non empty pipeline following if condition
  12. tWithValue.Execute(os.Stdout, nil)
  13. tIfElse := template.New("template test")
  14. tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} Print IF part. {{else}} Print ELSE part.{{end}}\n")) //non empty pipeline following if condition
  15. tIfElse.Execute(os.Stdout, nil)
  16. }

点号和with-end

with语句将点号设为管道的值。如果管道是空的,那么不管with-end块之间有什么,都会被忽略。在被嵌套时,点号根据最近的作用域取得值。以下程序演示了这点:

  1. package main
  2. import (
  3. "os"
  4. "html/template"
  5. )
  6. func main() {
  7. t := template.New("test")
  8. t, _ = t.Parse("{{with `hello`}}{{.}}{{end}}!\n")
  9. t.Execute(os.Stdout, nil)
  10. t, _ = t.Parse("{{with `hello`}}{{.}} {{with `Mary`}}{{.}}{{end}}{{end}}!\n")
  11. t.Execute(os.Stdout, nil)
  12. }

模板变量$

可以在模板内为管道设置本地变量,变量名以$符号作为前缀。变量名只能包含字母、数字和下划线。以下示例使用了多种形式的有效变量名。

  1. package main
  2. import (
  3. "os"
  4. "html/template"
  5. )
  6. func main() {
  7. t := template.New("test")
  8. t = template.Must(t.Parse("{{with $3 := `hello`}}{{$3}}{{end}}!\n"))
  9. t.Execute(os.Stdout, nil)
  10. t = template.Must(t.Parse("{{with $x3 := `hola`}}{{$x3}}{{end}}!\n"))
  11. t.Execute(os.Stdout, nil)
  12. t = template.Must(t.Parse("{{with $x_1 := `hey`}}{{$x_1}} {{.}} {{$x_1}}{{end}}!\n"))
  13. t.Execute(os.Stdout, nil)
  14. }

range-end

range-end 结构格式为:{{range pipeline}} T1 {{else}} T0 {{end}}。

range被用于在集合上迭代:管道的值必须是数组、切片或map。如果管道的值长度为零,点号的值不受影响,且执行T0;否则,点号被设置为数组、切片或map内元素的值,并执行T1。

  1. {{range .}}
  2. {{.}}
  3. {{end}}
  4. s := []int{1,2,3,4}
  5. t.Execute(os.Stdout, s)

模板预定义函数

也有一些可以在模板代码中使用的预定义函数,例如printf函数工作方式类似于fmt.Sprintf:

  1. package main
  2. import (
  3. "os"
  4. "html/template"
  5. )
  6. func main() {
  7. t := template.New("test")
  8. t = template.Must(t.Parse("{{with $x := `hello`}}{{printf `%s %s` $x `Mary`}}{{end}}!\n"))
  9. t.Execute(os.Stdout, nil)
  10. }

网页服务器功能

  1. package main
  2. import (
  3. "bytes"
  4. "expvar"
  5. "flag"
  6. "fmt"
  7. "io"
  8. "log"
  9. "net/http"
  10. "os"
  11. "strconv"
  12. )
  13. var helloRequests = expvar.NewInt("hello-requests")
  14. var webroot = flag.String("root", "/home/user", "web root directory")
  15. var booleanflag = flag.Bool("boolean", true, "another flag for testing")
  16. type Counter struct {
  17. n int
  18. }
  19. type Chan chan int
  20. func main() {
  21. flag.Parse()
  22. http.Handle("/", http.HandlerFunc(Logger))
  23. http.Handle("/go/hello", http.HandlerFunc(HelloServer))
  24. ctr := new(Counter)
  25. expvar.Publish("counter", ctr)
  26. http.Handle("/counter", ctr)
  27. http.Handle("/go/", http.StripPrefix("/go/", http.FileServer(http.Dir(*webroot))))
  28. http.Handle("/flags", http.HandlerFunc(FlagServer))
  29. http.Handle("/args", http.HandlerFunc(ArgServer))
  30. http.Handle("/chan", ChanCreate())
  31. http.Handle("/date", http.HandlerFunc(DateServer))
  32. err := http.ListenAndServe(":12345", nil)
  33. if err != nil {
  34. log.Panicln("ListenAndServe:", err)
  35. }
  36. }
  37. func Logger(w http.ResponseWriter, req *http.Request) {
  38. log.Print(req.URL.String())
  39. w.WriteHeader(404)
  40. w.Write([]byte("oops"))
  41. }
  42. func HelloServer(w http.ResponseWriter, req *http.Request) {
  43. helloRequests.Add(1)
  44. io.WriteString(w, "hello world!\n")
  45. }
  46. func (ctr *Counter) String() string { return fmt.Sprintf("%d", ctr.n) }
  47. func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  48. switch req.Method {
  49. case "GET":
  50. ctr.n++
  51. case "POST":
  52. buf := new(bytes.Buffer)
  53. io.Copy(buf, req.Body)
  54. body := buf.String()
  55. if n, err := strconv.Atoi(body); err != nil {
  56. fmt.Fprintf(w, "bad POST: %v\nbody: [%v]\n", err, body)
  57. } else {
  58. ctr.n = n
  59. fmt.Fprint(w, "counter reset\n")
  60. }
  61. }
  62. fmt.Fprintf(w, "counter = %d\n", ctr.n)
  63. }
  64. func FlagServer(w http.ResponseWriter, req *http.Request) {
  65. w.Header().Set("Content-Type", "text/plain; charset=utf-8")
  66. fmt.Fprint(w, "Flags:\n")
  67. flag.VisitAll(func(f *flag.Flag) {
  68. if f.Value.String() != f.DefValue {
  69. fmt.Fprintf(w, "%s = %s [default = %s]\n", f.Name, f.Value.String(), f.DefValue)
  70. } else {
  71. fmt.Fprintf(w, "%s = %s\n", f.Name, f.Value.String())
  72. }
  73. })
  74. }
  75. func ArgServer(w http.ResponseWriter, req *http.Request) {
  76. for _, s := range os.Args {
  77. fmt.Fprint(w, s, " ")
  78. }
  79. }
  80. func ChanCreate() Chan {
  81. c := make(Chan)
  82. go func(c Chan) {
  83. for x := 0; ; x++ {
  84. c <- x
  85. }
  86. }(c)
  87. return c
  88. }
  89. func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
  90. io.WriteString(w, fmt.Sprintf("channel send #%d\n", <-ch))
  91. }
  92. func DateServer(rw http.ResponseWriter, req *http.Request) {
  93. rw.Header().Set("Content-Type", "text/plain; charset=utf-8")
  94. r, w, err := os.Pipe()
  95. if err != nil {
  96. fmt.Fprintf(rw, "pipe: %s\n", err)
  97. return
  98. }
  99. p, err := os.StartProcess("/bin/date", []string{"date"}, &os.ProcAttr{Files: []*os.File{nil, w, w}})
  100. defer r.Close()
  101. w.Close()
  102. if err != nil {
  103. fmt.Fprintf(rw, "fork/exec: %s\n", err)
  104. return
  105. }
  106. defer p.Release()
  107. io.Copy(rw, r)
  108. wait, err := p.Wait()
  109. if err != nil {
  110. fmt.Fprintf(rw, "wait: %s\n", err)
  111. return
  112. }
  113. if !wait.Exited() {
  114. fmt.Fprintf(rw, "date: %v\n", wait)
  115. return
  116. }
  117. }

用rpc实现远程过程调用

RPC(Remote Procedure Call Protocol),远程过程调用协议,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。它假定某些传输协议的存在,如TCP或UDP,以便为通信程序之间携带信息数据。通过它可以使函数调用模式网络化。客户端就像调用本地函数一样,然后客户端把这些参数打包之后通过网络传递到服务端,服务端解包到处理过程中执行,然后执行的结果反馈给客户端。

运行时,一次客户机对服务器的RPC调用,其内部操作大致有如下十步:

  • 调用客户端句柄;执行传送参数
  • 调用本地系统内核发送网络消息
  • 消息传送到远程主机
  • 服务器句柄得到消息并取得参数
  • 执行远程过程
  • 执行的过程将结果返回服务器句柄
  • 服务器句柄返回结果,调用远程系统内核
  • 消息传回本地主机
  • 客户句柄由内核接收消息
  • 客户接收句柄返回的结果

Go标准包中提供了对RPC的支持,支持三个级别的RPC:TCP、HTTP、JSONRPC。但Go的RPC包是独一无二的RPC,它和传统的RPC系统不同,它只支持Go开发的服务器与客户端之间的交互,因为在内部,它们采用了Gob来编码。

Go RPC的函数只有符合下面的条件才能被远程访问,不然会被忽略,详细的要求如下:

  • 函数必须是导出的(首字母大写)
  • 必须有两个导出类型的参数
  • 第一个参数是接收的参数,第二个参数是返回给客户端的参数,第二个参数必须是指针类型的
  • 函数还要有一个返回值error
  1. func (t *T) MethodName(argType T1, replyType *T2) error

T、T1和T2类型必须能被encoding/god包编解码。

HTTP RPC

服务端示例:

  1. package main
  2. import (
  3. "errors"
  4. "fmt"
  5. "net/http"
  6. "net/rpc"
  7. )
  8. type Args struct {
  9. A, B int
  10. }
  11. type Quotient struct {
  12. Quo, Rem int
  13. }
  14. type Arith int
  15. func (t *Arith) Multiply(args *Args, reply *int) error {
  16. *reply = args.A * args.B
  17. return nil
  18. }
  19. func (t *Arith) Divide(args *Args, quo *Quotient) error {
  20. if args.B == 0 {
  21. return errors.New("divide by zero")
  22. }
  23. quo.Quo = args.A / args.B
  24. quo.Rem = args.A % args.B
  25. return nil
  26. }
  27. func main() {
  28. arith := new(Arith)
  29. rpc.Register(arith)
  30. rpc.HandleHTTP()
  31. err := http.ListenAndServe(":8081", nil)
  32. if err != nil {
  33. fmt.Println(err.Error())
  34. }
  35. }

客户端示例:

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net/rpc"
  6. "os"
  7. )
  8. type Args struct {
  9. A, B int
  10. }
  11. type Quotient struct {
  12. Quo, Rem int
  13. }
  14. func main() {
  15. if len(os.Args) != 2 {
  16. fmt.Println("Usage: ", os.Args[0], "server")
  17. os.Exit(1)
  18. }
  19. serverAddress := os.Args[1]
  20. client, err := rpc.DialHTTP("tcp", serverAddress+":8081")
  21. if err != nil {
  22. log.Fatal("dialing:", err)
  23. }
  24. args := Args{17, 8}
  25. var reply int
  26. err = client.Call("Arith.Multiply", args, &reply)
  27. if err != nil {
  28. log.Fatal("arith error:", err)
  29. }
  30. fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
  31. var quot Quotient
  32. err = client.Call("Arith.Divide", args, &quot)
  33. if err != nil {
  34. log.Fatal("arith error:", err)
  35. }
  36. fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
  37. }

执行go run http_rpc_client.go localhost:

  1. Arith: 17*8=136
  2. Arith: 17/8=2 remainder 1

通过上面的调用可以看到参数和返回值是我们定义的struct类型,在服务端我们把它们当做调用函数的参数的类型,在客户端作为client.Call的第2,3两个参数的类型。客户端最重要的就是这个Call函数,它有3个参数,第1个要调用的函数的名字,第2个是要传递的参数,第3个要返回的参数(主要是指针类型),通过上面的代码例子我们可以发现,使用Go的RPC实现相当的简单,方便。

TCP RPC

服务端代码:

  1. package main
  2. import (
  3. "errors"
  4. "fmt"
  5. "net"
  6. "net/rpc"
  7. "os"
  8. )
  9. type Args struct {
  10. A, B int
  11. }
  12. type Quotient struct {
  13. Quo, Rem int
  14. }
  15. type Arith int
  16. func (t *Arith) Multiply(args *Args, reply *int) error {
  17. *reply = args.A * args.B
  18. return nil
  19. }
  20. func (t *Arith) Divide(args *Args, quo *Quotient) error {
  21. if args.B == 0 {
  22. return errors.New("divide by zero")
  23. }
  24. quo.Quo = args.A / args.B
  25. quo.Rem = args.A % args.B
  26. return nil
  27. }
  28. func main() {
  29. arith := new(Arith)
  30. rpc.Register(arith)
  31. tcpAddr, err := net.ResolveTCPAddr("tcp", ":8082")
  32. checkError(err)
  33. listener, err := net.ListenTCP("tcp", tcpAddr)
  34. checkError(err)
  35. for {
  36. conn, err := listener.Accept()
  37. if err != nil {
  38. continue
  39. }
  40. rpc.ServeConn(conn)
  41. }
  42. }
  43. func checkError(err error) {
  44. if err != nil {
  45. fmt.Println("Fatal error", err.Error())
  46. os.Exit(1)
  47. }
  48. }

客户端代码:

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net/rpc"
  6. "os"
  7. )
  8. type Args struct {
  9. A, B int
  10. }
  11. type Quotient struct {
  12. Quo, Rem int
  13. }
  14. func main() {
  15. if len(os.Args) != 2 {
  16. fmt.Println("Usage: ", os.Args[0], "server:port")
  17. os.Exit(1)
  18. }
  19. service := os.Args[1]
  20. client, err := rpc.Dial("tcp", service)
  21. if err != nil {
  22. log.Fatal("dialing:", err)
  23. }
  24. args := Args{17, 8}
  25. var reply int
  26. err = client.Call("Arith.Multiply", args, &reply)
  27. if err != nil {
  28. log.Fatal("arith error:", err)
  29. }
  30. fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
  31. var quot Quotient
  32. err = client.Call("Arith.Divide", args, &quot)
  33. if err != nil {
  34. log.Fatal("arith error:", err)
  35. }
  36. fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
  37. }

JSON RPC

JSON RPC是数据编码采用了JSON,而不是god编码,其他和上面介绍的RPC概念一样。

服务端代码:

  1. package main
  2. import (
  3. "errors"
  4. "fmt"
  5. "net"
  6. "net/rpc"
  7. "net/rpc/jsonrpc"
  8. "os"
  9. )
  10. type Args struct {
  11. A, B int
  12. }
  13. type Quotient struct {
  14. Quo, Rem int
  15. }
  16. type Arith int
  17. func (t *Arith) Multiply(args *Args, reply *int) error {
  18. *reply = args.A * args.B
  19. return nil
  20. }
  21. func (t *Arith) Divide(args *Args, quo *Quotient) error {
  22. if args.B == 0 {
  23. return errors.New("divide by zero")
  24. }
  25. quo.Quo = args.A / args.B
  26. quo.Rem = args.A % args.B
  27. return nil
  28. }
  29. func main() {
  30. arith := new(Arith)
  31. rpc.Register(arith)
  32. tcpAddr, err := net.ResolveTCPAddr("tcp", ":8082")
  33. checkError(err)
  34. listener, err := net.ListenTCP("tcp", tcpAddr)
  35. checkError(err)
  36. for {
  37. conn, err := listener.Accept()
  38. if err != nil {
  39. continue
  40. }
  41. jsonrpc.ServeConn(conn)
  42. }
  43. }
  44. func checkError(err error) {
  45. if err != nil {
  46. fmt.Println("Fatal error", err.Error())
  47. os.Exit(1)
  48. }
  49. }

客户端代码:

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net/rpc/jsonrpc"
  6. "os"
  7. )
  8. type Args struct {
  9. A, B int
  10. }
  11. type Quotient struct {
  12. Quo, Rem int
  13. }
  14. func main() {
  15. if len(os.Args) != 2 {
  16. fmt.Println("Usage: ", os.Args[0], "server:port")
  17. log.Fatal(1)
  18. }
  19. service := os.Args[1]
  20. client, err := jsonrpc.Dial("tcp", service)
  21. if err != nil {
  22. log.Fatal("dialing:", err)
  23. }
  24. args := Args{17, 8}
  25. var reply int
  26. err = client.Call("Arith.Multiply", args, &reply)
  27. if err != nil {
  28. log.Fatal("arith error:", err)
  29. }
  30. fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
  31. var quot Quotient
  32. err = client.Call("Arith.Divide", args, &quot)
  33. if err != nil {
  34. log.Fatal("arith error:", err)
  35. }
  36. fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
  37. }

REST

什么是REST

REST(REpresentational State Transfer)指的是一组架构约束条件和原则,满足这些条件和原则的应用程序或设计就是RESTful。

  • 资源(Resources)REST是”表现层状态转化”,其实它省略了主语,表现层指的是资源的表现层。
    所谓的资源就是我们平常上网访问的一张图片、一个文档、一个视频等。这些资源通过URI来定位,也就是一个URI表示一个资源。

  • 表现层(Representation)
    资源是做一个具体的实体信息,它可以有多种的展现方式。而把实体展现出来就是表现层,例如一个txt文本信息,它可以输出成html、json、xml等格式,
    URI确定一个资源,但是如何确定它的具体表现形式呢?应该在HTTP请求的头信息中用Accept和Content-Type字段指定,这两个字段才是对”表现层”的描述

  • 状态转化(State Transfer)
    访问一个网站,就代表了客户端和服务器的一个互动过程。在这个过程中,肯定涉及到数据和状态的变化。而HTTP协议是无状态的,那么这些状态肯定保存在服务器端,所以如果客户端想要通知服务器端改变数据和状态的变化,肯定要通过某种方式来通知它。
    客户端能通知服务器端的手段,只能是HTTP协议。具体来说,就是HTTP协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。

综合上面的解释,总结下什么是RESTful架构:

  • 每一个URI代表一个资源;
  • 客户端和服务器之间,传递这种资源的某种表现层;
  • 客户端通过四个HTTP动词,对服务器端资源进行操作,实现”表现层状态转化”;

Web应用要满足REST最重要的原则是:客户端和服务器之间的交互在请求之间是无状态的,即从客户端到服务器的每个请求都必须包含理解请求所必需的信息。如果服务器在请求之间的任何时间点重启,客户端不会得到通知。此外此请求可以由任何可用服务器回答,这十分适合云计算之类的环境。因为是无状态的,所以客户端可以缓存数据以改进性能。

另一个重要的REST原则是系统分层,这表示组件无法了解除了与它直接交互的层次以外的组件。通过将系统知识限制在单个层,可以限制整个系统的复杂性,从而促进了底层的独立性。

RESTful的实现

Go没有为REST提供直接支持,但是因为RESTful是基于HTTP协议实现的,所以可以利用net/http包来自己实现,当然需要针对REST做一些改造,REST是根据不同的method来处理相应的资源。

RESTful服务充分利用每一个HTTP方法,包括DELETE和PUT。有时,HTTP客户端只能发出GET和POST请求:

  • HTML标准只能通过链接和表单支持GET和POST。在没有Ajax支持的网页浏览器中不能发出PUT和DELETE命令
  • 有些防火墙会挡住HTTP PUT和DELETE请求,要绕过这个限制,客户端需要把实际的PUT和DELETE请求通过 POST 请求穿透过来。RESTful 服务则要负责在收到的 POST 请求中找到原始的 HTTP 方法并还原。

我们现在可以通过POST里面增加隐藏字段_method这种方式可以来模拟PUT、DELETE等方式,但是服务器端需要做转换。我现在的项目里面就按照这种方式来做的REST接口。当然Go语言里面完全按照RESTful来实现是很容易的,我们通过下面的例子来说明如何实现RESTful的应用设计。

  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "net/http"
  6. "github.com/julienschmidt/httprouter"
  7. )
  8. func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
  9. fmt.Fprint(w, "Welcome!\n")
  10. }
  11. func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  12. fmt.Fprintf(w, "hello, %s\n", ps.ByName("name"))
  13. }
  14. func getuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  15. uid := ps.ByName("uid")
  16. fmt.Fprintf(w, "you are get user %s", uid)
  17. }
  18. func modifyuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  19. uid := ps.ByName("uid")
  20. fmt.Fprintf(w, "you are modify user %s", uid)
  21. }
  22. func deleteuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  23. uid := ps.ByName("uid")
  24. fmt.Fprintf(w, "you are delete user %s", uid)
  25. }
  26. func adduser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
  27. uid := ps.ByName("uid")
  28. fmt.Fprintf(w, "you are add user %s", uid)
  29. }
  30. func main() {
  31. router := httprouter.New()
  32. router.GET("/", Index)
  33. router.GET("/hello/:name", Hello)
  34. router.GET("/user/:uid", getuser)
  35. router.POST("/adduser/:uid", adduser)
  36. router.DELETE("/deluser/:uid", deleteuser)
  37. router.PUT("/moduser/:uid", modifyuser)
  38. log.Fatal(http.ListenAndServe(":8083", router))
  39. }

WebSocket

WebSocket是HTML5的重要特性,实现了基于浏览器的远程socket,它使浏览器和服务器可以进行全双工通信,许多浏览器都已对此做了支持。

在WebSocket出现之前,为了实现即时通信,采用的技术都是“轮询”,即在特定的时间间隔内,由浏览器对服务器发出HTTP Request,服务器在收到请求后,返回最新的数据给浏览器刷新,“轮询”使得浏览器需要对服务器不断发出请求,这样会占用大量带宽。

WebSocket采用了一些特殊的报头,使得浏览器和服务器只需要做一个握手的动作,就可以在浏览器和服务器之间建立一条连接通道。且此连接会保持在活动状态,你可以使用JavaScript来向连接写入或从中接收数据,就像在使用一个常规的TCP Socket一样。它解决了Web实时化的问题,相比传统HTTP有如下好处:

  • 一个Web客户端只建立一个TCP连接
  • Websocket服务端可以推送数据到web客户端
  • 有更加轻量级的头,减少数据传送量

WebSocket URL的起始输入是ws://或是wss://(在SSL上)。一个带有特定报头的HTTP握手被发送到了服务器端,接着在服务器端或是客户端就可以通过JavaScript来使用某种套接口(socket),这一套接口可被用来通过事件句柄异步地接收数据。

Go语言标准包里面没有提供对WebSocket的支持,但是在由官方维护的go.net子包中有对这个的支持,你可以通过如下的命令获取该包:go get golang.org/x/net/websocket

WebSocket分为客户端和服务端,接下来我们将实现一个简单的例子:用户输入信息,客户端通过WebSocket将信息发送给服务器端,服务器端收到信息之后主动Push信息到客户端,然后客户端将输出其收到的信息,客户端的代码如下:

  1. <html>
  2. <head></head>
  3. <body>
  4. <script type="text/javascript">
  5. var sock = null;
  6. var wsuri = "ws://127.0.0.1:1234";
  7. window.onload = function() {
  8. console.log("onload");
  9. sock = new WebSocket(wsuri);
  10. sock.onopen = function() {
  11. console.log("connected to " + wsuri);
  12. }
  13. sock.onclose = function(e) {
  14. console.log("connection closed (" + e.code + ")");
  15. }
  16. sock.onmessage = function(e) {
  17. console.log("message received: " + e.data);
  18. }
  19. };
  20. function send() {
  21. var msg = document.getElementById('message').value;
  22. sock.send(msg);
  23. };
  24. </script>
  25. <h1>WebSocket Echo Test</h1>
  26. <form>
  27. <p>
  28. Message: <input id="message" type="text" value="Hello, world!">
  29. </p>
  30. </form>
  31. <button onclick="send();">Send Message</button>
  32. </body>
  33. </html>

服务端代码:

  1. package main
  2. import (
  3. "golang.org/x/net/websocket"
  4. "fmt"
  5. "log"
  6. "net/http"
  7. )
  8. func Echo(ws *websocket.Conn) {
  9. var err error
  10. for {
  11. var reply string
  12. if err = websocket.Message.Receive(ws, &reply); err != nil {
  13. fmt.Println("Can't receive")
  14. break
  15. }
  16. fmt.Println("Received back from client: " + reply)
  17. msg := "Received: " + reply
  18. fmt.Println("Sending to client: " + msg)
  19. if err = websocket.Message.Send(ws, msg); err != nil {
  20. fmt.Println("Can't send")
  21. break
  22. }
  23. }
  24. }
  25. func main() {
  26. http.Handle("/", websocket.Handler(Echo))
  27. if err := http.ListenAndServe(":1234", nil); err != nil {
  28. log.Fatal("ListenAndServe:", err)
  29. }
  30. }

Socket

什么是Socket

Socket起源于Unix,而Unix基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。Socket就是该模式的一个实现,网络的Socket数据传输是一种特殊的I/O,Socket也是一种文件描述符。Socket也具有一个类似于打开文件的函数调用:Socket(),该函数返回一个整型的Socket描述符,随后的连接建立、数据传输等操作都是通过该Socket实现的。

常用的Socket类型有两种:流式Socket(SOCK_STREAM)和数据报式Socket(SOCK_DGRAM)。流式是一种面向连接的Socket,针对于面向连接的TCP服务应用;数据报式Socket是一种无连接的Socket,对应于无连接的UDP服务应用。

Socket如何通信

网络中的进程之间如何通过Socket通信呢?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中需要互相通信的进程,就可以利用这个标志在他们之间进行交互。

使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是为什么说“一切皆Socket”。

通过上面的介绍我们知道Socket有两种:TCP Socket和UDP Socket,TCP和UDP是协议,而要确定一个进程的需要三元组,需要IP地址和端口。

Go支持的IP类型

在Go的net包中定义了很多类型、函数和方法用来网络编程,其中IP的定义如下:type IP []byte

在net包中有很多函数来操作IP,其中ParseIP(s tring) IP函数会把一个IPv4或IPv6的地址转化成IP类型,

  1. package main
  2. import (
  3. "fmt"
  4. "net"
  5. "os"
  6. )
  7. func main() {
  8. if len(os.Args) != 2 {
  9. fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0])
  10. os.Exit(1)
  11. }
  12. name := os.Args[1]
  13. addr := net.ParseIP(name)
  14. if addr == nil {
  15. fmt.Println("Invalid address")
  16. } else {
  17. fmt.Println("The address is ", addr.String())
  18. }
  19. os.Exit(0)
  20. }

TCP Socket

当我们知道如何通过网络端口访问一个服务时,那么我们能够做什么呢?作为客户端来说,我们可以通过向远端某台机器的的某个网络端口发送一个请求,然后得到在机器的此端口上监听的服务反馈的信息。作为服务端,我们需要把服务绑定到某个指定端口,并且在此端口上监听,当有客户端来访问时能够读取信息并且写入反馈信息。

在Go语言的net包中有一个类型TCPConn,这个类型可以用来作为客户端和服务端交互的通道,它有两个主要的函数:

  1. func (c *TCPConn) Write(b []byte) (int, error)
  2. func (c *TCPConn) Read(b []byte) (int, error)

还有我们需要知道一个TCPAddr类型,它表示一个TCP的地址信息,定义如下:

  1. type TCPAddr struct {
  2. IP IP
  3. Port int
  4. Zone string // ipv6 scoped addressing zone
  5. }

在Go语言中通过ResolveTCPAddr获取一个TCPAddr,

  1. func ResolveTCPAddr(net, addr string) (*TCPAddr, os.Error)
  • net参数是”tcp4”、”tcp6”、”tcp”中的任意一个,分别表示TCP(IPv4-only), TCP(IPv6-only)或者TCP(IPv4, IPv6的任意一个)。
  • addr表示域名或者IP地址,例如”www.google.com:80” 或者”127.0.0.1:22”。

TCP client

Go语言中通过net包中的DialTCP函数来建立一个TCP连接,并返回一个TCPConn类型的对象,当连接建立时服务器端也创建一个同类型的对象,此时客户端和服务器端通过各自拥有的TCPConn对象来进行数据交换。般而言,客户端通过TCPConn对象将请求信息发送到服务器端,读取服务器端响应的信息。服务器端读取并解析来自客户端的请求,并返回应答信息,这个连接只有当任一端关闭了连接之后才失效,不然这连接可以一直在使用。建立连接的函数定义如下:

  1. func DialTCP(network string, laddr, raddr *TCPAddr) (*TCPConn, error)
  • network参数是”tcp4”、”tcp6”、”tcp”中的任意一个,分别表示TCP(IPv4-only)、TCP(IPv6-only)或者TCP(IPv4,IPv6的任意一个)
  • laddr表示本机地址,一般设置为nil
  • raddr表示远程的服务地址

接下来我们写一个简单的例子,模拟一个基于HTTP协议的客户端请求去连接一个Web服务端。我们要写一个简单的http请求头,格式类似如下:

  1. "HEAD / HTTP/1.0\r\n\r\n"
  1. package main
  2. import (
  3. "fmt"
  4. "net"
  5. "os"
  6. )
  7. func main() {
  8. if len(os.Args) != 2 {
  9. fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
  10. os.Exit(1)
  11. }
  12. service := os.Args[1]
  13. tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
  14. checkError(err)
  15. conn, err := net.DialTCP("tcp", nil, tcpAddr)
  16. checkError(err)
  17. _, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
  18. checkError(err)
  19. // resulte, err := ioutil.ReadAll(conn)
  20. result := make([]byte, 256)
  21. _, err = conn.Read(result)
  22. checkError(err)
  23. fmt.Println(string(result))
  24. os.Exit(0)
  25. }
  26. func checkError(err error) {
  27. if err != nil {
  28. fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
  29. os.Exit(1)
  30. }
  31. }

通过上面的代码我们可以看出:首先程序将用户的输入作为参数service传入net.ResolveTCPAddr获取一个tcpAddr,然后把tcpAddr传入DialTCP后创建了一个TCP连接conn,通过conn来发送请求信息,最后通过ioutil.ReadAll从conn中读取全部的文本,也就是服务端响应反馈的信息。

TCP Server

上面我们编写了一个TCP的客户端程序,也可以通过net包来创建一个服务器端程序,在服务器端我们需要绑定服务到指定的非激活端口,并监听此端口,当有客户端请求到达的时候可以接收到来自客户端连接的请求。net包中有相应功能的函数,函数定义如下:

  1. func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error)
  2. func (l *TCPListener) Accept() (Conn, error)
  1. package main
  2. import (
  3. "fmt"
  4. "net"
  5. "os"
  6. "time"
  7. )
  8. func main() {
  9. service := ":1200"
  10. tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
  11. checkError(err)
  12. listener, err := net.ListenTCP("tcp", tcpAddr)
  13. checkError(err)
  14. for {
  15. conn, err := listener.Accept()
  16. if err != nil {
  17. continue
  18. }
  19. go handleClient(conn)
  20. }
  21. }
  22. func handleClient(conn net.Conn) {
  23. defer conn.Close()
  24. daytime := time.Now().String()
  25. conn.Write([]byte(daytime))
  26. }
  27. func checkError(err error) {
  28. if err != nil {
  29. fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
  30. os.Exit(1)
  31. }
  32. }

如果我们需要通过从客户端发送不同的请求来获取不同的时间格式,而且需要一个长连接,该怎么做呢?请看:

  1. package main
  2. import (
  3. "fmt"
  4. "net"
  5. "os"
  6. "strconv"
  7. "strings"
  8. "time"
  9. )
  10. func main() {
  11. service := ":1200"
  12. tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
  13. checkError(err)
  14. listener, err := net.ListenTCP("tcp", tcpAddr)
  15. checkError(err)
  16. for {
  17. conn, err := listener.Accept()
  18. if err != nil {
  19. continue
  20. }
  21. go handleClient(conn)
  22. }
  23. }
  24. func handleClient(conn net.Conn) {
  25. conn.SetReadDeadline(time.Now().Add(2 * time.Minute)) // set 2 minutes timeout
  26. request := make([]byte, 128) // set maxium request length to 128B to prevent flood attack
  27. defer conn.Close()
  28. for {
  29. read_len, err := conn.Read(request)
  30. if err != nil {
  31. fmt.Println(err)
  32. break
  33. }
  34. if read_len == 0 {
  35. break
  36. } else if strings.TrimSpace(string(request[:read_len])) == "timestamp " {
  37. daytime := strconv.FormatInt(time.Now().Unix(), 10)
  38. conn.Write([]byte(daytime))
  39. } else {
  40. daytime := time.Now().String()
  41. conn.Write([]byte(daytime))
  42. }
  43. request = make([]byte, 128)
  44. }
  45. }
  46. func checkError(err error) {
  47. if err != nil {
  48. fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
  49. os.Exit(1)
  50. }
  51. }

在上面这个例子中,我们使用conn.Read()不断读取客户端发来的请求。由于我们需要保持与客户端的长连接,所以不能在读取完一次请求后就关闭连接。由于conn.SetReadDeadline()设置了超时,当一定时间内客户端无请求发送,conn便会自动关闭,下面的for循环即会因为连接已关闭而跳出。需要注意的是,request在创建时需要指定一个最大长度以防止flood attack;每次读取到请求处理完毕后,需要清理request,因为conn.Read()会将新读取到的内容append到原内容之后。

控制TCP连接

TCP有很多连接控制函数,用的多的有如下函数:

  1. func DialTimeout(net, addr string, timeout time.Duration) (Conn, error)

设置建立连接的超时时间,客户端和服务端都适用,当超过设置时间时,连接自动关闭。

  1. func (c *TCPConn) SetReadDeadline(t time.Time) error
  2. func (c *TCPConn) SetWriteDeadline(t time.Time) error

用来设置写入/读取一个连接的超时时间。当超过设置时间时,连接自动关闭。

  1. func (c *TCPConn) SetKeepAlive(keeplive bool) os.Error

设置keepAlive属性。操作系统层在tcp上没有数据和ACK的时候,会间隔性的发送keepalive包,操作系统可以通过该包来判断一个tcp连接是否已经断开,在windows上默认2个小时没有收到数据和keepalive包的时候认为tcp连接已经断开,这个功能和我们通常在应用层加的心跳包的功能类似。

UDP SOCKET

UDP主要函数有如下:

  1. func ResolveUDPAddr(net, addr string) (*UDPAddr, os.Error)
  2. func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err os.Error)
  3. func ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, err os.Error)
  4. func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err os.Error)
  5. func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, err os.Error)

客户端代码:

  1. package main
  2. import (
  3. "fmt"
  4. "net"
  5. "os"
  6. )
  7. func main() {
  8. if len(os.Args) != 2 {
  9. fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
  10. os.Exit(1)
  11. }
  12. service := os.Args[1]
  13. udpAddr, err := net.ResolveUDPAddr("udp4", service)
  14. checkError(err)
  15. conn, err := net.DialUDP("udp", nil, udpAddr)
  16. checkError(err)
  17. _, err = conn.Write([]byte("anything"))
  18. checkError(err)
  19. var buf [512]byte
  20. n, err := conn.Read(buf[0:])
  21. checkError(err)
  22. fmt.Println(string(buf[0:n]))
  23. os.Exit(0)
  24. }
  25. func checkError(err error) {
  26. if err != nil {
  27. fmt.Fprintf(os.Stderr, "Fatal error %s", err.Error())
  28. os.Exit(1)
  29. }
  30. }

服务端代码:

  1. package main
  2. import (
  3. "fmt"
  4. "net"
  5. "os"
  6. "time"
  7. )
  8. func main() {
  9. service := ":1200"
  10. udpAddr, err := net.ResolveUDPAddr("udp4", service)
  11. checkError(err)
  12. conn, err := net.ListenUDP("udp", udpAddr)
  13. checkError(err)
  14. for {
  15. handleClient(conn)
  16. }
  17. }
  18. func handleClient(conn *net.UDPConn) {
  19. var buf [512]byte
  20. _, addr, err := conn.ReadFromUDP(buf[0:])
  21. if err != nil {
  22. return
  23. }
  24. daytime := time.Now().String()
  25. conn.WriteToUDP([]byte(daytime), addr)
  26. }
  27. func checkError(err error) {
  28. if err != nil {
  29. fmt.Fprintf(os.Stderr, "Fatal error %s", err.Error())
  30. os.Exit(1)
  31. }
  32. }