Web sockets

Web sockets

Web sockets are designed to answer a common problem with web systems: the server is unable to initiate or push content to a user agent such as a browser. Web sockets allow a full duplex connection to be established to allow this. Go has nearly complete support for them.

Web sockets规范的设计是为了解决网络系统中的一个常见问题:服务器端无法发起或推送内容到用户代理,例如浏览器。Web sockets能够建立一个全双工的连接来进行这些操作。Go语言对此有近乎完整的支持。

Warning

警告

The Web Sockets package is not currently in the main Go 1 tree and is not included in the current distributions. To use it, you need to install it by

Web Sockets包当前并不在Go 1的主代码树里,也不包含在当前的分发包里。为了使用它,你必须先通过如下命令来安装它

    
go get code.google.com/p/go.net/websocket 
    
  

Introduction

介绍

The websockets model will change for release r61. This describes the new package, not the package in r60 and earlier. If you do not have r61, at the time of writing, use hg pull; hg update weekly to download it.

websockets模式(模型??)会在r61版本里做些变更。此文介绍的是新的包,而不是r60或者更早之前的版本。如果你当前还没有r61版本,使用 hg pull; hg update weekly 来下载它。

The standard model of interaction between a web user agent such as a browser and a web server such as Apache is that the user agent makes HTTP requests and the server makes a single reply to each one. In the case of a browser, the request is made by clicking on a link, entering a URL into the address bar, clicking on the forward or back buttons, etc. The response is treated as a new page and is loaded into a browser window.

用户代理(例如浏览器)和web服务器(例如Apache)之间进行交互的标准模型是这样的:用户代理发送HTTP请求,然后服务器响应每个请求。以浏览器举例,请求是指点击链接、在地址栏中输入网址、点击前进或后退按钮等行为。而响应则是在浏览器窗口里加载的页面。

This traditional model has many drawbacks. The first is that each request opens and closes a new TCP connection. HTTP 1.1 solved this by allowing persistent connections, so that a connection could be held open for a short period to allow for multiple requests (e.g. for images) to be made on the same server.

这种传统模型有很多缺点。首先,每个请求都会开启和关闭一个新的TCP连接。HTTP 1.1 通过持久化连接来解决这个问题,一个连接能够在较短的时期里保持打开状态,从而达到向同一个服务器发送多个请求(例如加载图片)的目的。

While HTTP 1.1 persistent connections alleviate the problem of slow loading of a page with many graphics, it does not improve the interaction model. Even with forms, the model is still that of submitting the form and displaying the response as a new page. JavaScript helps in allowing error checking to be performed on form data before submission, but does not change the model.

虽然 HTTP 1.1 的持久化连接减轻了有很多图片的页面的加载缓慢问题,但它还是没有改进旧的交互模型。特别是在有表单的情况,模型依然是提交表单然后展现一个新页面作为响应。虽然在JavaScript的帮助下能做到在提交表单前进行错误检查,但依然没有改变这种模型。

AJAX (Asynchronous JavaScript and XML) made a significant advance to the user interaction model. This allows a browser to make a request and just use the response to update the display in place using the HTML Document Object Model (DOM). But again the interaction model is the same. AJAX just affects how the browser manages the returned pages. There is no explicit extra support in Go for AJAX, as none is needed: the HTTP server just sees an ordinary HTTP POST request with possibly some XML or JSON data, and this can be dealt with using techniques already discussed.

AJAX(异步JavaScript和XML)对交互模型做了极大的改进。它允许浏览器发送请求然后通过DOM将响应内容更新到页面上适当的部分。但是交互模型的本质还是没有变。AJAX只是影响了浏览器对返回页面的处理方式。Go没有为AJAX做明显的额外支持,其实也没有这个必要:HTTP服务器看到的仍然是常规的HTTP POST请求(可能带有一些XML或者JSON数据),这种请求可以被已经谈及的技术进行处理。

All of these are still browser to server communication. What is missing is server initiated communications to the browser. This can be filled by Web sockets: the browser (or any user agent) keeps open a long-lived TCP connection to a Web sockets server. The TCP connection allows either side to send arbitrary packets, so any application protocol can be used on a web socket.

