├── EXPERIMENTAL ├── .gitignore ├── example ├── Makefile ├── example.go └── www │ ├── json.js │ └── index.html ├── .gitmodules ├── util.go ├── servemux.go ├── codec.go ├── session.go ├── LICENSE ├── config.go ├── message.go ├── transport.go ├── transport_flashsocket.go ├── doc.go ├── transport_jsonppolling.go ├── transport_xhrpolling.go ├── transport_xhrmultipart.go ├── transport_websocket.go ├── transport_htmlfile.go ├── client.go ├── socketio_test.go ├── README.md ├── codec_sio_test.go ├── codec_siostreaming.go ├── codec_siostreaming_test.go ├── codec_sio.go ├── connection.go └── socketio.go /EXPERIMENTAL: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | example/example 2 | *.o 3 | *.a 4 | *.[568vq] 5 | [568vq].out 6 | _testmain.go 7 | -------------------------------------------------------------------------------- /example/Makefile: -------------------------------------------------------------------------------- 1 | include $(GOROOT)/src/Make.inc 2 | 3 | TARG = example 4 | GOFILES = example.go 5 | 6 | include $(GOROOT)/src/Make.cmd 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "example/www/vendor/socket.io-client"] 2 | path = example/www/vendor/socket.io-client 3 | url = git://github.com/LearnBoost/socket.io-client 4 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | package socketio 2 | 3 | import ( 4 | "log" 5 | "os" 6 | ) 7 | 8 | type nopWriter struct{} 9 | 10 | func (nw nopWriter) Write(p []byte) (n int, err error) { 11 | return len(p), nil 12 | } 13 | 14 | var ( 15 | NOPLogger = log.New(nopWriter{}, "", 0) 16 | DefaultLogger = log.New(os.Stdout, "", log.Ldate|log.Ltime) 17 | ) 18 | -------------------------------------------------------------------------------- /servemux.go: -------------------------------------------------------------------------------- 1 | package socketio 2 | 3 | import ( 4 | "net/http" 5 | "strings" 6 | ) 7 | 8 | type ServeMux struct { 9 | *http.ServeMux 10 | sio *SocketIO 11 | } 12 | 13 | func NewServeMux(sio *SocketIO) *ServeMux { 14 | return &ServeMux{http.NewServeMux(), sio} 15 | } 16 | 17 | func (mux *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { 18 | if strings.HasPrefix(r.URL.Path, mux.sio.config.Resource) { 19 | rest := r.URL.Path[len(mux.sio.config.Resource):] 20 | n := strings.Index(rest, "/") 21 | if n < 0 { 22 | n = len(rest) 23 | } 24 | if t, ok := mux.sio.transportLookup[rest[:n]]; ok { 25 | mux.sio.handle(t, w, r) 26 | return 27 | } 28 | } 29 | 30 | mux.ServeMux.ServeHTTP(w, r) 31 | } 32 | -------------------------------------------------------------------------------- /codec.go: -------------------------------------------------------------------------------- 1 | package socketio 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | ) 8 | 9 | var ( 10 | ErrMalformedPayload = errors.New("malformed payload") 11 | ) 12 | 13 | // A Codec wraps Encode and Decode methods. 14 | // 15 | // Encode takes an interface{}, encodes it and writes it to the given io.Writer. 16 | // Decode takes a slice of bytes and decodes them into messages. If the given payload 17 | // can't be decoded, an ErrMalformedPayload error will be returned. 18 | type Codec interface { 19 | NewEncoder() Encoder 20 | NewDecoder(*bytes.Buffer) Decoder 21 | } 22 | 23 | type Decoder interface { 24 | Decode() ([]Message, error) 25 | Reset() 26 | } 27 | 28 | type Encoder interface { 29 | Encode(io.Writer, interface{}) error 30 | } 31 | -------------------------------------------------------------------------------- /session.go: -------------------------------------------------------------------------------- 1 | package socketio 2 | 3 | import ( 4 | "crypto/rand" 5 | "io" 6 | ) 7 | 8 | // SessionID is just a string for now. 9 | type SessionID string 10 | 11 | const ( 12 | // Length of the session ids. 13 | SessionIDLength = 16 14 | 15 | // Charset from which to build the session ids. 16 | SessionIDCharset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" 17 | ) 18 | 19 | // NewSessionID creates a new ~random session id that is SessionIDLength long and 20 | // consists of random characters from the SessionIDCharset. 21 | func NewSessionID() (sid SessionID, err error) { 22 | b := make([]byte, SessionIDLength) 23 | 24 | if _, err = io.ReadFull(rand.Reader, b); err != nil { 25 | return 26 | } 27 | 28 | for i := 0; i < SessionIDLength; i++ { 29 | b[i] = SessionIDCharset[b[i]%uint8(len(SessionIDCharset))] 30 | } 31 | 32 | sid = SessionID(b) 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2010 Jukka-Pekka Kekkonen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the 'Software'), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY , 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package socketio 2 | 3 | import ( 4 | "log" 5 | "time" 6 | ) 7 | 8 | // Config represents a set of configurable settings used by the server 9 | type Config struct { 10 | // Maximum number of connections. 11 | MaxConnections int 12 | 13 | // Maximum amount of messages to store for a connection. If a connection 14 | // has QueueLength amount of undelivered messages, the following Sends will 15 | // return ErrQueueFull error. 16 | QueueLength int 17 | 18 | // The size of the read buffer in bytes. 19 | ReadBufferSize int 20 | 21 | // The interval between heartbeats 22 | HeartbeatInterval time.Duration 23 | 24 | // Period in ns during which the client must reconnect or it is considered 25 | // disconnected. 26 | ReconnectTimeout time.Duration 27 | 28 | // Origins to allow for cross-domain requests. 29 | // For example: ["localhost:8080", "myblog.com:*"]. 30 | Origins []string 31 | 32 | // Transports to use. 33 | Transports []Transport 34 | 35 | // Codec to use. 36 | Codec Codec 37 | 38 | // The resource to bind to, e.g. /socket.io/ 39 | Resource string 40 | 41 | // Logger to use. 42 | Logger *log.Logger 43 | } 44 | 45 | var DefaultConfig = Config{ 46 | MaxConnections: 0, 47 | QueueLength: 10, 48 | ReadBufferSize: 2048, 49 | HeartbeatInterval: 10e9, 50 | ReconnectTimeout: 10e9, 51 | Origins: nil, 52 | Transports: DefaultTransports, 53 | Codec: SIOCodec{}, 54 | Resource: "/socket.io/", 55 | Logger: DefaultLogger, 56 | } 57 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package socketio 2 | 3 | // The different message types that are available. 4 | const ( 5 | // MessageText is interpreted just as a string. 6 | MessageText = iota 7 | 8 | // MessageJSON is interpreted as a JSON encoded string. 9 | MessageJSON 10 | 11 | // MessageHeartbeat is interpreted as a heartbeat. 12 | MessageHeartbeat 13 | 14 | // MessageHeartbeat is interpreted as a heartbeat. 15 | MessageHandshake 16 | 17 | // MessageDisconnect is interpreted as a forced disconnection. 18 | MessageDisconnect 19 | ) 20 | 21 | // Heartbeat is a server-invoked keep-alive strategy, where 22 | // the server sends an integer to the client and the client 23 | // must respond with the same value during some short period. 24 | type heartbeat int 25 | 26 | // Disconnect is a message that indicates a forced disconnection. 27 | type disconnect int 28 | 29 | // Handshake is the first message that is going to be sent to the 30 | // client when it first connects. It is made of the server-generated 31 | // session id. 32 | type handshake string 33 | 34 | // Message wraps heartbeat, messageType and data methods. 35 | // 36 | // Heartbeat returns the heartbeat value encapsulated in the message and an true 37 | // or if the message does not encapsulate a heartbeat a false is returned. 38 | // MessageType returns messageText, messageHeartbeat or messageJSON. 39 | // Data returns the raw (full) message received. 40 | type Message interface { 41 | heartbeat() (heartbeat, bool) 42 | 43 | Annotations() map[string]string 44 | Annotation(string) (string, bool) 45 | Data() string 46 | Bytes() []byte 47 | Type() uint8 48 | JSON() ([]byte, bool) 49 | } 50 | -------------------------------------------------------------------------------- /example/example.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "container/vector" 5 | "http" 6 | "log" 7 | "socketio" 8 | "sync" 9 | ) 10 | 11 | type Announcement struct { 12 | Announcement string `json:"announcement"` 13 | } 14 | 15 | type Buffer struct { 16 | Buffer []interface{} `json:"buffer"` 17 | } 18 | 19 | type Message struct { 20 | Message []string `json:"message"` 21 | } 22 | 23 | // A very simple chat server 24 | func main() { 25 | buffer := new(vector.Vector) 26 | mutex := new(sync.Mutex) 27 | 28 | // create the socket.io server and mux it to /socket.io/ 29 | config := socketio.DefaultConfig 30 | config.Origins = []string{"localhost:8080"} 31 | sio := socketio.NewSocketIO(&config) 32 | 33 | go func() { 34 | if err := sio.ListenAndServeFlashPolicy(":843"); err != nil { 35 | log.Println(err) 36 | } 37 | }() 38 | 39 | // when a client connects - send it the buffer and broadcasta an announcement 40 | sio.OnConnect(func(c *socketio.Conn) { 41 | mutex.Lock() 42 | c.Send(Buffer{buffer.Copy()}) 43 | mutex.Unlock() 44 | sio.Broadcast(Announcement{"connected: " + c.String()}) 45 | }) 46 | 47 | // when a client disconnects - send an announcement 48 | sio.OnDisconnect(func(c *socketio.Conn) { 49 | sio.Broadcast(Announcement{"disconnected: " + c.String()}) 50 | }) 51 | 52 | // when a client send a message - broadcast and store it 53 | sio.OnMessage(func(c *socketio.Conn, msg socketio.Message) { 54 | payload := Message{[]string{c.String(), msg.Data()}} 55 | mutex.Lock() 56 | buffer.Push(payload) 57 | mutex.Unlock() 58 | sio.Broadcast(payload) 59 | }) 60 | 61 | log.Println("Server starting. Tune your browser to http://localhost:8080/") 62 | 63 | mux := sio.ServeMux() 64 | mux.Handle("/", http.FileServer(http.Dir("www/"))) 65 | 66 | if err := http.ListenAndServe(":8080", mux); err != nil { 67 | log.Fatal("ListenAndServe:", err) 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /transport.go: -------------------------------------------------------------------------------- 1 | package socketio 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | ) 9 | 10 | var ( 11 | // ErrNotConnected is used when some action required the connection to be online, 12 | // but it wasn't. 13 | ErrNotConnected = errors.New("not connected") 14 | 15 | // ErrConnected is used when some action required the connection to be offline, 16 | // but it wasn't. 17 | ErrConnected = errors.New("already connected") 18 | 19 | emptyResponse = []byte{} 20 | okResponse = []byte("ok") 21 | ) 22 | 23 | // DefaultTransports holds the defaults 24 | var DefaultTransports = []Transport{ 25 | NewXHRPollingTransport(10e9, 5e9), 26 | NewXHRMultipartTransport(0, 5e9), 27 | NewWebsocketTransport(0, 5e9), 28 | NewHTMLFileTransport(0, 5e9), 29 | NewFlashsocketTransport(0, 5e9), 30 | NewJSONPPollingTransport(0, 5e9), 31 | } 32 | 33 | // Transport is the interface that wraps the Resource and newSocket methods. 34 | // 35 | // Resource returns the resource name of the transport, e.g. "websocket". 36 | // NewSocket creates a new socket that embeds the corresponding transport 37 | // mechanisms. 38 | type Transport interface { 39 | Resource() string 40 | newSocket() socket 41 | } 42 | 43 | // Socket is the interface that wraps the basic Read, Write, Close and String 44 | // methods. Additionally it has Transport and accept methods. 45 | // 46 | // Transport returns the Transport that created this socket. 47 | // Accept takes the http.ResponseWriter / http.Request -pair from a http handler 48 | // and hijacks the connection for itself. The third parameter is a function callback 49 | // that will be invoked when the connection has been succesfully hijacked and the socket 50 | // is ready to be used. 51 | type socket interface { 52 | io.ReadWriteCloser 53 | fmt.Stringer 54 | 55 | Transport() Transport 56 | accept(http.ResponseWriter, *http.Request, func()) error 57 | } 58 | -------------------------------------------------------------------------------- /example/www/json.js: -------------------------------------------------------------------------------- 1 | if(!this.JSON){JSON=function(){function f(n){return n<10?'0'+n:n;} 2 | Date.prototype.toJSON=function(){return this.getUTCFullYear()+'-'+ 3 | f(this.getUTCMonth()+1)+'-'+ 4 | f(this.getUTCDate())+'T'+ 5 | f(this.getUTCHours())+':'+ 6 | f(this.getUTCMinutes())+':'+ 7 | f(this.getUTCSeconds())+'Z';};var m={'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','"':'\\"','\\':'\\\\'};function stringify(value,whitelist){var a,i,k,l,r=/["\\\x00-\x1f\x7f-\x9f]/g,v;switch(typeof value){case'string':return r.test(value)?'"'+value.replace(r,function(a){var c=m[a];if(c){return c;} 8 | c=a.charCodeAt();return'\\u00'+Math.floor(c/16).toString(16)+ 9 | (c%16).toString(16);})+'"':'"'+value+'"';case'number':return isFinite(value)?String(value):'null';case'boolean':case'null':return String(value);case'object':if(!value){return'null';} 10 | if(typeof value.toJSON==='function'){return stringify(value.toJSON());} 11 | a=[];if(typeof value.length==='number'&&!(value.propertyIsEnumerable('length'))){l=value.length;for(i=0;iparent.s._(%s, document);", jp) 109 | return fmt.Fprintf(s.rwc, "%x\r\n%s\r\n", buf.Len(), buf.String()) 110 | } 111 | 112 | func (s *htmlfileSocket) Close() error { 113 | if !s.connected { 114 | return ErrNotConnected 115 | } 116 | 117 | s.connected = false 118 | return s.rwc.Close() 119 | } 120 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package socketio 2 | 3 | import ( 4 | "bytes" 5 | "code.google.com/p/go.net/websocket" 6 | "errors" 7 | "io" 8 | "strconv" 9 | ) 10 | 11 | // Client is a toy interface. 12 | type Client interface { 13 | io.Closer 14 | 15 | Dial(string, string) error 16 | Send(interface{}) error 17 | OnDisconnect(func()) 18 | OnMessage(func(Message)) 19 | SessionID() SessionID 20 | } 21 | 22 | // WebsocketClient is a toy that implements the Client interface. 23 | type WebsocketClient struct { 24 | connected bool 25 | enc Encoder 26 | dec Decoder 27 | decBuf bytes.Buffer 28 | codec Codec 29 | sessionid SessionID 30 | ws *websocket.Conn 31 | onDisconnect func() 32 | onMessage func(Message) 33 | } 34 | 35 | func NewWebsocketClient(codec Codec) (wc *WebsocketClient) { 36 | wc = &WebsocketClient{enc: codec.NewEncoder(), codec: codec} 37 | wc.dec = codec.NewDecoder(&wc.decBuf) 38 | return 39 | } 40 | 41 | func (wc *WebsocketClient) Dial(rawurl string, origin string) (err error) { 42 | var messages []Message 43 | var nr int 44 | 45 | if wc.connected { 46 | return ErrConnected 47 | } 48 | 49 | if wc.ws, err = websocket.Dial(rawurl, "", origin); err != nil { 50 | return 51 | } 52 | 53 | // read handshake 54 | buf := make([]byte, 2048) 55 | if nr, err = wc.ws.Read(buf); err != nil { 56 | wc.ws.Close() 57 | return errors.New("Dial: " + err.Error()) 58 | } 59 | wc.decBuf.Write(buf[0:nr]) 60 | 61 | if messages, err = wc.dec.Decode(); err != nil { 62 | wc.ws.Close() 63 | return errors.New("Dial: " + err.Error()) 64 | } 65 | 66 | if len(messages) != 1 { 67 | wc.ws.Close() 68 | return errors.New("Dial: expected exactly 1 message, but got " + strconv.Itoa(len(messages))) 69 | } 70 | 71 | // TODO: Fix me: The original Socket.IO codec does not have a special encoding for handshake 72 | // so we should just assume that the first message is the handshake. 73 | // The old codec should be gone pretty soon (waiting for 0.7 release) so this might suffice 74 | // until then. 75 | if _, ok := wc.codec.(SIOCodec); !ok { 76 | if messages[0].Type() != MessageHandshake { 77 | wc.ws.Close() 78 | return errors.New("Dial: expected handshake, but got " + messages[0].Data()) 79 | } 80 | } 81 | 82 | wc.sessionid = SessionID(messages[0].Data()) 83 | if wc.sessionid == "" { 84 | wc.ws.Close() 85 | return errors.New("Dial: received empty sessionid") 86 | } 87 | 88 | wc.connected = true 89 | 90 | go wc.reader() 91 | return 92 | } 93 | 94 | func (wc *WebsocketClient) SessionID() SessionID { 95 | return wc.sessionid 96 | } 97 | 98 | func (wc *WebsocketClient) reader() { 99 | var err error 100 | var nr int 101 | var messages []Message 102 | buf := make([]byte, 2048) 103 | 104 | defer wc.Close() 105 | 106 | for { 107 | if nr, err = wc.ws.Read(buf); err != nil { 108 | return 109 | } 110 | if nr > 0 { 111 | wc.decBuf.Write(buf[0:nr]) 112 | if messages, err = wc.dec.Decode(); err != nil { 113 | return 114 | } 115 | 116 | for _, msg := range messages { 117 | if hb, ok := msg.heartbeat(); ok { 118 | if err = wc.Send(heartbeat(hb)); err != nil { 119 | return 120 | } 121 | } else if wc.onMessage != nil { 122 | wc.onMessage(msg) 123 | } 124 | } 125 | } 126 | } 127 | } 128 | 129 | func (wc *WebsocketClient) OnDisconnect(f func()) { 130 | wc.onDisconnect = f 131 | } 132 | func (wc *WebsocketClient) OnMessage(f func(Message)) { 133 | wc.onMessage = f 134 | } 135 | 136 | func (wc *WebsocketClient) Send(payload interface{}) error { 137 | if wc.ws == nil { 138 | return ErrNotConnected 139 | } 140 | 141 | return wc.enc.Encode(wc.ws, payload) 142 | } 143 | 144 | func (wc *WebsocketClient) Close() error { 145 | if !wc.connected { 146 | return ErrNotConnected 147 | } 148 | wc.connected = false 149 | 150 | if wc.onDisconnect != nil { 151 | wc.onDisconnect() 152 | } 153 | 154 | return wc.ws.Close() 155 | } 156 | -------------------------------------------------------------------------------- /socketio_test.go: -------------------------------------------------------------------------------- 1 | package socketio 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | "time" 8 | ) 9 | 10 | const ( 11 | serverAddr = "127.0.0.1:6070" 12 | 13 | eventConnect = iota 14 | eventDisconnect 15 | eventMessage 16 | eventCrash 17 | ) 18 | 19 | var events chan *event 20 | var server *SocketIO 21 | 22 | type event struct { 23 | conn *Conn 24 | eventType uint8 25 | msg Message 26 | } 27 | 28 | func echoServer(addr string, config *Config) <-chan *event { 29 | events := make(chan *event) 30 | 31 | server = NewSocketIO(config) 32 | server.OnConnect(func(c *Conn) { 33 | events <- &event{c, eventConnect, nil} 34 | }) 35 | server.OnDisconnect(func(c *Conn) { 36 | events <- &event{c, eventDisconnect, nil} 37 | }) 38 | server.OnMessage(func(c *Conn, msg Message) { 39 | if err := c.Send(msg.Data()); err != nil { 40 | fmt.Println("server echo send error: ", err) 41 | } 42 | events <- &event{c, eventMessage, msg} 43 | }) 44 | go func() { 45 | http.ListenAndServe(addr, server.ServeMux()) 46 | events <- &event{nil, eventCrash, nil} 47 | }() 48 | 49 | return events 50 | } 51 | 52 | func TestWebsocket(t *testing.T) { 53 | finished := make(chan bool, 1) 54 | clientMessage := make(chan Message) 55 | clientDisconnect := make(chan bool) 56 | numMessages := 313 57 | 58 | config := DefaultConfig 59 | config.QueueLength = numMessages * 2 60 | config.Codec = SIOStreamingCodec{} 61 | config.Origins = []string{serverAddr} 62 | serverEvents := echoServer(serverAddr, &config) 63 | 64 | client := NewWebsocketClient(SIOStreamingCodec{}) 65 | client.OnMessage(func(msg Message) { 66 | clientMessage <- msg 67 | }) 68 | client.OnDisconnect(func() { 69 | clientDisconnect <- true 70 | }) 71 | 72 | time.Sleep(1e9) 73 | /* 74 | go func() { 75 | time.Sleep(5e9) 76 | if _, ok := <-finished; !ok { 77 | t.Fatalf("timeout") 78 | } 79 | }()*/ 80 | 81 | err := client.Dial("ws://"+serverAddr+"/socket.io/websocket", "http://"+serverAddr+"/") 82 | if err != nil { 83 | t.Fatal(err) 84 | } 85 | 86 | // expect connection 87 | serverEvent := <-serverEvents 88 | if serverEvent.eventType != eventConnect || serverEvent.conn.sessionid != client.SessionID() { 89 | t.Fatalf("Expected eventConnect but got %v", serverEvent) 90 | } 91 | 92 | iook := make(chan bool) 93 | 94 | go func() { 95 | for i := 0; i < numMessages; i++ { 96 | if err = client.Send(i); err != nil { 97 | t.Fatal("Send:", err) 98 | } 99 | } 100 | iook <- true 101 | }() 102 | 103 | go func() { 104 | for i := 0; i < numMessages; i++ { 105 | serverEvent = <-serverEvents 106 | t.Logf("Server event %q", serverEvent) 107 | 108 | expect := fmt.Sprintf("%d", i) 109 | if serverEvent.eventType != eventMessage || serverEvent.conn.sessionid != client.SessionID() { 110 | t.Fatalf("Expected eventMessage but got %#v", serverEvent) 111 | } 112 | if serverEvent.msg.Data() != expect { 113 | t.Fatalf("Server expected %s but received %s", expect, serverEvent.msg.Data()) 114 | } else { 115 | t.Logf("Server received %s", serverEvent.msg.Data()) 116 | } 117 | } 118 | iook <- true 119 | }() 120 | 121 | go func() { 122 | for i := 0; i < numMessages; i++ { 123 | msg := <-clientMessage 124 | t.Log("Client received", msg.Data()) 125 | 126 | expect := fmt.Sprintf("%d", i) 127 | if msg.Data() != expect { 128 | t.Fatalf("Client expected %s but received %s", expect, msg.Data()) 129 | } 130 | } 131 | iook <- true 132 | }() 133 | 134 | for i := 0; i < 3; i++ { 135 | <-iook 136 | } 137 | 138 | go func() { 139 | if err = client.Close(); err != nil { 140 | t.Fatal("Close:", err) 141 | } 142 | }() 143 | 144 | t.Log("Waiting for client disconnect") 145 | <-clientDisconnect 146 | 147 | t.Log("Waiting for server disconnect") 148 | serverEvent = <-serverEvents 149 | if serverEvent.eventType != eventDisconnect || serverEvent.conn.sessionid != client.SessionID() { 150 | t.Fatalf("Expected disconnect event, but got %q", serverEvent) 151 | } 152 | 153 | finished <- true 154 | } 155 | -------------------------------------------------------------------------------- /example/www/index.html: -------------------------------------------------------------------------------- 1 | 2 | 33 | 34 | 35 | socket.io client test 36 | 37 | 38 | 39 | 40 | 41 | 42 | 74 | 75 |

