是什么

  1. WebSocket 是通过单个 TCP 连接提供全双工(双向通信)通信信道的计算机通信协议。
  2. 在 WebSocket API 中,浏览器和服务器只需要做一个握手的动作,然后,浏览器和服务器之间就形成了一条快速通道。两者之间就直接可以数据互相传送。——长链接,直到关闭

在 WebSocket 出现之前,为了实现即时通信,采用的技术都是“轮询”,即在特定的时间间隔内,由浏览器对服务器发出 HTTP Request,服务器在收到请求后,返回最新的数据给浏览器刷新,“轮询”使得浏览器需要对服务器不断发出请求,这样会占用大量带宽。

WebSocket 采用了一些特殊的报头,使得浏览器和服务器只需要做一个握手的动作,就可以在浏览器和服务器之间建立一条连接通道。且此连接会保持在活动状态,你可以使用 JavaScript 来向连接写入或从中接收数据,就像在使用一个常规的 TCP Socket 一样。它解决了 Web 实时化的问题,相比传统 HTTP 有如下好处:

  • 一个 Web 客户端只建立一个 TCP 连接
  • Websocket 服务端可以推送 (push) 数据到 web 客户端。
  • 有更加轻量级的头,减少数据传送量

WebSocket URL 的起始输入是 ws:// 或是 wss://(在 SSL 上)。

过程

  1. 客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息 “Upgrade: WebSocket” 表明这是一个申请协议升级的 HTTP 请求
  2. 服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了
  3. 双方就可以通过这个连接通道自由的传递信息
  4. 持续存在直到客户端或者服务器端的某一方主动的关闭连接。

WebSocket 原理

WebSocket 的协议颇为简单,在第一次 handshake 通过以后,连接便建立成功,其后的通讯数据都是以”\x00″开头, 以”\xFF”结尾。在客户端,这个是透明的,WebSocket 组件会自动将原始数据“掐头去尾”。

在知乎上看到一个完美的回答

实现

echo 框架版 echo_server.go, 使用的第三方库: github.com/gorilla/websocket

package main

import (
    "fmt"
    "log"
    "time"

    "github.com/labstack/echo"

    "github.com/gorilla/websocket"
    "github.com/labstack/echo/middleware"
)

var (
    upgrader = websocket.Upgrader{}
)

func hello(c echo.Context) error {
    ws, err := upgrader.Upgrade(c.Response(), c.Request(), nil)
    if err != nil {
        return err
    }
    defer ws.Close()

    for {
        // Write
        time.Sleep(time.Second)
        err := ws.WriteMessage(websocket.TextMessage, []byte("Hello, Client!"))
        if err != nil {
            log.Fatal(err)
        }

        // Read
        _, msg, err := ws.ReadMessage()
        if err != nil {
            log.Fatal(err)
        }
        fmt.Printf("%s\n", msg)
    }
}

func main() {
    e := echo.New()
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())
    e.GET("/echo", hello)
    e.Logger.Fatal(e.Start(":6666"))
}

官方库:golang.org/x/net/websocket

package main

import (
    "fmt"
    "golang.org/x/net/websocket"
    "net/http"
    "os"
    "time"
)

//错误处理函数
func checkErr(err error, extra string) bool {
    if err != nil {
        formatStr := " Err : %s\n";
        if extra != "" {
            formatStr = extra + formatStr;
        }

        fmt.Fprintf(os.Stderr, formatStr, err.Error());
        return true;
    }

    return false;
}

func svrConnHandler(conn *websocket.Conn) {
    request := make([]byte, 128);
    defer conn.Close();
    for {
        readLen, err := conn.Read(request)
        if checkErr(err, "Read") {
            break;
        }

        //socket被关闭了
        if readLen == 0 {
            fmt.Println("Client connection close!");
            break;
        } else {
            //输出接收到的信息
            fmt.Println(string(request[:readLen]))

            time.Sleep(time.Second);
            //发送
            conn.Write([]byte("World !"));
        }

        request = make([]byte, 128);
    }
}

func main() {
    http.Handle("/echo", websocket.Handler(svrConnHandler));
    err := http.ListenAndServe(":6666", nil);
    checkErr(err, "ListenAndServe");
    fmt.Println("Func finish.");
}

client 测试代码:

package main

import (
    "fmt"
    "golang.org/x/net/websocket"
    "os"
    "sync"
)

var gLocker sync.Mutex;    //全局锁
var gCondition *sync.Cond; //全局条件变量

var origin = "http://127.0.0.1:6666/"
var url = "ws://127.0.0.1:6666/echo"

//错误处理函数
func checkErr(err error, extra string) bool {
    if err != nil {
        formatStr := " Err : %s\n";
        if extra != "" {
            formatStr = extra + formatStr;
        }

        fmt.Fprintf(os.Stderr, formatStr, err.Error());
        return true;
    }

    return false;
}

//连接处理函数
func clientConnHandler(conn *websocket.Conn) {
    gLocker.Lock();
    defer gLocker.Unlock();
    defer conn.Close();
    request := make([]byte, 128);
    for {
        readLen, err := conn.Read(request)
        if checkErr(err, "Read") {
            gCondition.Signal();
            break;
        }

        //socket被关闭了
        if readLen == 0 {
            fmt.Println("Server connection close!");

            //条件变量同步通知
            gCondition.Signal();
            break;
        } else {
            //输出接收到的信息
            fmt.Println(string(request[:readLen]))

            //发送
            conn.Write([]byte("Hello !"));
        }

        request = make([]byte, 128);
    }
}

func main() {
    conn, err := websocket.Dial(url, "", origin);
    if checkErr(err, "Dial") {
        return;
    }

    gLocker.Lock();
    gCondition = sync.NewCond(&gLocker);
    _, err = conn.Write([]byte("Hello !"));
    go clientConnHandler(conn);

    //主线程阻塞,等待Singal结束
    for {
        //条件变量同步等待
        gCondition.Wait();
        break;
    }
    gLocker.Unlock();
    fmt.Println("Client finish.")
}