diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..91cc826b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +profile.cov diff --git a/.travis.yml b/.travis.yml index 7141eabd..ca458f73 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,14 @@ +sudo: false language: go -go: 1.5 +env: + CI_SERVICE: travis-ci +go: + - master + - tip install: - - go get "github.com/smartystreets/goconvey/convey" - - go get -v . + - make getdev + - go get github.com/mattn/goveralls + - go get golang.org/x/tools/cmd/cover +script: + - make test + - ./gosweep.sh diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..10c99626 --- /dev/null +++ b/Makefile @@ -0,0 +1,11 @@ +all: + go install ./... + +getdev: + go get -t ./... + +test: + go test -race -cover -covermode=atomic ./... + +bench: + go test -bench . -benchmem ./... diff --git a/adapter.go b/adapter.go deleted file mode 100644 index 2ee86b10..00000000 --- a/adapter.go +++ /dev/null @@ -1,70 +0,0 @@ -package socketio - -import "sync" - -// BroadcastAdaptor is the adaptor to handle broadcasts. -type BroadcastAdaptor interface { - - // Join causes the socket to join a room. - Join(room string, socket Socket) error - - // Leave causes the socket to leave a room. - Leave(room string, socket Socket) error - - // Send will send an event with args to the room. If "ignore" is not nil, the event will be excluded from being sent to "ignore". - Send(ignore Socket, room, event string, args ...interface{}) error -} - -var newBroadcast = newBroadcastDefault - -type broadcast struct { - m map[string]map[string]Socket - sync.RWMutex -} - -func newBroadcastDefault() BroadcastAdaptor { - return &broadcast{ - m: make(map[string]map[string]Socket), - } -} - -func (b *broadcast) Join(room string, socket Socket) error { - b.Lock() - sockets, ok := b.m[room] - if !ok { - sockets = make(map[string]Socket) - } - sockets[socket.Id()] = socket - b.m[room] = sockets - b.Unlock() - return nil -} - -func (b *broadcast) Leave(room string, socket Socket) error { - b.Lock() - defer b.Unlock() - sockets, ok := b.m[room] - if !ok { - return nil - } - delete(sockets, socket.Id()) - if len(sockets) == 0 { - delete(b.m, room) - return nil - } - b.m[room] = sockets - return nil -} - -func (b *broadcast) Send(ignore Socket, room, event string, args ...interface{}) error { - b.RLock() - sockets := b.m[room] - for id, s := range sockets { - if ignore != nil && ignore.Id() == id { - continue - } - s.Emit(event, args...) - } - b.RUnlock() - return nil -} diff --git a/attachment.go b/attachment.go deleted file mode 100644 index 2f04c082..00000000 --- a/attachment.go +++ /dev/null @@ -1,168 +0,0 @@ -package socketio - -import ( - "bytes" - "encoding/json" - "fmt" - "io" - "reflect" -) - -// Attachment is an attachment handler used in emit args. All attachments will be sent as binary data in the transport layer. When using an attachment, make sure it is a pointer. -// -// For example: -// -// type Arg struct { -// Title string `json:"title"` -// File *Attachment `json:"file"` -// } -// -// f, _ := os.Open("./some_file") -// arg := Arg{ -// Title: "some_file", -// File: &Attachment{ -// Data: f, -// } -// } -// -// socket.Emit("send file", arg) -// socket.On("get file", func(so Socket, arg Arg) { -// b, _ := ioutil.ReadAll(arg.File.Data) -// }) -type Attachment struct { - Data io.ReadWriter - num int -} - -func encodeAttachments(v interface{}) []io.Reader { - index := 0 - return encodeAttachmentValue(reflect.ValueOf(v), &index) -} - -func encodeAttachmentValue(v reflect.Value, index *int) []io.Reader { - v = reflect.Indirect(v) - ret := []io.Reader{} - if !v.IsValid() { - return ret - } - switch v.Kind() { - case reflect.Struct: - if v.Type().Name() == "Attachment" { - a, ok := v.Addr().Interface().(*Attachment) - if !ok { - panic("can't convert") - } - a.num = *index - ret = append(ret, a.Data) - (*index)++ - return ret - } - for i, n := 0, v.NumField(); i < n; i++ { - var r []io.Reader - r = encodeAttachmentValue(v.Field(i), index) - ret = append(ret, r...) - } - case reflect.Map: - if v.IsNil() { - return ret - } - for _, key := range v.MapKeys() { - var r []io.Reader - r = encodeAttachmentValue(v.MapIndex(key), index) - ret = append(ret, r...) - } - case reflect.Slice: - if v.IsNil() { - return ret - } - fallthrough - case reflect.Array: - for i, n := 0, v.Len(); i < n; i++ { - var r []io.Reader - r = encodeAttachmentValue(v.Index(i), index) - ret = append(ret, r...) - } - case reflect.Interface: - ret = encodeAttachmentValue(reflect.ValueOf(v.Interface()), index) - } - return ret -} - -func decodeAttachments(v interface{}, binary [][]byte) error { - return decodeAttachmentValue(reflect.ValueOf(v), binary) -} - -func decodeAttachmentValue(v reflect.Value, binary [][]byte) error { - v = reflect.Indirect(v) - if !v.IsValid() { - return fmt.Errorf("invalid value") - } - switch v.Kind() { - case reflect.Struct: - if v.Type().Name() == "Attachment" { - a, ok := v.Addr().Interface().(*Attachment) - if !ok { - panic("can't convert") - } - if a.num >= len(binary) || a.num < 0 { - return fmt.Errorf("out of range") - } - if a.Data == nil { - a.Data = bytes.NewBuffer(nil) - } - for b := binary[a.num]; len(b) > 0; { - n, err := a.Data.Write(b) - if err != nil { - return err - } - b = b[n:] - } - return nil - } - for i, n := 0, v.NumField(); i < n; i++ { - if err := decodeAttachmentValue(v.Field(i), binary); err != nil { - return err - } - } - case reflect.Map: - if v.IsNil() { - return nil - } - for _, key := range v.MapKeys() { - if err := decodeAttachmentValue(v.MapIndex(key), binary); err != nil { - return err - } - } - case reflect.Slice: - if v.IsNil() { - return nil - } - fallthrough - case reflect.Array: - for i, n := 0, v.Len(); i < n; i++ { - if err := decodeAttachmentValue(v.Index(i), binary); err != nil { - return err - } - } - case reflect.Interface: - if err := decodeAttachmentValue(reflect.ValueOf(v.Interface()), binary); err != nil { - return err - } - } - return nil -} - -func (a Attachment) MarshalJSON() ([]byte, error) { - return []byte(fmt.Sprintf("{\"_placeholder\":true,\"num\":%d}", a.num)), nil -} - -func (a *Attachment) UnmarshalJSON(b []byte) error { - var v struct { - Num int `json:"num"` - } - if err := json.Unmarshal(b, &v); err != nil { - return err - } - a.num = v.Num - return nil -} diff --git a/attachment_test.go b/attachment_test.go deleted file mode 100644 index 42d9aa36..00000000 --- a/attachment_test.go +++ /dev/null @@ -1,184 +0,0 @@ -package socketio - -import ( - "bytes" - "encoding/json" - "io" - "testing" - - . "github.com/smartystreets/goconvey/convey" -) - -type NoAttachment struct { - I int `json:"i"` -} - -type HaveAttachment struct { - NoAttachment - A *Attachment `json:"a"` -} - -func TestEncodeAttachments(t *testing.T) { - var input interface{} - var target []io.Reader - buf1 := bytes.NewBufferString("data1") - buf2 := bytes.NewBufferString("data2") - attachment1 := &Attachment{Data: buf1} - attachment2 := &Attachment{Data: buf2} - - test := func() { - attachment1.num = -1 - attachment2.num = -1 - attachments := encodeAttachments(input) - if len(attachments)+len(target) > 0 { - So(attachments, ShouldResemble, target) - } - } - - Convey("No attachment", t, func() { - input = &NoAttachment{} - target = nil - - test() - }) - - Convey("Many attachment", t, func() { - input = &HaveAttachment{A: attachment1} - target = []io.Reader{buf1} - - test() - - So(attachment1.num, ShouldEqual, 0) - }) - - Convey("Array of attachments", t, func() { - input = [...]interface{}{HaveAttachment{A: attachment1}, &HaveAttachment{A: attachment2}} - target = []io.Reader{buf1, buf2} - - test() - - So(attachment1.num, ShouldEqual, 0) - So(attachment2.num, ShouldEqual, 1) - }) - - Convey("Slice of attachments", t, func() { - input = []interface{}{HaveAttachment{A: attachment1}, &HaveAttachment{A: attachment2}} - target = []io.Reader{buf1, buf2} - - test() - - So(attachment1.num, ShouldEqual, 0) - So(attachment2.num, ShouldEqual, 1) - }) - - Convey("Map of attachments", t, func() { - input = map[string]interface{}{"test": HaveAttachment{A: attachment1}, "testp": &HaveAttachment{A: attachment2}} - - attachment1.num = -1 - attachment2.num = -1 - attachments := encodeAttachments(input) - - So(attachment1.num, ShouldBeIn, []int{0, 1}) - switch attachment1.num { - case 0: - So(attachment2.num, ShouldEqual, 1) - target = []io.Reader{buf1, buf2} - So(attachments, ShouldResemble, target) - case 1: - So(attachment2.num, ShouldEqual, 0) - target = []io.Reader{buf2, buf1} - So(attachments, ShouldResemble, target) - } - }) - - Convey("Encode attachment", t, func() { - input = map[string]interface{}{"test": HaveAttachment{A: attachment1}} - - attachment1.num = -1 - encodeAttachments(input) - - b, err := json.Marshal(input) - So(err, ShouldBeNil) - So(string(b), ShouldEqual, `{"test":{"i":0,"a":{"_placeholder":true,"num":0}}}`) - }) - -} - -func TestDecodeAttachments(t *testing.T) { - var input [][]byte - var v interface{} - buf1 := bytes.NewBuffer(nil) - buf2 := bytes.NewBuffer(nil) - var attachment1 *Attachment - var attachment2 *Attachment - - test := func() { - err := decodeAttachments(v, input) - So(err, ShouldBeNil) - if attachment1 != nil { - So(buf1.String(), ShouldEqual, "data1") - } - if attachment2 != nil { - So(buf2.String(), ShouldEqual, "data2") - } - buf1.Reset() - buf2.Reset() - } - - Convey("No attachment", t, func() { - input = nil - v = NoAttachment{} - - test() - }) - - Convey("Many attachment", t, func() { - input = [][]byte{[]byte("data1")} - attachment1 = &Attachment{Data: buf1} - attachment1.num = 0 - v = HaveAttachment{A: attachment1} - - test() - }) - - Convey("Array of attachments", t, func() { - input = [][]byte{[]byte("data1"), []byte("data2")} - attachment1 = &Attachment{Data: buf1} - attachment1.num = 0 - attachment2 = &Attachment{Data: buf2} - attachment2.num = 1 - v = [...]interface{}{HaveAttachment{A: attachment1}, &HaveAttachment{A: attachment2}} - - test() - }) - - Convey("Slice of attachments", t, func() { - input = [][]byte{[]byte("data1"), []byte("data2")} - attachment1 = &Attachment{Data: buf1} - attachment1.num = 0 - attachment2 = &Attachment{Data: buf2} - attachment2.num = 1 - v = []interface{}{HaveAttachment{A: attachment1}, &HaveAttachment{A: attachment2}} - - test() - }) - - Convey("Map of attachments", t, func() { - input = [][]byte{[]byte("data1"), []byte("data2")} - attachment1 = &Attachment{Data: buf1} - attachment1.num = 0 - attachment2 = &Attachment{Data: buf2} - attachment2.num = 1 - v = map[string]interface{}{"test": HaveAttachment{A: attachment1}, "testp": &HaveAttachment{A: attachment2}} - - test() - }) - - Convey("Deocde json", t, func() { - b := []byte(`{"i":0,"a":{"_placeholder":true,"num":2}}`) - v := &HaveAttachment{} - err := json.Unmarshal(b, &v) - So(err, ShouldBeNil) - So(v.A.num, ShouldEqual, 2) - }) -} diff --git a/caller.go b/caller.go deleted file mode 100644 index 15666b44..00000000 --- a/caller.go +++ /dev/null @@ -1,82 +0,0 @@ -package socketio - -import ( - "errors" - "fmt" - "reflect" -) - -type caller struct { - Func reflect.Value - Args []reflect.Type - NeedSocket bool -} - -func newCaller(f interface{}) (*caller, error) { - fv := reflect.ValueOf(f) - if fv.Kind() != reflect.Func { - return nil, fmt.Errorf("f is not func") - } - ft := fv.Type() - if ft.NumIn() == 0 { - return &caller{ - Func: fv, - }, nil - } - args := make([]reflect.Type, ft.NumIn()) - for i, n := 0, ft.NumIn(); i < n; i++ { - args[i] = ft.In(i) - } - needSocket := false - if args[0].Name() == "Socket" { - args = args[1:] - needSocket = true - } - return &caller{ - Func: fv, - Args: args, - NeedSocket: needSocket, - }, nil -} - -func (c *caller) GetArgs() []interface{} { - ret := make([]interface{}, len(c.Args)) - for i, argT := range c.Args { - if argT.Kind() == reflect.Ptr { - argT = argT.Elem() - } - v := reflect.New(argT) - ret[i] = v.Interface() - } - return ret -} - -func (c *caller) Call(so Socket, args []interface{}) []reflect.Value { - var a []reflect.Value - diff := 0 - if c.NeedSocket { - diff = 1 - a = make([]reflect.Value, len(args)+1) - a[0] = reflect.ValueOf(so) - } else { - a = make([]reflect.Value, len(args)) - } - - if len(args) != len(c.Args) { - return []reflect.Value{reflect.ValueOf([]interface{}{}), reflect.ValueOf(errors.New("Arguments do not match"))} - } - - for i, arg := range args { - v := reflect.ValueOf(arg) - if c.Args[i].Kind() != reflect.Ptr { - if v.IsValid() { - v = v.Elem() - } else { - v = reflect.Zero(c.Args[i]) - } - } - a[i+diff] = v - } - - return c.Func.Call(a) -} diff --git a/conn.go b/conn.go new file mode 100644 index 00000000..49c0a0c7 --- /dev/null +++ b/conn.go @@ -0,0 +1,276 @@ +package socketio + +import ( + "net" + "net/http" + "net/url" + "reflect" + "sync" + + "github.com/tensor146/go-socket.io/parser" + + "github.com/tensor146/go-engine.io" + + "github.com/pkg/errors" +) + +// Conn is a connection in go-socket.io +type Conn interface { + // ID returns session id + ID() string + Close() error + URL() url.URL + LocalAddr() net.Addr + RemoteAddr() net.Addr + RemoteHeader() http.Header + + // Context of this connection. You can save one context for one + // connection, and share it between all handlers. The handlers + // is called in one goroutine, so no need to lock context if it + // only be accessed in one connection. + Context() interface{} + SetContext(v interface{}) + Namespace() string + Emit(msg string, v ...interface{}) +} + +type errorMessage struct { + namespace string + error +} + +type writePacket struct { + header parser.Header + data []interface{} +} + +type conn struct { + engineio.Conn + encoder *parser.Encoder + decoder *parser.Decoder + errorChan chan errorMessage + writeChan chan writePacket + quitChan chan struct{} + handlers map[string]*namespaceHandler + namespaces map[string]*namespaceConn + closeOnce sync.Once + id uint64 + sync.Mutex +} + +func newConn(c engineio.Conn, handlers map[string]*namespaceHandler) (*conn, error) { + mtx := sync.Mutex{} + ret := &conn{ + Conn: c, + encoder: parser.NewEncoder(c, mtx), + decoder: parser.NewDecoder(c, mtx), + errorChan: make(chan errorMessage), + writeChan: make(chan writePacket), + quitChan: make(chan struct{}), + handlers: handlers, + namespaces: make(map[string]*namespaceConn), + } + if err := ret.connect(); err != nil { + ret.Close() + return nil, err + } + go ret.serveError() + go ret.serveWrite() + go ret.serveRead() + return ret, nil +} + +func (c *conn) Close() error { + var err error + c.closeOnce.Do(func() { + err = c.Conn.Close() + close(c.quitChan) + }) + return err +} + +func (c *conn) connect() error { + root := newNamespaceConn(c, "/") + c.namespaces[""] = root + header := parser.Header{ + Type: parser.Connect, + } + handler, ok := c.handlers[header.Namespace] + if ok { + handler.dispatch(root, header, "", nil) + } + if err := c.encoder.Encode(header, nil); err != nil { + return err + } + + return nil +} + +func (c *conn) nextID() uint64 { + c.id++ + return c.id +} + +func (c *conn) write(header parser.Header, args []reflect.Value) { + data := make([]interface{}, len(args)) + for i := range data { + data[i] = args[i].Interface() + } + pkg := writePacket{ + header: header, + data: data, + } + select { + case c.writeChan <- pkg: + case <-c.quitChan: + return + } +} + +func (c *conn) onError(namespace string, err error) { + onErr := errorMessage{ + namespace: namespace, + error: errors.Wrap(err, "error"), + } + select { + case c.errorChan <- onErr: + case <-c.quitChan: + return + } +} + +func (c *conn) parseArgs(types []reflect.Type) ([]reflect.Value, error) { + return c.decoder.DecodeArgs(types) +} + +func (c *conn) serveError() { + defer c.Close() + for { + select { + case <-c.quitChan: + return + case msg := <-c.errorChan: + if handler := c.namespace(msg.namespace); handler != nil { + connect, _ := c.namespaces[msg.namespace] + handler.onError(msg.error, connect) + } + } + } +} + +func (c *conn) serveWrite() { + defer c.Close() + for { + select { + case <-c.quitChan: + return + case pkg := <-c.writeChan: + if err := c.encoder.Encode(pkg.header, pkg.data); err != nil { + c.onError(pkg.header.Namespace, err) + } + } + } +} + +func (c *conn) serveRead() { + var header parser.Header + + defer c.Close() + defer func() { + conn, ok := c.namespaces[header.Namespace] + if ok { + handler, ok := c.handlers[header.Namespace] + if ok { + handler.dispatch(conn, parser.Header{ + Type: parser.Event, + Namespace: header.Namespace, + ID: header.ID, + NeedAck: false, + }, "_close", nil) + } + } + }() + + var event string + for { + if err := c.decoder.DecodeHeader(&header, &event); err != nil { + c.onError("", err) + return + } + + if header.Namespace == "/" { + header.Namespace = "" + } + switch header.Type { + case parser.Ack: + conn, ok := c.namespaces[header.Namespace] + if !ok { + c.decoder.DiscardLast() + continue + } + conn.dispatch(header) + case parser.Event: + conn, ok := c.namespaces[header.Namespace] + if !ok { + c.decoder.DiscardLast() + continue + } + handler, ok := c.handlers[header.Namespace] + if !ok { + c.decoder.DiscardLast() + continue + } + types := handler.getTypes(header, event) + args, err := c.decoder.DecodeArgs(types) + if err != nil { + c.onError(header.Namespace, err) + return + } + ret, err := handler.dispatch(conn, header, event, args) + if err != nil { + c.onError(header.Namespace, err) + return + } + if len(ret) > 0 { + header.Type = parser.Ack + c.write(header, ret) + } + case parser.Connect: + if err := c.decoder.DiscardLast(); err != nil { + c.onError(header.Namespace, err) + return + } + conn, ok := c.namespaces[header.Namespace] + if !ok { + conn = newNamespaceConn(c, header.Namespace) + c.namespaces[header.Namespace] = conn + } + handler, ok := c.handlers[header.Namespace] + if ok { + handler.dispatch(conn, header, "", nil) + } + c.write(header, nil) + case parser.Disconnect: + types := []reflect.Type{reflect.TypeOf("")} + args, err := c.decoder.DecodeArgs(types) + if err != nil { + c.onError(header.Namespace, err) + return + } + conn, ok := c.namespaces[header.Namespace] + if !ok { + c.decoder.DiscardLast() + continue + } + delete(c.namespaces, header.Namespace) + handler, ok := c.handlers[header.Namespace] + if ok { + handler.dispatch(conn, header, "", args) + } + } + } +} + +func (c *conn) namespace(nsp string) *namespaceHandler { + return c.handlers[nsp] +} diff --git a/example/asset/index.html b/example/asset/index.html index 757ffe39..a3ea43b3 100644 --- a/example/asset/index.html +++ b/example/asset/index.html @@ -18,21 +18,23 @@
- - + +