tcp服务器
这部分将使用TCP协议和协程范式编写一个简单的客户端-服务端应用,一个(web)服务器应用需要响应众多客户端的并发请求:Go会为每一个客户端产生一个协程用来处理请求。需要使用net包中网络通信的功能。它包含了处理TCP/IP以及UDP协议、域名解析等方法。
server.go:
package mainimport ("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 mainimport ("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 mainimport ("fmt""net""os")func main() {conn, err := net.Dial("tcp", "192.0.32.10:80") // tcp ipv4checkConnection(conn, err)conn, err = net.Dial("udp", "192.0.32.10:80") // udpcheckConnection(conn, err)conn, err = net.Dial("tcp", "[2620:0:2d0:200::10]:80") // tcp ipv6checkConnection(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 mainimport ("fmt""io""net")func main() {var (host = "www.apache.org"port = "80"remote = host + ":" + portmsg string = "GET / \n"data = make([]uint8, 4096)read = truecount = 0)// 创建一个socketcon, 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 mainimport ("flag""fmt""net""syscall")const maxRead = 25func 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] = 0switch err {case nil:handleMsg(length, err, ibuf)case syscall.EAGAIN: // try againcontinuedefault: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 mainimport ("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 mainimport ("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 mainimport ("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.gopackage mainimport ("fmt""log""net/http""sort""strconv""strings")type statistics struct {numbers []float64mean float64median 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 responsefmt.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 []float64var text stringif 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 = numberssort.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) / 2result := 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 mainimport ("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 stringBody []byte}
示例:wiki.go:
package mainimport ("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 errortype Page struct {Title stringBody []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 foundhttp.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 mainimport ("fmt""os""text/template")type Person struct {Name stringnonExporteAgeField 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 mainimport ("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 mainimport ("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 iftEmpty.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 conditiontWithValue.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 conditiontIfElse.Execute(os.Stdout, nil)}
点号和with-end
with语句将点号设为管道的值。如果管道是空的,那么不管with-end块之间有什么,都会被忽略。在被嵌套时,点号根据最近的作用域取得值。以下程序演示了这点:
package mainimport ("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 mainimport ("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 mainimport ("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 mainimport ("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 intfunc 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 = nfmt.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 mainimport ("errors""fmt""net/http""net/rpc")type Args struct {A, B int}type Quotient struct {Quo, Rem int}type Arith intfunc (t *Arith) Multiply(args *Args, reply *int) error {*reply = args.A * args.Breturn 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.Bquo.Rem = args.A % args.Breturn nil}func main() {arith := new(Arith)rpc.Register(arith)rpc.HandleHTTP()err := http.ListenAndServe(":8081", nil)if err != nil {fmt.Println(err.Error())}}
客户端示例:
package mainimport ("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 interr = 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 Quotienterr = 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=136Arith: 17/8=2 remainder 1
通过上面的调用可以看到参数和返回值是我们定义的struct类型,在服务端我们把它们当做调用函数的参数的类型,在客户端作为client.Call的第2,3两个参数的类型。客户端最重要的就是这个Call函数,它有3个参数,第1个要调用的函数的名字,第2个是要传递的参数,第3个要返回的参数(主要是指针类型),通过上面的代码例子我们可以发现,使用Go的RPC实现相当的简单,方便。
TCP RPC
服务端代码:
package mainimport ("errors""fmt""net""net/rpc""os")type Args struct {A, B int}type Quotient struct {Quo, Rem int}type Arith intfunc (t *Arith) Multiply(args *Args, reply *int) error {*reply = args.A * args.Breturn 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.Bquo.Rem = args.A % args.Breturn 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 mainimport ("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 interr = 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 Quotienterr = 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 mainimport ("errors""fmt""net""net/rpc""net/rpc/jsonrpc""os")type Args struct {A, B int}type Quotient struct {Quo, Rem int}type Arith intfunc (t *Arith) Multiply(args *Args, reply *int) error {*reply = args.A * args.Breturn 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.Bquo.Rem = args.A % args.Breturn 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 mainimport ("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 interr = 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 Quotienterr = 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 mainimport ("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 mainimport ("golang.org/x/net/websocket""fmt""log""net/http")func Echo(ws *websocket.Conn) {var err errorfor {var reply stringif err = websocket.Message.Receive(ws, &reply); err != nil {fmt.Println("Can't receive")break}fmt.Println("Received back from client: " + reply)msg := "Received: " + replyfmt.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 mainimport ("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 IPPort intZone 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 mainimport ("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 mainimport ("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 mainimport ("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 timeoutrequest := make([]byte, 128) // set maxium request length to 128B to prevent flood attackdefer 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) errorfunc (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 mainimport ("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]byten, 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 mainimport ("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)}}
