tcp服务器
这部分将使用TCP协议和协程范式编写一个简单的客户端-服务端应用,一个(web)服务器应用需要响应众多客户端的并发请求:Go会为每一个客户端产生一个协程用来处理请求。需要使用net包中网络通信的功能。它包含了处理TCP/IP以及UDP协议、域名解析等方法。
server.go:
package main
import (
"fmt"
"net"
)
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]))
}
}
func main() {
fmt.Println("Starting the server ...")
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)
}
}
在 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:
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')
trimmedClient := strings.Trim(clientName, "\n")
// 给服务器发送信息直到程序退出
for {
fmt.Println("What to send to the server? Type Q to quit")
input, _ := inputReader.ReadString('\n')
trimmedInput := strings.Trim(input, "\n")
if trimmedInput == "Q" {
return
}
_, err = conn.Write([]byte(trimmedClient + "says: " + trimmedInput))
}
}
客户端通过 net.Dial 创建了一个和服务器之间的连接。它通过无限循环从 os.Stdin 接收来自键盘的输入,直到输入了“Q”。注意裁剪 \r 和 \n 字符(仅 Windows 平台需要)。裁剪后的输入被 connection 的 Write 方法发送到服务器。当然,服务器必须先启动好,如果服务器并未开始监听,客户端是无法成功连接的。
在网络编程中 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)
}
下班是一个使用net包从socket中打开,写入、读取数据的例子:
package main
import (
"fmt"
"io"
"net"
)
func main() {
var (
host = "www.apache.org"
port = "80"
remote = host + ":" + port
msg string = "GET / \n"
data = make([]uint8, 4096)
read = true
count = 0
)
// 创建一个socket
con, err := net.Dial("tcp", remote)
// 发送消息,一个http Get请求
io.WriteString(con, msg)
// 读取服务器的响应
for read {
count, err = con.Read(data)
read = (err == nil)
fmt.Printf(string(data[0:count]))
}
con.Close()
}
下边这个版本的 simple_tcp_server.go 从很多方面优化了第一个 tcp 服务器的示例 server.go 并且拥有更好的结构,它只用了 80 行代码!
simple_tcp_server.go:
package main
import (
"flag"
"fmt"
"net"
"syscall"
)
const maxRead = 25
func initServer(hostAndPort string) net.Listener {
serverAddr, err := net.ResolveTCPAddr("tcp", hostAndPort)
checkError(err, "Resolving address:port failed: '"+hostAndPort+"'")
listener, err := net.ListenTCP("tcp", serverAddr)
checkError(err, "ListenTCP: ")
println("Listening to: ", listener.Addr().String())
return listener
}
func connectionHandler(conn net.Conn) {
connFrom := conn.RemoteAddr().String()
println("Connection from: ", connFrom)
sayHello(conn)
for {
var ibuf []byte = make([]byte, maxRead+1)
length, err := conn.Read(ibuf[0:maxRead])
ibuf[maxRead] = 0
switch err {
case nil:
handleMsg(length, err, ibuf)
case syscall.EAGAIN: // try again
continue
default:
goto DISCONNECT
}
}
DISCONNECT:
err := conn.Close()
println("Closed connection: ", connFrom)
checkError(err, "Close: ")
}
func sayHello(to net.Conn) {
obuf := []byte{'L', 'e', 't', '\'', 's', ' ', 'G', 'O', '!', '\n'}
wrote, err := to.Write(obuf)
checkError(err, "Write: write "+string(wrote)+" bytes.")
}
func handleMsg(length int, err error, msg []byte) {
if length > 0 {
print("<", length, ":")
for i := 0; ; i++ {
if msg[i] == 0 {
break
}
fmt.Printf("%c", msg[i])
}
print(">")
}
}
func checkError(error error, info string) {
if error != nil {
panic("ERROR: " + info + " " + error.Error()) // terminate program
}
}
func main() {
flag.Parse()
if flag.NArg() != 2 {
panic("usage: host port")
}
hostAndPort := fmt.Sprintf("%s:%s", flag.Arg(0), flag.Arg(1))
listener := initServer(hostAndPort)
for {
conn, err := listener.Accept()
checkError(err, "Accept: ")
go connectionHandler(conn)
}
}
- 在 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"]
的第一个返回参数:
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写入了Hello
和r.URL.Path[1:]
组成的字符串:末尾的[]表示”创建一个从索引为1的字符到结尾的子切片“,用来丢弃路径开头的”/“,fmt.Fprint()函数完成了本次写入;另一种可行的写法是io.WriteString(w, "hello\n")
。
总结:第一个参数是请求的路径,第二个参数是当路径被请求时,需要调用的处理函数的引用。
webserver.go:
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("ListenAndServer", err.Error())
}
}
- 前两行(没有错误处理代码)可以替换成以下写法:
http.ListenAndServe(":8080", http.HandleFunc(HelloServer))
fmt.Fprint
和fmt.Fprintf
都是可以用来写入http.ResponseWriter
的函数(他们实现了io.Writer
)
fmt.Fprintf(w, "<h1>%s</h1><div>%s</div>", title, body)
- 如果你需要使用安全的https连接,使用
http.ListenAndServeTLS()
代替http.ListenAndServe()
- 除了
http.HandleFunc("/", Hfunc)
,其中的Hfunc
是一个处理函数,签名为:
func Hfunc(w http.ResponseWriter, req *http.Request) {
...
}
也可以使用这种方式:http.Handle("/",http.HandlerFunc(Hfunc))
HandlerFunc只是定义了上述HFunc签名的别名:
type HandlerFunc func(ResponseWriter, *Request)
它是一个可以把普通的函数当做HTTP处理器(Handler)的适配器。如果函数f声明的合适,HandlerFunc(f)就是一个执行f函数的Handler对象。
http.Handle的第二个参数也可以是T类型的对象obj:http.Handle("/", obj)
,如果T有ServeHTTP方法,那就实现了http的Handler接口:
func (obj *Typ) ServeHTTP( w http.ResponseWriter, req *http.Request ) {
...
}
访问并读取页面数据
在下边这个程序中,数组中的url都将被访问:会发送一个简单的http.Head()
请求查看返回值;它的声明如下:func Head(url string) (r *Response, err error)
,返回的响应Response其状态码会被打印出来
package main
import (
"fmt"
"net/http"
)
var urls = []string{
"https://www.baidu.com",
"https://golang.org",
"https://www.google.com",
}
func main() {
for _, url := range urls {
reps, err := http.Head(url)
if err != nil {
fmt.Println("Error", url, err)
}
fmt.Println(url, ": ", reps.Status)
}
}
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 来测试这个程序:
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, req *http.Request) {
io.WriteString(w, "<h1> hello, world </h1>")
}
func FormServer(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Context-Type", "text/html")
switch req.Method {
case "GET":
io.WriteString(w, form)
case "POST":
io.WriteString(w, req.FormValue("in"))
}
}
func main() {
http.HandleFunc("/test1", SimpleServer)
http.HandleFunc("/test2", FormServer)
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
练习:编写一个网页程序,可以让用户输入一连串的数字,然后将它们打印出来,计算出这些数字的均值和中值
// statistics.go
package main
import (
"fmt"
"log"
"net/http"
"sort"
"strconv"
"strings"
)
type statistics struct {
numbers []float64
mean float64
median float64
}
const form = `<html><body><form action="/" method="POST">
<label for="numbers">Numbers (comma or space-separated):</label><br>
<input type="text" name="numbers" size="30"><br />
<input type="submit" value="Calculate">
</form></html></body>`
const error = `<p class="error">%s</p>`
var pageTop = ""
var pageBottom = ""
func main() {
http.HandleFunc("/", homePage)
if err := http.ListenAndServe(":9001", nil); err != nil {
log.Fatal("failed to start server", err)
}
}
func homePage(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Type", "text/html")
err := request.ParseForm() // Must be called before writing response
fmt.Fprint(writer, pageTop, form)
if err != nil {
fmt.Fprintf(writer, error, err)
} else {
if numbers, message, ok := processRequest(request); ok {
stats := getStats(numbers)
fmt.Fprint(writer, formatStats(stats))
} else if message != "" {
fmt.Fprintf(writer, error, message)
}
}
fmt.Fprint(writer, pageBottom)
}
func processRequest(request *http.Request) ([]float64, string, bool) {
var numbers []float64
var text string
if slice, found := request.Form["numbers"]; found && len(slice) > 0 {
//处理如果网页中输入的是中文逗号
if strings.Contains(slice[0], ",") {
text = strings.Replace(slice[0], ",", " ", -1)
} else {
text = strings.Replace(slice[0], ",", " ", -1)
}
for _, field := range strings.Fields(text) {
if x, err := strconv.ParseFloat(field, 64); err != nil {
return numbers, "'" + field + "' is invalid", false
} else {
numbers = append(numbers, x)
}
}
}
if len(numbers) == 0 {
return numbers, "", false // no data first time form is shown
}
return numbers, "", true
}
func getStats(numbers []float64) (stats statistics) {
stats.numbers = numbers
sort.Float64s(stats.numbers)
stats.mean = sum(numbers) / float64(len(numbers))
stats.median = median(numbers)
return
}
func sum(numbers []float64) (total float64) {
for _, x := range numbers {
total += x
}
return
}
func median(numbers []float64) float64 {
middle := len(numbers) / 2
result := numbers[middle]
if len(numbers)%2 == 0 {
result = (result + numbers[middle-1]) / 2
}
return result
}
func formatStats(stats statistics) string {
return fmt.Sprintf(`<table border="1">
<tr><th colspan="2">Results</th></tr>
<tr><td>Numbers</td><td>%v</td></tr>
<tr><td>Count</td><td>%d</td></tr>
<tr><td>Mean</td><td>%f</td></tr>
<tr><td>Median</td><td>%f</td></tr>
</table>`, stats.numbers, len(stats.numbers), stats.mean, stats.median)
}
确保网页应用健壮
当网页应用的处理函数发生panic,服务器会简单地终止运行。网页服务器必须是足够健壮的程序,能够承受任何可能的突发问题。
首先想到的是在每个处理函数中使用defer/recover,但会产生太多的重复代码。使用闭包的错误处理模式是更优雅的方案,它可以被简单应用到任何网页服务器程序中。
为增强代码可读性,为网页处理函数创建一个类型:
type HandleFnc func(http.ResponseWriter, *http.Request)
错误处理函数logPanics:
func logPaincs(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.RemoteAdder, x)
}
}()
function(writer, request)
}
}
然后用logPanics来包装对处理函数的调用:
http.HandleFunc("/test1", logPanics(SimpleServer))
http.HandleFunc("/test2", logPanics(FormServer))
rebust_webserver.go:
package main
import (
"io"
"log"
"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, req *http.Request) {
io.WriteString(w, "<h1> hello, world </h1>")
}
type HandleFnc func(http.ResponseWriter, *http.Request)
func FormServer(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Context-Type", "text/html")
switch req.Method {
case "GET":
io.WriteString(w, form)
case "POST":
io.WriteString(w, req.FormValue("in"))
}
}
func main() {
http.HandleFunc("/test1", logPanics(SimpleServer))
http.HandleFunc("/test2", logPanics(FormServer))
if err := http.ListenAndServe(":8080", nil); err != nil {
panic(err)
}
}
func logPanics(function HandleFnc) HandleFnc {
return func(writer http.ResponseWriter, req *http.Request) {
defer func() {
if x := recover(); x != nil {
log.Printf("[%v] caught panic: %v", req.RemoteAddr, x)
}
}()
function(writer, req)
}
}
用模板编写网页应用
以下程序是用 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字段存放内容,由字节切片组成:
type Page struct {
Title string
Body []byte
}
示例:wiki.go:
package main
import (
"html/template"
"io/ioutil"
"log"
"net/http"
"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("ListenAndServer: ", 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 { //page not found
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: tile}
}
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.StatusNotFound)
}
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
}
- io/ioutil方便地读写文件,regexp用于验证输入标题,template来动态创建html文档;
- 为避免黑客构造特殊输入攻击服务器,用如下正则表达式检查用户在浏览器输入的URL:
var titleValidator = regexp.MustCompile("^[a-zA-Z0-9]+$")
makeHandler会用它对请求管控。
必须有一种机制把Page结构体数据插入到网页的标题和内容中,可以利用template包通过如下步骤完成:
- 现在文本编辑器中创建html模板文件,例如view.html:
<h1> {{.Title |html}}</h1>
<p>[<a href="/edit/{{.Title |html}}">edit</a>]</p>
<div>{{printf "%s" .Body |html}}</div>
{{.Title |html}}``{{printf "%s" .Body |html}}
template.Must(template.ParseFiles(tmpl + ".html"))
把模板文件转换为*template.Template
类型的对象,为了高效,在程序运行时仅做一次解析,在init()函数中处理可以方便地达到目的。所有模板对象都被保持在内存中,存放在以html文件名作为索引的map中:templates = make(map[string] *template.Template)
为了真正从模板和结构体构建出页面,必须使用:
templates[tmpl].Exectu(w, p)
renderTemplate
- 现在文本编辑器中创建html模板文件,例如view.html:
- 在 main() 中网页服务器用 ListenAndServe 启动并监听 8080 端口。需要先为紧接在 URL localhost:8080/ 之后, 以view, edit 或 save 开头的 url 路径定义一些处理函数。
在此定义了 3 个处理函数,由于包含重复的启动代码,我们将其提取到单独的 makeHandler 函数中。这是一个值得研究的特殊高阶函数:其参数是一个函数,返回一个新的闭包函数:
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)
}
}
- 闭包封闭了函数变量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表单中,它开头是这样的:
<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:
package main
import (
"fmt"
"os"
"text/template"
)
type Person struct {
Name string
nonExporteAgeField string
}
func main() {
t := template.New("hello")
t, _ = t.Parse("hello {{.Name}}")
// t, _ = t.Parse("your age is {{.nonExporteAgeField}}")
// t, _ = t.Parse("hello {{.}}")
p := Person{Name: "test", nonExporteAgeField: "30"}
if err := t.Execute(os.Stdout, p); err != nil {
fmt.Println("There was an error:", err.Error())
}
}
数据结构中包含一个未导出的字段,当尝试把它整合到类似这样的定义字符串:
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:
package main
import (
"fmt"
"html/template"
)
func main() {
tOk := template.New("ok")
template.Must(tOk.Parse("/* and a comment */ 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
运行Execute产生的结果来自模板的输出,它包含静态文本,以及被{{}}包裹的称之为管道的文本。
可以对管道数据的输出结果用if-else-end设置条件约束:如果管道是空的,类似于:
{{if ``}} Will not print. {{end}}
或者
{{if `anything`}} Print IF part. {{else}} Print ELSE part.{{end}}
template_ifelse.go:
package main
import (
"os"
"html/template"
)
func main() {
tEmpty := template.New("template test")
tEmpty = template.Must(tEmpty.Parse("Empty pipeline if demo: {{if ``}} Will not print. {{end}}\n")) //empty pipeline following if
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")) //non empty pipeline following if condition
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")) //non empty pipeline following if condition
tIfElse.Execute(os.Stdout, nil)
}
点号和with-end
with语句将点号设为管道的值。如果管道是空的,那么不管with-end块之间有什么,都会被忽略。在被嵌套时,点号根据最近的作用域取得值。以下程序演示了这点:
package main
import (
"os"
"html/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)
}
模板变量$
可以在模板内为管道设置本地变量,变量名以$符号作为前缀。变量名只能包含字母、数字和下划线。以下示例使用了多种形式的有效变量名。
package main
import (
"os"
"html/template"
)
func main() {
t := template.New("test")
t = template.Must(t.Parse("{{with $3 := `hello`}}{{$3}}{{end}}!\n"))
t.Execute(os.Stdout, nil)
t = template.Must(t.Parse("{{with $x3 := `hola`}}{{$x3}}{{end}}!\n"))
t.Execute(os.Stdout, nil)
t = template.Must(t.Parse("{{with $x_1 := `hey`}}{{$x_1}} {{.}} {{$x_1}}{{end}}!\n"))
t.Execute(os.Stdout, nil)
}
range-end
range-end 结构格式为:{{range pipeline}} T1 {{else}} T0 {{end}}。
range被用于在集合上迭代:管道的值必须是数组、切片或map。如果管道的值长度为零,点号的值不受影响,且执行T0;否则,点号被设置为数组、切片或map内元素的值,并执行T1。
{{range .}}
{{.}}
{{end}}
s := []int{1,2,3,4}
t.Execute(os.Stdout, s)
模板预定义函数
也有一些可以在模板代码中使用的预定义函数,例如printf函数工作方式类似于fmt.Sprintf:
package main
import (
"os"
"html/template"
)
func main() {
t := template.New("test")
t = template.Must(t.Parse("{{with $x := `hello`}}{{printf `%s %s` $x `Mary`}}{{end}}!\n"))
t.Execute(os.Stdout, nil)
}
网页服务器功能
package main
import (
"bytes"
"expvar"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
)
var helloRequests = expvar.NewInt("hello-requests")
var webroot = flag.String("root", "/home/user", "web root directory")
var booleanflag = flag.Bool("boolean", true, "another flag for testing")
type Counter struct {
n int
}
type Chan chan int
func main() {
flag.Parse()
http.Handle("/", http.HandlerFunc(Logger))
http.Handle("/go/hello", http.HandlerFunc(HelloServer))
ctr := new(Counter)
expvar.Publish("counter", ctr)
http.Handle("/counter", ctr)
http.Handle("/go/", http.StripPrefix("/go/", http.FileServer(http.Dir(*webroot))))
http.Handle("/flags", http.HandlerFunc(FlagServer))
http.Handle("/args", http.HandlerFunc(ArgServer))
http.Handle("/chan", ChanCreate())
http.Handle("/date", http.HandlerFunc(DateServer))
err := http.ListenAndServe(":12345", nil)
if err != nil {
log.Panicln("ListenAndServe:", err)
}
}
func Logger(w http.ResponseWriter, req *http.Request) {
log.Print(req.URL.String())
w.WriteHeader(404)
w.Write([]byte("oops"))
}
func HelloServer(w http.ResponseWriter, req *http.Request) {
helloRequests.Add(1)
io.WriteString(w, "hello world!\n")
}
func (ctr *Counter) String() string { return fmt.Sprintf("%d", ctr.n) }
func (ctr *Counter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case "GET":
ctr.n++
case "POST":
buf := new(bytes.Buffer)
io.Copy(buf, req.Body)
body := buf.String()
if n, err := strconv.Atoi(body); err != nil {
fmt.Fprintf(w, "bad POST: %v\nbody: [%v]\n", err, body)
} else {
ctr.n = n
fmt.Fprint(w, "counter reset\n")
}
}
fmt.Fprintf(w, "counter = %d\n", ctr.n)
}
func FlagServer(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
fmt.Fprint(w, "Flags:\n")
flag.VisitAll(func(f *flag.Flag) {
if f.Value.String() != f.DefValue {
fmt.Fprintf(w, "%s = %s [default = %s]\n", f.Name, f.Value.String(), f.DefValue)
} else {
fmt.Fprintf(w, "%s = %s\n", f.Name, f.Value.String())
}
})
}
func ArgServer(w http.ResponseWriter, req *http.Request) {
for _, s := range os.Args {
fmt.Fprint(w, s, " ")
}
}
func ChanCreate() Chan {
c := make(Chan)
go func(c Chan) {
for x := 0; ; x++ {
c <- x
}
}(c)
return c
}
func (ch Chan) ServeHTTP(w http.ResponseWriter, req *http.Request) {
io.WriteString(w, fmt.Sprintf("channel send #%d\n", <-ch))
}
func DateServer(rw http.ResponseWriter, req *http.Request) {
rw.Header().Set("Content-Type", "text/plain; charset=utf-8")
r, w, err := os.Pipe()
if err != nil {
fmt.Fprintf(rw, "pipe: %s\n", err)
return
}
p, err := os.StartProcess("/bin/date", []string{"date"}, &os.ProcAttr{Files: []*os.File{nil, w, w}})
defer r.Close()
w.Close()
if err != nil {
fmt.Fprintf(rw, "fork/exec: %s\n", err)
return
}
defer p.Release()
io.Copy(rw, r)
wait, err := p.Wait()
if err != nil {
fmt.Fprintf(rw, "wait: %s\n", err)
return
}
if !wait.Exited() {
fmt.Fprintf(rw, "date: %v\n", wait)
return
}
}
用rpc实现远程过程调用
RPC(Remote Procedure Call Protocol),远程过程调用协议,是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。它假定某些传输协议的存在,如TCP或UDP,以便为通信程序之间携带信息数据。通过它可以使函数调用模式网络化。客户端就像调用本地函数一样,然后客户端把这些参数打包之后通过网络传递到服务端,服务端解包到处理过程中执行,然后执行的结果反馈给客户端。
运行时,一次客户机对服务器的RPC调用,其内部操作大致有如下十步:
- 调用客户端句柄;执行传送参数
- 调用本地系统内核发送网络消息
- 消息传送到远程主机
- 服务器句柄得到消息并取得参数
- 执行远程过程
- 执行的过程将结果返回服务器句柄
- 服务器句柄返回结果,调用远程系统内核
- 消息传回本地主机
- 客户句柄由内核接收消息
- 客户接收句柄返回的结果
Go标准包中提供了对RPC的支持,支持三个级别的RPC:TCP、HTTP、JSONRPC。但Go的RPC包是独一无二的RPC,它和传统的RPC系统不同,它只支持Go开发的服务器与客户端之间的交互,因为在内部,它们采用了Gob来编码。
Go RPC的函数只有符合下面的条件才能被远程访问,不然会被忽略,详细的要求如下:
- 函数必须是导出的(首字母大写)
- 必须有两个导出类型的参数
- 第一个参数是接收的参数,第二个参数是返回给客户端的参数,第二个参数必须是指针类型的
- 函数还要有一个返回值error
func (t *T) MethodName(argType T1, replyType *T2) error
T、T1和T2类型必须能被encoding/god包编解码。
HTTP RPC
服务端示例:
package main
import (
"errors"
"fmt"
"net/http"
"net/rpc"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {
if args.B == 0 {
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
rpc.HandleHTTP()
err := http.ListenAndServe(":8081", nil)
if err != nil {
fmt.Println(err.Error())
}
}
客户端示例:
package main
import (
"fmt"
"log"
"net/rpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server")
os.Exit(1)
}
serverAddress := os.Args[1]
client, err := rpc.DialHTTP("tcp", serverAddress+":8081")
if err != nil {
log.Fatal("dialing:", err)
}
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, ")
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
执行go run http_rpc_client.go localhost:
Arith: 17*8=136
Arith: 17/8=2 remainder 1
通过上面的调用可以看到参数和返回值是我们定义的struct类型,在服务端我们把它们当做调用函数的参数的类型,在客户端作为client.Call的第2,3两个参数的类型。客户端最重要的就是这个Call函数,它有3个参数,第1个要调用的函数的名字,第2个是要传递的参数,第3个要返回的参数(主要是指针类型),通过上面的代码例子我们可以发现,使用Go的RPC实现相当的简单,方便。
TCP RPC
服务端代码:
package main
import (
"errors"
"fmt"
"net"
"net/rpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {
if args.B == 0 {
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
tcpAddr, err := net.ResolveTCPAddr("tcp", ":8082")
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
rpc.ServeConn(conn)
}
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error", err.Error())
os.Exit(1)
}
}
客户端代码:
package main
import (
"fmt"
"log"
"net/rpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server:port")
os.Exit(1)
}
service := os.Args[1]
client, err := rpc.Dial("tcp", service)
if err != nil {
log.Fatal("dialing:", err)
}
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, ")
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
JSON RPC
JSON RPC是数据编码采用了JSON,而不是god编码,其他和上面介绍的RPC概念一样。
服务端代码:
package main
import (
"errors"
"fmt"
"net"
"net/rpc"
"net/rpc/jsonrpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
type Arith int
func (t *Arith) Multiply(args *Args, reply *int) error {
*reply = args.A * args.B
return nil
}
func (t *Arith) Divide(args *Args, quo *Quotient) error {
if args.B == 0 {
return errors.New("divide by zero")
}
quo.Quo = args.A / args.B
quo.Rem = args.A % args.B
return nil
}
func main() {
arith := new(Arith)
rpc.Register(arith)
tcpAddr, err := net.ResolveTCPAddr("tcp", ":8082")
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
jsonrpc.ServeConn(conn)
}
}
func checkError(err error) {
if err != nil {
fmt.Println("Fatal error", err.Error())
os.Exit(1)
}
}
客户端代码:
package main
import (
"fmt"
"log"
"net/rpc/jsonrpc"
"os"
)
type Args struct {
A, B int
}
type Quotient struct {
Quo, Rem int
}
func main() {
if len(os.Args) != 2 {
fmt.Println("Usage: ", os.Args[0], "server:port")
log.Fatal(1)
}
service := os.Args[1]
client, err := jsonrpc.Dial("tcp", service)
if err != nil {
log.Fatal("dialing:", err)
}
args := Args{17, 8}
var reply int
err = client.Call("Arith.Multiply", args, &reply)
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d*%d=%d\n", args.A, args.B, reply)
var quot Quotient
err = client.Call("Arith.Divide", args, ")
if err != nil {
log.Fatal("arith error:", err)
}
fmt.Printf("Arith: %d/%d=%d remainder %d\n", args.A, args.B, quot.Quo, quot.Rem)
}
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的应用设计。
package main
import (
"fmt"
"log"
"net/http"
"github.com/julienschmidt/httprouter"
)
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Welcome!\n")
}
func Hello(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
fmt.Fprintf(w, "hello, %s\n", ps.ByName("name"))
}
func getuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
uid := ps.ByName("uid")
fmt.Fprintf(w, "you are get user %s", uid)
}
func modifyuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
uid := ps.ByName("uid")
fmt.Fprintf(w, "you are modify user %s", uid)
}
func deleteuser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
uid := ps.ByName("uid")
fmt.Fprintf(w, "you are delete user %s", uid)
}
func adduser(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
uid := ps.ByName("uid")
fmt.Fprintf(w, "you are add user %s", uid)
}
func main() {
router := httprouter.New()
router.GET("/", Index)
router.GET("/hello/:name", Hello)
router.GET("/user/:uid", getuser)
router.POST("/adduser/:uid", adduser)
router.DELETE("/deluser/:uid", deleteuser)
router.PUT("/moduser/:uid", modifyuser)
log.Fatal(http.ListenAndServe(":8083", router))
}
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信息到客户端,然后客户端将输出其收到的信息,客户端的代码如下:
<html>
<head></head>
<body>
<script type="text/javascript">
var sock = null;
var wsuri = "ws://127.0.0.1:1234";
window.onload = function() {
console.log("onload");
sock = new WebSocket(wsuri);
sock.onopen = function() {
console.log("connected to " + wsuri);
}
sock.onclose = function(e) {
console.log("connection closed (" + e.code + ")");
}
sock.onmessage = function(e) {
console.log("message received: " + e.data);
}
};
function send() {
var msg = document.getElementById('message').value;
sock.send(msg);
};
</script>
<h1>WebSocket Echo Test</h1>
<form>
<p>
Message: <input id="message" type="text" value="Hello, world!">
</p>
</form>
<button onclick="send();">Send Message</button>
</body>
</html>
服务端代码:
package main
import (
"golang.org/x/net/websocket"
"fmt"
"log"
"net/http"
)
func Echo(ws *websocket.Conn) {
var err error
for {
var reply string
if err = websocket.Message.Receive(ws, &reply); err != nil {
fmt.Println("Can't receive")
break
}
fmt.Println("Received back from client: " + reply)
msg := "Received: " + reply
fmt.Println("Sending to client: " + msg)
if err = websocket.Message.Send(ws, msg); err != nil {
fmt.Println("Can't send")
break
}
}
}
func main() {
http.Handle("/", websocket.Handler(Echo))
if err := http.ListenAndServe(":1234", nil); err != nil {
log.Fatal("ListenAndServe:", err)
}
}
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类型,
package main
import (
"fmt"
"net"
"os"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s ip-addr\n", os.Args[0])
os.Exit(1)
}
name := os.Args[1]
addr := net.ParseIP(name)
if addr == nil {
fmt.Println("Invalid address")
} else {
fmt.Println("The address is ", addr.String())
}
os.Exit(0)
}
TCP Socket
当我们知道如何通过网络端口访问一个服务时,那么我们能够做什么呢?作为客户端来说,我们可以通过向远端某台机器的的某个网络端口发送一个请求,然后得到在机器的此端口上监听的服务反馈的信息。作为服务端,我们需要把服务绑定到某个指定端口,并且在此端口上监听,当有客户端来访问时能够读取信息并且写入反馈信息。
在Go语言的net包中有一个类型TCPConn,这个类型可以用来作为客户端和服务端交互的通道,它有两个主要的函数:
func (c *TCPConn) Write(b []byte) (int, error)
func (c *TCPConn) Read(b []byte) (int, error)
还有我们需要知道一个TCPAddr类型,它表示一个TCP的地址信息,定义如下:
type TCPAddr struct {
IP IP
Port int
Zone string // ipv6 scoped addressing zone
}
在Go语言中通过ResolveTCPAddr获取一个TCPAddr,
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对象将请求信息发送到服务器端,读取服务器端响应的信息。服务器端读取并解析来自客户端的请求,并返回应答信息,这个连接只有当任一端关闭了连接之后才失效,不然这连接可以一直在使用。建立连接的函数定义如下:
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请求头,格式类似如下:
"HEAD / HTTP/1.0\r\n\r\n"
package main
import (
"fmt"
"net"
"os"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
os.Exit(1)
}
service := os.Args[1]
tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
checkError(err)
conn, err := net.DialTCP("tcp", nil, tcpAddr)
checkError(err)
_, err = conn.Write([]byte("HEAD / HTTP/1.0\r\n\r\n"))
checkError(err)
// resulte, err := ioutil.ReadAll(conn)
result := make([]byte, 256)
_, err = conn.Read(result)
checkError(err)
fmt.Println(string(result))
os.Exit(0)
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
通过上面的代码我们可以看出:首先程序将用户的输入作为参数service传入net.ResolveTCPAddr获取一个tcpAddr,然后把tcpAddr传入DialTCP后创建了一个TCP连接conn,通过conn来发送请求信息,最后通过ioutil.ReadAll从conn中读取全部的文本,也就是服务端响应反馈的信息。
TCP Server
上面我们编写了一个TCP的客户端程序,也可以通过net包来创建一个服务器端程序,在服务器端我们需要绑定服务到指定的非激活端口,并监听此端口,当有客户端请求到达的时候可以接收到来自客户端连接的请求。net包中有相应功能的函数,函数定义如下:
func ListenTCP(network string, laddr *TCPAddr) (*TCPListener, error)
func (l *TCPListener) Accept() (Conn, error)
package main
import (
"fmt"
"net"
"os"
"time"
)
func main() {
service := ":1200"
tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
go handleClient(conn)
}
}
func handleClient(conn net.Conn) {
defer conn.Close()
daytime := time.Now().String()
conn.Write([]byte(daytime))
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
如果我们需要通过从客户端发送不同的请求来获取不同的时间格式,而且需要一个长连接,该怎么做呢?请看:
package main
import (
"fmt"
"net"
"os"
"strconv"
"strings"
"time"
)
func main() {
service := ":1200"
tcpAddr, err := net.ResolveTCPAddr("tcp4", service)
checkError(err)
listener, err := net.ListenTCP("tcp", tcpAddr)
checkError(err)
for {
conn, err := listener.Accept()
if err != nil {
continue
}
go handleClient(conn)
}
}
func handleClient(conn net.Conn) {
conn.SetReadDeadline(time.Now().Add(2 * time.Minute)) // set 2 minutes timeout
request := make([]byte, 128) // set maxium request length to 128B to prevent flood attack
defer conn.Close()
for {
read_len, err := conn.Read(request)
if err != nil {
fmt.Println(err)
break
}
if read_len == 0 {
break
} else if strings.TrimSpace(string(request[:read_len])) == "timestamp " {
daytime := strconv.FormatInt(time.Now().Unix(), 10)
conn.Write([]byte(daytime))
} else {
daytime := time.Now().String()
conn.Write([]byte(daytime))
}
request = make([]byte, 128)
}
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err.Error())
os.Exit(1)
}
}
在上面这个例子中,我们使用conn.Read()不断读取客户端发来的请求。由于我们需要保持与客户端的长连接,所以不能在读取完一次请求后就关闭连接。由于conn.SetReadDeadline()设置了超时,当一定时间内客户端无请求发送,conn便会自动关闭,下面的for循环即会因为连接已关闭而跳出。需要注意的是,request在创建时需要指定一个最大长度以防止flood attack;每次读取到请求处理完毕后,需要清理request,因为conn.Read()会将新读取到的内容append到原内容之后。
控制TCP连接
TCP有很多连接控制函数,用的多的有如下函数:
func DialTimeout(net, addr string, timeout time.Duration) (Conn, error)
设置建立连接的超时时间,客户端和服务端都适用,当超过设置时间时,连接自动关闭。
func (c *TCPConn) SetReadDeadline(t time.Time) error
func (c *TCPConn) SetWriteDeadline(t time.Time) error
用来设置写入/读取一个连接的超时时间。当超过设置时间时,连接自动关闭。
func (c *TCPConn) SetKeepAlive(keeplive bool) os.Error
设置keepAlive属性。操作系统层在tcp上没有数据和ACK的时候,会间隔性的发送keepalive包,操作系统可以通过该包来判断一个tcp连接是否已经断开,在windows上默认2个小时没有收到数据和keepalive包的时候认为tcp连接已经断开,这个功能和我们通常在应用层加的心跳包的功能类似。
UDP SOCKET
UDP主要函数有如下:
func ResolveUDPAddr(net, addr string) (*UDPAddr, os.Error)
func DialUDP(net string, laddr, raddr *UDPAddr) (c *UDPConn, err os.Error)
func ListenUDP(net string, laddr *UDPAddr) (c *UDPConn, err os.Error)
func (c *UDPConn) ReadFromUDP(b []byte) (n int, addr *UDPAddr, err os.Error)
func (c *UDPConn) WriteToUDP(b []byte, addr *UDPAddr) (n int, err os.Error)
客户端代码:
package main
import (
"fmt"
"net"
"os"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintf(os.Stderr, "Usage: %s host:port", os.Args[0])
os.Exit(1)
}
service := os.Args[1]
udpAddr, err := net.ResolveUDPAddr("udp4", service)
checkError(err)
conn, err := net.DialUDP("udp", nil, udpAddr)
checkError(err)
_, err = conn.Write([]byte("anything"))
checkError(err)
var buf [512]byte
n, err := conn.Read(buf[0:])
checkError(err)
fmt.Println(string(buf[0:n]))
os.Exit(0)
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error %s", err.Error())
os.Exit(1)
}
}
服务端代码:
package main
import (
"fmt"
"net"
"os"
"time"
)
func main() {
service := ":1200"
udpAddr, err := net.ResolveUDPAddr("udp4", service)
checkError(err)
conn, err := net.ListenUDP("udp", udpAddr)
checkError(err)
for {
handleClient(conn)
}
}
func handleClient(conn *net.UDPConn) {
var buf [512]byte
_, addr, err := conn.ReadFromUDP(buf[0:])
if err != nil {
return
}
daytime := time.Now().String()
conn.WriteToUDP([]byte(daytime), addr)
}
func checkError(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "Fatal error %s", err.Error())
os.Exit(1)
}
}