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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
