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 | // client.go |
客户端通过net.Dial
创建了一个和服务器之间的连接
它通过无限循环中的 os.Stdin
接收来自键盘的输入直到输入了 “Q”。注意使用\r
和\n
换行符分割字符串(在windows平台下使用\r\n)。接下来分割后的输入通过 connection
的Write
方法被发送到服务器。
在网络编程中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 | package main |
前两行(没有错误处理代码)可以替换成以下写法:
http.ListenAndServe(":8080", http.HandlerFunc(HelloServer))
读取并访问页面
在下边这个程序中,数组中的 url 都将被访问:会发送一个简单的 http.Head()
请求查看返回值;它的声明如下:func Head(url string) (r *Response, err error)
返回状态码会被打印出来。
1 | package main |
输出为:1
2
3http://www.google.com/ : 302 Found
http://golang.org/ : 200 OK
http://blog.golang.org/ : 200 OK
在下边的程序中我们使用 http.Get()
获取网页内容; Get 的返回值 res 中的 Body 属性包含了网页内容,然后我们用 ioutil.ReadAll
把它读出来:
1 | package main |
访问不存在的网站时,这里有一个 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
对象的一个重要属性req
:req.Method
,这是一个包含 GET 或 POST 字符串,用来描述网页是以何种方式被请求的。
go 为所有的 HTTP 状态码定义了常量,比如:
1 | http.StatusContinue = 100 |
你可以使用 w.header().Set("Content-Type", "../..")
设置头信息
比如在网页应用发送html字符串的时候,在输出之前执行 w.Header().Set("Content-Type", "text/html")
。