You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

通过ngrok暴露TCPListener时客户端连接超时问题咨询

问题根因

一共四个直接导致故障的点,按影响优先级排序:

  • 服务端监听地址绑定错误
    你写死了TcpListener绑定127.0.0.1回环地址,仅能接收来自本机回环接口的连接。ngrok的TCP隧道转发流量不属于回环接口流量,会被操作系统直接拦截,根本到不了应用层,这是连接超时的核心原因。
  • 服务端生命周期逻辑错误
    你在循环末尾直接给stoppingToken赋值为已取消状态的Token,无论第一次连接处理成功还是失败,服务跑完一轮循环就会直接退出,停止端口监听;同时你把TcpListener的初始化、Start()逻辑放在了循环内部,每次循环都会重新监听端口,存在端口释放的时间窗,很容易出现连接被拒的问题。
  • 客户端连接地址未生效
    你贴的客户端代码里硬编码连接localhost:2222,如果只是改了配置参数没改代码里的硬编码值,客户端根本不会连ngrok的公网地址,自然连不上暴露出去的服务。
  • 网络流读写不规范
    客户端调用stream.Write时没传偏移和长度参数,读写时也没判断实际返回的字节长度,极端场景下会出现发送空数据、读半包的问题。
修复方案

服务端代码修复

将监听地址改为IPAddress.Any(即0.0.0.0)监听所有网卡流量,把监听器初始化移到循环外部,删掉强制取消Token的错误逻辑,用finally块做资源释放:

using Serilog;
using System.Net;
using System.Net.Sockets;

namespace TcpServer
{
    public class ServerService : BackgroundService
    {
        private readonly ILogger<ServerService> _logger;

        public ServerService(ILogger<ServerService> logger)
        {
            _logger = logger;
        }

        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            var acknowledgement = new byte[] { 2 };
            return Task.Run(() =>
            {
                // 监听器初始化移到循环外,绑定所有网卡地址
                var server = new TcpListener(IPAddress.Any, 2222);
                server.Start();
                Log.Debug("Listening on endpoint {Endpoint}", server.LocalEndpoint);

                while (!stoppingToken.IsCancellationRequested)
                {
                    TcpClient? client = null;
                    NetworkStream? stream = null;
                    try
                    {
                        client = server.AcceptTcpClient();
                        Log.Debug("Connection request accepted");

                        stream = client.GetStream();
                        var received = stream.ReadByte();
                        Log.Information("Message {Message} received", received);

                        stream.Write(acknowledgement, 0, 1);
                        Log.Debug("Acknowledgement sent");
                    }
                    catch (Exception ex)
                    {
                        Log.Error("Exception {Exception} occurred", ex);
                    }
                    finally
                    {
                        // 统一释放资源
                        stream?.Close();
                        client?.Close();
                    }
                }
                server.Stop();
                Log.CloseAndFlush();
            }, stoppingToken);
        }
    }
}

客户端代码修复

去掉硬编码的本地地址,从配置读取ngrok分配的公网地址和端口,补全流读写的参数,增加重试间隔避免瞬间打满重试次数:

using Serilog;
using Serilog.Core;
using System.Net;
using System.Net.Sockets;

namespace TcpClient
{
    public class ClientService : BackgroundService
    {
        private readonly ILogger<ClientService> _logger;
        private readonly IConfiguration _configuration;

        public ClientService(ILogger<ClientService> logger, IConfiguration configuration)
        {
            _logger = logger;
            _configuration = configuration;
        }

        protected override Task ExecuteAsync(CancellationToken stoppingToken)
        {
            var messageToSend = new byte[] { 1 };
            var receiveBuffer = new byte[1];
            var noTries = 5;

            return Task.Run(() =>
            {
                // 从配置读取ngrok分配的公网主机、端口
                var ngrokHost = _configuration.GetValue<string>("Ngrok:Host");
                var ngrokPort = _configuration.GetValue<int>("Ngrok:Port");

                while (!stoppingToken.IsCancellationRequested && noTries-- > 0)
                {
                    System.Net.Sockets.TcpClient? client = null;
                    NetworkStream? stream = null;
                    try
                    {
                        client = new System.Net.Sockets.TcpClient();
                        // 设置连接超时避免长时间卡等
                        client.ConnectAsync(ngrokHost, ngrokPort).Wait(TimeSpan.FromSeconds(5), stoppingToken);
                        Log.Debug("Connected to the remote server");

                        stream = client.GetStream();
                        // 补全写入的偏移、长度参数
                        stream.Write(messageToSend, 0, 1);
                        Log.Information("Message sent");

                        var readLen = stream.Read(receiveBuffer, 0, 1);
                        if (readLen == 1)
                        {
                            Log.Debug("Acknowledgement received");
                            noTries = 0;
                        }
                    }
                    catch (Exception ex)
                    {
                        Log.Error("Exception {Exception} occurred", ex);
                        // 重试间隔1秒,避免瞬间打满重试次数
                        Task.Delay(1000, stoppingToken).Wait(stoppingToken);
                    }
                    finally
                    {
                        stream?.Close();
                        client?.Close();
                    }
                }
                Log.CloseAndFlush();
            }, stoppingToken);
        }
    }
}

验证步骤

  1. 启动修复后的服务端,用telnet 127.0.0.1 2222确认本地端口监听正常
  2. 执行ngrok tcp 2222启动隧道,从控制台输出的转发规则中提取公网主机和端口(格式类似0.tcp.ap.ngrok.io:12345)
  3. 将提取到的主机、端口填入客户端配置,先在本机用telnet连公网地址确认连通性,再启动客户端即可正常通信
本地运行正常的原因

本地测试时客户端直连127.0.0.1:2222,刚好匹配服务端绑定的回环地址,所以能正常通信;走ngrok隧道时流量不经过回环接口,自然会出现连接超时。

内容的提问来源于stack exchange,提问作者0lt

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 09:54:23