Performance Comparison of Java-WebSocket and Netty-WebSocket Implementations: Thread Usage and High‑Connection Tests

This article compares Java-WebSocket and Netty-WebSocket implementations by examining their thread models, running single‑connection and high‑connection benchmarks (including a Go WebSocket server), and summarizing the performance and resource‑usage differences observed.

FunTester
FunTester
FunTester
Performance Comparison of Java-WebSocket and Netty-WebSocket Implementations: Thread Usage and High‑Connection Tests

When testing WebSocket protocol connections or WebSocket APIs under scenarios with a large number of connections, the previously used implementations Java-WebSocket and Netty-WebSocket show a huge performance gap, with Netty‑WebSocket designed specifically to address performance issues.

The article focuses on the resource consumption differences, especially thread usage, between the two implementations.

Theoretical Differences

Java-WebSocket

According to reliable sources, the main difference lies in the number of threads used to manage WebSocket connections. When creating a client with org.java_websocket.client.WebSocketClient, three threads are spawned:

ConnectThread : handles the connection establishment when WebSocketClient.connect() is called.

WriteThread : processes outgoing messages invoked via WebSocket.send().

ReadThread : continuously listens for incoming messages from the server.

These threads allow the client to handle connection, sending, and receiving in the background without blocking the main thread, keeping the application responsive.

Netty-WebSocket

Netty does not bind WebSocket connections to a fixed number of threads. It uses a single io.netty.channel.EventLoopGroup backed by a thread‑pool, and does not create additional threads per connection.

Test Service

A simple Go WebSocket server is used for the tests. The server code is:

func CreateServer(port int, path string) {
    var upgrader = websocket.Upgrader{
        ReadBufferSize:   1024,
        WriteBufferSize:  1024,
        HandshakeTimeout: 5 * time.Second,
    }
    http.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
        conn, _ := upgrader.Upgrade(w, r, nil)
        conn.WriteMessage(websocket.TextMessage, []byte("msg"))
        for {
            msgType, msg, err := conn.ReadMessage()
            if err != nil {
                log.Println(err)
                return
            }
            fmt.Printf("%s receive: %s
", conn.RemoteAddr(), string(msg))
            if err = conn.WriteMessage(msgType, msg); err != nil {
                log.Println("ffahv")
                return
            }
        }
    })
    http.ListenAndServe(":"+strconv.Itoa(port), nil)
}

Single‑Connection Comparison

Empty Java Process

A baseline test monitors thread usage of an empty Java process.

Java-WebSocket

Only one WebSocket client is created. The test code is:

package com.funtest.websocket

import com.funtester.frame.SourceCode
import com.funtester.socket.WebSocketFunClient

class WebSocket extends SourceCode {
    static String url = "ws://localhost:12345/test"
    static void main(String[] args) {
        def instance = WebSocketFunClient.getInstance(url)
        instance.connect()
        instance.send("Hello FunTester")
        waitForKey("按任意键退出")
    }
}

Thread monitoring shows three additional threads created by the client.

Netty-WebSocket

The same logic is implemented with Netty. The test code is:

package com.funtest.websocket

import com.funtester.frame.SourceCode
import com.funtester.socket.netty.WebSocketConnector
import groovy.util.logging.Log4j2

@Log4j2
class NettySocket extends SourceCode {
    static void main(String[] args) {
        String serverIp = "ws://127.0.0.1"
        int serverPort = 12345
        def h = { String x -> log.info("收到消息:{}", x) }
        WebSocketConnector client = new WebSocketConnector(serverIp, serverPort, "/test", h)
        client.connect()
        client.getHandshakeFuture().get()
        client.sendText("Hello FunTester").get()
        waitForKey("按任意键退出")
    }
}

Only one extra thread is observed.

Conclusion

Java-WebSocket creates three extra threads, while Netty-WebSocket creates only one, using the default io.netty.channel.EventLoopGroup strategy.

1000‑Connection Tests

Netty-WebSocket

Testing 1000 concurrent connections with the same client logic shows modest thread usage (still a single EventLoopGroup). The code used is the same as the single‑connection test but executed 1000 times.

Java-WebSocket

Creating 1000 connections is too slow, so a 100‑connection test is performed. The code creates 100 instances of WebSocketFunClient in a loop.

Netty Extreme Test

When only the connection count is tested without extra event‑handling threads, the implementation can be forced to use a single thread, demonstrating the scalability of Netty.

Final Verdict

Netty proves to be far more stable and resource‑efficient for high‑connection scenarios.

Code Updates

Further improvements were made to the Netty‑WebSocket implementation, adding support for custom WebSocket paths, message‑handling closures, and fixing bugs.

WebSocketConnector

package com.funtester.socket.netty

