YunaiV/ruoyi-vue-pro · critical · RuntimeException

[startTcpServer][TCP Server 启动失败]

Error message

[startTcpServer][TCP Server 启动失败]

What it means

Thrown by startTcpServer() when netServer.listen().get() fails. Unlike the EMQX HTTP server (which uses a 10s timeout), this call has NO timeout on .get(), so a stuck listen could block indefinitely, though bind failures normally resolve quickly. It wraps any Vert.x bind/listen error. Because IotModbusTcpServerProtocol.start() calls this in its try block, the exception aborts protocol startup and triggers stop0() cleanup.

Source

Thrown at yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/modbus/tcpserver/IotModbusTcpServerProtocol.java:259

        log.info("[stop][IoT Modbus TCP Server 协议 {} 已停止]", getId());
    }

    /**
     * 启动 TCP Server
     */
    private void startTcpServer() {
        // 1. 创建 TCP Server
        NetServerOptions options = new NetServerOptions()
                .setPort(properties.getPort());
        netServer = vertx.createNetServer(options);

        // 2. 设置连接处理器
        netServer.connectHandler(this::handleConnection);
        try {
            netServer.listen().toCompletionStage().toCompletableFuture().get();
            log.info("[startTcpServer][TCP Server 启动成功, port={}]", properties.getPort());
        } catch (Exception e) {
            throw new RuntimeException("[startTcpServer][TCP Server 启动失败]", e);
        }
    }

    /**
     * 处理新连接
     */
    private void handleConnection(NetSocket socket) {
        log.info("[handleConnection][新连接, remoteAddress={}]", socket.remoteAddress());

        // 1. 创建 RecordParser 并设置为数据处理器
        RecordParser recordParser =  frameDecoder.createRecordParser((frame, frameFormat) -> {
            // 【重要】帧处理分发,即消息处理
            upstreamHandler.handleFrame(socket, frame, frameFormat);
        });
        socket.handler(recordParser);

        // 2.1 连接关闭处理
        socket.closeHandler(v -> {

View on GitHub (pinned to 0418084e22)

Solutions

  1. Check the port is free: ss -ltnp | grep <port> / lsof -i:<port>, kill the holder or change properties.port.
  2. Use a port >=1024 unless the gateway has CAP_NET_BIND_SERVICE / runs privileged.
  3. Ensure only one gateway instance / one protocol instance binds the port.
  4. If a previous process left the port in TIME_WAIT, wait or set SO_REUSEADDR (Vert.x enables it by default on NetServer).
  5. Add a timeout to .get() to fail fast instead of hanging: netServer.listen().toCompletionStage().toCompletableFuture().get(10, TimeUnit.SECONDS).

Example fix

// before (no timeout, can hang)
netServer.listen().toCompletionStage().toCompletableFuture().get();

// after (fail fast)
netServer.listen().toCompletionStage().toCompletableFuture().get(10, TimeUnit.SECONDS);
Defensive patterns

Strategy: validation

Validate before calling

// before start(), confirm the TCP port is free
try (java.net.ServerSocket probe = new java.net.ServerSocket(properties.getPort())) {
    // port is free
} catch (IOException e) {
    throw new IllegalStateException("Modbus TCP Server 端口被占用: " + properties.getPort(), e);
}

Try / catch

// isolate per-protocol startup so one bind failure doesn't abort the whole gateway
// (in IotProtocolManager.start, wrap each protocol.start() in try/catch)

Prevention

When it happens

Trigger: properties.getPort() already bound by another process; privileged port (<1024) bound without OS permission; NetServerOptions invalid; another protocol instance or a previous unclean gateway shutdown still holds the port.

Common situations: Port conflict with another Modbus TCP Server instance or service; duplicate gateway process; OS-level bind permission; previous process did not release the port (TIME_WAIT).

Related errors


AI-assisted analysis of YunaiV/ruoyi-vue-pro@0418084e22 (2026-08-14). Data as JSON: /api/errors/980c1fdf45d0831e. Report an issue: GitHub.