当使用 Socket 进行通信时,由于各种不同的因素,都有可能导致死连接停留在服务器端,假如服务端需要处理的连接较多,就有可能造成服务器资源严重浪费,对此,本文将阐述其原理以及解决方法。
在写 Socket 进行通讯时,我们必须预料到各种可能发生的情况并对其进行处理,通常情况下,有以下两种情况可能造成死连接:
a) 通讯程序编写不完善
这里要指出的一点就是,绝大多数程序都是由于程序编写不完善所造成的死连接,即对 Socket 未能进行完善的管理,导致占用端口导致服务器资源耗尽。当然,很多情况下,程序可能不是我们所写,而由于程序代码的复杂、杂乱等原因所导致难以维护也是我们所需要面对的。
网上有很多文章都提到 Socket 长时间处于 CLOSE_WAIT 状态下的问题,说可以使用 Keepalive 选项设置 TCP 心跳来解决,但是却发现设置选项后未能收到效果 。
因此,这里我分享出自己的解决方案:
Windows 中对于枚举系统网络连接有一些非常方便的 API:
- GetTcpTable : 获得 TCP 连接表
- GetExtendedTcpTable : 获得扩展后的 TCP 连接表,相比 GetTcpTable 更为强大,可以获取与连接的进程 ID
- SetTcpEntry : 设置 TCP 连接状态,但据 MSDN 所述,只能设置状态为 DeleteTcb,即删除连接
相信大多数朋友看到这些 API ,就已经了解到我们下一步要做什么了;枚举所有 TCP 连接,筛选出本进程的连接,最后判断是否 CLOSE_WAIT 状态,如果是,则使用 SetTcpEntry 关闭。
其实 Sysinternal 的 TcpView 工具也是应用上述 API 实现其功能的,此工具为我常用的网络诊断工具,同时也可作为一个简单的手动式网络防火墙。
下面来看 Zealic 封装后的代码:
TcpManager.cs
1/**
2<code>
3 <revsion>$Rev: 0 $</revision>
4 <owner name="Zealic" mail="rszealic(at)gmail.com" />
5</code>
6**/
7using System;
8using System.Collections.Generic;
9using System.Diagnostics;
10using System.Net;
11using System.Net.NetworkInformation;
12using System.Runtime.InteropServices;
13
14
15namespace Zealic.Network
16{
17 /// <summary>
18 /// TCP 管理器
19 /// </summary>
20 public static class TcpManager
21 {
22 #region PInvoke define
23 private const int TCP_TABLE_OWNER_PID_ALL = 5;
24
25 [DllImport("iphlpapi.dll", SetLastError = true)]
26 private static extern uint GetExtendedTcpTable(
27 IntPtr pTcpTable, ref int dwOutBufLen, bool sort, int ipVersion, int tblClass, int reserved);
28
29 [DllImport("iphlpapi.dll")]
30 private static extern int SetTcpEntry(ref MIB_TCPROW pTcpRow);
31
32
33 [StructLayout(LayoutKind.Sequential)]
34 private struct MIB_TCPROW
35 {
36 public TcpState dwState;
37 public int dwLocalAddr;
38 public int dwLocalPort;
39 public int dwRemoteAddr;
40 public int dwRemotePort;
41 }
42
43 [StructLayout(LayoutKind.Sequential)]
44 private struct MIB_TCPROW_OWNER_PID
45 {
46 public TcpState dwState;
47 public uint dwLocalAddr;
48 public int dwLocalPort;
49 public uint dwRemoteAddr;
50 public int dwRemotePort;
51 public int dwOwningPid;
52 }
53
54 [StructLayout(LayoutKind.Sequential)]
55 private struct MIB_TCPTABLE_OWNER_PID
56 {
57 public uint dwNumEntries;
58 private MIB_TCPROW_OWNER_PID table;
59 }
60 #endregion
61
62 private static MIB_TCPROW_OWNER_PID[] GetAllTcpConnections()
63 {
64 const int NO_ERROR = 0;
65 const int IP_v4 = 2;
66 MIB_TCPROW_OWNER_PID[] tTable = null;
67 int buffSize = 0;
68 GetExtendedTcpTable(IntPtr.Zero, ref buffSize, true, IP_v4, TCP_TABLE_OWNER_PID_ALL, 0);
69 IntPtr buffTable = Marshal.AllocHGlobal(buffSize);
70 try
71 {
72 if (NO_ERROR != GetExtendedTcpTable(buffTable, ref buffSize, true, IP_v4, TCP_TABLE_OWNER_PID_ALL, 0)) return null;
73 MIB_TCPTABLE_OWNER_PID tab =
74 (MIB_TCPTABLE_OWNER_PID)Marshal.PtrToStructure(buffTable, typeof(MIB_TCPTABLE_OWNER_PID));
75 IntPtr rowPtr = (IntPtr)((long)buffTable + Marshal.SizeOf(tab.dwNumEntries));
76 tTable = new MIB_TCPROW_OWNER_PID[tab.dwNumEntries];
77
78 int rowSize = Marshal.SizeOf(typeof(MIB_TCPROW_OWNER_PID));
79 for (int i = 0; i < tab.dwNumEntries; i++)
80 {
81 MIB_TCPROW_OWNER_PID tcpRow =
82 (MIB_TCPROW_OWNER_PID)Marshal.PtrToStructure(rowPtr, typeof(MIB_TCPROW_OWNER_PID));
83 tTable[i] = tcpRow;
84 rowPtr = (IntPtr)((int)rowPtr + rowSize);
85 }
86 }
87 finally
88 {
89 Marshal.FreeHGlobal(buffTable);
90 }
91 return tTable;
92 }
93
94 private static int TranslatePort(int port)
95 {
96 return ((port & 0xFF) << 8 | (port & 0xFF00) >> 8);
97 }
98
99 public static bool Kill(TcpConnectionInfo conn)
100 {
101 if (conn == null) throw new ArgumentNullException("conn");
102 MIB_TCPROW row = new MIB_TCPROW();
103 row.dwState = TcpState.DeleteTcb;
104#pragma warning disable 612,618
105 row.dwLocalAddr = (int)conn.LocalEndPoint.Address.Address;
106#pragma warning restore 612,618
107 row.dwLocalPort = TranslatePort(conn.LocalEndPoint.Port);
108#pragma warning disable 612,618
109 row.dwRemoteAddr = (int)conn.RemoteEndPoint.Address.Address;
110#pragma warning restore 612,618
111 row.dwRemotePort = TranslatePort(conn.RemoteEndPoint.Port);
112 return SetTcpEntry(ref row) == 0;
113 }
114
115 public static bool Kill(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint)
116 {
117 if (localEndPoint == null) throw new ArgumentNullException("localEndPoint");
118 if (remoteEndPoint == null) throw new ArgumentNullException("remoteEndPoint");
119 MIB_TCPROW row = new MIB_TCPROW();
120 row.dwState = TcpState.DeleteTcb;
121#pragma warning disable 612,618
122 row.dwLocalAddr = (int)localEndPoint.Address.Address;
123#pragma warning restore 612,618
124 row.dwLocalPort = TranslatePort(localEndPoint.Port);
125#pragma warning disable 612,618
126 row.dwRemoteAddr = (int)remoteEndPoint.Address.Address;
127#pragma warning restore 612,618
128 row.dwRemotePort = TranslatePort(remoteEndPoint.Port);
129 return SetTcpEntry(ref row) == 0;
130 }
131
132
133 public static TcpConnectionInfo[] GetTableByProcess(int pid)
134 {
135 MIB_TCPROW_OWNER_PID[] tcpRows = GetAllTcpConnections();
136 if (tcpRows == null) return null;
137 List<TcpConnectionInfo> list = new List<TcpConnectionInfo>();
138 foreach (MIB_TCPROW_OWNER_PID row in tcpRows)
139 {
140 if (row.dwOwningPid == pid)
141 {
142 int localPort = TranslatePort(row.dwLocalPort);
143 int remotePort = TranslatePort(row.dwRemotePort);
144 TcpConnectionInfo conn =
145 new TcpConnectionInfo(
146 new IPEndPoint(row.dwLocalAddr, localPort),
147 new IPEndPoint(row.dwRemoteAddr, remotePort),
148 row.dwState);
149 list.Add(conn);
150 }
151 }
152 return list.ToArray();
153 }
154
155 public static TcpConnectionInfo[] GetTalbeByCurrentProcess()
156 {
157 return GetTableByProcess(Process.GetCurrentProcess().Id);
158 }
159
160 }
161}
TcpConnectionInfo.cs
1/**
2<code>
3 <revsion>$Rev: 608 $</revision>
4 <owner name="Zealic" mail="rszealic(at)gmail.com" />
5</code>
6**/
7using System;
8using System.Collections.Generic;
9using System.Net;
10using System.Net.NetworkInformation;
11
12
13namespace Zealic.Network
14{
15 /// <summary>
16 /// TCP 连接信息
17 /// </summary>
18 public sealed class TcpConnectionInfo : IEquatable<TcpConnectionInfo>, IEqualityComparer<TcpConnectionInfo>
19 {
20 private readonly IPEndPoint _LocalEndPoint;
21 private readonly IPEndPoint _RemoteEndPoint;
22 private readonly TcpState _State;
23
24 public TcpConnectionInfo(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, TcpState state)
25 {
26 if (localEndPoint == null) throw new ArgumentNullException("localEndPoint");
27 if (remoteEndPoint == null) throw new ArgumentNullException("remoteEndPoint");
28 _LocalEndPoint = localEndPoint;
29 _RemoteEndPoint = remoteEndPoint;
30 _State = state;
31 }
32
33 public IPEndPoint LocalEndPoint
34 {
35 get { return _LocalEndPoint; }
36 }
37
38 public IPEndPoint RemoteEndPoint
39 {
40 get { return _RemoteEndPoint; }
41 }
42
43 public TcpState State
44 {
45 get { return _State; }
46 }
47
48 public bool Equals(TcpConnectionInfo x, TcpConnectionInfo y)
49 {
50 return (x.LocalEndPoint.Equals(y.LocalEndPoint) && x.RemoteEndPoint.Equals(y.RemoteEndPoint));
51 }
52
53 public int GetHashCode(TcpConnectionInfo obj)
54 {
55 return obj.LocalEndPoint.GetHashCode() ^ obj.RemoteEndPoint.GetHashCode();
56 }
57
58 public bool Equals(TcpConnectionInfo other)
59 {
60 return Equals(this, other);
61 }
62
63 public override bool Equals(object obj)
64 {
65 if (obj == null || !(obj is TcpConnectionInfo))
66 return false;
67 return Equals(this, (TcpConnectionInfo)obj);
68 }
69
70 }
71}
至此,我们可以通过 TcpManager 类的 GetTableByProcess 方法获取进程中所有的 TCP 连接信息,然后通过 Kill 方法强制关连接以回收系统资源,虽然很C很GX,但是很有效。
通常情况下,我们可以使用 Timer 来定时检测进程中的 TCP 连接状态,确定其是否处于 CLOSE_WAIT 状态,当超过指定的次数/时间时,就把它干掉。
不过,相对这样的解决方法,我还是推荐在设计 Socket 服务端程序的时候,一定要管理所有的连接,而非上述方法。
b) 网络/硬件故障
现在我们再来看第二种情况,当网络/硬件故障时,如何应对;与上面不同,这样的情况 TCP 可能处于 ESTABLISHED、CLOSE_WAIT、FIN_WAIT 等状态中的任何一种,这时才是 Keepalive 该出马的时候。
默认情况下 Keepalive 的时间设置为两小时,如果是请求比较多的服务端程序,两小时未免太过漫长,等到它时间到,估计连黄花菜都凉了,好在我们可以通过 Socket.IOControl 方法手动设置其属性,以达到我们的目的。
关键代码如下:
1// 假设 accepted 到的 Socket 为变量 client
2// ...
3// 设置 TCP 心跳,空闲 15 秒,每 5 秒检查一次
4byte[] inOptionValues = new byte[4 * 3];
5BitConverter.GetBytes((uint)1).CopyTo(inOptionValues, 0);
6BitConverter.GetBytes((uint)15000).CopyTo(inOptionValues, 4);
7BitConverter.GetBytes((uint)5000).CopyTo(inOptionValues, 8);
8client.IOControl(IOControlCode.KeepAliveValues, inOptionValues, null);
以上代码的作用就是设置 TCP 心跳为 5 秒,当三次检测到无法与客户端连接后,将会关闭 Socket。
相信上述代码加上说明,对于有一定基础读者理解起来应该不难,今天到此为止。
c) 结束语
其实对于 Socket 程序设计来说,良好的通信协议才是稳定的保证,类似于这样的问题,如果在应用程序通信协议中加入自己的心跳包,不仅可以处理多种棘手的问题,还可以在心跳中加入自己的简单校验功能,防止包数据被 WPE 等软件篡改。但是,很多情况下这些都不是我们所能决定的,因此,才有了本文中提出的方法。
警告 :本文系 Zealic 创作,并基于 CC 3.0 共享创作许可协议 发布,如果您转载此文或使用其中的代码,请务必先阅读协议内容。