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

重启IIS为何能解决SignalR应用的Socket通信异常问题?

问题描述

我开发了一个基于SignalR的Web应用,通过Socket与一个无法控制的第三方服务器交互(无源码及协议文档)。每个SignalR客户端向Hub发送命令后,Hub会新建Socket连接,向第三方服务器发送命令并接收一到多个响应,再返回给客户端。

目前遇到的问题:第三方服务器会周期性停止向Hub返回响应,部署环境和本地调试都会出现。我为每个客户端命令创建独立Socket连接,接收完响应就关闭,但重启IIS总能解决问题。我搞不懂重启生效的原因,想找到根源彻底解决。

核心代码(已翻译为中文注释/文本)
public async Task SendCommand(string device, string? command)
{
    var ipendPoint = new IPEndPoint(ipaddress, port);
    var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
    {
        ReceiveTimeout = (int)TimeSpan.FromSeconds(5).TotalMilliseconds,
        SendTimeout = (int)TimeSpan.FromSeconds(30).TotalMilliseconds,
    };
    await socket.ConnectAsync(ipendPoint);

    try
    {
        while (true)
        {
            string? response = await SendCommandToServerAsync(socket, ipendPoint, device, command);
            command = null;
            if (response == null)
            {
                await Task.Delay(2000);
                continue;
            }

            if (response.Trim() != "GET&")
            { // 如果只收到'GET&'则不更新客户端,因为没有有效提示
                var parsedResponse = ParseResponse(response);
                await Clients.Group(device).SendAsync("ReceiveResponse", device, parsedResponse);
            }

            if (response.Trim().EndsWith("GET&"))
            {
                break;
            }

            await Task.Delay(2000);
        }
    }
    finally
    {
        if (socket.Connected)
        {
            socket.Close();
        }
    }
}

private async Task<string?> SendCommandToServerAsync(Socket socket, IPEndPoint ipendPoint, string device, string? command)
{
    var responseSB = new StringBuilder();
    var debugSB = new StringBuilder();

    var text = device + "&" + command?.ToUpper() + "\n";
    var bRequest = Encoding.UTF8.GetBytes(text.ToString());

    debugSB.Append("请求内容: " + text);

    string? result;
    try
    {
        if (command != null)
        {
            debugSB.AppendLine("已发送请求");
            await socket.SendAsync(bRequest);
        }
        else
        {
            debugSB.AppendLine("命令为空");
        }
        await Task.Delay(900);
        byte[] array = new byte[8192];
        int num2 = 0;
        string text2;
        while (true)
        {
            await Task.Delay(300);
            if (!socket.Connected)
            {
                debugSB.AppendLine("Socket未连接");
                socket.Connect(ipendPoint);
            }

            int num3 = await socket.ReceiveAsync(array, SocketFlags.None);
            debugSB.AppendLine("收到 " + num3.ToString() + " 字节数据");
            num2++;
            if (num3 > 0)
            {
                text2 = Encoding.Default.GetString(array, 0, num3);
                responseSB.AppendLine("收到内容: " + text2);
                string text3 = text2;
                text3 = text3.Replace("BEEP&" + '�', "");
                if (!(text3 == "") && text2.IndexOf('�') >= 0)
                {
                    break;
                }
                else
                {
                    _emailSender.SendEmail("george.duckett@aqualisa.co.uk", "AquaBar 信息", "到达此处,响应内容: " + text2);
                }
            }
            else if (num2 == 20)
            {
                debugSB.AppendLine("跳转到Block_12");
                goto Block_12;
            }
        }

        debugSB.AppendLine("退出循环");

        string[] array2 = text2.Split(['�']);
        if (array2.Length > 2 && array2[0] == array2[1] && array2[0] != "BEEP&" && array2[0] != "PROMPT&")
        {
            text2 = array2[1];
        }
        return text2.Replace('�', ' ');
    Block_12:
        result = "请按回车继续..GET&";

        _emailSender.SendEmail("george.duckett@aqualisa.co.uk", "AquaBar 信息", "到达'请按回车继续'分支,响应内容: " + responseSB.ToString() + Environment.NewLine + Environment.NewLine + "调试信息: " + Environment.NewLine + debugSB.ToString());
    }
    catch (SocketException ex) when (ex.SocketErrorCode == SocketError.TimedOut)
    {
        result = null;
    }
    catch (IOException)
    {
        result = null;
    }
    catch (Exception)
    {
        result = "BEEP& ***错误***\n服务器连接错误。\n请联系管理员。";
    }
    return result;
}
重启生效的可能原因
  1. Socket资源泄漏:虽然代码里在finally块关闭Socket,但异常场景下可能存在资源未完全释放的情况(比如Socket.Close()没有彻底释放句柄)。重启IIS会清空进程所有资源,包括泄漏的Socket句柄,让新连接能正常建立。
  2. TCP连接TIME_WAIT堆积:每次关闭Socket后,TCP连接会进入TIME_WAIT状态(默认几分钟),如果请求量较大,本地会堆积大量TIME_WAIT连接,导致服务器端认为来自该IP的连接数超限,拒绝响应。重启IIS会清空本地TIME_WAIT连接池,恢复正常连接能力。
  3. 线程池耗尽:代码中Socket断开时调用同步的socket.Connect(ipendPoint),在异步方法中阻塞线程,长期积累会耗尽ASP.NET线程池,后续请求无法处理。重启IIS会重置线程池,恢复处理能力。
  4. 协议解析阻塞:代码依赖特定终止符'�'判断响应结束,如果服务器某次返回的响应没有这个符号,接收循环会一直等待直到超时,这类阻塞请求积累后会占用大量资源,重启IIS会清空这些阻塞的请求线程。
根源解决建议
  • 用using语句管理Socket生命周期:确保无论是否异常都能彻底释放Socket资源,替换手动Close:
    using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp)
    {
        ReceiveTimeout = (int)TimeSpan.FromSeconds(5).TotalMilliseconds,
        SendTimeout = (int)TimeSpan.FromSeconds(30).TotalMilliseconds,
    };
    await socket.ConnectAsync(ipendPoint);
    
  • 优化TCP连接回收:设置LingerOption避免TIME_WAIT堆积:
    socket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.Linger, new LingerOption(true, 0));
    
  • 替换同步Connect为异步:Socket断开时用await socket.ConnectAsync(ipendPoint)替代同步方法,避免阻塞线程。
  • 增强解析逻辑容错性:在接收循环中增加时间超时判断,不仅计数num2,还要记录开始时间,超过设定时长(比如10秒)就终止循环并标记异常,避免无限等待。
  • 完善日志监控:记录每个Socket连接的完整生命周期(建立、发送、接收、关闭)及原始响应数据,出现问题时快速定位是连接问题还是解析问题。

内容的提问来源于stack exchange,提问作者George Duckett

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 11:50:54