前面提及的都还是浏览器向服务器通讯。所缺少的是服务器向浏览器发起通讯。Web sockets正好可以填补这个空缺:浏览器(或者任何其它的用户代理)保持开启一条和Web sockets服务器的TCP长连接。这条TCP连接允许各边发送任意的数据包,因此可以在web socket上使用任何的应用层协议。

How a websocket is started is by the user agent sending a special HTTP request that says "switch to web sockets". The TCP connection underlying the HTTP request is kept open, but both user agent and server switch to using the web sockets protocol instead of getting an HTTP response and closing the socket.

websocket是由用户代理发送一条“切换到web sockets”特殊HTTP请求开始的。HTTP请求所使用的TCP连接仍然保持开启状态,而不是在获取到一个HTTP响应后关闭,同时用户代理和服务器端则切换到使用web sockets协议。

Note that it is still the browser or user agent that initiates the Web socket connection. The browser does not run a TCP server of its own. While the specification is complex, the protocol is designed to be fairly easy to use. The client opens an HTTP connection and then replaces the HTTP protocol with its own WS protocol, re-using the same TCP connection.

值得注意的是,仍然是由浏览器或者用户代理来发起一条Web socket连接的。浏览器自身并没有运行一个TCP服务器??虽然规范很复杂,但协议还是设计得相当易用的。客户端开启一条HTTP连接,接着用WS协议取代HTTP协议,重用了同一条TCP连接。

Web socket server

Web socket服务器端

A web socket server starts off by being an HTTP server, accepting TCP conections and handling the HTTP requests on the TCP connection. When a request comes in that switches that connection to a being a web socket connection, the protocol handler is changed from an HTTP handler to a WebSocket handler. So it is only that TCP connection that gets its role changed: the server continues to be an HTTP server for other requests, while the TCP socket underlying that one connection is used as a web socket.

web socket服务器端最初是HTTP服务器端,接受TCP连接,处理该连接上的HTTP请求。当将该连接变换成web socket连接的请求到来之后,协议处理器从一个HTTP处理器转变成WebSocket处理器。所以仅仅是TCP连接的角色变化了:当前连接所有的TCP socket被当成web socket来使用;对于其它请求而言,服务器仍然是一个HTTP服务器。

One of the simple servers HHTP we discussed in Chapter 8: HTTP registered varous handlers such as a file handler or a function handler. To handle web socket requests we simply register a different type of handler - a web socket handler. Which handler the server uses is based on the URL pattern. For example, a file handler might be registered for "/", a function handler for "/cgi-bin/..." and a web sockets handler for "/ws".

章节8: HTTP里我们讨论的一个简单的服务器注册了各式各样的处理器,例如文件处理器、函数处理器等。为了处理web socket请求,我们仅需另外注册一种类型的处理器-web socket处理器。服务器基于URL模式来选出对应处理器。例如,"/"为文件处理器,"/cgi-bin/..."为函数处理器,"/ws"为web sockets处理器。

An HTTP server that is only expecting to be used for web sockets might run by

如下将运行一个仅用于web sockets的HTTP服务器


func main() {
        http.Handle("/", websocket.Handler(WSHandler))
        err := http.ListenAndServe(":12345", nil)
        checkError(err)
}

A more complex server might handle both HTTP and web socket requests simply by adding in more handlers.

通过添加更多的处理器,一个更为复杂的服务器可以同时处理HTTP请求和web socket请求。

The Message object

Message对象

HTTP is a stream protocol. Web sockets are frame-based. You prepare a block of data (of any size) and send it as a set of frames. Frames can contain either strings in UTF-8 encoding or a sequence of bytes.

HTTP是流协议。Web sockets是基于帧的。你可以生成任意大小的一块数据,将其作为一组帧来发送。帧可以包含UTF-8编码的字符串或者字节序列。

The simplest way of using web sockets is just to prepare a block of data and ask the Go websocket library to package it as a set of frame data, send them across the wire and receive it as the same block. The websocket package contains a convenience object Message to do just that. The Message object has two methods, Send and Receive which take a websocket as first parameter. The second parameter is either the address of a variable to store data in, or the data to be sent. Code to send string data would look like

