ws icon indicating copy to clipboard operation
ws copied to clipboard

EOF error on ws.Upgrade

Open piotrpersona opened this issue 3 years ago • 8 comments

I receive EOF error on ws.Upgrade during high load on a server. I took code from example and run ~2k concurrent connections to websocket server. Another thing is that I pass JWT token in URL. It seems that readLine inside Upgrade is causing error: https://github.com/gobwas/ws/blob/master/server.go#L452 and then error is returned from here: https://github.com/gobwas/ws/blob/master/util.go#L183

Example code to reproduce the issue: Server:

package main

import (
	"log"
	"net"

	"github.com/gobwas/ws"
)

func main() {
	ln, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatal(err)
	}

	for {
		conn, err := ln.Accept()
		if err != nil {
			log.Fatal("accept err", err)
			continue
		}
		_, err = ws.Upgrade(conn)
		if err != nil {
			log.Fatal("upgrade err", err)
			continue
		}
	}
}

client:

package main

import (
	"fmt"
	"log"
	"sync"

	"github.com/gorilla/websocket"
)

func main() {
	for {
		var wg sync.WaitGroup
		for i := 0; i < 2000; i++ {
			wg.Add(1)
			go func(i int) {
				defer wg.Done()
				url := "ws://localhost:8080/ws?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
				_, _, err := websocket.DefaultDialer.Dial(url, nil)
				if err != nil {
					err = fmt.Errorf("cannot dial, err: %w", err)
					log.Println(err)
					return
				}
				return
			}(i)
		}
		wg.Wait()
		fmt.Println("done")
	}
}

piotrpersona avatar Aug 11 '22 11:08 piotrpersona

This issue maybe relates to tcp config,check somaxconn size and fd size in your environment.

Jekinnnnnn avatar Aug 12 '22 02:08 Jekinnnnnn

somaxconn: kern.ipc.somaxconn: 18000

$ ulimit -a
Maximum size of core files created                           (kB, -c) 0
Maximum size of a process’s data segment                     (kB, -d) unlimited
Maximum size of files created by the shell                   (kB, -f) unlimited
Maximum size that may be locked into memory                  (kB, -l) unlimited
Maximum resident set size                                    (kB, -m) unlimited
Maximum number of open file descriptors                          (-n) 8000
Maximum stack size                                           (kB, -s) 8176
Maximum amount of cpu time in seconds                   (seconds, -t) unlimited
Maximum number of processes available to a single user           (-u) 2666
Maximum amount of virtual memory available to the shell      (kB, -v) unlimited

It seems that messages are cut off when reading from connection, for example:

...Upgrade\r\nSec-WebSocket-Key: 122CV3QQHRFoIKR44OzQAA==\r\nSec-WebSocket-Version: 13\r\nUpgrade:"

Each message is 512 bytes-long.

Can I ask for a guide, what are the other settings I should check? I'm out of ideas right now

piotrpersona avatar Aug 16 '22 09:08 piotrpersona

ln, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatal(err)
	}

Try to use http server,like Readme shows,because websocket bases on http 1.1or above version not tcp. If you want to get token before Upgrade,I give a reference below

type Token struct {
	Token string `json:"token" bingding:"required"`
}
var param Token
if err := c.ShouldBindJSON(&param); err != nil {
	return
}
log.Println("token=", param.Token)

c in code is *gin.Context

Jekinnnnnn avatar Aug 16 '22 09:08 Jekinnnnnn

Thank you, I will give it a try.

The server I posted above is from README "The lower-level example without wsutil" example. Http 1.1 uses tcp behind the scenes. According to this article: https://www.freecodecamp.org/news/million-websockets-and-go-cc58418460bb/ it is fine to use net.Listen and Accept along with gobwas/ws package (without wsutil).

I wonder if the example from README is sufficient to handle mentioned a million websockets connections.

piotrpersona avatar Aug 16 '22 10:08 piotrpersona

Sorry,I didn't notice that example,maybe lower-level only supports pure url without parameters?

Jekinnnnnn avatar Aug 16 '22 10:08 Jekinnnnnn

Unfortunately, I think ws.Upgrade has some issues. Here is simpler example.

Server:

package main

import (
	"fmt"
	"net"

	"github.com/gobwas/ws"
)

func main() {
	ln, err := net.Listen("tcp", ":8111")
	if err != nil {
		panic(err)
	}

	for {
		conn, err := ln.Accept()
		if err != nil {
			fmt.Println("accept err", err)
			continue
		}

		_, err = ws.Upgrade(conn)
		if err != nil {
			fmt.Println("upgrade", err)
		}
	}
}

client:

package main

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/gorilla/websocket"
)

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
			defer cancel()
			_, _, err := websocket.DefaultDialer.DialContext(ctx, "ws://localhost:8111", nil)
			if err != nil {
				fmt.Println(err)
			}
		}()
	}
	wg.Wait()
}

There are no params in URL, but I receive upgrade EOF. For some reason, this method returns EOF: https://github.com/gobwas/ws/blob/master/util.go#L169.

This is non-deterministic. I have to run client code few times to observe EOF error

piotrpersona avatar Aug 16 '22 13:08 piotrpersona

这个情况确实是必现的。因为你的client在链接建立后,就自己销毁了。 相当于客户端主动断开链接。 server upgrade 失败 EOF。这个应该是合理的。就是会打印日志

linguanyuanA avatar May 09 '23 01:05 linguanyuanA

I'm having similar issue - reading from web socket server returns EOF... consistently (first read after ws.Upgrade) Looks like this lib is unusable in the current form, should'a went with gorilla instead... but it's archived now

let4be avatar Jun 15 '23 17:06 let4be