From a9e720e7abc1cd24445748a84d368d923a5f879b Mon Sep 17 00:00:00 2001 From: crusader Date: Mon, 10 Jul 2017 20:54:40 +0900 Subject: [PATCH] Project have been initialized. --- .gitignore | 65 ++++++++++++ .vscode/settings.json | 11 +++ glide.yaml | 11 +++ golint.json | 39 ++++++++ main.go | 50 ++++++++++ service/pool.go | 104 +++++++++++++++++++ websocket/config.go | 214 ++++++++++++++++++++++++++++++++++++++++ websocket/connection.go | 12 +++ websocket/server.go | 70 +++++++++++++ 9 files changed, 576 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/settings.json create mode 100644 glide.yaml create mode 100644 golint.json create mode 100644 main.go create mode 100644 service/pool.go create mode 100644 websocket/config.go create mode 100644 websocket/connection.go create mode 100644 websocket/server.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f67da3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,65 @@ +# Created by .ignore support plugin (hsz.mobi) +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff: +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/dictionaries + +# Sensitive or high-churn files: +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.xml +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml + +# Gradle: +.idea/**/gradle.xml +.idea/**/libraries + +# Mongo Explorer plugin: +.idea/**/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +### Go template +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ +.idea/ +*.iml + +vendor/ +glide.lock \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..46c249c --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,11 @@ +// Place your settings in this file to overwrite default and user settings. +{ + // Specifies Lint tool name. + "go.lintTool": "gometalinter", + + // Flags to pass to Lint tool (e.g. ["-min_confidence=.8"]) + "go.lintFlags": [ + "--config=${workspaceRoot}/golint.json" + ] + +} \ No newline at end of file diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 0000000..fb75d63 --- /dev/null +++ b/glide.yaml @@ -0,0 +1,11 @@ +package: git.loafle.net/overflow/overflow_websocket_service +import: +- package: github.com/satori/go.uuid + version: ^1.1.0 +- package: github.com/gorilla/websocket + version: ^1.2.0 +- package: google.golang.org/grpc + version: ^1.4.2 +- package: golang.org/x/net + subpackages: + - websocket diff --git a/golint.json b/golint.json new file mode 100644 index 0000000..d585e7c --- /dev/null +++ b/golint.json @@ -0,0 +1,39 @@ +{ + "DisableAll": true, + "Enable": [ + "aligncheck", + "deadcode", + "dupl", + "errcheck", + "gas", + "goconst", + "gocyclo", + "gofmt", + "goimports", + "golint", + "gotype", + "ineffassign", + "interfacer", + "lll", + "megacheck", + "misspell", + "structcheck", + "test", + "testify", + "unconvert", + "varcheck", + "vet", + "vetshadow" + ], + "Aggregate": true, + "Concurrency": 16, + "Cyclo": 60, + "Deadline": "60s", + "DuplThreshold": 50, + "EnableGC": true, + "LineLength": 120, + "MinConfidence": 0.8, + "MinOccurrences": 3, + "MinConstLength": 3, + "Sort": ["severity"] +} \ No newline at end of file diff --git a/main.go b/main.go new file mode 100644 index 0000000..32199c8 --- /dev/null +++ b/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "log" + + "google.golang.org/grpc" + + "git.loafle.net/overflow/overflow_websocket_service/service" +) + +var ( + host = "localhost:8080" +) + +func main() { + connPool, err := service.New(10, 30, func() (*grpc.ClientConn, error) { + return grpc.Dial(*contentGrpcAddr, grpc.WithDialer(dialer), grpc.WithInsecure()) + }) + if err != nil { + log.Fatalf("create service connection pool error: %v\n", err) + } + defer connPool.Destroy() + connPool.Ping = func(conn *grpc.ClientConn) bool { + // check connection status + return true + } + connPool.Close = func(conn *grpc.ClientConn) { + // close connection + conn.Close() + } + + // ws := websocket.New(websocket.Config{}) + // http.HandleFunc("/ws", ws.Handler()) + + // ws.OnConnection(handleWebsocketConnection) + + // http.ListenAndServe(host, nil) +} + +func handleWebsocketConnection(c websocket.Connection) { +} + +func callGrpcService(connPool *service.Pool) { + conn, err := connPool.Get() + if err != nil { + log.Printf("get connection error: %v\n", err) + } + // * Important + defer connPool.Put(conn) +} diff --git a/service/pool.go b/service/pool.go new file mode 100644 index 0000000..391fc0c --- /dev/null +++ b/service/pool.go @@ -0,0 +1,104 @@ +package service + +import ( + "fmt" + "sync" + + grpc "google.golang.org/grpc" +) + +type ConnectionGenerator func() (*grpc.ClientConn, error) +type ConnectionChecker func(*grpc.ClientConn) bool +type ConnectionCloser func(*grpc.ClientConn) + +type Pool struct { + New ConnectionGenerator + Ping ConnectionChecker + Close ConnectionCloser + Get func() (*grpc.ClientConn, error) + Put func(conn *grpc.ClientConn) + Destroy func() + store chan *grpc.ClientConn + mtx sync.Mutex + initCapacity int + maxCapacity int +} + +func New(initCap int, maxCap int, connector ConnectionGenerator) (*Pool, error) { + if initCap < 0 || maxCap <= 0 || initCap > maxCap { + return nil, fmt.Errorf("invalid capacity settings") + } + p := new(Pool) + p.store = make(chan *grpc.ClientConn, maxCap) + + if connector != nil { + p.New = connector + } + + for i := 0; i < initCap; i++ { + conn, err := p.create() + if err != nil { + return p, err + } + p.store <- conn + } + return p, nil +} + +func (p *Pool) Len() int { + return len(p.store) +} + +func (p *Pool) Get() (*grpc.ClientConn, error) { + if p.store == nil { + // pool aleardy destroyed, returns new connection + return p.create() + } + for { + select { + case conn := <-p.store: + if p.Ping != nil && p.Ping(conn) == false { + continue + } + return conn, nil + default: + return p.create() + } + } +} + +func (p *Pool) Put(conn *grpc.ClientConn) { + select { + case p.store <- conn: + return + default: + // pool is full, close passed connection + if p.Close != nil { + p.Close(conn) + } + return + } +} + +func (p *Pool) Destroy() { + p.mtx.Lock() + defer p.mtx.Unlock() + if p.store == nil { + // pool aleardy destroyed + return + } + close(p.store) + for conn := range p.store { + if p.Close != nil { + p.Close(conn) + } + } + p.store = nil +} + +func (p *Pool) create() (*grpc.ClientConn, error) { + if p.New == nil { + return nil, fmt.Errorf("Pool.New is nil, can not create connection") + } + return p.New() +} diff --git a/websocket/config.go b/websocket/config.go new file mode 100644 index 0000000..adfa389 --- /dev/null +++ b/websocket/config.go @@ -0,0 +1,214 @@ +package websocket + +import ( + "net/http" + "time" + + uuid "github.com/satori/go.uuid" +) + +const ( + // DefaultWriteTimeout is default value of Write Timeout + DefaultWriteTimeout = 0 + // DefaultReadTimeout is default value of Read Timeout + DefaultReadTimeout = 0 + // DefaultPongTimeout is default value of Pong Timeout + DefaultPongTimeout = 60 * time.Second + // DefaultPingPeriod is default value of Ping Period + DefaultPingPeriod = (DefaultPongTimeout * 9) / 10 + // DefaultMaxMessageSize is default value of Max Message Size + DefaultMaxMessageSize = 1024 + // DefaultReadBufferSize is default value of Read Buffer Size + DefaultReadBufferSize = 4096 + // DefaultWriteBufferSize is default value of Write Buffer Size + DefaultWriteBufferSize = 4096 +) + +var ( + // DefaultIDGenerator returns the UUID of the client + DefaultIDGenerator = func(*http.Request) string { return uuid.NewV4().String() } +) + +type ( + // OptionSetter sets a configuration field to the websocket config + // used to help developers to write less and configure only what they really want and nothing else + OptionSetter interface { + Set(c *Config) + } + + // OptionSet implements the OptionSetter + OptionSet func(c *Config) +) + +// Set is the func which makes the OptionSet an OptionSetter, this is used mostly +func (o OptionSet) Set(c *Config) { + o(c) +} + +// Config is configuration of the websocket server +type Config struct { + Error func(res http.ResponseWriter, req *http.Request, status int, reason error) + CheckOrigin func(req *http.Request) bool + WriteTimeout time.Duration + ReadTimeout time.Duration + PongTimeout time.Duration + PingPeriod time.Duration + MaxMessageSize int64 + BinaryMessages bool + ReadBufferSize int + WriteBufferSize int + IDGenerator func(*http.Request) string +} + +// Set is the func which makes the OptionSet an OptionSetter, this is used mostly +func (c Config) Set(main *Config) { + main.Error = c.Error + main.CheckOrigin = c.CheckOrigin + main.WriteTimeout = c.WriteTimeout + main.ReadTimeout = c.ReadTimeout + main.PongTimeout = c.PongTimeout + main.PingPeriod = c.PingPeriod + main.MaxMessageSize = c.MaxMessageSize + main.BinaryMessages = c.BinaryMessages + main.ReadBufferSize = c.ReadBufferSize + main.WriteBufferSize = c.WriteBufferSize + main.IDGenerator = c.IDGenerator +} + +// Error sets the error handler +func Error(val func(res http.ResponseWriter, req *http.Request, status int, reason error)) OptionSet { + return func(c *Config) { + c.Error = val + } +} + +// CheckOrigin sets a handler which will check if different origin(domains) are allowed to contact with +// the websocket server +func CheckOrigin(val func(req *http.Request) bool) OptionSet { + return func(c *Config) { + c.CheckOrigin = val + } +} + +// WriteTimeout time allowed to write a message to the connection. +// Default value is 15 * time.Second +func WriteTimeout(val time.Duration) OptionSet { + return func(c *Config) { + c.WriteTimeout = val + } +} + +// ReadTimeout time allowed to read a message from the connection. +// Default value is 15 * time.Second +func ReadTimeout(val time.Duration) OptionSet { + return func(c *Config) { + c.ReadTimeout = val + } +} + +// PongTimeout allowed to read the next pong message from the connection +// Default value is 60 * time.Second +func PongTimeout(val time.Duration) OptionSet { + return func(c *Config) { + c.PongTimeout = val + } +} + +// PingPeriod send ping messages to the connection with this period. Must be less than PongTimeout +// Default value is (PongTimeout * 9) / 10 +func PingPeriod(val time.Duration) OptionSet { + return func(c *Config) { + c.PingPeriod = val + } +} + +// MaxMessageSize max message size allowed from connection +// Default value is 1024 +func MaxMessageSize(val int64) OptionSet { + return func(c *Config) { + c.MaxMessageSize = val + } +} + +// BinaryMessages set it to true in order to denotes binary data messages instead of utf-8 text +// compatible if you wanna use the Connection's EmitMessage to send a custom binary data to the client, +// like a native server-client communication. +// defaults to false +func BinaryMessages(val bool) OptionSet { + return func(c *Config) { + c.BinaryMessages = val + } +} + +// ReadBufferSize is the buffer size for the underline reader +func ReadBufferSize(val int) OptionSet { + return func(c *Config) { + c.ReadBufferSize = val + } +} + +// WriteBufferSize is the buffer size for the underline writer +func WriteBufferSize(val int) OptionSet { + return func(c *Config) { + c.WriteBufferSize = val + } +} + +// IDGenerator used to create (and later on, set) +// an ID for each incoming websocket connections (clients). +// The request is an argument which you can use to generate the ID (from headers for example). +// If empty then the ID is generated by func: uuid.NewV4().String() +func IDGenerator(val func(*http.Request) string) OptionSet { + return func(c *Config) { + c.IDGenerator = val + } +} + +// Validate validates the configuration +func (c Config) Validate() Config { + + if c.WriteTimeout < 0 { + c.WriteTimeout = DefaultWriteTimeout + } + + if c.ReadTimeout < 0 { + c.ReadTimeout = DefaultReadTimeout + } + + if c.PongTimeout < 0 { + c.PongTimeout = DefaultPongTimeout + } + + if c.PingPeriod <= 0 { + c.PingPeriod = DefaultPingPeriod + } + + if c.MaxMessageSize <= 0 { + c.MaxMessageSize = DefaultMaxMessageSize + } + + if c.ReadBufferSize <= 0 { + c.ReadBufferSize = DefaultReadBufferSize + } + + if c.WriteBufferSize <= 0 { + c.WriteBufferSize = DefaultWriteBufferSize + } + + if c.Error == nil { + c.Error = func(res http.ResponseWriter, req *http.Request, status int, reason error) { + } + } + + if c.CheckOrigin == nil { + c.CheckOrigin = func(req *http.Request) bool { + return true + } + } + + if c.IDGenerator == nil { + c.IDGenerator = DefaultIDGenerator + } + + return c +} diff --git a/websocket/connection.go b/websocket/connection.go new file mode 100644 index 0000000..dffa33d --- /dev/null +++ b/websocket/connection.go @@ -0,0 +1,12 @@ +package websocket + +import ( + "net/http" + "sync" +) + +type connection struct { + id string + httpRequest http.Request + writeMTX sync.Mutex +} diff --git a/websocket/server.go b/websocket/server.go new file mode 100644 index 0000000..249df5d --- /dev/null +++ b/websocket/server.go @@ -0,0 +1,70 @@ +package websocket + +import "net/http" + +// Server is the websocket server, +// listens on the config's port, the critical part is the event OnConnection +type Server interface { + // Set sets an option aka configuration field to the websocket server + Set(...OptionSetter) + // Handler returns the http.Handler which is setted to the 'Websocket Endpoint path', + // the client should target to this handler's developer's custom path + // ex: http.Handle("/myendpoint", mywebsocket.Handler()) + // Handler calls the HandleConnection, so + // Use Handler or HandleConnection manually, DO NOT USE both. + // Note: you can always create your own upgrader which returns an UnderlineConnection and call only the HandleConnection manually (as Iris web framework does) + Handler() http.Handler + // HandleConnection creates & starts to listening to a new connection + // DO NOT USE Handler() and HandleConnection at the sametime, see Handler for more + // NOTE: You don't need this, this is needed only when we want to 'hijack' the upgrader + // (used for Iris and fasthttp before Iris v6) + HandleConnection(*http.Request, UnderlineConnection) + // OnConnection this is the main event you, as developer, will work with each of the websocket connections + OnConnection(cb ConnectionFunc) + + /* + connection actions, same as the connection's method, + but these methods accept the connection ID, + which is useful when the developer maps + this id with a database field (using config.IDGenerator). + */ + + // IsConnected returns true if the connection with that ID is connected to the server + // useful when you have defined a custom connection id generator (based on a database) + // and you want to check if that connection is already connected (on multiple tabs) + IsConnected(connID string) bool + + // Join joins a websocket client to a room, + // first parameter is the room name and the second the connection.ID() + // + // You can use connection.Join("room name") instead. + Join(roomName string, connID string) + + // LeaveAll kicks out a connection from ALL of its joined rooms + LeaveAll(connID string) + + // Leave leaves a websocket client from a room, + // first parameter is the room name and the second the connection.ID() + // + // You can use connection.Leave("room name") instead. + // Returns true if the connection has actually left from the particular room. + Leave(roomName string, connID string) bool + + // GetConnectionsByRoom returns a list of Connection + // are joined to this room. + GetConnectionsByRoom(roomName string) []Connection + + // Disconnect force-disconnects a websocket connection + // based on its connection.ID() + // What it does? + // 1. remove the connection from the list + // 2. leave from all joined rooms + // 3. fire the disconnect callbacks, if any + // 4. close the underline connection and return its error, if any. + // + // You can use the connection.Disconnect() instead. + Disconnect(connID string) error +} + +type server struct { +}