最简单的使用web sockets的方法就是准备好数据块然后让Go的websocket库将其封装成一组帧数据,通过网络线路发送,然后接收生成同样的数据块。websocket包里有个很好使的Message对象来做这些。Message对象有SendReceive两个方法,它们的第一个参数是一个websocket对象,第二个参数是存放数据的地址。发送字符串数据的代码示例如下


 msgToSend := "Hello"
 err := websocket.Message.Send(ws, msgToSend)

 var msgToReceive string
 err := websocket.Message.Receive(conn, &msgToReceive)

Code to send byte data would look like

发送字节序列的代码示例如下


 dataToSend := []byte{0, 1, 2}
 err := websocket.Message.Send(ws, dataToSend)

 var dataToReceive []byte
 err := websocket.Message.Receive(conn, &dataToReceive)

An echo server to send and receive string data is given below. Note that in web sockets either side can initiate sending of messages, and in this server we send messages from the server to a client when it connects (send/receive) instead of the more normal receive/send server. The server is

下面将给出一个发送和接收字符串数据的echo服务器代码。值得注意的是,在web sockets协议里,各边都可以发起消息的发送。这回,当客户端连接后,服务器端先向客户端发送消息(发送/接收),而不是传统的接收/发送。服务器端代码如下


/* EchoServer
 */
package main

import (
        "fmt"
        "net/http"
        "os"
        // "io"
 "code.google.com/p/go.net/websocket"
)

func Echo(ws *websocket.Conn) {
        fmt.Println("Echoing")

        for n := 0; n < 10; n++ {
                msg := "Hello  " + string(n+48)
                fmt.Println("Sending to client: " + msg)
                err := websocket.Message.Send(ws, msg)
                if err != nil {
                        fmt.Println("Can't send")
                        break
                }

                var reply string
                err = websocket.Message.Receive(ws, &reply)
                if err != nil {
                        fmt.Println("Can't receive")
                        break
                }
                fmt.Println("Received back from client: " + reply)
        }
}

func main() {

        http.Handle("/", websocket.Handler(Echo))
        err := http.ListenAndServe(":12345", nil)
        checkError(err)
}

func checkError(err error) {
        if err != nil {
                fmt.Println("Fatal error ", err.Error())
                os.Exit(1)
        }
}

A client that talks to this server is

和服务器端进行会话的客户端代码如下


/* EchoClient
 */
package main

import (
        "code.google.com/p/go.net/websocket"
        "fmt"
        "io"
        "os"
)

func main() {
        if len(os.Args) != 2 {
                fmt.Println("Usage: ", os.Args[0], "ws://host:port")
                os.Exit(1)
        }
        service := os.Args[1]

        conn, err := websocket.Dial(service, "", "http://localhost")
        checkError(err)
        var msg string
        for {
                err := websocket.Message.Receive(conn, &msg)
                if err != nil {
                        if err == io.EOF {
                                // graceful shutdown by server
                         break
                        }
                        fmt.Println("Couldn't receive msg " + err.Error())
                        break
                }
                fmt.Println("Received from server: " + msg)
                // return the msg
         err = websocket.Message.Send(conn, msg)
                if err != nil {
                        fmt.Println("Coduln't return msg")
                        break
                }
        }
        os.Exit(0)
}

func checkError(err error) {
        if err != nil {
                fmt.Println("Fatal error ", err.Error())
                os.Exit(1)
        }
}

The url for the client running on the same machine as the server should be ws://localhost:12345/

当客户端和服务器端运行在同一台机器上时,客户端所需的url参数是 ws://localhost:12345/

The JSON object

JSON对象

It is expected that many websocket clients and servers will exchange data in JSON format. For Go programs this means that a Go object will be marshalled into JSON format as described in Chapter 4: Serialisation and then sent as a UTF-8 string, while the receiver will read this string and unmarshal it back into a Go object.

正如所预期的那样,许多websocket客户端和服务器端希望通过JSON格式来进行数据交换。在Go程序里,这意味着Go中的对象将编组成JSON格式(在章节 4: Serialisation里有描述)然后以UTF-8字符串发送;而接收方将读取该字符串然后将其解组成Go对象。

