The Way to Go(九)

web 应用

tcp 服务器

一个(web)服务器应用需要响应众多客户端的并发请求:go 会为每一个客户端产生一个协程用来处理请求。我们需要使用 net 包中网络通信的功能。它包含了用于 TCP/IP 以及 UDP 协议、域名解析等方法。

服务器代码,单独的一个文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// server.go

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 获取客户端发送的数据字节数;当客户端发送的所有数据都被读取完成时,协程就结束了。这段程序会为每一个客户端连接创建一个独立的协程。必须先运行服务器代码,再运行客户端代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// client.go
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方法被发送到服务器。

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

一个简单的网页服务器

Http 是一个比 tcp 更高级的协议,它描述了客户端浏览器如何与网页服务器进行通信。Go 有自己的 net/http 包,我们来看看它。首先编写一个“Hello world!”:

我们引入了 http 包并启动了网页服务器,使用http.ListenAndServe("localhost:8080", nil) 函数,如果成功会返回空,否则会返回一个错误

http.URL 描述了 web 服务器的地址,内含存放了 url 字符串的 Path 属性;http.Request 描述了客户端请求,内含一个 URL 属性

如果 req 请求是一个 POST 类型的 html 表单,“var1” 就是 html 表单中一个输入属性的名称,然后用户输入的值就可以通过 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函数中完成的,就是在这个例子中当根路径 “/”(url 地址是 http://localhost:8080 )被请求的时候(或者这个服务器上的其他地址),HelloServer 函数就被执行了。这个函数是http.HandlerFunc 类型的,它们通常用使用 Prehandler 来命名,在前边加了一个 Pref 前缀。

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

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

总结:第一个参数是请求的路径,第二个参数是处理这个路径请求的函数的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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())
}
}

前两行(没有错误处理代码)可以替换成以下写法:

http.ListenAndServe(":8080", http.HandlerFunc(HelloServer))

读取并访问页面

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

返回状态码会被打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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)
}
}

输出为:

1
2
3
http://www.google.com/ : 302 Found
http://golang.org/ : 200 OK
http://blog.golang.org/ : 200 OK

在下边的程序中我们使用 http.Get() 获取网页内容; Get 的返回值 res 中的 Body 属性包含了网页内容,然后我们用 ioutil.ReadAll把它读出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import (
"fmt"
"io/ioutil"
"log"
"net/http"
)

func main() {
res, err := http.Get("http://www.google.com")
checkError(err)
data, err := ioutil.ReadAll(res.Body)
checkError(err)
fmt.Printf("Got: %q", string(data))
}

func checkError(err error) {
if err != nil {
log.Fatalf("Get : %v", err)
}
}

访问不存在的网站时,这里有一个 CheckError 输出错误的例子:

1
2011/09/30 11:24:15 Get: Get http://www.google.bex: dial tcp www.google.bex:80:GetHostByName: No such host is known.

http 包中的其他重要的函数:

  • http.Redirect(w ResponseWriter, r *Request, url string, code int):这个函数会让浏览器重定向到url(是请求的url的相对路径)以及状态码。
  • http.NotFound(w ResponseWriter, r *Request):这个函数将返回网页没有找到,HTTP 404 错误。

  • http.Error(w ResponseWriter, error string, code int):这个函数返回特定的错误信息和 HTTP 代码。

  • http.Request 对象的一个重要属性 reqreq.Method,这是一个包含 GET 或 POST 字符串,用来描述网页是以何种方式被请求的。

go 为所有的 HTTP 状态码定义了常量,比如:

1
2
3
4
5
6
7
8
http.StatusContinue     = 100
http.StatusOK = 200
http.StatusFound = 302
http.StatusBadRequest = 400
http.StatusUnauthorized = 401
http.StatusForbidden = 403
http.StatusNotFound = 404
http.StatusInternalServerError = 500

你可以使用 w.header().Set("Content-Type", "../..")设置头信息

比如在网页应用发送html字符串的时候,在输出之前执行 w.Header().Set("Content-Type", "text/html")

------ 本文结束 ------
0%