go websocket


原文链接: go websocket

https://github.com/eranyanay/1m-go-websockets

使用Golang构建Websocket接口服务

Golang可以使用gorilla/websocket这个框架来实现websocket接口的构造.这个框架可以用于写客户端和服务器.

我们依然从一个helloworld开始.这个例子我们在客户端连同服务端后立即发送一个helloworld消息给后端服务器,服务器接到后则返回一个helloworld消息给客户端.
客户端在接收到服务器消息后发送一个close消息给服务器,服务器就断开和客户端的连接.

  • 服务端

package main

import (
	"flag"
	"log"
	"net/http"

	"github.com/gorilla/websocket"
)

var addr = flag.String("addr", "localhost:5000", "http service address")

var upgrader = websocket.Upgrader{} // use default options

func helloworldWsHanddler(ws *websocket.Conn) {
	for {
		mt, message, err := ws.ReadMessage()
		if err != nil {
			log.Println("read:", err)
			break
		} else {
			switch mt {
			case websocket.CloseMessage:
				{
					log.Println("get close signal")
					break
				}
			case websocket.PingMessage, websocket.PongMessage:
				{
					log.Println("get ping pong")
				}
			case websocket.TextMessage:
				{
					log.Printf("recv: %s", message)
					msg := string(message)
					switch msg {
					case "close":
						{
							break
						}
					case "helloworld":
						{
							err = ws.WriteMessage(websocket.TextMessage, []byte("Hello World"))
							if err != nil {
								log.Println("write:", err)
								break
							}
						}
					default:
						{
							err = ws.WriteMessage(websocket.TextMessage, []byte("unkonwn command"))
							if err != nil {
								log.Println("write:", err)
								break
							}
						}
					}
				}
			case websocket.BinaryMessage:
				{
					log.Println("not support Binary now")
				}
			default:
				{
					log.Println("not support now")
				}
			}
		}
	}
}

func helloworldHttpHanddler(w http.ResponseWriter, r *http.Request) {
	ws, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Print("upgrade:", err)
		return
	}
	defer ws.Close()
	helloworldWsHanddler(ws)
}

func main() {
	flag.Parse()
	log.SetFlags(0)
	http.HandleFunc("/helloworld", helloworldHttpHanddler)
	log.Fatal(http.ListenAndServe(*addr, nil))
}

服务端我们需要用一个http服务器监听一个url.在有客户端访问后,使用websocket.Upgrader{}来将http访问提升为websocket连接.使用
mt, message, err := ws.ReadMessage()来获取消息.返回值的第一项为消息类型,第二项为message体.消息类型包含如下几种:

  • websocket.BinaryMessage字节流数据
  • websocket.TextMessage文本数据
  • websocket.PingMessage保持连接用的消息
  • websocket.PongMessage保持连接用的消息
  • websocket.CloseMessage关闭信号

而发送回去一般使用的ws.WriteMessage(int , []byte) error来发送消息.

因为mt有多种情况,我们一般使用switch来区分.一般为了可读性我们使用websocket.TextMessage结合json来传递信息;
当然go语言天生亲和protobuf,所以也可以使用websocket.BinaryMessage结合protobuf来传递消息.

  • 客户端

package main

import (
	"flag"
	"log"
	"net/url"
	"os"
	"os/signal"
	"time"

	"github.com/gorilla/websocket"
)

var addr = flag.String("addr", "localhost:5000", "http service address")