The websocket convenience object JSON will do this for you. It has methods Send and Receive for sending and receiving data, just like the Message object.

websocket里的JSON对象将很方便地为你做这些。它带有SendReceive两个方法来发送接收数据,正如Message对象。

A client that sends a Person object in JSON format is

下面这个客户端将以JSON格式来发送Person对象


/* PersonClientJSON
 */
package main

import (
        "code.google.com/p/go.net/websocket"
        "fmt"
        "os"
)

type Person struct {
        Name   string
        Emails []string
}

func main() {
        if len(os.Args) != 2 {
                fmt.Println("Usage: ", os.Args[0], "ws://host:port")
                os.Exit(1)
        }
        service := os.Args[1]

        conn, err := websocket.Dial(service, "",
                "http://localhost")
        checkError(err)

        person := Person{Name: "Jan",
                Emails: []string{"ja@newmarch.name", "jan.newmarch@gmail.com"},
        }

        err = websocket.JSON.Send(conn, person)
        if err != nil {
                fmt.Println("Couldn't send msg " + err.Error())
        }
        os.Exit(0)
}

func checkError(err error) {
        if err != nil {
                fmt.Println("Fatal error ", err.Error())
                os.Exit(1)
        }
}

and a server that reads it is

读取该数据的服务器端


/* PersonServerJSON
 */
package main

import (
        "code.google.com/p/go.net/websocket"
        "fmt"
        "net/http"
        "os"
)

type Person struct {
        Name   string
        Emails []string
}

func ReceivePerson(ws *websocket.Conn) {
        var person Person
        err := websocket.JSON.Receive(ws, &person)
        if err != nil {
                fmt.Println("Can't receive")
        } else {

                fmt.Println("Name: " + person.Name)
                for _, e := range person.Emails {
                        fmt.Println("An email: " + e)
                }
        }
}

func main() {

        http.Handle("/", websocket.Handler(ReceivePerson))
        err := http.ListenAndServe(":12345", nil)
        checkError(err)
}

func checkError(err error) {
        if err != nil {
                fmt.Println("Fatal error ", err.Error())
                os.Exit(1)
        }
}

The Codec type

Codec 类型

The Message and JSON objects are both instances of the type Codec. This type is defined by

Message对象和JSON对象都是 类型Codec的实例。该类型的定义如下


type Codec struct {
    Marshal   func(v interface{}) (data []byte, payloadType byte, err os.Error)
    Unmarshal func(data []byte, payloadType byte, v interface{}) (err os.Error)
}

The type Codec implements the Send and Receive methods used earlier.

Codec类型实现了前面提及的SendReceive方法。

It is likely that websockets will also be used to exchange XML data. We can build an XML Codec object by wrapping the XML marshal and unmarshal methods discussed in Chapter 12: XML to give a suitable Codec object.

websocket同样可能被用来交换XML数据。我们可以通过包含章节 12: XML里提及的XML marshal 和 unmarshal 函数来创建一个合适的Codec对象。

We can create a XMLCodec package in this way:

我们将以这种方式来创建XMLCodec




package xmlcodec

import (
        "encoding/xml"
        "code.google.com/p/go.net/websocket"
)

func xmlMarshal(v interface{}) (msg []byte, payloadType byte, err error) {
        //buff := &bytes.Buffer{}
 msg, err = xml.Marshal(v)
        //msgRet := buff.Bytes()
 return msg, websocket.TextFrame, nil
}

func xmlUnmarshal(msg []byte, payloadType byte, v interface{}) (err error) {
        // r := bytes.NewBuffer(msg)
 err = xml.Unmarshal(msg, v)
        return err
}

var XMLCodec = websocket.Codec{xmlMarshal, xmlUnmarshal}

We can then serialise Go objects such as a Person into an XML document and send it from a client to a server by

接下来我们就可以序列化像Person这样的Go对象到XML文档,然后发送给服务器端


/* PersonClientXML
 */
package main

import (
        "code.google.com/p/go.net/websocket"
        "fmt"
        "os"
        "xmlcodec"
)

type Person struct {
        Name   string
        Emails []string
}

