websocket: from 1k to 100k on a simple raspeberry pi 3 model b by using a bit lower level of the new ws lib api and restore the previous sync.Map for server's live connections, relative: https://github.com/kataras/iris/issues/1178

Former-commit-id: 40da148afb66a42d47285efce324269d66ed3b0e
This commit is contained in:
Gerasimos (Makis) Maropoulos 2019-02-18 04:42:57 +02:00
parent eb22309aec
commit 65c1fbf7f2
8 changed files with 295 additions and 96 deletions

View File

@ -6,6 +6,7 @@ import (
"math/rand" "math/rand"
"os" "os"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/kataras/iris/websocket2" "github.com/kataras/iris/websocket2"
@ -16,7 +17,28 @@ var (
f *os.File f *os.File
) )
const totalClients = 1200 const totalClients = 100000
var connectionFailures uint64
var (
disconnectErrors []error
connectErrors []error
errMu sync.Mutex
)
func collectError(op string, err error) {
errMu.Lock()
defer errMu.Unlock()
switch op {
case "disconnect":
disconnectErrors = append(disconnectErrors, err)
case "connect":
connectErrors = append(connectErrors, err)
}
}
func main() { func main() {
var err error var err error
@ -26,29 +48,93 @@ func main() {
} }
defer f.Close() defer f.Close()
start := time.Now()
wg := new(sync.WaitGroup) wg := new(sync.WaitGroup)
for i := 0; i < totalClients/2; i++ { for i := 0; i < totalClients/4; i++ {
wg.Add(1) wg.Add(1)
go connect(wg, 5*time.Second) go connect(wg, 5*time.Second)
} }
for i := 0; i < totalClients/2; i++ { for i := 0; i < totalClients/4; i++ {
wg.Add(1)
waitTime := time.Duration(rand.Intn(5)) * time.Millisecond
time.Sleep(waitTime)
go connect(wg, 7*time.Second+waitTime)
}
for i := 0; i < totalClients/4; i++ {
wg.Add(1) wg.Add(1)
waitTime := time.Duration(rand.Intn(10)) * time.Millisecond waitTime := time.Duration(rand.Intn(10)) * time.Millisecond
time.Sleep(waitTime) time.Sleep(waitTime)
go connect(wg, 10*time.Second+waitTime) go connect(wg, 10*time.Second+waitTime)
} }
for i := 0; i < totalClients/4; i++ {
wg.Add(1)
waitTime := time.Duration(rand.Intn(20)) * time.Millisecond
time.Sleep(waitTime)
go connect(wg, 25*time.Second+waitTime)
}
wg.Wait() wg.Wait()
fmt.Println("ALL OK.") fmt.Println("--------================--------------")
time.Sleep(5 * time.Second) fmt.Printf("execution time [%s]", time.Since(start))
fmt.Println()
if connectionFailures > 0 {
fmt.Printf("Finished with %d/%d connection failures. Please close the server-side manually.\n", connectionFailures, totalClients)
}
if n := len(connectErrors); n > 0 {
fmt.Printf("Finished with %d connect errors:\n", n)
var lastErr error
var sameC int
for i, err := range connectErrors {
if lastErr != nil {
if lastErr.Error() == err.Error() {
sameC++
continue
}
}
if sameC > 0 {
fmt.Printf("and %d more like this...\n", sameC)
sameC = 0
continue
}
fmt.Printf("[%d] - %v\n", i+1, err)
lastErr = err
}
}
if n := len(disconnectErrors); n > 0 {
fmt.Printf("Finished with %d disconnect errors\n", n)
for i, err := range disconnectErrors {
if err == websocket.ErrAlreadyDisconnected {
continue
}
fmt.Printf("[%d] - %v\n", i+1, err)
}
}
if connectionFailures == 0 && len(connectErrors) == 0 && len(disconnectErrors) == 0 {
fmt.Println("ALL OK.")
}
fmt.Println("--------================--------------")
} }
func connect(wg *sync.WaitGroup, alive time.Duration) { func connect(wg *sync.WaitGroup, alive time.Duration) {
c, err := websocket.Dial(nil, url, websocket.ConnectionConfig{}) c, err := websocket.Dial(nil, url, websocket.ConnectionConfig{})
if err != nil { if err != nil {
panic(err) atomic.AddUint64(&connectionFailures, 1)
collectError("connect", err)
wg.Done()
return
} }
c.OnError(func(err error) { c.OnError(func(err error) {
@ -68,7 +154,7 @@ func connect(wg *sync.WaitGroup, alive time.Duration) {
go func() { go func() {
time.Sleep(alive) time.Sleep(alive)
if err := c.Disconnect(); err != nil { if err := c.Disconnect(); err != nil {
panic(err) collectError("disconnect", err)
} }
wg.Done() wg.Done()
@ -80,6 +166,8 @@ func connect(wg *sync.WaitGroup, alive time.Duration) {
break break
} }
c.Emit("chat", scanner.Text()) if text := scanner.Text(); len(text) > 1 {
c.Emit("chat", text)
}
} }
} }

View File

@ -11,9 +11,3 @@ Far curiosity incommode now led smallness allowance. Favour bed assure son thing
Windows talking painted pasture yet its express parties use. Sure last upon he same as knew next. Of believed or diverted no rejoiced. End friendship sufficient assistance can prosperous met. As game he show it park do. Was has unknown few certain ten promise. No finished my an likewise cheerful packages we. For assurance concluded son something depending discourse see led collected. Packages oh no denoting my advanced humoured. Pressed be so thought natural. Windows talking painted pasture yet its express parties use. Sure last upon he same as knew next. Of believed or diverted no rejoiced. End friendship sufficient assistance can prosperous met. As game he show it park do. Was has unknown few certain ten promise. No finished my an likewise cheerful packages we. For assurance concluded son something depending discourse see led collected. Packages oh no denoting my advanced humoured. Pressed be so thought natural.
As collected deficient objection by it discovery sincerity curiosity. Quiet decay who round three world whole has mrs man. Built the china there tried jokes which gay why. Assure in adieus wicket it is. But spoke round point and one joy. Offending her moonlight men sweetness see unwilling. Often of it tears whole oh balls share an. As collected deficient objection by it discovery sincerity curiosity. Quiet decay who round three world whole has mrs man. Built the china there tried jokes which gay why. Assure in adieus wicket it is. But spoke round point and one joy. Offending her moonlight men sweetness see unwilling. Often of it tears whole oh balls share an.
Lose eyes get fat shew. Winter can indeed letter oppose way change tended now. So is improve my charmed picture exposed adapted demands. Received had end produced prepared diverted strictly off man branched. Known ye money so large decay voice there to. Preserved be mr cordially incommode as an. He doors quick child an point at. Had share vexed front least style off why him.
He unaffected sympathize discovered at no am conviction principles. Girl ham very how yet hill four show. Meet lain on he only size. Branched learning so subjects mistress do appetite jennings be in. Esteems up lasting no village morning do offices. Settled wishing ability musical may another set age. Diminution my apartments he attachment is entreaties announcing estimating. And total least her two whose great has which. Neat pain form eat sent sex good week. Led instrument sentiments she simplicity.
Months on ye at by esteem desire warmth former. Sure that that way gave any fond now. His boy middleton sir nor engrossed affection excellent. Dissimilar compliment cultivated preference eat sufficient may. Well next door soon we mr he four. Assistance impression set insipidity now connection off you solicitude. Under as seems we me stuff those style at. Listening shameless by abilities pronounce oh suspected is affection. Next it draw in draw much bred.

View File

@ -10,7 +10,7 @@ import (
"github.com/kataras/iris/websocket2" "github.com/kataras/iris/websocket2"
) )
const totalClients = 1200 const totalClients = 100000
func main() { func main() {
app := iris.New() app := iris.New()

View File

@ -808,7 +808,7 @@ func DialContext(ctx stdContext.Context, url string, cfg ConnectionConfig) (Clie
ctx = stdContext.Background() ctx = stdContext.Background()
} }
if !strings.HasPrefix(url, "ws://") || !strings.HasPrefix(url, "wss://") { if !strings.HasPrefix(url, "ws://") && !strings.HasPrefix(url, "wss://") {
url = "ws://" + url url = "ws://" + url
} }

View File

@ -1,4 +0,0 @@
# This is the official list of Iris Websocket authors for copyright
# purposes.
Gerasimos Maropoulos <kataras2006@hotmail.com>

View File

@ -1,27 +0,0 @@
Copyright (c) 2017-2018 The Iris Websocket Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Iris nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -5,6 +5,7 @@ import (
stdContext "context" stdContext "context"
"errors" "errors"
"io" "io"
"io/ioutil"
"net" "net"
"strconv" "strconv"
"strings" "strings"
@ -267,6 +268,9 @@ type (
ctx context.Context ctx context.Context
values ConnectionValues values ConnectionValues
server *Server server *Server
writer *wsutil.Writer
// #119 , websocket writers are not protected by locks inside the gorilla's websocket code // #119 , websocket writers are not protected by locks inside the gorilla's websocket code
// so we must protect them otherwise we're getting concurrent connection error on multi writers in the same time. // so we must protect them otherwise we're getting concurrent connection error on multi writers in the same time.
writerMu sync.Mutex writerMu sync.Mutex
@ -304,6 +308,8 @@ func newConnection(conn net.Conn, cfg ConnectionConfig) *connection {
c.defaultMessageType = BinaryMessage c.defaultMessageType = BinaryMessage
} }
// c.writer = wsutil.NewWriter(conn, c.getState(), c.defaultMessageType)
return c return c
} }
@ -350,17 +356,26 @@ func (c *connection) getState() ws.State {
// Write writes a raw websocket message with a specific type to the client // Write writes a raw websocket message with a specific type to the client
// used by ping messages and any CloseMessage types. // used by ping messages and any CloseMessage types.
func (c *connection) Write(websocketMessageType ws.OpCode, data []byte) error { func (c *connection) Write(websocketMessageType ws.OpCode, data []byte) (err error) {
// for any-case the app tries to write from different goroutines, // for any-case the app tries to write from different goroutines,
// we must protect them because they're reporting that as bug... // we must protect them because they're reporting that as bug...
c.writerMu.Lock() c.writerMu.Lock()
defer c.writerMu.Unlock()
if writeTimeout := c.config.WriteTimeout; writeTimeout > 0 { if writeTimeout := c.config.WriteTimeout; writeTimeout > 0 {
// set the write deadline based on the configuration // set the write deadline based on the configuration
c.underline.SetWriteDeadline(time.Now().Add(writeTimeout)) c.underline.SetWriteDeadline(time.Now().Add(writeTimeout))
} }
err := wsutil.WriteMessage(c.underline, c.getState(), websocketMessageType, data) // 2.
c.writerMu.Unlock() // if websocketMessageType != c.defaultMessageType {
// err = wsutil.WriteMessage(c.underline, c.getState(), websocketMessageType, data)
// } else {
// _, err = c.writer.Write(data)
// c.writer.Flush()
// }
err = wsutil.WriteMessage(c.underline, c.getState(), websocketMessageType, data)
if err != nil { if err != nil {
// if failed then the connection is off, fire the disconnect // if failed then the connection is off, fire the disconnect
c.Disconnect() c.Disconnect()
@ -440,29 +455,125 @@ func (c *connection) isErrClosed(err error) bool {
} }
func (c *connection) startReader() { func (c *connection) startReader() {
defer c.Disconnect()
hasReadTimeout := c.config.ReadTimeout > 0 hasReadTimeout := c.config.ReadTimeout > 0
for { controlHandler := wsutil.ControlFrameHandler(c.underline, c.getState())
if c == nil || c.underline == nil || atomic.LoadUint32(&c.disconnected) > 0 { rd := wsutil.Reader{
return Source: c.underline,
} State: c.getState(),
CheckUTF8: false,
SkipHeaderCheck: false,
OnIntermediate: controlHandler,
}
for {
if hasReadTimeout { if hasReadTimeout {
// set the read deadline based on the configuration // set the read deadline based on the configuration
c.underline.SetReadDeadline(time.Now().Add(c.config.ReadTimeout)) c.underline.SetReadDeadline(time.Now().Add(c.config.ReadTimeout))
} }
data, code, err := wsutil.ReadData(c.underline, c.getState()) hdr, err := rd.NextFrame()
if code == CloseMessage || c.isErrClosed(err) { if err != nil {
c.Disconnect() return
}
if hdr.OpCode.IsControl() {
if err := controlHandler(hdr, &rd); err != nil {
return
}
continue
}
if hdr.OpCode&TextMessage == 0 && hdr.OpCode&BinaryMessage == 0 {
if err := rd.Discard(); err != nil {
return
}
continue
}
data, err := ioutil.ReadAll(&rd)
if err != nil {
return return
} }
if err != nil {
c.FireOnError(err)
}
c.messageReceived(data) c.messageReceived(data)
// 4.
// var buf bytes.Buffer
// data, code, err := wsutil.ReadData(struct {
// io.Reader
// io.Writer
// }{c.underline, &buf}, c.getState())
// if err != nil {
// if _, closed := err.(*net.OpError); closed && code == 0 {
// c.Disconnect()
// return
// } else if _, closed = err.(wsutil.ClosedError); closed {
// c.Disconnect()
// return
// // > 1200 conns but I don't know why yet:
// } else if err == ws.ErrProtocolOpCodeReserved || err == ws.ErrProtocolNonZeroRsv {
// c.Disconnect()
// return
// } else if err == io.EOF || err == io.ErrUnexpectedEOF {
// c.Disconnect()
// return
// }
// c.FireOnError(err)
// }
// c.messageReceived(data)
// 2.
// header, err := reader.NextFrame()
// if err != nil {
// println("next frame err: " + err.Error())
// return
// }
// if header.OpCode == ws.OpClose { // io.EOF.
// return
// }
// payload := make([]byte, header.Length)
// _, err = io.ReadFull(reader, payload)
// if err != nil {
// return
// }
// if header.Masked {
// ws.Cipher(payload, header.Mask, 0)
// }
// c.messageReceived(payload)
// data, code, err := wsutil.ReadData(c.underline, c.getState())
// // if code == CloseMessage || c.isErrClosed(err) {
// // c.Disconnect()
// // return
// // }
// if err != nil {
// if _, closed := err.(*net.OpError); closed && code == 0 {
// c.Disconnect()
// return
// } else if _, closed = err.(wsutil.ClosedError); closed {
// c.Disconnect()
// return
// // > 1200 conns but I don't know why yet:
// } else if err == ws.ErrProtocolOpCodeReserved || err == ws.ErrProtocolNonZeroRsv {
// c.Disconnect()
// return
// } else if err == io.EOF || err == io.ErrUnexpectedEOF {
// c.Disconnect()
// return
// }
// c.FireOnError(err)
// }
// c.messageReceived(data)
} }
} }
@ -801,6 +912,16 @@ var ErrBadHandshake = ws.ErrHandshakeBadConnection
// //
// Custom dialers can be used by wrapping the iris websocket connection via `websocket.WrapConnection`. // Custom dialers can be used by wrapping the iris websocket connection via `websocket.WrapConnection`.
func Dial(ctx stdContext.Context, url string, cfg ConnectionConfig) (ClientConnection, error) { func Dial(ctx stdContext.Context, url string, cfg ConnectionConfig) (ClientConnection, error) {
c, err := dial(ctx, url, cfg)
if err != nil {
time.Sleep(1 * time.Second)
c, err = dial(ctx, url, cfg)
}
return c, err
}
func dial(ctx stdContext.Context, url string, cfg ConnectionConfig) (ClientConnection, error) {
if ctx == nil { if ctx == nil {
ctx = stdContext.Background() ctx = stdContext.Background()
} }

View File

@ -45,9 +45,9 @@ type (
// Use a route to serve this file on a specific path, i.e // Use a route to serve this file on a specific path, i.e
// app.Any("/iris-ws.js", func(ctx iris.Context) { ctx.Write(mywebsocketServer.ClientSource) }) // app.Any("/iris-ws.js", func(ctx iris.Context) { ctx.Write(mywebsocketServer.ClientSource) })
ClientSource []byte ClientSource []byte
connections map[string]*connection // key = the Connection ID. connections sync.Map // key = the Connection ID. // key = the Connection ID.
rooms map[string][]string // by default a connection is joined to a room which has the connection id as its name rooms map[string][]string // by default a connection is joined to a room which has the connection id as its name
mu sync.RWMutex // for rooms and connections. mu sync.RWMutex // for rooms.
onConnectionListeners []ConnectionFunc onConnectionListeners []ConnectionFunc
//connectionPool sync.Pool // sadly we can't make this because the websocket connection is live until is closed. //connectionPool sync.Pool // sadly we can't make this because the websocket connection is live until is closed.
upgrader ws.HTTPUpgrader upgrader ws.HTTPUpgrader
@ -64,7 +64,7 @@ func New(cfg Config) *Server {
return &Server{ return &Server{
config: cfg, config: cfg,
ClientSource: bytes.Replace(ClientSource, []byte(DefaultEvtMessageKey), cfg.EvtMessagePrefix, -1), ClientSource: bytes.Replace(ClientSource, []byte(DefaultEvtMessageKey), cfg.EvtMessagePrefix, -1),
connections: make(map[string]*connection), connections: sync.Map{}, // ready-to-use, this is not necessary.
rooms: make(map[string][]string), rooms: make(map[string][]string),
onConnectionListeners: make([]ConnectionFunc, 0), onConnectionListeners: make([]ConnectionFunc, 0),
upgrader: ws.DefaultHTTPUpgrader, // ws.DefaultUpgrader, upgrader: ws.DefaultHTTPUpgrader, // ws.DefaultUpgrader,
@ -126,14 +126,19 @@ func (s *Server) Upgrade(ctx context.Context) Connection {
} }
func (s *Server) addConnection(c *connection) { func (s *Server) addConnection(c *connection) {
s.mu.Lock() s.connections.Store(c.id, c)
s.connections[c.id] = c
s.mu.Unlock()
} }
func (s *Server) getConnection(connID string) (*connection, bool) { func (s *Server) getConnection(connID string) (*connection, bool) {
c, ok := s.connections[connID] if cValue, ok := s.connections.Load(connID); ok {
return c, ok // this cast is not necessary,
// we know that we always save a connection, but for good or worse let it be here.
if conn, ok := cValue.(*connection); ok {
return conn, ok
}
}
return nil, false
} }
// wrapConnection wraps an underline connection to an iris websocket connection. // wrapConnection wraps an underline connection to an iris websocket connection.
@ -278,24 +283,34 @@ func (s *Server) leave(roomName string, connID string) (left bool) {
// GetTotalConnections returns the number of total connections // GetTotalConnections returns the number of total connections
func (s *Server) GetTotalConnections() (n int) { func (s *Server) GetTotalConnections() (n int) {
s.mu.RLock() s.connections.Range(func(k, v interface{}) bool {
n = len(s.connections) n++
s.mu.RUnlock() return true
})
return return
} }
// GetConnections returns all connections // GetConnections returns all connections
func (s *Server) GetConnections() []Connection { func (s *Server) GetConnections() []Connection {
s.mu.RLock() // first call of Range to get the total length, we don't want to use append or manually grow the list here for many reasons.
conns := make([]Connection, len(s.connections)) length := s.GetTotalConnections()
conns := make([]Connection, length, length)
i := 0 i := 0
for _, c := range s.connections { // second call of Range.
conns[i] = c s.connections.Range(func(k, v interface{}) bool {
conn, ok := v.(*connection)
if !ok {
// if for some reason (should never happen), the value is not stored as *connection
// then stop the iteration and don't continue insertion of the result connections
// in order to avoid any issues while end-dev will try to iterate a nil entry.
return false
}
conns[i] = conn
i++ i++
} return true
})
s.mu.RUnlock()
return conns return conns
} }
@ -317,8 +332,10 @@ func (s *Server) GetConnectionsByRoom(roomName string) []Connection {
if connIDs, found := s.rooms[roomName]; found { if connIDs, found := s.rooms[roomName]; found {
for _, connID := range connIDs { for _, connID := range connIDs {
// existence check is not necessary here. // existence check is not necessary here.
if conn, ok := s.connections[connID]; ok { if cValue, ok := s.connections.Load(connID); ok {
conns = append(conns, conn) if conn, ok := cValue.(*connection); ok {
conns = append(conns, conn)
}
} }
} }
} }
@ -358,20 +375,32 @@ func (s *Server) emitMessage(from, to string, data []byte) {
} }
} }
} else { } else {
s.mu.RLock()
// it suppose to send the message to all opened connections or to all except the sender. // it suppose to send the message to all opened connections or to all except the sender.
for _, conn := range s.connections { s.connections.Range(func(k, v interface{}) bool {
if to != All && to != conn.id { // if it's not suppose to send to all connections (including itself) connID, ok := k.(string)
if to == Broadcast && from == conn.id { // if broadcast to other connections except this if !ok {
// here we do the opossite of previous block, // should never happen.
// just skip this connection when it's suppose to send the message to all connections except the sender. return true
continue
}
} }
conn.writeDefault(data) if to != All && to != connID { // if it's not suppose to send to all connections (including itself)
} if to == Broadcast && from == connID { // if broadcast to other connections except this
s.mu.RUnlock() // here we do the opossite of previous block,
// just skip this connection when it's suppose to send the message to all connections except the sender.
return true
}
}
// not necessary cast.
conn, ok := v.(*connection)
if ok {
// send to the client(s) when the top validators passed
conn.writeDefault(data)
}
return ok
})
} }
} }
@ -395,9 +424,7 @@ func (s *Server) Disconnect(connID string) (err error) {
// fire the disconnect callbacks, if any. // fire the disconnect callbacks, if any.
conn.fireDisconnect() conn.fireDisconnect()
s.mu.Lock() s.connections.Delete(connID)
delete(s.connections, conn.id)
s.mu.Unlock()
err = conn.underline.Close() err = conn.underline.Close()
} }