func main() {
	flag.Parse()
	log.SetFlags(0)

	interrupt := make(chan os.Signal, 1)
	signal.Notify(interrupt, os.Interrupt)

	u := url.URL{Scheme: "ws", Host: *addr, Path: "/helloworld"}
	log.Printf("connecting to %s", u.String())

	ws, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
	if err != nil {
		log.Fatal("dial:", err)
	}
	defer ws.Close()

	done := make(chan struct{})
	// open后就发送
	err = ws.WriteMessage(websocket.TextMessage, []byte("helloworld"))
	if err != nil {
		log.Println("write:", err)
	}

	go func() {
		defer close(done)
		for {
			mt, message, err := ws.ReadMessage()
			if err != nil {
				log.Println("read:", err)
				return
			} else {
				switch mt {
				case websocket.CloseMessage:
					{
						log.Println("disconnected")
						return
					}
				case websocket.PingMessage, websocket.PongMessage:
					{
						log.Println("get ping pong")
					}
				case websocket.TextMessage:
					{
						msg := string(message)
						log.Printf("recv: %s", msg)
						return
					}
				case websocket.BinaryMessage:
					{
						log.Println("not support Binary now")
						return
					}
				default:
					{
						log.Println("not support now")
						return
					}
				}
			}
		}
	}()

	for {
		select {
		case <-done:
			return

		case <-interrupt:
			log.Println("interrupt")

			// Cleanly close the connection by sending a close message and then
			// waiting (with timeout) for the server to close the connection.
			err := ws.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
			if err != nil {
				log.Println("write close:", err)
				return
			}
			select {
			case <-done:
			case <-time.After(time.Second):
			}
			return
		}
	}
}

客户端部分与上面也类似.区分好消息类型就行

比较坑的是ws.WriteMessage输入和ws.ReadMessage()都是一个[]byte类型的数据.因此即便是websocket.TextMessage类型的数据也要做好类型转换.

package main

import (
	"flag"
	"log"
	"os"
	"syscall"
	"time"

	"github.com/pytool/chaos/beat"
	"github.com/sevlyar/go-daemon"
)

var (
	signal = flag.String("s", "", `send signal to the daemon
		quit — graceful shutdown
		stop — fast shutdown
		reload — reloading the configuration file`)
)

const APPNAME = "snap"

func Daemon() {
	flag.Parse()
	daemon.AddCommand(daemon.StringFlag(signal, "quit"), syscall.SIGQUIT, termHandler)
	daemon.AddCommand(daemon.StringFlag(signal, "stop"), syscall.SIGTERM, termHandler)
	daemon.AddCommand(daemon.StringFlag(signal, "reload"), syscall.SIGHUP, reloadHandler)
	os.Mkdir("/tmp/"+APPNAME, os.ModePerm)
	cntxt := &daemon.Context{
		PidFileName: "/tmp/" + APPNAME + "/" + APPNAME + ".pid",
		PidFilePerm: 0644,
		LogFileName: "/tmp/" + APPNAME + "/" + APPNAME + ".log",
		LogFilePerm: 0640,
		WorkDir:     "./",
		Umask:       027,
		Args:        []string{"[snap-daemon]"},
	}

	if len(daemon.ActiveFlags()) > 0 {
		d, err := cntxt.Search()
		if err != nil {
			log.Fatalln("Unable send signal to the daemon:", err)
		}
		daemon.SendCommands(d)
		return
	}

	d, err := cntxt.Reborn()
	if err != nil {
		log.Fatalln(err)
	}
	if d != nil {
		return
	}
	defer cntxt.Release()

	log.Println("- - - - - - - - - - - - - - -")
	log.Println("daemon started")

	go worker()

	err = daemon.ServeSignals()
	if err != nil {
		log.Println("Error:", err)
	}
	log.Println("daemon terminated")
}

var (
	stop = make(chan struct{})
	done = make(chan struct{})
)

func worker() {

	// go app.Run()
	cancel := beat.Websocket()

	for {

		log.Println("loop")
		time.Sleep(time.Second)
		// 接收 控制台退出信号
		if _, ok := <-stop; ok {
			log.Println("recv ch stop")
			break
		}
	}
	cancel()
	log.Println("done")
	// 等待完全退出
	done <- struct{}{}
}

func termHandler(sig os.Signal) error {
	log.Println("terminating...")
	stop <- struct{}{}
	if sig == syscall.SIGQUIT {
		<-done
	}
	return daemon.ErrStop
}

func reloadHandler(sig os.Signal) error {
	log.Println("configuration reloaded")

	return nil
}
`