func main() {
        if len(os.Args) != 2 {
                fmt.Println("Usage: ", os.Args[0], "ws://host:port")
                os.Exit(1)
        }
        service := os.Args[1]

        conn, err := websocket.Dial(service, "", "http://localhost")
        checkError(err)

        person := Person{Name: "Jan",
                Emails: []string{"ja@newmarch.name", "jan.newmarch@gmail.com"},
        }

        err = xmlcodec.XMLCodec.Send(conn, person)
        if err != nil {
                fmt.Println("Couldn't send msg " + err.Error())
        }
        os.Exit(0)
}

func checkError(err error) {
        if err != nil {
                fmt.Println("Fatal error ", err.Error())
                os.Exit(1)
        }
}

A server which receives this and just prints information to the console is

服务器端接收到该文档后将里面的信息打印到终端


/* PersonServerXML
 */
package main

import (
        "code.google.com/p/go.net/websocket"
        "fmt"
        "net/http"
        "os"
        "xmlcodec"
)

type Person struct {
        Name   string
        Emails []string
}

func ReceivePerson(ws *websocket.Conn) {
        var person Person
        err := xmlcodec.XMLCodec.Receive(ws, &person)
        if err != nil {
                fmt.Println("Can't receive")
        } else {

                fmt.Println("Name: " + person.Name)
                for _, e := range person.Emails {
                        fmt.Println("An email: " + e)
                }
        }
}

func main() {

        http.Handle("/", websocket.Handler(ReceivePerson))
        err := http.ListenAndServe(":12345", nil)
        checkError(err)
}

func checkError(err error) {
        if err != nil {
                fmt.Println("Fatal error ", err.Error())
                os.Exit(1)
        }
}

Web sockets over TLS

Web sockets over TLS

A web socket can be built above a secure TLS socket. We discussed in Chapter 8: HTTP how to use a TLS socket using the certificates from Chapter 7: Security. That is used unchanged for web sockets. that is, we use http.ListenAndServeTLS instead of http.ListenAndServe.

web socket可以建立在安全的TLS套接字上。在章节 8: HTTP里,我们讨论了如果通过使用章节 7: Security里的证书来使用TLS套接字。这同样适用于web socket。也就是说,我们将使用http.ListenAndServeTLS 而不是 http.ListenAndServe

Here is the echo server using TLS

这里是一个使用了TLS的echo服务器


/* EchoServer
 */
package main

import (
        "code.google.com/p/go.net/websocket"
        "fmt"
        "net/http"
        "os"
)

func Echo(ws *websocket.Conn) {
        fmt.Println("Echoing")

        for n := 0; n < 10; n++ {
                msg := "Hello  " + string(n+48)
                fmt.Println("Sending to client: " + msg)
                err := websocket.Message.Send(ws, msg)
                if err != nil {
                        fmt.Println("Can't send")
                        break
                }

                var reply string
                err = websocket.Message.Receive(ws, &reply)
                if err != nil {
                        fmt.Println("Can't receive")
                        break
                }
                fmt.Println("Received back from client: " + reply)
        }
}

func main() {

        http.Handle("/", websocket.Handler(Echo))
        err := http.ListenAndServeTLS(":12345", "jan.newmarch.name.pem",
                "private.pem", nil)
        checkError(err)
}

func checkError(err error) {
        if err != nil {
                fmt.Println("Fatal error ", err.Error())
                os.Exit(1)
        }
}

The client is the same echo client as before. All that changes is the url, which uses the "wss" scheme instead of the "ws" scheme:

客户端仍然是前面的echo客户端,所做的改变仅仅是将url里的"ws"模式替换成"wss"模式。


  EchoClient wss://localhost:12345/

Conclusion

结论

The web sockets standard is nearing completion and no major changes are anticipated. This will allow HTTP user agents and servers to set up bi-directional socket connections and should make certain interaction styles much easier. Go has nearly complete support for web sockets.

web sockets标准已接近完成,预计也不会有较大的改动。它允许HTTP客户代理和服务器建立一个双向的套接字连接,从而易化了某些交互方式。Go对web sockets有近乎完整的支持。


Copyright © Jan Newmarch, jan@newmarch.name

If you like this book, please contribute using Flattr
or donate using PayPal