import com.funtester.frame.execute.ThreadPoolUtil
import groovy.util.logging.Log4j2
import io.netty.bootstrap.Bootstrap
import io.netty.channel.*
import io.netty.channel.group.ChannelGroup
import io.netty.channel.group.DefaultChannelGroup
import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.SocketChannel
import io.netty.channel.socket.nio.NioSocketChannel
import io.netty.handler.codec.http.DefaultHttpHeaders
import io.netty.handler.codec.http.HttpClientCodec
import io.netty.handler.codec.http.HttpObjectAggregator
import io.netty.handler.codec.http.websocketx.*
import io.netty.handler.stream.ChunkedWriteHandler
import io.netty.util.concurrent.GlobalEventExecutor

@Log4j2
class WebSocketConnector {
    static Bootstrap bootstrap = new Bootstrap()
    static EventLoopGroup group = new NioEventLoopGroup(ThreadPoolUtil.getFactory("N"))
    static { bootstrap.group(group).channel(NioSocketChannel.class) }
    static ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE)
    WebSocketClientHandshaker handShaker
    ChannelPromise handshakeFuture
    String host
    int port
    String path
    Channel channel
    WebSocketIoHandler handler
    WebSocketConnector(String host, int port, String path, Closure closure = null) {
        this.host = host
        this.port = port
        this.path = path
        String URL = this.host + ":" + this.port + path
        URI uri = new URI(URL)
        handler = new WebSocketIoHandler(WebSocketClientHandshakerFactory.newHandshaker(uri, WebSocketVersion.V13, null, true, new DefaultHttpHeaders()))
        if (closure != null) handler.closure = closure
        bootstrap.option(ChannelOption.TCP_NODELAY, true)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline()
                        pipeline.addLast(new HttpClientCodec())
                        pipeline.addLast(new ChunkedWriteHandler())
                        pipeline.addLast(new HttpObjectAggregator(1024 * 1024))
                        pipeline.addLast(handler)
                    }
                })
    }
    void connect() {
        try {
            ChannelFuture future = bootstrap.connect(this.host - "ws://" - "wss://", this.port).sync()
            this.channel = future.channel()
            clients.add(channel)
        } catch (Exception e) {
            log.error("连接服务失败", e)
        } finally {
            this.handshakeFuture = handler.handshakeFuture()
        }
    }
    ChannelFuture sendText(String msg) { channel.writeAndFlush(new TextWebSocketFrame(msg)) }
    ChannelFuture ping() { channel.writeAndFlush(new PingWebSocketFrame()) }
    void close() { group.shutdownGracefully() }
}

WebSocketIoHandler

package com.funtester.socket.netty

import groovy.util.logging.Log4j2
import io.netty.channel.*
import io.netty.channel.group.ChannelGroup
import io.netty.channel.group.DefaultChannelGroup
import io.netty.handler.codec.http.FullHttpResponse
import io.netty.handler.codec.http.websocketx.*
import io.netty.handler.timeout.IdleState
import io.netty.handler.timeout.IdleStateEvent
import io.netty.util.concurrent.GlobalEventExecutor

/** WebSocket protocol client IO handler */
@Log4j2
class WebSocketIoHandler extends SimpleChannelInboundHandler<Object> {
    private ChannelGroup clients = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE)
    private final WebSocketClientHandshaker handShaker
    Closure closure
    private ChannelPromise handshakeFuture
    WebSocketIoHandler(WebSocketClientHandshaker handShaker) { this.handShaker = handShaker }
    ChannelFuture handshakeFuture() { return handshakeFuture }
    @Override
    void handlerAdded(ChannelHandlerContext ctx) { handshakeFuture = ctx.newPromise() }
    @Override
    void channelActive(ChannelHandlerContext ctx) { handShaker.handshake(ctx.channel()) }
    @Override
    void channelInactive(ChannelHandlerContext ctx) {
        ctx.close()
        log.warn("WebSocket链路与服务器连接已断开.")
    }
    @Override
    void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        Channel ch = ctx.channel()
        if (!handShaker.isHandshakeComplete()) {
            handShaker.finishHandshake(ch, (FullHttpResponse) msg)
            handshakeFuture.setSuccess()
            return
        }
        WebSocketFrame frame = (WebSocketFrame) msg
        if (frame instanceof TextWebSocketFrame) {
            if (closure != null) closure(((TextWebSocketFrame) frame).text())
        } else if (frame instanceof CloseWebSocketFrame) {
            log.info("WebSocket Client closing")
            ch.close()
        }
    }
    @Override
    void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("WebSocket链路由于发生异常,与服务器连接已断开.", cause)
        if (!handshakeFuture.isDone()) handshakeFuture.setFailure(cause)
        ctx.close()
        super.exceptionCaught(ctx, cause)
    }
    @Override
    void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt
            if (event.state() == IdleState.WRITER_IDLE || event.state() == IdleState.READER_IDLE) {
                ctx.channel().writeAndFlush(new TextWebSocketFrame("dsf"))
            }
        } else {
            super.userEventTriggered(ctx, evt)
        }
    }
}

Overall, the article demonstrates that Netty‑based WebSocket clients consume far fewer threads and scale better under massive connection loads compared to the Java‑WebSocket library.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendJavaNettyThreadWebSocket
FunTester
Written by

FunTester

10k followers, 1k articles | completely useless

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.