? 當使用 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
/** <code><revsion>$Rev: 0 $</revision><owner name="Zealic" mail="rszealic(at)gmail.com" /> </code> **/ using System; using System.Collections.Generic; using System.Diagnostics; using System.Net; using System.Net.NetworkInformation; using System.Runtime.InteropServices;namespace Zealic.Network {/// <summary>/// TCP 管理器/// </summary>public static class TcpManager{#region PInvoke defineprivate const int TCP_TABLE_OWNER_PID_ALL = 5;[DllImport("iphlpapi.dll", SetLastError = true)]private static extern uint GetExtendedTcpTable(IntPtr pTcpTable, ref int dwOutBufLen, bool sort, int ipVersion, int tblClass, int reserved);[DllImport("iphlpapi.dll")]private static extern int SetTcpEntry(ref MIB_TCPROW pTcpRow);[StructLayout(LayoutKind.Sequential)]private struct MIB_TCPROW{public TcpState dwState;public int dwLocalAddr;public int dwLocalPort;public int dwRemoteAddr;public int dwRemotePort;}[StructLayout(LayoutKind.Sequential)]private struct MIB_TCPROW_OWNER_PID{public TcpState dwState;public uint dwLocalAddr;public int dwLocalPort;public uint dwRemoteAddr;public int dwRemotePort;public int dwOwningPid;}[StructLayout(LayoutKind.Sequential)]private struct MIB_TCPTABLE_OWNER_PID{public uint dwNumEntries;private MIB_TCPROW_OWNER_PID table;}#endregionprivate static MIB_TCPROW_OWNER_PID[] GetAllTcpConnections(){const int NO_ERROR = 0;const int IP_v4 = 2;MIB_TCPROW_OWNER_PID[] tTable = null;int buffSize = 0;GetExtendedTcpTable(IntPtr.Zero, ref buffSize, true, IP_v4, TCP_TABLE_OWNER_PID_ALL, 0);IntPtr buffTable = Marshal.AllocHGlobal(buffSize);try{if (NO_ERROR != GetExtendedTcpTable(buffTable, ref buffSize, true, IP_v4, TCP_TABLE_OWNER_PID_ALL, 0)) return null;MIB_TCPTABLE_OWNER_PID tab =(MIB_TCPTABLE_OWNER_PID)Marshal.PtrToStructure(buffTable, typeof(MIB_TCPTABLE_OWNER_PID));IntPtr rowPtr = (IntPtr)((long)buffTable + Marshal.SizeOf(tab.dwNumEntries));tTable = new MIB_TCPROW_OWNER_PID[tab.dwNumEntries];int rowSize = Marshal.SizeOf(typeof(MIB_TCPROW_OWNER_PID));for (int i = 0; i < tab.dwNumEntries; i++){MIB_TCPROW_OWNER_PID tcpRow =(MIB_TCPROW_OWNER_PID)Marshal.PtrToStructure(rowPtr, typeof(MIB_TCPROW_OWNER_PID));tTable[i] = tcpRow;rowPtr = (IntPtr)((int)rowPtr + rowSize);}}finally{Marshal.FreeHGlobal(buffTable);}return tTable;}private static int TranslatePort(int port){return ((port & 0xFF) << 8 | (port & 0xFF00) >> 8);}public static bool Kill(TcpConnectionInfo conn){if (conn == null) throw new ArgumentNullException("conn");MIB_TCPROW row = new MIB_TCPROW();row.dwState = TcpState.DeleteTcb; #pragma warning disable 612,618row.dwLocalAddr = (int)conn.LocalEndPoint.Address.Address; #pragma warning restore 612,618row.dwLocalPort = TranslatePort(conn.LocalEndPoint.Port); #pragma warning disable 612,618row.dwRemoteAddr = (int)conn.RemoteEndPoint.Address.Address; #pragma warning restore 612,618row.dwRemotePort = TranslatePort(conn.RemoteEndPoint.Port);return SetTcpEntry(ref row) == 0;}public static bool Kill(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint){if (localEndPoint == null) throw new ArgumentNullException("localEndPoint");if (remoteEndPoint == null) throw new ArgumentNullException("remoteEndPoint");MIB_TCPROW row = new MIB_TCPROW();row.dwState = TcpState.DeleteTcb; #pragma warning disable 612,618row.dwLocalAddr = (int)localEndPoint.Address.Address; #pragma warning restore 612,618row.dwLocalPort = TranslatePort(localEndPoint.Port); #pragma warning disable 612,618row.dwRemoteAddr = (int)remoteEndPoint.Address.Address; #pragma warning restore 612,618row.dwRemotePort = TranslatePort(remoteEndPoint.Port);return SetTcpEntry(ref row) == 0;}public static TcpConnectionInfo[] GetTableByProcess(int pid){MIB_TCPROW_OWNER_PID[] tcpRows = GetAllTcpConnections();if (tcpRows == null) return null;List<TcpConnectionInfo> list = new List<TcpConnectionInfo>();foreach (MIB_TCPROW_OWNER_PID row in tcpRows){if (row.dwOwningPid == pid){int localPort = TranslatePort(row.dwLocalPort);int remotePort = TranslatePort(row.dwRemotePort);TcpConnectionInfo conn =new TcpConnectionInfo(new IPEndPoint(row.dwLocalAddr, localPort),new IPEndPoint(row.dwRemoteAddr, remotePort),row.dwState);list.Add(conn);}}return list.ToArray();}public static TcpConnectionInfo[] GetTalbeByCurrentProcess(){return GetTableByProcess(Process.GetCurrentProcess().Id);}} } |
TcpConnectionInfo.cs
/** <code><revsion>$Rev: 608 $</revision><owner name="Zealic" mail="rszealic(at)gmail.com" /> </code> **/ using System; using System.Collections.Generic; using System.Net; using System.Net.NetworkInformation;namespace Zealic.Network {/// <summary>/// TCP 連接信息/// </summary>public sealed class TcpConnectionInfo : IEquatable<TcpConnectionInfo>, IEqualityComparer<TcpConnectionInfo>{private readonly IPEndPoint _LocalEndPoint;private readonly IPEndPoint _RemoteEndPoint;private readonly TcpState _State;public TcpConnectionInfo(IPEndPoint localEndPoint, IPEndPoint remoteEndPoint, TcpState state){if (localEndPoint == null) throw new ArgumentNullException("localEndPoint");if (remoteEndPoint == null) throw new ArgumentNullException("remoteEndPoint");_LocalEndPoint = localEndPoint;_RemoteEndPoint = remoteEndPoint;_State = state;}public IPEndPoint LocalEndPoint{get { return _LocalEndPoint; }}public IPEndPoint RemoteEndPoint{get { return _RemoteEndPoint; }}public TcpState State{get { return _State; }}public bool Equals(TcpConnectionInfo x, TcpConnectionInfo y){return (x.LocalEndPoint.Equals(y.LocalEndPoint) && x.RemoteEndPoint.Equals(y.RemoteEndPoint));}public int GetHashCode(TcpConnectionInfo obj){return obj.LocalEndPoint.GetHashCode() ^ obj.RemoteEndPoint.GetHashCode();}public bool Equals(TcpConnectionInfo other){return Equals(this, other);}public override bool Equals(object obj){if (obj == null || !(obj is TcpConnectionInfo))return false;return Equals(this, (TcpConnectionInfo)obj);}} } |
?
? 至此,我們可以通過 TcpManager 類的 GetTableByProcess 方法獲取進程中所有的 TCP 連接信息,然后通過? Kill 方法強制關連接以回收系統資源,雖然很C很GX,但是很有效。
? 通常情況下,我們可以使用 Timer 來定時檢測進程中的 TCP 連接狀態,確定其是否處于 CLOSE_WAIT 狀態,當超過指定的次數/時間時,就把它干掉。
? 不過,相對這樣的解決方法,我還是推薦在設計 Socket 服務端程序的時候,一定要管理所有的連接,而非上述方法。
?
b) 網絡/硬件故障
? 現在我們再來看第二種情況,當網絡/硬件故障時,如何應對;與上面不同,這樣的情況 TCP 可能處于 ESTABLISHED、CLOSE_WAIT、FIN_WAIT 等狀態中的任何一種,這時才是 Keepalive 該出馬的時候。
? 默認情況下 Keepalive 的時間設置為兩小時,如果是請求比較多的服務端程序,兩小時未免太過漫長,等到它時間到,估計連黃花菜都涼了,好在我們可以通過 Socket.IOControl 方法手動設置其屬性,以達到我們的目的。
? 關鍵代碼如下:
// 假設 accepted 到的 Socket 為變量 client ... // 設置 TCP 心跳,空閑 15 秒,每 5 秒檢查一次 byte[] inOptionValues = new byte[4 * 3]; BitConverter.GetBytes((uint)1).CopyTo(inOptionValues, 0); BitConverter.GetBytes((uint)15000).CopyTo(inOptionValues, 4); BitConverter.GetBytes((uint)5000).CopyTo(inOptionValues, 8); client.IOControl(IOControlCode.KeepAliveValues, inOptionValues, null); |
? 以上代碼的作用就是設置 TCP 心跳為 5 秒,當三次檢測到無法與客戶端連接后,將會關閉 Socket。
? 相信上述代碼加上說明,對于有一定基礎讀者理解起來應該不難,今天到此為止。
?
c) 結束語
? 其實對于 Socket 程序設計來說,良好的通信協議才是穩定的保證,類似于這樣的問題,如果在應用程序通信協議中加入自己的心跳包,不僅可以處理多種棘手的問題,還可以在心跳中加入自己的簡單校驗功能,防止包數據被 WPE 等軟件篡改。但是,很多情況下這些都不是我們所能決定的,因此,才有了本文中提出的方法。
? 警告 :本文系 Zealic 創作,并基于 CC 3.0 共享創作許可協議 發布,如果您轉載此文或使用其中的代碼,請務必先閱讀協議內容。
Zealic 于 2008-3-15