diff --git a/README.md b/README.md index afe1739..f329189 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ [![Current Release](https://img.shields.io/badge/release-v0.1.0-brightgreen.svg)](https://github.com/composer22/chattypantz/releases/tag/v0.1.0) [![Coverage Status](https://coveralls.io/repos/composer22/chattypantz/badge.svg?branch=master)](https://coveralls.io/r/composer22/chattypantz?branch=master) +![chattypantz-logo](assets/img/chattypantz.png) + A demo chat server and client written in [Go.](http://golang.org) ## About @@ -15,20 +17,20 @@ Some key objectives in this demonstration: * Clients connect to the server on ws://{host:port} * Messages sent by a client are broadcasted to other clients connected to the same chat room. * The server only supports JSON text messages. Binary websocket frames will be discarded and the clients sending those frames will be disconnected with a message. -* When a client connects to a chat room, the server broadcasts "{nickname} joined the room." to clients that were already connected to the same chat room. +* When a client connects to a chat room, the server broadcasts "{nickname} has joined the room." to clients that were already connected to the same chat room. * When a client disconnects, the server broadcasts "{nickname} has left the room." to clients connected to the same chat room. * An unlimited amount of chat rooms can be created on the server (unless it runs out of memory or file descriptors). * An unlimited amount of clients can join each chat room on the server (unless it runs out of memory or file descriptors). +* Only one connection per unique nickname allowed per chat room. +* The server should provide an optional idle timeout setting. If a user doesn't interact withing n-seconds, the user should be automatically disconnected. +* The server should provide an optional parameter to limit connections to the server. +* The server should provide an optional parameter to limit the number of rooms created. +* Heartbeat (alive) and statistics should be provided via http:// API endpoints. -Additional objectives: - -* Only one connection per nickname allowed per chat room. -* The server should provide an idle timeout setting. If a user doesn't interact withing n-seconds, the user should be automatically disconnected. -* The server should provide a parameter to limit connections and number of rooms. -* Alive and statistics should be provided by http:// API endpoints. -* Chat history for each room should be stored in a file for each chat. When the user logs in to a room, the history should be provided to the client. A max history option should be provided. +Future objectives: -For TODOs, please see TODO.md +* Chat history for each room should be stored in a file. When the user logs in to a room, the history should be provided to the client. A max history option should be provided. +* More sophisticated client example code in html and js. ## Usage @@ -44,7 +46,6 @@ Server options: -L, --profiler_port PORT *PORT the profiler is listening on (default: off). -n, --connections MAX *MAX client connections allowed (default: unlimited). -r, --rooms MAX *MAX chatrooms allowed (default: unlimited). - -y, --history MAX *MAX num of history records per room (default: 15). -i, --idle MAX *MAX idle time in seconds allowed (default: unlimited). -X, --procs MAX *MAX processor cores to use from the machine. @@ -94,9 +95,10 @@ Spaces must be encoded in JSON calls. # ChatReqTypeListRooms = 103 /send {"reqType":103} -# Join a room or join a room with hidden name. +# Join a room # ChatReqTypeJoin = 104 /send {"roomName":"Your\ Room","reqType":104} +# or join a room with hidden name. /send {"roomName":"Your\ Room","reqType":104,"content":"hidden"} # Get a list of nicknames in a room. @@ -148,9 +150,7 @@ Date: Fri, 03 Apr 2015 17:29:17 +0000 Server: San Francisco X-Request-Id: DC8D9C2E-8161-4FC0-937F-4CA7037970D5 Content-Length: 0 - ``` - ## Building This code currently requires version 1.42 or higher of Go. @@ -176,10 +176,11 @@ A prebuilt docker image is available at (http://www.docker.com) [chattypantz](ht If you have docker installed, run: ``` docker pull composer22/chattypantz:latest +or +docker pull composer22/chattypantz: ``` See /docker directory README for more information on how to run it. - ## License (The MIT License) diff --git a/TODO.md b/TODO.md index dc0d5c4..3fe3fca 100644 --- a/TODO.md +++ b/TODO.md @@ -1,12 +1,5 @@ # OnDeck -- [ ] Server - stats aggregation + API -- [ ] Server - API for alive -- [ ] Front end code. -- [ ] Enable history in a room and load n records from that log. # Backlog -- [ ] Token login -- [ ] TLS access - -# Done - +- [ ] Enable history for each room and load n-ary records from that log. +- [ ] More sophisticated client example code. diff --git a/assets/img/chattypantz.png b/assets/img/chattypantz.png new file mode 100644 index 0000000..9cfbd37 Binary files /dev/null and b/assets/img/chattypantz.png differ diff --git a/chattypantz.go b/chattypantz.go index f11560e..5b96c2c 100644 --- a/chattypantz.go +++ b/chattypantz.go @@ -30,8 +30,6 @@ func main() { flag.IntVar(&opts.MaxConns, "--connections", server.DefaultMaxConns, "Maximum client connections allowed.") flag.IntVar(&opts.MaxRooms, "r", server.DefaultMaxRooms, "Maximum chat rooms allowed.") flag.IntVar(&opts.MaxRooms, "--rooms", server.DefaultMaxRooms, "Maximum chat rooms allowed.") - flag.IntVar(&opts.MaxHistory, "y", server.DefaultMaxHistory, "Maximum chat room history allowed.") - flag.IntVar(&opts.MaxHistory, "--history", server.DefaultMaxHistory, "Maximum chat room history allowed.") flag.IntVar(&opts.MaxIdle, "i", server.DefaultMaxIdle, "Maximum client idle allowed.") flag.IntVar(&opts.MaxIdle, "--idle", server.DefaultMaxIdle, "Maximum client idle allowed.") flag.IntVar(&opts.MaxProcs, "X", server.DefaultMaxProcs, "Maximum processor cores to use.") diff --git a/client/EchoTest.html b/client/EchoTest.html deleted file mode 100644 index 2608acd..0000000 --- a/client/EchoTest.html +++ /dev/null @@ -1,40 +0,0 @@ - - - - -

WebSocket Echo Test

-
-

- Message: -

-
- - - diff --git a/server/chat_room.go b/server/chat_room.go index 25570fd..43e5e9f 100644 --- a/server/chat_room.go +++ b/server/chat_room.go @@ -129,33 +129,50 @@ func (r *ChatRoom) message(q *ChatRequest) { // leave removes the chatter from the room and notifies the group the chatter has left. func (r *ChatRoom) leave(q *ChatRequest) { name := q.Who.nickname + r.mu.Lock() delete(r.chatters, q.Who) + r.mu.Unlock() r.sendResponse(q.Who, ChatRspTypeLeave, fmt.Sprintf(`You have left room "%s".`, r.name), nil) r.sendResponseAll(ChatRspTypeLeave, fmt.Sprintf("%s has left the room.", name), nil) } // ChatRoomStats is a simple structure for returning statistic information on the room. type ChatRoomStats struct { - Name string `json:"name"` // The name of the room. - Start time.Time `json:"start"` // The start time of the room. - LastReq time.Time `json:"lastReq"` // The last request time to the room. - LastRsp time.Time `json:"lastRsp"` // The last response time from the room. - ReqCount uint64 `json:"reqcount"` // Total requests received. - RspCount uint64 `json:"rspCount"` // Total responses sent. + Name string `json:"name"` // The name of the room. + Start time.Time `json:"start"` // The start time of the room. + LastReq time.Time `json:"lastReq"` // The last request time to the room. + LastRsp time.Time `json:"lastRsp"` // The last response time from the room. + ReqCount uint64 `json:"reqcount"` // Total requests received. + RspCount uint64 `json:"rspCount"` // Total responses sent. + Chatters []*ChatRoomChatterStat `json:"chatters"` // Stats on chatters in the room +} + +type ChatRoomChatterStat struct { + Nickname string `json:"nickname"` // The nickname of the chatter. + RemoteAddr string `json:"remoteAddr"` // The remote IP and port of the chatter. } // stats returns status information on the room. func (r *ChatRoom) stats() *ChatRoomStats { r.mu.Lock() defer r.mu.Unlock() - return &ChatRoomStats{ + s := &ChatRoomStats{ Name: r.name, Start: r.start, LastReq: r.lastReq, LastRsp: r.lastRsp, ReqCount: r.reqCount, RspCount: r.rspCount, + Chatters: []*ChatRoomChatterStat{}, + } + for c := range r.chatters { + st := c.stats() + s.Chatters = append(s.Chatters, &ChatRoomChatterStat{ + Nickname: st.Nickname, + RemoteAddr: st.RemoteAddr, + }) } + return s } // isMember validates if the member exists in the room. @@ -177,6 +194,7 @@ func (r *ChatRoom) isMemberName(n string) bool { // sendResponse sends a message to a single chatter in the room. func (r *ChatRoom) sendResponse(c *Chatter, rt int, ct string, l []string) { c.mu.Lock() + defer c.mu.Unlock() if c.connected { if l == nil { l = []string{} @@ -189,7 +207,6 @@ func (r *ChatRoom) sendResponse(c *Chatter, rt int, ct string, l []string) { c.rspq <- rsp } } - c.mu.Unlock() } // sendResponseAll sends a message to all chatters in the room. diff --git a/server/chat_room_manager.go b/server/chat_room_manager.go index a0e3215..bb8f11c 100644 --- a/server/chat_room_manager.go +++ b/server/chat_room_manager.go @@ -9,9 +9,10 @@ import ( // ChatRoomManager represents a hub of chat rooms for the server. type ChatRoomManager struct { rooms map[string]*ChatRoom // A list of rooms on the server. - maxRooms int // Maximum number of rooms allowed to be created + maxRooms int // Maximum number of rooms allowed to be created. log *ChatLogger // Application log for events. - wg sync.WaitGroup // Synchronizer for manager reqq + wg sync.WaitGroup // Synchronizer for manager reqq. + mu sync.Mutex // Lock for update. } // ChatRoomManagerNew is a factory function that returns a new instance of a chat room manager. @@ -25,6 +26,8 @@ func ChatRoomManagerNew(n int, cl *ChatLogger) *ChatRoomManager { // list returns a list of chat room names. func (m *ChatRoomManager) list() []string { + m.mu.Lock() + defer m.mu.Unlock() var names []string for n := range m.rooms { names = append(names, n) @@ -34,7 +37,9 @@ func (m *ChatRoomManager) list() []string { // find will find a chat room for a given name. func (m *ChatRoomManager) find(n string) (*ChatRoom, error) { + m.mu.Lock() r, ok := m.rooms[n] + m.mu.Unlock() if !ok { return nil, errors.New(fmt.Sprintf(`Chatroom "%s" not found.`, n)) } @@ -49,8 +54,10 @@ func (m *ChatRoomManager) findCreate(n string) (*ChatRoom, error) { return nil, errors.New("Maximum number of rooms reached. Cannot create new room.") } r = ChatRoomNew(n, m.log, &m.wg) + m.mu.Lock() m.rooms[n] = r m.wg.Add(1) + m.mu.Unlock() go r.Run() } return r, nil @@ -58,6 +65,8 @@ func (m *ChatRoomManager) findCreate(n string) (*ChatRoom, error) { // removeChatterAllRooms releases the chatter from any rooms. func (m *ChatRoomManager) removeChatterAllRooms(c *Chatter) { + m.mu.Lock() + defer m.mu.Unlock() for _, r := range m.rooms { if q, err := ChatRequestNew(c, r.name, ChatReqTypeLeave, ""); err == nil { r.reqq <- q @@ -67,6 +76,8 @@ func (m *ChatRoomManager) removeChatterAllRooms(c *Chatter) { // getRoomStats returns statistics from each room. func (m *ChatRoomManager) getRoomStats() []*ChatRoomStats { + m.mu.Lock() + defer m.mu.Unlock() var s = []*ChatRoomStats{} for _, r := range m.rooms { s = append(s, r.stats()) @@ -76,6 +87,8 @@ func (m *ChatRoomManager) getRoomStats() []*ChatRoomStats { // shutDownRooms releases all rooms from processing and memory. func (m *ChatRoomManager) shutDownRooms() { + m.mu.Lock() + defer m.mu.Unlock() // Close the channel which signals a stop run for _, r := range m.rooms { close(r.reqq) diff --git a/server/chatter.go b/server/chatter.go index 010f156..9574016 100644 --- a/server/chatter.go +++ b/server/chatter.go @@ -163,7 +163,7 @@ func (c *Chatter) listRooms() { // ChatterStats is a simple structure for returning statistic information on the chatter. type ChatterStats struct { Nickname string `json:"nickname"` // The nickname of the chatter. - RemoteAddr string `json:"remoteAddr"` // The remote IP and port of the chatter + RemoteAddr string `json:"remoteAddr"` // The remote IP and port of the chatter. Start time.Time `json:"start"` // The start time of the chatter. LastReq time.Time `json:"lastReq"` // The last request time from the chatter. LastRsp time.Time `json:"lastRsp"` // The last response time to the chatter. diff --git a/server/const.go b/server/const.go index d68f583..7375783 100644 --- a/server/const.go +++ b/server/const.go @@ -1,15 +1,14 @@ package server const ( - version = "0.1.0" // Application and server version. - DefaultHostname = "localhost" // The hostname of the server. - DefaultPort = 6660 // Port to receive requests: see IANA Port Numbers. - DefaultProfPort = 0 // Profiler port to receive requests. * - DefaultMaxConns = 0 // Maximum number of connections allowed. * - DefaultMaxRooms = 0 // Maximum number of chat rooms allowed. * - DefaultMaxHistory = 15 // Maximum number of chat history records per room. - DefaultMaxIdle = 0 // Maximum idle seconds per user connection. * - DefaultMaxProcs = 0 // Maximum number of computer processors to utilize. * + version = "0.1.0" // Application and server version. + DefaultHostname = "localhost" // The hostname of the server. + DefaultPort = 6660 // Port to receive requests: see IANA Port Numbers. + DefaultProfPort = 0 // Profiler port to receive requests. * + DefaultMaxConns = 0 // Maximum number of connections allowed. * + DefaultMaxRooms = 0 // Maximum number of chat rooms allowed. * + DefaultMaxIdle = 0 // Maximum idle seconds per user connection. * + DefaultMaxProcs = 0 // Maximum number of computer processors to utilize. * // * zeros = no change or no limitation or not enabled. diff --git a/server/info.go b/server/info.go index 91d7b30..96747e1 100644 --- a/server/info.go +++ b/server/info.go @@ -4,17 +4,16 @@ import "encoding/json" // Info provides basic config information to/about the running server. type Info struct { - Version string `json:"version"` // Version of the server. - Name string `json:"name"` // The name of the server. - Hostname string `json:"hostname"` // The hostname of the server. - UUID string `json:"UUID"` // Unique ID of the server. - Port int `json:"port"` // Port the server is listening on. - ProfPort int `json:"profPort"` // Profiler port the server is listening on. - MaxConns int `json:"maxConns"` // The maximum concurrent clients accepted. - MaxRooms int `json:"maxRooms"` // The maximum number of chat rooms allowed. - MaxHistory int `json:"maxHistory"` // The maximum number of history recs to retain per room. - MaxIdle int `json:"maxIdle"` // The maximum client idle time in seconds before disconnect. - Debug bool `json:"debugEnabled"` // Is debugging enabled on the server. + Version string `json:"version"` // Version of the server. + Name string `json:"name"` // The name of the server. + Hostname string `json:"hostname"` // The hostname of the server. + UUID string `json:"UUID"` // Unique ID of the server. + Port int `json:"port"` // Port the server is listening on. + ProfPort int `json:"profPort"` // Profiler port the server is listening on. + MaxConns int `json:"maxConns"` // The maximum concurrent clients accepted. + MaxRooms int `json:"maxRooms"` // The maximum number of chat rooms allowed. + MaxIdle int `json:"maxIdle"` // The maximum client idle time in seconds before disconnect. + Debug bool `json:"debugEnabled"` // Is debugging enabled on the server. } // InfoNew is a factory function that returns a new instance of Info. diff --git a/server/info_test.go b/server/info_test.go index 998f318..4282801 100644 --- a/server/info_test.go +++ b/server/info_test.go @@ -9,7 +9,7 @@ import ( const ( testInfoExpectedJSONResult = `{"version":"9.8.7","name":"Test Server","hostname":"0.0.0.0",` + `"UUID":"ABCDEFGHIJKLMNOPQRSTUVWXYZ","port":6661,"profPort":6061,"maxConns":999,` + - `"maxRooms":888,"maxHistory":777,"maxIdle":666,"debugEnabled":true}` + `"maxRooms":888,"maxIdle":777,"debugEnabled":true}` ) func TestInfoNew(t *testing.T) { @@ -22,8 +22,7 @@ func TestInfoNew(t *testing.T) { i.ProfPort = 6061 i.MaxConns = 999 i.MaxRooms = 888 - i.MaxHistory = 777 - i.MaxIdle = 666 + i.MaxIdle = 777 i.Debug = true }) tp := reflect.TypeOf(info) @@ -55,8 +54,7 @@ func TestInfoString(t *testing.T) { i.ProfPort = 6061 i.MaxConns = 999 i.MaxRooms = 888 - i.MaxHistory = 777 - i.MaxIdle = 666 + i.MaxIdle = 777 i.Debug = true }) actual := fmt.Sprint(info) diff --git a/server/options.go b/server/options.go index deb3800..93e5e84 100644 --- a/server/options.go +++ b/server/options.go @@ -5,16 +5,15 @@ import "encoding/json" // Options represents parameters that are passed to the application to be used in constructing // the server. type Options struct { - Name string `json:"name"` // The name of the server. - Hostname string `json:"hostname"` // The hostname of the server. - Port int `json:"port"` // The default port of the server. - ProfPort int `json:"profPort"` // The profiler port of the server. - MaxConns int `json:"maxConns"` // The maximum concurrent clients accepted. - MaxRooms int `json:"maxRooms"` // The maximum number of chat rooms allowed. - MaxHistory int `json:"maxHistory"` // The maximum number of history to retain per room. - MaxIdle int `json:"maxIdle"` // The maximum client idle time in seconds before disconnect. - MaxProcs int `json:"maxProcs"` // The maximum number of processor cores available. - Debug bool `json:"debugEnabled"` // Is debugging enabled in the application or server. + Name string `json:"name"` // The name of the server. + Hostname string `json:"hostname"` // The hostname of the server. + Port int `json:"port"` // The default port of the server. + ProfPort int `json:"profPort"` // The profiler port of the server. + MaxConns int `json:"maxConns"` // The maximum concurrent clients accepted. + MaxRooms int `json:"maxRooms"` // The maximum number of chat rooms allowed. + MaxIdle int `json:"maxIdle"` // The maximum client idle time in seconds before disconnect. + MaxProcs int `json:"maxProcs"` // The maximum number of processor cores available. + Debug bool `json:"debugEnabled"` // Is debugging enabled in the application or server. } // String is an implentation of the Stringer interface so the structure is returned as a string diff --git a/server/options_test.go b/server/options_test.go index 74fd2d2..c7d4aa5 100644 --- a/server/options_test.go +++ b/server/options_test.go @@ -7,23 +7,21 @@ import ( const ( testOptionsExpectedJSONResult = `{"name":"Test Options","hostname":"0.0.0.0","port":6661,` + - `"profPort":6061,"maxConns":1001,"maxRooms":999,"maxHistory":888,"maxIdle":777,` + - `"maxProcs":666,"debugEnabled":true}` + `"profPort":6061,"maxConns":1001,"maxRooms":999,"maxIdle":888,"maxProcs":777,"debugEnabled":true}` ) func TestOptionsString(t *testing.T) { t.Parallel() opts := &Options{ - Name: "Test Options", - Hostname: "0.0.0.0", - Port: 6661, - ProfPort: 6061, - MaxConns: 1001, - MaxRooms: 999, - MaxHistory: 888, - MaxIdle: 777, - MaxProcs: 666, - Debug: true, + Name: "Test Options", + Hostname: "0.0.0.0", + Port: 6661, + ProfPort: 6061, + MaxConns: 1001, + MaxRooms: 999, + MaxIdle: 888, + MaxProcs: 777, + Debug: true, } actual := fmt.Sprint(opts) if actual != testOptionsExpectedJSONResult { diff --git a/server/server.go b/server/server.go index ddfeb0e..7a20da4 100644 --- a/server/server.go +++ b/server/server.go @@ -23,16 +23,17 @@ import ( // Server is the main structure that represents a server instance. type Server struct { - info *Info // Basic server information used to run the server. - opts *Options // Original options used to create the server. - stats *Stats // Server statistics since it started. - mu sync.Mutex // For locking access to server attributes. - running bool // Is the server running? - log *ChatLogger // Log instance for recording error and other messages. - roomMngr *ChatRoomManager // Manager of chat rooms. - done chan bool // A channel to signal to web socked to close. - srvr *http.Server // HTTP server. - wg sync.WaitGroup // Synchronization of channel close. + info *Info // Basic server information used to run the server. + opts *Options // Original options used to create the server. + stats *Stats // Server statistics since it started. + mu sync.Mutex // For locking access to server attributes. + running bool // Is the server running? + log *ChatLogger // Log instance for recording error and other messages. + roomMngr *ChatRoomManager // Manager of chat rooms. + chatters map[*Chatter]bool // A list of chatters connected to the server. + done chan bool // A channel to signal to web socked to close. + srvr *http.Server // HTTP server. + wg sync.WaitGroup // Synchronization of channel close. } // New is a factory function that returns a new server instance. @@ -45,14 +46,14 @@ func New(ops *Options) *Server { i.ProfPort = ops.ProfPort i.MaxConns = ops.MaxConns i.MaxRooms = ops.MaxRooms - i.MaxHistory = ops.MaxHistory i.MaxIdle = ops.MaxIdle i.Debug = ops.Debug }), - opts: ops, - stats: StatsNew(), - log: ChatLoggerNew(), - running: false, + opts: ops, + stats: StatsNew(), + log: ChatLoggerNew(), + chatters: map[*Chatter]bool{}, + running: false, } if s.info.Debug { @@ -169,7 +170,13 @@ func (s *Server) chatHandler(ws *websocket.Conn) { s.log.LogConnect(ws.Request()) s.incrementStats(ws.Request()) c := ChatterNew(s, ws) + s.mu.Lock() + s.chatters[c] = true // register chatter + s.mu.Unlock() c.Run() + s.mu.Lock() + delete(s.chatters, c) // unregister chatter + s.mu.Unlock() } // aliveHandler handles a client http:// "is the server alive?" request. @@ -186,6 +193,10 @@ func (s *Server) statsHandler(w http.ResponseWriter, r *http.Request) { s.initResponseHeader(w) s.mu.Lock() defer s.mu.Unlock() + s.stats.ChatterStats = []*ChatterStats{} + for c := range s.chatters { + s.stats.ChatterStats = append(s.stats.ChatterStats, c.stats()) + } s.stats.RoomStats = s.roomMngr.getRoomStats() mStats := &runtime.MemStats{} runtime.ReadMemStats(mStats) diff --git a/server/server_test.go b/server/server_test.go index 2432948..4421d4e 100644 --- a/server/server_test.go +++ b/server/server_test.go @@ -12,15 +12,14 @@ import ( ) const ( - testServerHostname = "localhost" - testServerPort = 6660 - testServerMaxConns = 4 - testServerMaxRooms = 2 - testServerMaxHistory = 5 - testChatRoomName1 = "Room1" - testChatRoomName2 = "Room2" - testChatRoomName3 = "Room3" - testChatterNickname = "ChatMonkey" + testServerHostname = "localhost" + testServerPort = 6660 + testServerMaxConns = 4 + testServerMaxRooms = 2 + testChatRoomName1 = "Room1" + testChatRoomName2 = "Room2" + testChatRoomName3 = "Room3" + testChatterNickname = "ChatMonkey" ) var ( @@ -60,11 +59,12 @@ var ( ChatRspTypeListRooms, testChatRoomName1) TestServerJoin = fmt.Sprintf(`{"roomName":"%s","reqType":%d}`, testChatRoomName1, ChatReqTypeJoin) - TestServerJoinHidden = fmt.Sprintf(`{"roomName":"%s","reqType":%d,"content":"hidden"}`, testChatRoomName1, ChatReqTypeJoin) - TestServerJoinErr = fmt.Sprintf(`{"roomName":"","reqType":%d}`, ChatReqTypeJoin) - TestServerJoin2 = fmt.Sprintf(`{"roomName":"%s","reqType":%d}`, testChatRoomName2, ChatReqTypeJoin) - TestServerJoin3 = fmt.Sprintf(`{"roomName":"%s","reqType":%d}`, testChatRoomName3, ChatReqTypeJoin) - TestServerJoinExp = fmt.Sprintf(`{"roomName":"%s","rspType":%d,`+ + TestServerJoinHidden = fmt.Sprintf(`{"roomName":"%s","reqType":%d,"content":"hidden"}`, + testChatRoomName1, ChatReqTypeJoin) + TestServerJoinErr = fmt.Sprintf(`{"roomName":"","reqType":%d}`, ChatReqTypeJoin) + TestServerJoin2 = fmt.Sprintf(`{"roomName":"%s","reqType":%d}`, testChatRoomName2, ChatReqTypeJoin) + TestServerJoin3 = fmt.Sprintf(`{"roomName":"%s","reqType":%d}`, testChatRoomName3, ChatReqTypeJoin) + TestServerJoinExp = fmt.Sprintf(`{"roomName":"%s","rspType":%d,`+ `"content":"%s has joined the room.","list":[]}`, testChatRoomName1, ChatRspTypeJoin, testChatterNickname) TestServerJoinExp2 = fmt.Sprintf(`{"roomName":"%s","rspType":%d,`+ `"content":"%s has joined the room.","list":[]}`, testChatRoomName2, ChatRspTypeJoin, testChatterNickname) @@ -121,16 +121,15 @@ func tTestIncrRoomStats() { func TestServerStartup(t *testing.T) { opts := &Options{ - Name: "Test Server", - Hostname: testServerHostname, - Port: testServerPort, - ProfPort: 6060, - MaxConns: testServerMaxConns, - MaxRooms: testServerMaxRooms, - MaxHistory: testServerMaxHistory, - MaxIdle: 0, - MaxProcs: 1, - Debug: true, + Name: "Test Server", + Hostname: testServerHostname, + Port: testServerPort, + ProfPort: 6060, + MaxConns: testServerMaxConns, + MaxRooms: testServerMaxRooms, + MaxIdle: 0, + MaxProcs: 1, + Debug: true, } runtime.GOMAXPROCS(1) testSrvr = New(opts) @@ -314,7 +313,8 @@ func TestServerValidWSSession(t *testing.T) { } result = string(rsp[:n]) if result != TestServerListNamesExp0 { - t.Errorf("Test hidden nickname. Get list of names error.\nExpected: %s\n\nActual: %s\n", TestServerListNamesExp0, result) + t.Errorf("Test hidden nickname. Get list of names error.\nExpected: %s\n\nActual: %s\n", + TestServerListNamesExp0, result) } // Unhide nickname @@ -354,7 +354,8 @@ func TestServerValidWSSession(t *testing.T) { } result = string(rsp[:n]) if result != TestServerListNamesExp1 { - t.Errorf("Test unhidden nickname. Get list of names error.\nExpected: %s\n\nActual: %s\n", TestServerListNamesExp1, result) + t.Errorf("Test unhidden nickname. Get list of names error.\nExpected: %s\n\nActual: %s\n", + TestServerListNamesExp1, result) } // Send a message to the room. @@ -415,7 +416,8 @@ func TestServerValidWSSession(t *testing.T) { } result = string(rsp[:n]) if result != TestServerListNamesExp0 { - t.Errorf("Test leave room. Get list of names error.\nExpected: %s\n\nActual: %s\n", TestServerListNamesExp0, result) + t.Errorf("Test leave room. Get list of names error.\nExpected: %s\n\nActual: %s\n", + TestServerListNamesExp0, result) } // Join the room as hidden @@ -455,7 +457,8 @@ func TestServerValidWSSession(t *testing.T) { } result = string(rsp[:n]) if result != TestServerListNamesExp0 { - t.Errorf("Test hidden nickname. Get list of names error.\nExpected: %s\n\nActual: %s\n", TestServerListNamesExp0, result) + t.Errorf("Test hidden nickname. Get list of names error.\nExpected: %s\n\nActual: %s\n", + TestServerListNamesExp0, result) } // Leave the room @@ -486,7 +489,8 @@ func TestServerValidWSSession(t *testing.T) { // Validate Chat Room statistics from this session. s := rm.stats() if s.Name != testChatRoomName1 { - t.Errorf("Room stats error. Name is incorrect. \nExpected: %s\n\nActual: %s\n", testChatRoomName1, s.Name) + t.Errorf("Room stats error. Name is incorrect. \nExpected: %s\n\nActual: %s\n", + testChatRoomName1, s.Name) } if s.Start.Before(testRoomStartTime) || s.Start.Equal(testRoomStartTime) { t.Errorf("Room stats error. Start Time is out of range.") @@ -498,10 +502,12 @@ func TestServerValidWSSession(t *testing.T) { t.Errorf("Room stats error. Last Response Time is out of range.") } if s.ReqCount != testRoomReqs { - t.Errorf("Room stats error. ReqCount is incorrect.\nExpected: %d\n\nActual: %d\n", testRoomReqs, s.ReqCount) + t.Errorf("Room stats error. ReqCount is incorrect.\nExpected: %d\n\nActual: %d\n", + testRoomReqs, s.ReqCount) } if s.RspCount != testRoomRsps { - t.Errorf("Room stats error. RsqCount is incorrect.\nExpected: %d\n\nActual: %d\n", testRoomRsps, s.RspCount) + t.Errorf("Room stats error. RsqCount is incorrect.\nExpected: %d\n\nActual: %d\n", + testRoomRsps, s.RspCount) } // Validate Chatter statistics for this session. @@ -522,10 +528,12 @@ func TestServerValidWSSession(t *testing.T) { t.Errorf("Chatter stats error. Last Response Time is out of range.") } if cs.ReqCount != testChatterReqs { - t.Errorf("Chatter stats error. ReqCount is incorrect.\nExpected: %d\n\nActual: %d\n", testChatterReqs, cs.ReqCount) + t.Errorf("Chatter stats error. ReqCount is incorrect.\nExpected: %d\n\nActual: %d\n", + testChatterReqs, cs.ReqCount) } if cs.RspCount != testChatterRsps { - t.Errorf("Chatter stats error. RsqCount is incorrect.\nExpected: %d\n\nActual: %d\n", testChatterRsps, cs.RspCount) + t.Errorf("Chatter stats error. RsqCount is incorrect.\nExpected: %d\n\nActual: %d\n", + testChatterRsps, cs.RspCount) } } @@ -560,7 +568,8 @@ func TestServerWSErrorSession(t *testing.T) { } result := string(rsp[:n]) if result != TestServerSetNicknameExpErr { - t.Errorf("Set nickname did not receive an error.\nExpected: %s\n\nActual: %s\n", TestServerSetNicknameExpErr, result) + t.Errorf("Set nickname did not receive an error.\nExpected: %s\n\nActual: %s\n", + TestServerSetNicknameExpErr, result) } // Join a room test err conditions @@ -578,7 +587,8 @@ func TestServerWSErrorSession(t *testing.T) { } result = string(rsp[:n]) if result != TestServerJoinExpErr { - t.Errorf("Join room did not receive an error.\nExpected: %s\n\nActual: %s\n", TestServerJoinExpErr, result) + t.Errorf("Join room did not receive an error.\nExpected: %s\n\nActual: %s\n", + TestServerJoinExpErr, result) } // Set nickname correctly for user 1 @@ -596,7 +606,8 @@ func TestServerWSErrorSession(t *testing.T) { } result = string(rsp[:n]) if result != TestServerSetNicknameExp { - t.Errorf("Set nickname received an error.\nExpected: %s\n\nActual: %s\n", TestServerSetNicknameExp, result) + t.Errorf("Set nickname received an error.\nExpected: %s\n\nActual: %s\n", + TestServerSetNicknameExp, result) } // Set nickname user 2 same as user 1 @@ -614,7 +625,8 @@ func TestServerWSErrorSession(t *testing.T) { } result = string(rsp[:n]) if result != TestServerSetNicknameExp { - t.Errorf("Set nickname received an error.\nExpected: %s\n\nActual: %s\n", TestServerSetNicknameExp, result) + t.Errorf("Set nickname received an error.\nExpected: %s\n\nActual: %s\n", + TestServerSetNicknameExp, result) } // User 1 joins room 1 @@ -650,7 +662,8 @@ func TestServerWSErrorSession(t *testing.T) { } result = string(rsp[:n]) if result != TestServerJoinExpErrX2 { - t.Errorf("Join 2X should have received an error.\nExpected: %s\n\nActual: %s\n", TestServerJoinExpErrX2, result) + t.Errorf("Join 2X should have received an error.\nExpected: %s\n\nActual: %s\n", + TestServerJoinExpErrX2, result) } // Hide user 1 nickname from room. @@ -668,7 +681,8 @@ func TestServerWSErrorSession(t *testing.T) { } result = string(rsp[:n]) if result != TestServerHideNicknameExp { - t.Errorf("Hide nickname received an error.\nExpected: %s\n\nActual: %s\n", TestServerHideNicknameExp, result) + t.Errorf("Hide nickname received an error.\nExpected: %s\n\nActual: %s\n", + TestServerHideNicknameExp, result) } // Posting ability should be disabled if name is hidden. @@ -686,7 +700,8 @@ func TestServerWSErrorSession(t *testing.T) { } result = string(rsp[:n]) if result != TestServerMsgExpErrHide { - t.Errorf("Message with hidded name should have received an error.\nExpected: %s\n\nActual: %s\n", TestServerMsgExpErrHide, result) + t.Errorf("Message with hidded name should have received an error.\nExpected: %s\n\nActual: %s\n", + TestServerMsgExpErrHide, result) } // Nickname already used in room should prevent joining. User 2 joins room 1 w/ same name User 1 @@ -704,7 +719,8 @@ func TestServerWSErrorSession(t *testing.T) { } result = string(rsp[:n]) if result != TestServerJoinExpErrSame { - t.Errorf("Join with same name should have received an error.\nExpected: %s\n\nActual: %s\n", TestServerJoinExpErrSame, result) + t.Errorf("Join with same name should have received an error.\nExpected: %s\n\nActual: %s\n", + TestServerJoinExpErrSame, result) } // Should not be able to grow room limitation. @@ -722,7 +738,8 @@ func TestServerWSErrorSession(t *testing.T) { } result = string(rsp[:n]) if result != TestServerJoinExp2 { - t.Errorf("Join should not receive an error.\nExpected: %s\n\nActual: %s\n", TestServerJoinExp2, result) + t.Errorf("Join should not receive an error.\nExpected: %s\n\nActual: %s\n", + TestServerJoinExp2, result) } if _, err := ws1.Write([]byte(TestServerJoin3)); err != nil { @@ -739,7 +756,8 @@ func TestServerWSErrorSession(t *testing.T) { } result = string(rsp[:n]) if result != TestServerJoinExpErrRoom { - t.Errorf("Join should have received an error.\nExpected: %s\n\nActual: %s\n", TestServerJoinExpErrRoom, result) + t.Errorf("Join should have received an error.\nExpected: %s\n\nActual: %s\n", + TestServerJoinExpErrRoom, result) } // Test Max timeout @@ -781,7 +799,8 @@ func TestHTTPRoutes(t *testing.T) { t.Errorf("/alive body should be empty.") } if r.StatusCode != http.StatusOK { - t.Errorf("/alive returned invalid status code.\nExpected: %d\n\nActual: %d\n", http.StatusOK, r.StatusCode) + t.Errorf("/alive returned invalid status code.\nExpected: %d\n\nActual: %d\n", + http.StatusOK, r.StatusCode) } rq, _ = http.NewRequest("GET", testSrvrURLStats, nil) @@ -796,7 +815,8 @@ func TestHTTPRoutes(t *testing.T) { t.Errorf("/status body should not be empty.") } if r.StatusCode != http.StatusOK { - t.Errorf("/status returned invalid status code.\nExpected: %d\n\nActual: %d\n", http.StatusOK, r.StatusCode) + t.Errorf("/status returned invalid status code.\nExpected: %d\n\nActual: %d\n", + http.StatusOK, r.StatusCode) } } diff --git a/server/stats.go b/server/stats.go index b04bf7a..8558443 100644 --- a/server/stats.go +++ b/server/stats.go @@ -7,20 +7,22 @@ import ( // Stats contains runtime statistics for the server. type Stats struct { - Start time.Time `json:"startTime"` // The start time of the server. - ReqCount int64 `json:"reqCount"` // How many requests came in to the server. - ReqBytes int64 `json:"reqBytes"` // Size of the requests in bytes. - RouteStats map[string]map[string]int64 `json:"routeStats"` // How many requests/bytes came into each route. - RoomStats []*ChatRoomStats `json:"roomStats"` // How many requests etc came into each room. + Start time.Time `json:"startTime"` // The start time of the server. + ReqCount int64 `json:"reqCount"` // How many requests came in to the server. + ReqBytes int64 `json:"reqBytes"` // Size of the requests in bytes. + RouteStats map[string]map[string]int64 `json:"routeStats"` // How many requests/bytes came into each route. + ChatterStats []*ChatterStats `json:"chatterStats"` // Statistics about each logged in chatter. + RoomStats []*ChatRoomStats `json:"roomStats"` // How many requests etc came into each room. } // StatsNew is a factory function that returns a new instance of statistics. // options is an optional list of functions that initialize the structure func StatsNew(opts ...func(*Stats)) *Stats { s := &Stats{ - Start: time.Now(), - RouteStats: make(map[string]map[string]int64), - RoomStats: []*ChatRoomStats{}, + Start: time.Now(), + RouteStats: make(map[string]map[string]int64), + ChatterStats: []*ChatterStats{}, + RoomStats: []*ChatRoomStats{}, } for _, f := range opts { f(s) diff --git a/server/stats_test.go b/server/stats_test.go index b1d6cea..d529c07 100644 --- a/server/stats_test.go +++ b/server/stats_test.go @@ -8,9 +8,9 @@ import ( ) const ( - testStatsExpectedJSONResult = `{"startTime":"2006-01-02T13:24:56Z","reqCount":0,"reqBytes":0,` + - `"routeStats":{"route1":{"requesBytes":202,"requestCounts":101},"route2":{"requesBytes":204,` + - `"requestCounts":103}},"roomStats":[]}` + testStatsExpectedJSONResult = `{"startTime":"2006-01-02T13:24:56Z","reqCount":0,` + + `"reqBytes":0,"routeStats":{"route1":{"requesBytes":202,"requestCounts":101},` + + `"route2":{"requesBytes":204,"requestCounts":103}},"chatterStats":[],"roomStats":[]}` ) func TestStatsNew(t *testing.T) { diff --git a/server/usage.go b/server/usage.go index 91564b7..4bf8883 100644 --- a/server/usage.go +++ b/server/usage.go @@ -17,7 +17,6 @@ Server options: -L, --profiler_port PORT *PORT the profiler is listening on (default: off). -n, --connections MAX *MAX client connections allowed (default: unlimited). -r, --rooms MAX *MAX chatrooms allowed (default: unlimited). - -y, --history MAX *MAX num of history records per room (default: 15). -i, --idle MAX *MAX idle time in seconds allowed (default: unlimited). -X, --procs MAX *MAX processor cores to use from the machine. diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..57da094 --- /dev/null +++ b/test/README.md @@ -0,0 +1,22 @@ +## Additional Test Scripts and Applications + +This folder contains scripts and files for testing requests against the server from the client side. + +### Dark Websocket Terminal + +https://github.com/cyberixae/dwst + +A chrome extention/application that allows you to send socket commands to the server. + +chattypantz.dark is a text file of commands to save typing. + +### Rested.App scripts + +see: http://www.helloresolven.com/portfolio/rested/ + +For http API testing. + +./rested/ + +- Alive.request - Validate alive ping is returning 200 OK. +- Status.request - Validate server is returning statistics, information and 200 OK. diff --git a/test/chattypantz.dark b/test/chattypantz.dark new file mode 100644 index 0000000..d95d86b --- /dev/null +++ b/test/chattypantz.dark @@ -0,0 +1,12 @@ +/connect ws://127.0.0.1:6660/v1.0/chat +/send {"reqType":101,"content":"ChatMonkey"} +/send {"reqType":102} +/send {"reqType":103} +/send {"roomName":"Your\ Room","reqType":104} +/send {"roomName":"Your\ Room","reqType":104,"content":"hidden"} +/send {"roomName":"Your\ Room","reqType":105} +/send {"roomName":"Your\ Room","reqType":106} +/send {"roomName":"Your\ Room","reqType":107} +/send {"roomName":"Your\ Room","reqType":108,"content":"Hello world!"} +/send {"roomName":"Your\ Room","reqType":109} +/disconnect diff --git a/test/rested/Alive.request b/test/rested/Alive.request new file mode 100644 index 0000000..04066c9 --- /dev/null +++ b/test/rested/Alive.request @@ -0,0 +1,47 @@ + + + + + baseURL + http://localhost:6660/v1.0/alive + followRedirect + + handleJSONPCallbacks + + headers + + + header + Accept + inUse + + value + application/json + + + header + Content-Type + inUse + + value + application/json + + + httpMethod + GET + jsonpScript + + paramBodyUIChoice + 0 + parameters + + parametersType + 1 + presentBeforeChallenge + + stringEncoding + 4 + usingHTTPBody + + + diff --git a/test/rested/Status.request b/test/rested/Status.request new file mode 100644 index 0000000..a5bb83a --- /dev/null +++ b/test/rested/Status.request @@ -0,0 +1,47 @@ + + + + + baseURL + http://localhost:6660/v1.0/stats + followRedirect + + handleJSONPCallbacks + + headers + + + header + Accept + inUse + + value + application/json + + + header + Content-Type + inUse + + value + application/json + + + httpMethod + GET + jsonpScript + + paramBodyUIChoice + 0 + parameters + + parametersType + 1 + presentBeforeChallenge + + stringEncoding + 4 + usingHTTPBody + + +