Sample chat client

76 |

(Loaned from the LearnBoost's Socket.IO-node project)

77 |

Connecting...

78 |
79 | 80 |
81 | 82 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-socket.io 2 | ============ 3 | 4 | The `socketio` package is a simple abstraction layer for different web browser- 5 | supported transport mechanisms. It is fully compatible with the 6 | [Socket.IO client](http://github.com/LearnBoost/Socket.IO) (version 0.6) JavaScript-library by 7 | [LearnBoost Labs](http://socket.io/). By writing custom codecs the `socketio` 8 | could be perhaps used with other clients, too. 9 | 10 | It provides an easy way for developers to rapidly prototype with the most 11 | popular browser transport mechanism today: 12 | 13 | - [HTML5 WebSockets](http://dev.w3.org/html5/websockets/) 14 | - [Adobe® Flash® Sockets](https://github.com/gimite/web-socket-js) 15 | - [JSONP Long Polling](http://en.wikipedia.org/wiki/JSONP#JSONP) 16 | - [XHR Long Polling](http://en.wikipedia.org/wiki/Comet_%28programming%29#XMLHttpRequest_long_polling) 17 | - [XHR Multipart Streaming](http://en.wikipedia.org/wiki/Comet_%28programming%29#XMLHttpRequest) 18 | - [ActiveX HTMLFile](http://cometdaily.com/2007/10/25/http-streaming-and-internet-explorer/) 19 | 20 | ## Compatibility with Socket.IO 0.7-> 21 | 22 | **Go-socket.io is currently compatible with Socket.IO 0.6 clients only.** 23 | 24 | ## Demo 25 | 26 | [The Paper Experiment](http://wall-r.com/paper) 27 | 28 | ## Crash course 29 | 30 | The `socketio` package works hand-in-hand with the standard `http` package (by 31 | plugging itself into `http.ServeMux`) and hence it doesn't need a 32 | full network port for itself. It has an callback-style event handling API. The 33 | callbacks are: 34 | 35 | - *SocketIO.OnConnect* 36 | - *SocketIO.OnDisconnect* 37 | - *SocketIO.OnMessage* 38 | 39 | Other utility-methods include: 40 | 41 | - *SocketIO.ServeMux* 42 | - *SocketIO.Broadcast* 43 | - *SocketIO.BroadcastExcept* 44 | - *SocketIO.GetConn* 45 | 46 | Each new connection will be automatically assigned an session id and 47 | using those the clients can reconnect without losing messages: the server 48 | persists clients' pending messages (until some configurable point) if they can't 49 | be immediately delivered. All writes are by design asynchronous and can be made 50 | through `Conn.Send`. The server also abstracts handshaking and various keep-alive mechanisms. 51 | 52 | Finally, the actual format on the wire is described by a separate `Codec`. The 53 | default bundled codecs, `SIOCodec` and `SIOStreamingCodec` are fully compatible 54 | with the LearnBoost's [Socket.IO client](http://github.com/LearnBoost/Socket.IO) 55 | (master and development branches). 56 | 57 | ## Example: A simple chat server 58 | 59 | package main 60 | 61 | import ( 62 | "http" 63 | "log" 64 | "socketio" 65 | ) 66 | 67 | func main() { 68 | sio := socketio.NewSocketIO(nil) 69 | 70 | sio.OnConnect(func(c *socketio.Conn) { 71 | sio.Broadcast(struct{ announcement string }{"connected: " + c.String()}) 72 | }) 73 | 74 | sio.OnDisconnect(func(c *socketio.Conn) { 75 | sio.BroadcastExcept(c, 76 | struct{ announcement string }{"disconnected: " + c.String()}) 77 | }) 78 | 79 | sio.OnMessage(func(c *socketio.Conn, msg socketio.Message) { 80 | sio.BroadcastExcept(c, 81 | struct{ message []string }{[]string{c.String(), msg.Data()}}) 82 | }) 83 | 84 | mux := sio.ServeMux() 85 | mux.Handle("/", http.FileServer("www/", "/")) 86 | 87 | if err := http.ListenAndServe(":8080", mux); err != nil { 88 | log.Fatal("ListenAndServe:", err) 89 | } 90 | } 91 | 92 | ## tl;dr 93 | 94 | You can get the code and run the bundled example by following these steps: 95 | 96 | $ git clone git://github.com/madari/go-socket.io.git 97 | $ cd go-socket.io 98 | $ git submodule update --init --recursive 99 | $ make install 100 | $ cd example 101 | $ make 102 | $ ./example 103 | 104 | ## License 105 | 106 | (The MIT License) 107 | 108 | Copyright (c) 2011 Jukka-Pekka Kekkonen <karatepekka@gmail.com> 109 | 110 | Permission is hereby granted, free of charge, to any person obtaining 111 | a copy of this software and associated documentation files (the 112 | 'Software'), to deal in the Software without restriction, including 113 | without limitation the rights to use, copy, modify, merge, publish, 114 | distribute, sublicense, and/or sell copies of the Software, and to 115 | permit persons to whom the Software is furnished to do so, subject to 116 | the following conditions: 117 | 118 | The above copyright notice and this permission notice shall be 119 | included in all copies or substantial portions of the Software. 120 | 121 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 122 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 123 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 124 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 125 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 126 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 127 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 128 | -------------------------------------------------------------------------------- /codec_sio_test.go: -------------------------------------------------------------------------------- 1 | package socketio 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | "unicode/utf8" 8 | ) 9 | 10 | func frame(data string, json bool) string { 11 | if json { 12 | return fmt.Sprintf("~m~%d~m~~j~%s", 3+utf8.RuneCountInString(data), data) 13 | } 14 | return fmt.Sprintf("~m~%d~m~%s", utf8.RuneCountInString(data), data) 15 | } 16 | 17 | type encodeTest struct { 18 | in interface{} 19 | out string 20 | } 21 | 22 | var encodeTests = []encodeTest{ 23 | { 24 | 123, 25 | frame("123", false), 26 | }, 27 | { 28 | "hello, world", 29 | frame("hello, world", false), 30 | }, 31 | { 32 | "öäö¥£♥", 33 | frame("öäö¥£♥", false), 34 | }, 35 | { 36 | "öäö¥£♥", 37 | frame("öäö¥£♥", false), 38 | }, 39 | { 40 | heartbeat(123456), 41 | frame("~h~123456", false), 42 | }, 43 | { 44 | handshake("abcdefg"), 45 | frame("abcdefg", false), 46 | }, 47 | { 48 | true, 49 | frame("true", true), 50 | }, 51 | { 52 | struct { 53 | Boolean bool 54 | Str string 55 | Array []int 56 | }{ 57 | false, 58 | "string♥", 59 | []int{1, 2, 3, 4}, 60 | }, 61 | frame(`{"Boolean":false,"Str":"string♥","Array":[1,2,3,4]}`, true), 62 | }, 63 | { 64 | []byte("hello, world"), 65 | frame("hello, world", false), 66 | }, 67 | } 68 | 69 | type decodeTestMessage struct { 70 | messageType uint8 71 | data string 72 | heartbeat heartbeat 73 | } 74 | 75 | type decodeTest struct { 76 | in string 77 | out []decodeTestMessage 78 | } 79 | 80 | // NOTE: if you change these -> adjust the benchmarks 81 | var decodeTests = []decodeTest{ 82 | { 83 | frame("", false), 84 | []decodeTestMessage{{MessageText, "", -1}}, 85 | }, 86 | { 87 | frame("~h~123", false), 88 | []decodeTestMessage{{MessageHeartbeat, "123", 123}}, 89 | }, 90 | { 91 | frame("wadap!", false), 92 | []decodeTestMessage{{MessageText, "wadap!", -1}}, 93 | }, 94 | { 95 | frame("♥wadap!", true), 96 | []decodeTestMessage{{MessageJSON, "♥wadap!", -1}}, 97 | }, 98 | { 99 | frame("hello, world!", true) + frame("~h~313", false) + frame("♥wadap!", false), 100 | []decodeTestMessage{ 101 | {MessageJSON, "hello, world!", -1}, 102 | {MessageHeartbeat, "313", 313}, 103 | {MessageText, "♥wadap!", -1}, 104 | }, 105 | }, 106 | { 107 | "1:3::fael!,", 108 | nil, 109 | }, 110 | { 111 | frame("wadap!", false), 112 | []decodeTestMessage{{MessageText, "wadap!", -1}}, 113 | }, 114 | } 115 | 116 | func TestEncode(t *testing.T) { 117 | codec := SIOCodec{} 118 | enc := codec.NewEncoder() 119 | buf := new(bytes.Buffer) 120 | 121 | for _, test := range encodeTests { 122 | t.Logf("in=%v out=%s", test.in, test.out) 123 | 124 | buf.Reset() 125 | if err := enc.Encode(buf, test.in); err != nil { 126 | t.Fatal("Encode:", err) 127 | } 128 | if string(buf.Bytes()) != test.out { 129 | t.Fatalf("Expected %q but got %q from %q", test.out, string(buf.Bytes()), test.in) 130 | } 131 | } 132 | } 133 | 134 | func TestDecode(t *testing.T) { 135 | codec := SIOCodec{} 136 | buf := new(bytes.Buffer) 137 | dec := codec.NewDecoder(buf) 138 | var messages []Message 139 | var err error 140 | 141 | for _, test := range decodeTests { 142 | t.Logf("in=%s out=%v", test.in, test.out) 143 | 144 | buf.WriteString(test.in) 145 | if messages, err = dec.Decode(); err != nil { 146 | if test.out == nil { 147 | continue 148 | } 149 | t.Fatal("Decode:", err) 150 | } 151 | if test.out == nil { 152 | t.Fatalf("Expected decode error, but got: %v, %v", messages, err) 153 | } 154 | if len(messages) != len(test.out) { 155 | t.Fatalf("Expected %d messages, but got %d", len(test.out), len(messages)) 156 | } 157 | for i, msg := range messages { 158 | if test.out[i].messageType != msg.Type() { 159 | t.Logf("Message was: %#v", msg) 160 | t.Fatalf("Expected type %d but got %d", test.out[i].messageType, msg.Type()) 161 | } 162 | if test.out[i].data != msg.Data() { 163 | t.Fatalf("Expected data %q but got %q", test.out[i].data, msg.Data()) 164 | } 165 | if test.out[i].messageType == MessageHeartbeat { 166 | if hb, ok := msg.heartbeat(); !ok || test.out[i].heartbeat != hb { 167 | t.Fatalf("Expected heartbeat %d but got %d (%v)", test.out[i].heartbeat, hb, err) 168 | } 169 | } 170 | } 171 | } 172 | } 173 | 174 | func TestDecodeStreaming(t *testing.T) { 175 | var messages []Message 176 | var err error 177 | codec := SIOCodec{} 178 | buf := new(bytes.Buffer) 179 | dec := codec.NewDecoder(buf) 180 | 181 | expectNothing := func(written string) { 182 | if messages, err = dec.Decode(); err != nil || messages == nil || len(messages) != 0 { 183 | t.Fatalf("Partial decode failed after writing %s. err=%#v messages=%#v", written, err, messages) 184 | } 185 | } 186 | 187 | buf.WriteString("~m~") 188 | expectNothing("~m~") 189 | buf.WriteString("6") 190 | expectNothing("~m~6") 191 | buf.WriteString("~m") 192 | expectNothing("~m~6~m") 193 | buf.WriteString("~") 194 | expectNothing("~m~6~m~") 195 | buf.WriteString("12345") 196 | expectNothing("~m~6~m~12345") 197 | buf.WriteString("6~m~") 198 | messages, err = dec.Decode() 199 | if err != nil { 200 | t.Fatalf("Did not expect errors: %s", err) 201 | } 202 | if messages == nil || len(messages) != 1 { 203 | t.Fatalf("Expected 1 message, got: %#v", messages) 204 | } 205 | if messages[0].Type() != MessageText || messages[0].Data() != "123456" { 206 | t.Fatalf("Expected data 123456 and text, got: %#v", messages[0]) 207 | } 208 | } 209 | -------------------------------------------------------------------------------- /codec_siostreaming.go: -------------------------------------------------------------------------------- 1 | package socketio 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "strconv" 10 | "unicode/utf8" 11 | ) 12 | 13 | // SIOStreamingCodec is the codec used by the official Socket.IO client by LearnBoost 14 | // under the development branch. This will be the default codec for 0.7 release. 15 | type SIOStreamingCodec struct{} 16 | 17 | type sioStreamingEncoder struct { 18 | elem bytes.Buffer 19 | } 20 | 21 | func (sc SIOStreamingCodec) NewEncoder() Encoder { 22 | return &sioStreamingEncoder{} 23 | } 24 | 25 | // Encode takes payload, encodes it and writes it to dst. Payload must be one 26 | // of the following: a heartbeat, a handshake, []byte, string, int or anything 27 | // than can be marshalled by the default json package. If payload can't be 28 | // encoded or the writing fails, an error will be returned. 29 | func (enc *sioStreamingEncoder) Encode(dst io.Writer, payload interface{}) (err error) { 30 | enc.elem.Reset() 31 | 32 | switch t := payload.(type) { 33 | case heartbeat: 34 | s := strconv.Itoa(int(t)) 35 | _, err = fmt.Fprintf(dst, "%d:%d:%s,", sioMessageTypeHeartbeat, len(s), s) 36 | 37 | case handshake: 38 | _, err = fmt.Fprintf(dst, "%d:%d:%s,", sioMessageTypeHandshake, len(t), t) 39 | 40 | case []byte: 41 | l := utf8.RuneCount(t) 42 | if l == 0 { 43 | break 44 | } 45 | _, err = fmt.Fprintf(dst, "%d:%d::%s,", sioMessageTypeMessage, 1+l, t) 46 | 47 | case string: 48 | l := utf8.RuneCountInString(t) 49 | if l == 0 { 50 | break 51 | } 52 | _, err = fmt.Fprintf(dst, "%d:%d::%s,", sioMessageTypeMessage, 1+l, t) 53 | 54 | case int: 55 | s := strconv.Itoa(t) 56 | if s == "" { 57 | break 58 | } 59 | _, err = fmt.Fprintf(dst, "%d:%d::%s,", sioMessageTypeMessage, 1+len(s), s) 60 | 61 | default: 62 | data, err := json.Marshal(payload) 63 | if len(data) == 0 || err != nil { 64 | break 65 | } 66 | err = json.Compact(&enc.elem, data) 67 | if err != nil { 68 | break 69 | } 70 | 71 | _, err = fmt.Fprintf(dst, "%d:%d:%s\n:", sioMessageTypeMessage, 2+len(SIOAnnotationJSON)+utf8.RuneCount(enc.elem.Bytes()), SIOAnnotationJSON) 72 | if err == nil { 73 | _, err = enc.elem.WriteTo(dst) 74 | if err == nil { 75 | _, err = dst.Write([]byte{','}) 76 | } 77 | } 78 | } 79 | 80 | return err 81 | } 82 | 83 | const ( 84 | sioStreamingDecodeStateBegin = iota 85 | sioStreamingDecodeStateType 86 | sioStreamingDecodeStateLength 87 | sioStreamingDecodeStateAnnotationKey 88 | sioStreamingDecodeStateAnnotationValue 89 | sioStreamingDecodeStateData 90 | sioStreamingDecodeStateTrailer 91 | ) 92 | 93 | type sioStreamingDecoder struct { 94 | src *bytes.Buffer 95 | buf bytes.Buffer 96 | msg *sioMessage 97 | key, value string 98 | length, state int 99 | } 100 | 101 | func (sc SIOStreamingCodec) NewDecoder(src *bytes.Buffer) Decoder { 102 | return &sioStreamingDecoder{ 103 | src: src, 104 | state: sioStreamingDecodeStateBegin, 105 | } 106 | } 107 | 108 | func (dec *sioStreamingDecoder) Reset() { 109 | dec.buf.Reset() 110 | dec.src.Reset() 111 | dec.msg = nil 112 | dec.state = sioStreamingDecodeStateBegin 113 | dec.key = "" 114 | dec.value = "" 115 | dec.length = 0 116 | } 117 | 118 | func (dec *sioStreamingDecoder) Decode() (messages []Message, err error) { 119 | messages = make([]Message, 0, 1) 120 | var c rune 121 | var typ uint64 122 | 123 | L: 124 | for { 125 | c, _, err = dec.src.ReadRune() 126 | if err != nil { 127 | break 128 | } 129 | 130 | if dec.state == sioStreamingDecodeStateBegin { 131 | dec.msg = &sioMessage{} 132 | dec.state = sioStreamingDecodeStateType 133 | dec.buf.Reset() 134 | } 135 | 136 | switch dec.state { 137 | case sioStreamingDecodeStateType: 138 | if c == ':' { 139 | if typ, err = strconv.ParseUint(dec.buf.String(), 10, 0); err != nil { 140 | dec.Reset() 141 | return nil, err 142 | } 143 | dec.msg.typ = uint8(typ) 144 | dec.buf.Reset() 145 | dec.state = sioStreamingDecodeStateLength 146 | continue 147 | } 148 | 149 | case sioStreamingDecodeStateLength: 150 | if c == ':' { 151 | if dec.length, err = strconv.Atoi(dec.buf.String()); err != nil { 152 | dec.Reset() 153 | return nil, err 154 | } 155 | dec.buf.Reset() 156 | 157 | switch dec.msg.typ { 158 | case sioMessageTypeMessage: 159 | dec.state = sioStreamingDecodeStateAnnotationKey 160 | 161 | case sioMessageTypeDisconnect: 162 | dec.state = sioStreamingDecodeStateTrailer 163 | 164 | default: 165 | dec.state = sioStreamingDecodeStateData 166 | } 167 | 168 | continue 169 | } 170 | 171 | case sioStreamingDecodeStateAnnotationKey: 172 | dec.length-- 173 | 174 | switch c { 175 | case ':': 176 | if dec.buf.Len() == 0 { 177 | dec.state = sioStreamingDecodeStateData 178 | } else { 179 | dec.key = dec.buf.String() 180 | dec.buf.Reset() 181 | dec.state = sioStreamingDecodeStateAnnotationValue 182 | } 183 | 184 | continue 185 | 186 | case '\n': 187 | if dec.buf.Len() == 0 { 188 | dec.Reset() 189 | return nil, errors.New("expecting key, but got...") 190 | } 191 | dec.key = dec.buf.String() 192 | if dec.msg.annotations == nil { 193 | dec.msg.annotations = make(map[string]string) 194 | } 195 | 196 | dec.msg.annotations[dec.key] = "" 197 | dec.buf.Reset() 198 | 199 | continue 200 | } 201 | 202 | case sioStreamingDecodeStateAnnotationValue: 203 | dec.length-- 204 | 205 | if c == '\n' || c == ':' { 206 | dec.value = dec.buf.String() 207 | if dec.msg.annotations == nil { 208 | dec.msg.annotations = make(map[string]string) 209 | } 210 | 211 | dec.msg.annotations[dec.key] = dec.value 212 | dec.buf.Reset() 213 | 214 | if c == '\n' { 215 | dec.state = sioStreamingDecodeStateAnnotationKey 216 | } else { 217 | dec.state = sioStreamingDecodeStateData 218 | } 219 | continue 220 | } 221 | 222 | case sioStreamingDecodeStateData: 223 | used := false 224 | if dec.length > 0 { 225 | dec.buf.WriteRune(c) 226 | dec.length-- 227 | used = true 228 | 229 | if dec.length > 0 { 230 | utf8str := dec.src.String() 231 | if utf8.RuneCountInString(utf8str) >= dec.length { 232 | buf := make([]byte, 4) 233 | for _, r := range utf8str { 234 | size := utf8.EncodeRune(buf, r) 235 | dec.buf.Write(buf[:size]) 236 | dec.src.Next(size) 237 | dec.length-- 238 | if dec.length == 0 { 239 | break 240 | } 241 | } 242 | continue 243 | } else { 244 | break L 245 | } 246 | } 247 | } 248 | 249 | data := dec.buf.Bytes() 250 | dec.msg.data = make([]byte, len(data)) 251 | copy(dec.msg.data, data) 252 | 253 | dec.buf.Reset() 254 | dec.state = sioStreamingDecodeStateTrailer 255 | if used { 256 | continue 257 | } 258 | fallthrough 259 | 260 | case sioStreamingDecodeStateTrailer: 261 | if c == ',' { 262 | messages = append(messages, dec.msg) 263 | dec.state = sioStreamingDecodeStateBegin 264 | continue 265 | } else { 266 | dec.Reset() 267 | return nil, errors.New("Expecting trailer but got... " + string(c)) 268 | } 269 | } 270 | 271 | dec.buf.WriteRune(c) 272 | } 273 | 274 | if err == io.EOF { 275 | err = nil 276 | } 277 | 278 | return 279 | } 280 | -------------------------------------------------------------------------------- /codec_siostreaming_test.go: -------------------------------------------------------------------------------- 1 | package socketio 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | "unicode/utf8" 8 | "unsafe" 9 | ) 10 | 11 | func streamingFrame(data string, typ int, json bool) string { 12 | switch typ { 13 | case 0: 14 | return "0:0:," 15 | 16 | case 2, 3: 17 | return fmt.Sprintf("%d:%d:%s,", typ, utf8.RuneCountInString(data), data) 18 | } 19 | 20 | if json { 21 | return fmt.Sprintf("%d:%d:j\n:%s,", typ, 3+utf8.RuneCountInString(data), data) 22 | } 23 | return fmt.Sprintf("%d:%d::%s,", typ, 1+utf8.RuneCountInString(data), data) 24 | } 25 | 26 | type streamingEncodeTest struct { 27 | in interface{} 28 | out string 29 | } 30 | 31 | var streamingEncodeTests = []streamingEncodeTest{ 32 | { 33 | 123, 34 | streamingFrame("123", 1, false), 35 | }, 36 | { 37 | "hello, world", 38 | streamingFrame("hello, world", 1, false), 39 | }, 40 | { 41 | "öäö¥£♥", 42 | streamingFrame("öäö¥£♥", 1, false), 43 | }, 44 | { 45 | "öäö¥£♥", 46 | streamingFrame("öäö¥£♥", 1, false), 47 | }, 48 | { 49 | heartbeat(123456), 50 | streamingFrame("123456", 2, false), 51 | }, 52 | { 53 | handshake("abcdefg"), 54 | streamingFrame("abcdefg", 3, false), 55 | }, 56 | { 57 | true, 58 | streamingFrame("true", 1, true), 59 | }, 60 | { 61 | struct { 62 | Boolean bool 63 | Str string 64 | Array []int 65 | }{ 66 | false, 67 | "string♥", 68 | []int{1, 2, 3, 4}, 69 | }, 70 | streamingFrame(`{"Boolean":false,"Str":"string♥","Array":[1,2,3,4]}`, 1, true), 71 | }, 72 | { 73 | []byte("hello, world"), 74 | streamingFrame("hello, world", 1, false), 75 | }, 76 | } 77 | 78 | type streamingDecodeTestMessage struct { 79 | messageType uint8 80 | data string 81 | heartbeat heartbeat 82 | } 83 | 84 | type streamingDecodeTest struct { 85 | in string 86 | out []streamingDecodeTestMessage 87 | } 88 | 89 | // NOTE: if you change these -> adjust the benchmarks 90 | var streamingDecodeTests = []streamingDecodeTest{ 91 | { 92 | streamingFrame("", 1, false), 93 | []streamingDecodeTestMessage{{MessageText, "", -1}}, 94 | }, 95 | { 96 | streamingFrame("123", 2, false), 97 | []streamingDecodeTestMessage{{MessageHeartbeat, "123", 123}}, 98 | }, 99 | { 100 | streamingFrame("wadap!", 1, false), 101 | []streamingDecodeTestMessage{{MessageText, "wadap!", -1}}, 102 | }, 103 | { 104 | streamingFrame("♥wadap!", 1, true), 105 | []streamingDecodeTestMessage{{MessageJSON, "♥wadap!", -1}}, 106 | }, 107 | { 108 | streamingFrame("hello, world!", 1, true) + streamingFrame("313", 2, false) + streamingFrame("♥wadap!", 1, false), 109 | []streamingDecodeTestMessage{ 110 | {MessageJSON, "hello, world!", -1}, 111 | {MessageHeartbeat, "313", 313}, 112 | {MessageText, "♥wadap!", -1}, 113 | }, 114 | }, 115 | { 116 | "1:3::fael!,", 117 | nil, 118 | }, 119 | { 120 | streamingFrame("wadap!", 1, false), 121 | []streamingDecodeTestMessage{{MessageText, "wadap!", -1}}, 122 | }, 123 | } 124 | 125 | func TestStreamingEncode(t *testing.T) { 126 | codec := SIOStreamingCodec{} 127 | enc := codec.NewEncoder() 128 | buf := new(bytes.Buffer) 129 | 130 | for _, test := range streamingEncodeTests { 131 | t.Logf("in=%v out=%s", test.in, test.out) 132 | 133 | buf.Reset() 134 | if err := enc.Encode(buf, test.in); err != nil { 135 | t.Fatal("Encode:", err) 136 | } 137 | if string(buf.Bytes()) != test.out { 138 | t.Fatalf("Expected %q but got %q from %q", test.out, string(buf.Bytes()), test.in) 139 | } 140 | } 141 | } 142 | 143 | func TestStreamingDecode(t *testing.T) { 144 | codec := SIOStreamingCodec{} 145 | buf := new(bytes.Buffer) 146 | dec := codec.NewDecoder(buf) 147 | var messages []Message 148 | var err error 149 | 150 | for _, test := range streamingDecodeTests { 151 | t.Logf("in=%s out=%v", test.in, test.out) 152 | 153 | buf.WriteString(test.in) 154 | if messages, err = dec.Decode(); err != nil { 155 | if test.out == nil { 156 | continue 157 | } 158 | t.Fatal("Decode:", err) 159 | } 160 | if test.out == nil { 161 | t.Fatalf("Expected decode error, but got: %v, %v", messages, err) 162 | } 163 | if len(messages) != len(test.out) { 164 | t.Fatalf("Expected %d messages, but got %d", len(test.out), len(messages)) 165 | } 166 | for i, msg := range messages { 167 | t.Logf("message: %#v", msg) 168 | if test.out[i].messageType != msg.Type() { 169 | t.Fatalf("Expected type %d but got %d", test.out[i].messageType, msg.Type()) 170 | } 171 | if test.out[i].data != msg.Data() { 172 | t.Fatalf("Expected data %q but got %q", test.out[i].data, msg.Data()) 173 | } 174 | if test.out[i].messageType == MessageHeartbeat { 175 | if hb, ok := msg.heartbeat(); !ok || test.out[i].heartbeat != hb { 176 | t.Fatalf("Expected heartbeat %d but got %d (%v)", test.out[i].heartbeat, hb, err) 177 | } 178 | } 179 | } 180 | } 181 | } 182 | 183 | func TestStreamingDecodeStreaming(t *testing.T) { 184 | var messages []Message 185 | var err error 186 | codec := SIOStreamingCodec{} 187 | buf := new(bytes.Buffer) 188 | dec := codec.NewDecoder(buf) 189 | 190 | expectNothing := func(written string) { 191 | if messages, err = dec.Decode(); err != nil || messages == nil || len(messages) != 0 { 192 | t.Fatalf("Partial decode failed after writing %s. err=%#v messages=%#v", written, err, messages) 193 | } 194 | } 195 | 196 | buf.WriteString("5") 197 | expectNothing("5") 198 | buf.WriteString(":9") 199 | expectNothing("5:9") 200 | buf.WriteString(":12345") 201 | expectNothing("5:9:12345") 202 | buf.WriteString("678") 203 | expectNothing("5:9:12345678") 204 | buf.WriteString("9") 205 | expectNothing("5:9:123456789") 206 | buf.WriteString(",typefornextmessagewhichshouldbeignored") 207 | messages, err = dec.Decode() 208 | if err != nil { 209 | t.Fatalf("Did not expect errors: %s", err) 210 | } 211 | if messages == nil || len(messages) != 1 { 212 | t.Fatalf("Expected 1 message, got: %#v", messages) 213 | } 214 | if messages[0].(*sioMessage).typ != 5 || messages[0].Data() != "123456789" { 215 | t.Fatalf("Expected data 123456789 and typ 5, got: %#v", messages[0]) 216 | } 217 | } 218 | 219 | func BenchmarkIntEncode(b *testing.B) { 220 | codec := SIOStreamingCodec{} 221 | enc := codec.NewEncoder() 222 | payload := 313313 223 | b.SetBytes(int64(unsafe.Sizeof(payload))) 224 | w := nopWriter{} 225 | 226 | for i := 0; i < b.N; i++ { 227 | enc.Encode(w, payload) 228 | } 229 | } 230 | 231 | func BenchmarkStringEncode(b *testing.B) { 232 | codec := SIOStreamingCodec{} 233 | enc := codec.NewEncoder() 234 | payload := "Hello, World!" 235 | b.SetBytes(int64(len(payload))) 236 | w := nopWriter{} 237 | 238 | for i := 0; i < b.N; i++ { 239 | enc.Encode(w, payload) 240 | } 241 | } 242 | 243 | func BenchmarkStructEncode(b *testing.B) { 244 | codec := SIOStreamingCodec{} 245 | enc := codec.NewEncoder() 246 | payload := struct { 247 | boolean bool 248 | str string 249 | array []int 250 | }{ 251 | false, 252 | "string♥", 253 | []int{1, 2, 3, 4}, 254 | } 255 | 256 | b.SetBytes(int64(unsafe.Sizeof(payload))) 257 | w := nopWriter{} 258 | 259 | for i := 0; i < b.N; i++ { 260 | enc.Encode(w, payload) 261 | } 262 | } 263 | 264 | func BenchmarkSingleFrameDecode(b *testing.B) { 265 | codec := SIOStreamingCodec{} 266 | buf := new(bytes.Buffer) 267 | dec := codec.NewDecoder(buf) 268 | data := []byte(decodeTests[2].in) 269 | b.SetBytes(int64(len(data))) 270 | 271 | for i := 0; i < b.N; i++ { 272 | buf.Write(data) 273 | dec.Decode() 274 | } 275 | } 276 | 277 | func BenchmarkThreeFramesDecode(b *testing.B) { 278 | codec := SIOStreamingCodec{} 279 | buf := new(bytes.Buffer) 280 | dec := codec.NewDecoder(buf) 281 | data := []byte(decodeTests[3].in) 282 | b.SetBytes(int64(len(data))) 283 | 284 | for i := 0; i < b.N; i++ { 285 | buf.Write(data) 286 | dec.Decode() 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /codec_sio.go: -------------------------------------------------------------------------------- 1 | package socketio 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "strconv" 10 | "unicode/utf8" 11 | ) 12 | 13 | // The various delimiters used for framing in the socket.io protocol. 14 | const ( 15 | SIOAnnotationRealm = "r" 16 | SIOAnnotationJSON = "j" 17 | 18 | sioMessageTypeDisconnect = 0 19 | sioMessageTypeMessage = 1 20 | sioMessageTypeHeartbeat = 2 21 | sioMessageTypeHandshake = 3 22 | ) 23 | 24 | var ( 25 | sioFrameDelim = []byte("~m~") 26 | sioFrameDelimJSON = []byte("~j~") 27 | sioFrameDelimHeartbeat = []byte("~h~") 28 | ) 29 | 30 | // SioMessage fulfills the message interface. 31 | type sioMessage struct { 32 | annotations map[string]string 33 | typ uint8 34 | data []byte 35 | } 36 | 37 | // MessageType checks if the message starts with sioFrameDelimJSON or 38 | // sioFrameDelimHeartbeat. If the prefix is something else, then the message 39 | // is interpreted as a basic messageText. 40 | func (sm *sioMessage) Type() uint8 { 41 | switch sm.typ { 42 | case sioMessageTypeMessage: 43 | if _, ok := sm.Annotation(SIOAnnotationJSON); ok { 44 | return MessageJSON 45 | } 46 | 47 | case sioMessageTypeDisconnect: 48 | return MessageDisconnect 49 | 50 | case sioMessageTypeHeartbeat: 51 | return MessageHeartbeat 52 | 53 | case sioMessageTypeHandshake: 54 | return MessageHandshake 55 | } 56 | 57 | return MessageText 58 | } 59 | 60 | func (sm *sioMessage) Annotations() map[string]string { 61 | return sm.annotations 62 | } 63 | 64 | func (sm *sioMessage) Annotation(key string) (value string, ok bool) { 65 | if sm.annotations == nil { 66 | return "", false 67 | } 68 | value, ok = sm.annotations[key] 69 | return 70 | } 71 | 72 | // Heartbeat looks for a heartbeat value in the message. If a such value 73 | // can be extracted, then that value and a true is returned. Otherwise a 74 | // false will be returned. 75 | func (sm *sioMessage) heartbeat() (heartbeat, bool) { 76 | if sm.typ == sioMessageTypeHeartbeat { 77 | if n, err := strconv.Atoi(string(sm.data)); err == nil { 78 | return heartbeat(n), true 79 | } 80 | } 81 | 82 | return -1, false 83 | } 84 | 85 | // Data returns the raw message as a string. 86 | func (sm *sioMessage) Data() string { 87 | return string(sm.data) 88 | } 89 | 90 | // Bytes returns the raw message. 91 | func (sm *sioMessage) Bytes() []byte { 92 | return sm.data 93 | } 94 | 95 | // JSON returns the JSON embedded in the message, if available. 96 | func (sm *sioMessage) JSON() ([]byte, bool) { 97 | if sm.Type() == MessageJSON { 98 | return sm.data, true 99 | } 100 | 101 | return nil, false 102 | } 103 | 104 | // SIOCodec is the codec used by the official Socket.IO client by LearnBoost. 105 | // Each message is framed with a prefix and goes like this: 106 | // DATA-LENGTH[]DATA. 107 | type SIOCodec struct{} 108 | 109 | type sioEncoder struct { 110 | elem bytes.Buffer 111 | } 112 | 113 | func (sc SIOCodec) NewEncoder() Encoder { 114 | return &sioEncoder{} 115 | } 116 | 117 | // Encode takes payload, encodes it and writes it to dst. Payload must be one 118 | // of the following: a heartbeat, a handshake, []byte, string, int or anything 119 | // than can be marshalled by the default json package. If payload can't be 120 | // encoded or the writing fails, an error will be returned. 121 | func (enc *sioEncoder) Encode(dst io.Writer, payload interface{}) (err error) { 122 | enc.elem.Reset() 123 | 124 | switch t := payload.(type) { 125 | case heartbeat: 126 | s := strconv.Itoa(int(t)) 127 | _, err = fmt.Fprintf(dst, "%s%d%s%s%s", sioFrameDelim, len(s)+len(sioFrameDelimHeartbeat), sioFrameDelim, sioFrameDelimHeartbeat, s) 128 | 129 | case handshake: 130 | _, err = fmt.Fprintf(dst, "%s%d%s%s", sioFrameDelim, len(t), sioFrameDelim, t) 131 | 132 | case []byte: 133 | l := utf8.RuneCount(t) 134 | if l == 0 { 135 | break 136 | } 137 | _, err = fmt.Fprintf(dst, "%s%d%s%s", sioFrameDelim, l, sioFrameDelim, t) 138 | 139 | case string: 140 | l := utf8.RuneCountInString(t) 141 | if l == 0 { 142 | break 143 | } 144 | _, err = fmt.Fprintf(dst, "%s%d%s%s", sioFrameDelim, l, sioFrameDelim, t) 145 | 146 | case int: 147 | s := strconv.Itoa(t) 148 | if s == "" { 149 | break 150 | } 151 | _, err = fmt.Fprintf(dst, "%s%d%s%s", sioFrameDelim, len(s), sioFrameDelim, s) 152 | 153 | default: 154 | data, err := json.Marshal(payload) 155 | if len(data) == 0 || err != nil { 156 | break 157 | } 158 | err = json.Compact(&enc.elem, data) 159 | if err != nil { 160 | break 161 | } 162 | 163 | _, err = fmt.Fprintf(dst, "%s%d%s%s", sioFrameDelim, utf8.RuneCount(enc.elem.Bytes())+len(sioFrameDelimJSON), sioFrameDelim, sioFrameDelimJSON) 164 | if err == nil { 165 | _, err = enc.elem.WriteTo(dst) 166 | } 167 | } 168 | 169 | return err 170 | } 171 | 172 | const ( 173 | sioDecodeStateBegin = iota 174 | sioDecodeStateHeaderBegin 175 | sioDecodeStateLength 176 | sioDecodeStateHeaderEnd 177 | sioDecodeStateData 178 | sioDecodeStateEnd 179 | ) 180 | 181 | type sioDecoder struct { 182 | src *bytes.Buffer 183 | buf bytes.Buffer 184 | msg *sioMessage 185 | key, value string 186 | length, state int 187 | } 188 | 189 | func (sc SIOCodec) NewDecoder(src *bytes.Buffer) Decoder { 190 | return &sioDecoder{ 191 | src: src, 192 | state: sioDecodeStateBegin, 193 | } 194 | } 195 | 196 | func (dec *sioDecoder) Reset() { 197 | dec.buf.Reset() 198 | dec.src.Reset() 199 | dec.msg = nil 200 | dec.state = sioDecodeStateBegin 201 | dec.key = "" 202 | dec.value = "" 203 | dec.length = 0 204 | } 205 | 206 | func (dec *sioDecoder) Decode() (messages []Message, err error) { 207 | messages = make([]Message, 0, 1) 208 | var c rune 209 | 210 | L: 211 | for { 212 | if dec.state == sioDecodeStateBegin { 213 | dec.msg = &sioMessage{} 214 | dec.state = sioDecodeStateHeaderBegin 215 | dec.buf.Reset() 216 | } 217 | 218 | c, _, err = dec.src.ReadRune() 219 | if err != nil { 220 | break 221 | } 222 | 223 | switch dec.state { 224 | case sioDecodeStateHeaderBegin: 225 | dec.buf.WriteRune(c) 226 | if dec.buf.Len() == len(sioFrameDelim) { 227 | if !bytes.Equal(dec.buf.Bytes(), sioFrameDelim) { 228 | dec.Reset() 229 | return nil, errors.New("Malformed header") 230 | } 231 | 232 | dec.state = sioDecodeStateLength 233 | dec.buf.Reset() 234 | } 235 | continue 236 | 237 | case sioDecodeStateLength: 238 | if c >= '0' && c <= '9' { 239 | dec.buf.WriteRune(c) 240 | continue 241 | } 242 | 243 | if dec.length, err = strconv.Atoi(dec.buf.String()); err != nil { 244 | dec.Reset() 245 | return nil, err 246 | } 247 | 248 | dec.buf.Reset() 249 | dec.state = sioDecodeStateHeaderEnd 250 | fallthrough 251 | 252 | case sioDecodeStateHeaderEnd: 253 | dec.buf.WriteRune(c) 254 | if dec.buf.Len() < len(sioFrameDelim) { 255 | continue 256 | } 257 | 258 | if !bytes.Equal(dec.buf.Bytes(), sioFrameDelim) { 259 | dec.Reset() 260 | return nil, errors.New("Malformed header") 261 | } 262 | 263 | dec.state = sioDecodeStateData 264 | dec.buf.Reset() 265 | if dec.length > 0 { 266 | continue 267 | } 268 | fallthrough 269 | 270 | case sioDecodeStateData: 271 | if dec.length > 0 { 272 | dec.buf.WriteRune(c) 273 | dec.length-- 274 | 275 | utf8str := dec.src.String() 276 | 277 | if dec.length > 0 { 278 | if utf8.RuneCountInString(utf8str) >= dec.length { 279 | buf := make([]byte, 4) 280 | for _, r := range utf8str { 281 | s := utf8.EncodeRune(buf, r) 282 | dec.buf.Write(buf[:s]) 283 | dec.src.Next(s) 284 | dec.length-- 285 | if dec.length == 0 { 286 | break 287 | } 288 | } 289 | } else { 290 | break L 291 | } 292 | } 293 | } 294 | 295 | data := dec.buf.Bytes() 296 | dec.msg.typ = sioMessageTypeMessage 297 | 298 | if bytes.HasPrefix(data, sioFrameDelimJSON) { 299 | dec.msg.annotations = make(map[string]string) 300 | dec.msg.annotations[SIOAnnotationJSON] = "" 301 | data = data[len(sioFrameDelimJSON):] 302 | } else if bytes.HasPrefix(data, sioFrameDelimHeartbeat) { 303 | dec.msg.typ = sioMessageTypeHeartbeat 304 | data = data[len(sioFrameDelimHeartbeat):] 305 | } 306 | dec.msg.data = make([]byte, len(data)) 307 | copy(dec.msg.data, data) 308 | 309 | messages = append(messages, dec.msg) 310 | 311 | dec.state = sioDecodeStateBegin 312 | dec.buf.Reset() 313 | continue 314 | } 315 | 316 | dec.buf.WriteRune(c) 317 | } 318 | 319 | if err == io.EOF { 320 | err = nil 321 | } 322 | 323 | return 324 | } 325 | -------------------------------------------------------------------------------- /connection.go: -------------------------------------------------------------------------------- 1 | package socketio 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "sync" 10 | "syscall" 11 | "time" 12 | ) 13 | 14 | var ( 15 | // ErrDestroyed is used when the connection has been disconnected (i.e. can't be used anymore). 16 | ErrDestroyed = errors.New("connection is disconnected") 17 | 18 | // ErrQueueFull is used when the send queue is full. 19 | ErrQueueFull = errors.New("send queue is full") 20 | 21 | errMissingPostData = errors.New("Missing HTTP post data-field") 22 | ) 23 | 24 | // Conn represents a single session and handles its handshaking, 25 | // message buffering and reconnections. 26 | type Conn struct { 27 | mutex sync.Mutex 28 | socket socket // The i/o connection that abstract the transport. 29 | sio *SocketIO // The server. 30 | sessionid SessionID 31 | online bool 32 | lastConnected time.Time 33 | lastDisconnected time.Time 34 | lastHeartbeat heartbeat 35 | numHeartbeats int 36 | ticker *time.Ticker 37 | queue chan interface{} // Buffers the outgoing messages. 38 | numConns int // Total number of reconnects. 39 | handshaked bool // Indicates if the handshake has been sent. 40 | disconnected bool // Indicates if the connection has been disconnected. 41 | wakeupFlusher chan byte // Used internally to wake up the flusher. 42 | wakeupReader chan byte // Used internally to wake up the reader. 43 | enc Encoder 44 | dec Decoder 45 | decBuf bytes.Buffer 46 | raddr string 47 | 48 | UserData interface{} // User data 49 | } 50 | 51 | // NewConn creates a new connection for the sio. It generates the session id and 52 | // prepares the internal structure for usage. 53 | func newConn(sio *SocketIO) (c *Conn, err error) { 54 | var sessionid SessionID 55 | if sessionid, err = NewSessionID(); err != nil { 56 | sio.Log("sio/newConn: newSessionID:", err) 57 | return 58 | } 59 | 60 | c = &Conn{ 61 | sio: sio, 62 | sessionid: sessionid, 63 | wakeupFlusher: make(chan byte), 64 | wakeupReader: make(chan byte), 65 | queue: make(chan interface{}, sio.config.QueueLength), 66 | enc: sio.config.Codec.NewEncoder(), 67 | } 68 | 69 | c.dec = sio.config.Codec.NewDecoder(&c.decBuf) 70 | 71 | return 72 | } 73 | 74 | // String returns a string representation of the connection and implements the 75 | // fmt.Stringer interface. 76 | func (c *Conn) String() string { 77 | return fmt.Sprintf("%v[%v]", c.sessionid, c.socket) 78 | } 79 | 80 | // SessionID return the session id of Conn 81 | func (c *Conn) SessionID() SessionID { 82 | return c.sessionid 83 | } 84 | 85 | // RemoteAddr returns the remote network address of the connection in IP:port format 86 | func (c *Conn) RemoteAddr() string { 87 | return c.raddr 88 | } 89 | 90 | // Send queues data for a delivery. It is totally content agnostic with one exception: 91 | // the given data must be one of the following: a handshake, a heartbeat, an int, a string or 92 | // it must be otherwise marshallable by the standard json package. If the send queue 93 | // has reached sio.config.QueueLength or the connection has been disconnected, 94 | // then the data is dropped and a an error is returned. 95 | func (c *Conn) Send(data interface{}) (err error) { 96 | c.mutex.Lock() 97 | defer c.mutex.Unlock() 98 | 99 | if c.disconnected { 100 | return ErrDestroyed 101 | } 102 | 103 | select { 104 | case c.queue <- data: 105 | default: 106 | return ErrQueueFull 107 | } 108 | 109 | return nil 110 | } 111 | 112 | func (c *Conn) Close() error { 113 | c.mutex.Lock() 114 | 115 | if c.disconnected { 116 | c.mutex.Unlock() 117 | return ErrNotConnected 118 | } 119 | 120 | c.disconnect() 121 | c.mutex.Unlock() 122 | 123 | c.sio.onDisconnect(c) 124 | return nil 125 | } 126 | 127 | // Handle takes over an http responseWriter/req -pair using the given Transport. 128 | // If the HTTP method is POST then request's data-field will be used as an incoming 129 | // message and the request is dropped. If the method is GET then a new socket encapsulating 130 | // the request is created and a new connection is establised (or the connection will be 131 | // reconnected). Finally, handle will wake up the reader and the flusher. 132 | func (c *Conn) handle(t Transport, w http.ResponseWriter, req *http.Request) (err error) { 133 | c.mutex.Lock() 134 | 135 | if c.disconnected { 136 | c.mutex.Unlock() 137 | return ErrNotConnected 138 | } 139 | 140 | if req.Method == "POST" { 141 | c.mutex.Unlock() 142 | 143 | if msg := req.FormValue("data"); msg != "" { 144 | w.Header().Set("Content-Type", "text/plain") 145 | w.Write(okResponse) 146 | c.receive([]byte(msg)) 147 | } else { 148 | c.sio.Log("sio/conn: handle: POST missing data-field:", c) 149 | err = errMissingPostData 150 | } 151 | 152 | return 153 | } 154 | 155 | didHandshake := false 156 | 157 | s := t.newSocket() 158 | err = s.accept(w, req, func() { 159 | if c.socket != nil { 160 | c.socket.Close() 161 | } 162 | c.socket = s 163 | c.online = true 164 | c.lastConnected = time.Now() 165 | 166 | if !c.handshaked { 167 | // the connection has not been handshaked yet. 168 | if err = c.handshake(); err != nil { 169 | c.sio.Log("sio/conn: handle/handshake:", err, c) 170 | c.socket.Close() 171 | return 172 | } 173 | 174 | c.raddr = req.RemoteAddr 175 | c.handshaked = true 176 | didHandshake = true 177 | 178 | go c.keepalive() 179 | go c.flusher() 180 | go c.reader() 181 | 182 | c.sio.Log("sio/conn: connected:", c) 183 | } else { 184 | c.sio.Log("sio/conn: reconnected:", c) 185 | } 186 | 187 | c.numConns++ 188 | 189 | select { 190 | case c.wakeupFlusher <- 1: 191 | default: 192 | } 193 | 194 | select { 195 | case c.wakeupReader <- 1: 196 | default: 197 | } 198 | 199 | if didHandshake { 200 | c.mutex.Unlock() 201 | c.sio.onConnect(c) 202 | } 203 | }) 204 | 205 | if !didHandshake { 206 | c.mutex.Unlock() 207 | } 208 | 209 | return 210 | } 211 | 212 | // Handshake sends the handshake to the socket. 213 | func (c *Conn) handshake() error { 214 | return c.enc.Encode(c.socket, handshake(c.sessionid)) 215 | } 216 | 217 | func (c *Conn) disconnect() { 218 | c.sio.Log("sio/conn: disconnected:", c) 219 | c.socket.Close() 220 | c.disconnected = true 221 | close(c.wakeupFlusher) 222 | close(c.wakeupReader) 223 | close(c.queue) 224 | } 225 | 226 | // Receive decodes and handles data received from the socket. 227 | // It uses c.sio.codec to decode the data. The received non-heartbeat 228 | // messages (frames) are then passed to c.sio.onMessage method and the 229 | // heartbeats are processed right away (TODO). 230 | func (c *Conn) receive(data []byte) { 231 | c.decBuf.Write(data) 232 | msgs, err := c.dec.Decode() 233 | if err != nil { 234 | c.sio.Log("sio/conn: receive/decode:", err, c) 235 | return 236 | } 237 | 238 | for _, m := range msgs { 239 | if hb, ok := m.heartbeat(); ok { 240 | c.lastHeartbeat = hb 241 | } else { 242 | c.sio.onMessage(c, m) 243 | } 244 | } 245 | } 246 | 247 | func (c *Conn) keepalive() { 248 | c.ticker = time.NewTicker(c.sio.config.HeartbeatInterval) 249 | defer c.ticker.Stop() 250 | 251 | Loop: 252 | for t := range c.ticker.C { 253 | c.mutex.Lock() 254 | 255 | if c.disconnected { 256 | c.mutex.Unlock() 257 | return 258 | } 259 | 260 | if (!c.online && t.Sub(c.lastDisconnected) > c.sio.config.ReconnectTimeout) || int(c.lastHeartbeat) < c.numHeartbeats { 261 | c.disconnect() 262 | c.mutex.Unlock() 263 | break 264 | } 265 | 266 | c.numHeartbeats++ 267 | 268 | select { 269 | case c.queue <- heartbeat(c.numHeartbeats): 270 | default: 271 | c.sio.Log("sio/keepalive: unable to queue heartbeat. fail now. TODO: FIXME", c) 272 | c.disconnect() 273 | c.mutex.Unlock() 274 | break Loop 275 | } 276 | 277 | c.mutex.Unlock() 278 | } 279 | 280 | c.sio.onDisconnect(c) 281 | } 282 | 283 | // Flusher waits for messages on the queue. It then 284 | // tries to write the messages to the underlaying socket and 285 | // will keep on trying until the wakeupFlusher is killed or the payload 286 | // can be delivered. It is responsible for persisting messages until they 287 | // can be succesfully delivered. No more than c.sio.config.QueueLength messages 288 | // should ever be waiting for a delivery. 289 | // 290 | // NOTE: the c.sio.config.QueueLength is not a "hard limit", because one could have 291 | // max amount of messages waiting in the queue and in the payload itself 292 | // simultaneously. 293 | func (c *Conn) flusher() { 294 | buf := new(bytes.Buffer) 295 | var err error 296 | var msg interface{} 297 | var n int 298 | 299 | for msg = range c.queue { 300 | buf.Reset() 301 | err = c.enc.Encode(buf, msg) 302 | n = 1 303 | 304 | if err == nil { 305 | 306 | DrainLoop: 307 | for n < c.sio.config.QueueLength { 308 | select { 309 | case msg = <-c.queue: 310 | n++ 311 | if err = c.enc.Encode(buf, msg); err != nil { 312 | break DrainLoop 313 | } 314 | 315 | default: 316 | break DrainLoop 317 | } 318 | } 319 | } 320 | if err != nil { 321 | c.sio.Logf("sio/conn: flusher/encode: lost %d messages (%d bytes): %s %s", n, buf.Len(), err, c) 322 | continue 323 | } 324 | 325 | FlushLoop: 326 | for { 327 | for { 328 | c.mutex.Lock() 329 | _, err = buf.WriteTo(c.socket) 330 | c.mutex.Unlock() 331 | 332 | if err == nil { 333 | break FlushLoop 334 | } else if err != syscall.EAGAIN { 335 | break 336 | } 337 | } 338 | 339 | if _, ok := <-c.wakeupFlusher; !ok { 340 | return 341 | } 342 | } 343 | } 344 | } 345 | 346 | // Reader reads from the c.socket until the c.wakeupReader is closed. 347 | // It is responsible for detecting unrecoverable read errors and timeouting 348 | // the connection. When a read fails previously mentioned reasons, it will 349 | // call the c.disconnect method and start waiting for the next event on the 350 | // c.wakeupReader channel. 351 | func (c *Conn) reader() { 352 | buf := make([]byte, c.sio.config.ReadBufferSize) 353 | 354 | for { 355 | c.mutex.Lock() 356 | socket := c.socket 357 | c.mutex.Unlock() 358 | 359 | for { 360 | nr, err := socket.Read(buf) 361 | if err != nil { 362 | if err != syscall.EAGAIN { 363 | if neterr, ok := err.(*net.OpError); ok && neterr.Timeout() { 364 | c.sio.Log("sio/conn: lost connection (timeout):", c) 365 | socket.Write(emptyResponse) 366 | } else { 367 | c.sio.Log("sio/conn: lost connection:", c) 368 | } 369 | break 370 | } 371 | } else if nr < 0 { 372 | break 373 | } else if nr > 0 { 374 | c.receive(buf[0:nr]) 375 | } 376 | } 377 | 378 | c.mutex.Lock() 379 | c.lastDisconnected = time.Now() 380 | socket.Close() 381 | if c.socket == socket { 382 | c.online = false 383 | } 384 | c.mutex.Unlock() 385 | 386 | if _, ok := <-c.wakeupReader; !ok { 387 | break 388 | } 389 | } 390 | } 391 | -------------------------------------------------------------------------------- /socketio.go: -------------------------------------------------------------------------------- 1 | package socketio 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "net" 8 | "net/http" 9 | "net/url" 10 | "strings" 11 | "sync" 12 | "syscall" 13 | ) 14 | 15 | // SocketIO handles transport abstraction and provide the user 16 | // a handfull of callbacks to observe different events. 17 | type SocketIO struct { 18 | sessions map[SessionID]*Conn // Holds the outstanding sessions. 19 | sessionsLock *sync.RWMutex // Protects the sessions. 20 | config Config // Holds the configuration values. 21 | serveMux *ServeMux 22 | transportLookup map[string]Transport 23 | 24 | // The callbacks set by the user 25 | callbacks struct { 26 | onConnect func(*Conn) // Invoked on new connection. 27 | onDisconnect func(*Conn) // Invoked on a lost connection. 28 | onMessage func(*Conn, Message) // Invoked on a message. 29 | isAuthorized func(*http.Request) bool // Auth test during new http request 30 | } 31 | } 32 | 33 | // NewSocketIO creates a new socketio server with chosen transports and configuration 34 | // options. If transports is nil, the DefaultTransports is used. If config is nil, the 35 | // DefaultConfig is used. 36 | func NewSocketIO(config *Config) *SocketIO { 37 | if config == nil { 38 | config = &DefaultConfig 39 | } 40 | 41 | sio := &SocketIO{ 42 | config: *config, 43 | sessions: make(map[SessionID]*Conn), 44 | sessionsLock: new(sync.RWMutex), 45 | transportLookup: make(map[string]Transport), 46 | } 47 | 48 | for _, t := range sio.config.Transports { 49 | sio.transportLookup[t.Resource()] = t 50 | } 51 | 52 | sio.serveMux = NewServeMux(sio) 53 | 54 | return sio 55 | } 56 | 57 | // Broadcast schedules data to be sent to each connection. 58 | func (sio *SocketIO) Broadcast(data interface{}) { 59 | sio.BroadcastExcept(nil, data) 60 | } 61 | 62 | // BroadcastExcept schedules data to be sent to each connection except 63 | // c. It does not care about the type of data, but it must marshallable 64 | // by the standard json-package. 65 | func (sio *SocketIO) BroadcastExcept(c *Conn, data interface{}) { 66 | sio.sessionsLock.RLock() 67 | defer sio.sessionsLock.RUnlock() 68 | 69 | for _, v := range sio.sessions { 70 | if v != c { 71 | v.Send(data) 72 | } 73 | } 74 | } 75 | 76 | // GetConn digs for a session with sessionid and returns it. 77 | func (sio *SocketIO) GetConn(sessionid SessionID) (c *Conn) { 78 | sio.sessionsLock.RLock() 79 | c = sio.sessions[sessionid] 80 | sio.sessionsLock.RUnlock() 81 | return 82 | } 83 | 84 | // Mux maps resources to the http.ServeMux mux under the resource given. 85 | // The resource must end with a slash and if the mux is nil, the 86 | // http.DefaultServeMux is used. It registers handlers for URLs like: 87 | // [/], e.g. /socket.io/websocket && socket.io/websocket/. 88 | func (sio *SocketIO) ServeMux() *ServeMux { 89 | return sio.serveMux 90 | } 91 | 92 | // OnConnect sets f to be invoked when a new session is established. It passes 93 | // the established connection as an argument to the callback. 94 | func (sio *SocketIO) OnConnect(f func(*Conn)) error { 95 | sio.callbacks.onConnect = f 96 | return nil 97 | } 98 | 99 | // OnDisconnect sets f to be invoked when a session is considered to be lost. It passes 100 | // the established connection as an argument to the callback. After disconnection 101 | // the connection is considered to be destroyed, and it should not be used anymore. 102 | func (sio *SocketIO) OnDisconnect(f func(*Conn)) error { 103 | sio.callbacks.onDisconnect = f 104 | return nil 105 | } 106 | 107 | // OnMessage sets f to be invoked when a message arrives. It passes 108 | // the established connection along with the received message as arguments 109 | // to the callback. 110 | func (sio *SocketIO) OnMessage(f func(*Conn, Message)) error { 111 | sio.callbacks.onMessage = f 112 | return nil 113 | } 114 | 115 | // SetAuthorization sets f to be invoked when a new http request is made. It passes 116 | // the http.Request as an argument to the callback. 117 | // The callback should return true if the connection is authorized or false if it 118 | // should be dropped. Not setting this callback results in a default pass-through. 119 | func (sio *SocketIO) SetAuthorization(f func(*http.Request) bool) error { 120 | sio.callbacks.isAuthorized = f 121 | return nil 122 | } 123 | 124 | func (sio *SocketIO) Log(v ...interface{}) { 125 | if sio.config.Logger != nil { 126 | sio.config.Logger.Println(v...) 127 | } 128 | } 129 | 130 | func (sio *SocketIO) Logf(format string, v ...interface{}) { 131 | if sio.config.Logger != nil { 132 | sio.config.Logger.Printf(format, v...) 133 | } 134 | } 135 | 136 | // Handle is invoked on every http-request coming through the muxer. 137 | // It is responsible for parsing the request and passing the http conn/req -pair 138 | // to the corresponding sio connections. It also creates new connections when needed. 139 | // The URL and method must be one of the following: 140 | // 141 | // OPTIONS * 142 | // GET resource 143 | // GET resource/sessionid 144 | // POST resource/sessionid 145 | func (sio *SocketIO) handle(t Transport, w http.ResponseWriter, req *http.Request) { 146 | var parts []string 147 | var c *Conn 148 | var err error 149 | 150 | if !sio.isAuthorized(req) { 151 | sio.Log("sio/handle: unauthorized request:", req) 152 | w.WriteHeader(http.StatusUnauthorized) 153 | return 154 | } 155 | 156 | if origin := req.Header.Get("Origin"); origin != "" { 157 | if _, ok := sio.verifyOrigin(origin); !ok { 158 | sio.Log("sio/handle: unauthorized origin:", origin) 159 | w.WriteHeader(http.StatusUnauthorized) 160 | return 161 | } 162 | 163 | w.Header().Set("Access-Control-Allow-Origin", origin) 164 | w.Header().Set("Access-Control-Allow-Credentials", "true") 165 | w.Header().Set("Access-Control-Allow-Methods", "POST, GET") 166 | } 167 | 168 | switch req.Method { 169 | case "OPTIONS": 170 | w.WriteHeader(http.StatusOK) 171 | return 172 | 173 | case "GET", "POST": 174 | break 175 | 176 | default: 177 | w.WriteHeader(http.StatusUnauthorized) 178 | return 179 | } 180 | 181 | // TODO: fails if the session id matches the transport 182 | if i := strings.LastIndex(req.URL.Path, t.Resource()); i >= 0 { 183 | pathLen := len(req.URL.Path) 184 | if req.URL.Path[pathLen-1] == '/' { 185 | pathLen-- 186 | } 187 | 188 | parts = strings.Split(req.URL.Path[i:pathLen], "/") 189 | } 190 | 191 | if len(parts) < 2 || parts[1] == "" { 192 | c, err = newConn(sio) 193 | if err != nil { 194 | sio.Log("sio/handle: unable to create a new connection:", err) 195 | w.WriteHeader(http.StatusInternalServerError) 196 | return 197 | } 198 | } else { 199 | c = sio.GetConn(SessionID(parts[1])) 200 | } 201 | 202 | // we should now have a connection 203 | if c == nil { 204 | sio.Log("sio/handle: unable to map request to connection:", req.RequestURI) 205 | w.WriteHeader(http.StatusBadRequest) 206 | return 207 | } 208 | 209 | // pass the http conn/req pair to the connection 210 | if err = c.handle(t, w, req); err != nil { 211 | sio.Logf("sio/handle: conn/handle: %s: %s", c, err) 212 | w.WriteHeader(http.StatusUnauthorized) 213 | } 214 | } 215 | 216 | // OnConnect is invoked by a connection when a new connection has been 217 | // established succesfully. The establised connection is passed as an 218 | // argument. It stores the connection and calls the user's OnConnect callback. 219 | func (sio *SocketIO) onConnect(c *Conn) { 220 | sio.sessionsLock.Lock() 221 | sio.sessions[c.sessionid] = c 222 | sio.sessionsLock.Unlock() 223 | 224 | if sio.callbacks.onConnect != nil { 225 | sio.callbacks.onConnect(c) 226 | } 227 | } 228 | 229 | // OnDisconnect is invoked by a connection when the connection is considered 230 | // to be lost. It removes the connection and calls the user's OnDisconnect callback. 231 | func (sio *SocketIO) onDisconnect(c *Conn) { 232 | sio.sessionsLock.Lock() 233 | delete(sio.sessions, c.sessionid) 234 | sio.sessionsLock.Unlock() 235 | 236 | if sio.callbacks.onDisconnect != nil { 237 | sio.callbacks.onDisconnect(c) 238 | } 239 | } 240 | 241 | // OnMessage is invoked by a connection when a new message arrives. It passes 242 | // this message to the user's OnMessage callback. 243 | func (sio *SocketIO) onMessage(c *Conn, msg Message) { 244 | if sio.callbacks.onMessage != nil { 245 | sio.callbacks.onMessage(c, msg) 246 | } 247 | } 248 | 249 | // isAuthorized is called during the handle() of any new http request 250 | // If the user has set a callback, this is a hook for returning whether 251 | // the connection is authorized. If no callback has been set, this method 252 | // always returns true as a pass-through 253 | func (sio *SocketIO) isAuthorized(req *http.Request) bool { 254 | if sio.callbacks.isAuthorized != nil { 255 | return sio.callbacks.isAuthorized(req) 256 | } 257 | return true 258 | } 259 | 260 | func (sio *SocketIO) verifyOrigin(reqOrigin string) (string, bool) { 261 | if sio.config.Origins == nil { 262 | return "", false 263 | } 264 | 265 | u, err := url.Parse(reqOrigin) 266 | if err != nil || u.Host == "" { 267 | return "", false 268 | } 269 | 270 | host := strings.SplitN(u.Host, ":", 2) 271 | 272 | for _, o := range sio.config.Origins { 273 | origin := strings.SplitN(o, ":", 2) 274 | if origin[0] == "*" || origin[0] == host[0] { 275 | if len(origin) < 2 || origin[1] == "*" { 276 | return o, true 277 | } 278 | if len(host) < 2 { 279 | switch u.Scheme { 280 | case "http", "ws": 281 | if origin[1] == "80" { 282 | return o, true 283 | } 284 | 285 | case "https", "wss": 286 | if origin[1] == "443" { 287 | return o, true 288 | } 289 | } 290 | } else if origin[1] == host[1] { 291 | return o, true 292 | } 293 | } 294 | } 295 | 296 | return "", false 297 | } 298 | 299 | func (sio *SocketIO) generatePolicyFile() []byte { 300 | buf := new(bytes.Buffer) 301 | buf.WriteString(` 302 | 303 | 304 | 305 | `) 306 | 307 | if sio.config.Origins != nil { 308 | for _, origin := range sio.config.Origins { 309 | parts := strings.SplitN(origin, ":", 2) 310 | if len(parts) < 1 { 311 | continue 312 | } 313 | host, port := "*", "*" 314 | if parts[0] != "" { 315 | host = parts[0] 316 | } 317 | if len(parts) == 2 && parts[1] != "" { 318 | port = parts[1] 319 | } 320 | 321 | fmt.Fprintf(buf, "\t\n", host, port) 322 | } 323 | } 324 | 325 | buf.WriteString("\n") 326 | return buf.Bytes() 327 | } 328 | 329 | func (sio *SocketIO) ListenAndServeFlashPolicy(laddr string) error { 330 | var listener net.Listener 331 | 332 | listener, err := net.Listen("tcp", laddr) 333 | if err != nil { 334 | return err 335 | } 336 | 337 | policy := sio.generatePolicyFile() 338 | 339 | for { 340 | conn, err := listener.Accept() 341 | if err != nil { 342 | sio.Log("ServeFlashsocketPolicy:", err) 343 | continue 344 | } 345 | 346 | go func() { 347 | defer conn.Close() 348 | 349 | buf := make([]byte, 20) 350 | if _, err := io.ReadFull(conn, buf); err != nil { 351 | sio.Log("ServeFlashsocketPolicy:", err) 352 | return 353 | } 354 | if !bytes.Equal([]byte(" 0 { 367 | nw += n 368 | continue 369 | } else { 370 | sio.Log("ServeFlashsocketPolicy: wrote 0 bytes") 371 | return 372 | } 373 | } 374 | sio.Log("ServeFlashsocketPolicy: served", conn.RemoteAddr()) 375 | }() 376 | } 377 | 378 | return nil 379 | } 380 | --------------------------------------------------------------------------------