当使用 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 define
private 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;
}
#endregion private 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,618
row.dwLocalAddr = (int)conn.LocalEndPoint.Address.Address;
#pragma warning restore 612,618
row.dwLocalPort = TranslatePort(conn.LocalEndPoint.Port);
#pragma warning disable 612,618
row.dwRemoteAddr = (int)conn.RemoteEndPoint.Address.Address;
#pragma warning restore 612,618
row.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,618
row.dwLocalAddr = (int)localEndPoint.Address.Address;
#pragma warning restore 612,618
row.dwLocalPort = TranslatePort(localEndPoint.Port);
#pragma warning disable 612,618
row.dwRemoteAddr = (int)remoteEndPoint.Address.Address;
#pragma warning restore 612,618
row.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 共享创作许可协议 发布,如果您转载此文或使用其中的代码,请务必先阅读协议内容。

Socket 死连接详解的更多相关文章

  1. socket原理详解

    1.什么是socket 我们知道进程通信的方法有管道.命名管道.信号.消息队列.共享内存.信号量,这些方法都要求通信的两个进程位于同一个主机.但是如果通信双方不在同一个主机又该如何进行通信呢?在计算机 ...

  2. windows socket函数详解

    windows socket函数详解 近期一直用第三方库写网络编程,反倒是遗忘了网络编程最底层的知识.因而产生了整理Winsock函数库的想法.以下知识点均来源于MSDN,本人只做翻译工作.虽然很多前 ...

  3. Android Socket通信详解

    一.Socket通信简介  Android与服务器的通信方式主要有两种,一是Http通信,一是Socket通信.两者的最大差异在于,http连接使用的是“请求—响应方式”,即在请求时建立连接通道,当客 ...

  4. socket接口详解

    1. socket概述 socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信. socket起源于UNIX,在Unix一切 ...

  5. c/c++ socket函数详解

    c/c++ socket函数详解 注意: 使用socketAPI前,要先将相关链接库(Ws2_32.lib)加入链接,并使用WSAStartUp函数初始化.每个socket函数都可能失败(返回-1), ...

  6. (转)python标准库中socket模块详解

    python标准库中socket模块详解 socket模块简介 原文:http://www.lybbn.cn/data/datas.php?yw=71 网络上的两个程序通过一个双向的通信连接实现数据的 ...

  7. Linux的SOCKET编程详解(转)

    Linux的SOCKET编程详解 1. 网络中进程之间如何通信 进 程通信的概念最初来源于单机系统.由于每个进程都在自己的地址范围内运行,为保证两个相互通信的进 程之间既互不干扰又协调一致工作,操作系 ...

  8. 常用socket函数详解

    常用socket函数详解 关于socket函数,每个的意义和基本功能都知道,但每次使用都会去百度,参数到底是什么,返回值代表什么意义,就是说用的少,也记得不够精确.每次都查半天,经常烦恼于此.索性都弄 ...

  9. 【ARM-Linux开发】Linux的SOCKET编程详解

    Linux的SOCKET编程详解 1. 网络中进程之间如何通信 进 程通信的概念最初来源于单机系统.由于每个进程都在自己的地址范围内运行,为保证两个相互通信的进 程之间既互不干扰又协调一致工作,操作系 ...

随机推荐

  1. Node.js初级

    package.json文件字段说明 name:包名.包名是唯一的,只能包含小写字母.数字和下划线. version:包版本号. description:包说明. keywords:关键字数组.用于搜 ...

  2. android_ViewPager_实现导航页

    android_ViewPager_实现导航页 既然是实现导航页的效果,那么我们肯定是要实现ViewPager的 要实现的效果如下 1.用户进入欢迎页面 2.判断是否是第一次进入,如果是,则进入导航页 ...

  3. 准备开一个地图SDK的开源项目

    最近有点空闲时间了, 准备开一个地图SDK的开源项目, 现在的地图SDK已经有很多了, 再做一个跟重新发明个轮子差不多, 但还想做的原因是想在别的轮子的基础上造个轮子... 初步设想是基于开源的地图渲 ...

  4. 最火的Android开源项目(完结篇)

    65. AndroidSideMenu AndroidSideMenu能够让你轻而易举地创建侧滑菜单.需要注意的是,该项目自身并不提供任何创建菜单的工具,因此,开发者可以自由创建内部菜单. 66. A ...

  5. Android-怎样用命令行进行打包

    转载请标明出处:http://blog.csdn.net/goldenfish1919/article/details/40978859 1.生成R文件 aapt package -f -m -J . ...

  6. 利用PS脚本自动删除7天之前建立的目录-方法1!

    目前有一个备份目录,目录名称为d:\temp\bak目录,在这目录下,根据备份要求,自动生成了如下目录的列表: 20131012 20131011 20131010 20131009 20131008 ...

  7. Delphi 根据快捷方式路径取源文件地址

    unit Unit1;interfaceuses  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, ...

  8. 电子设计省赛--SPWM(死区时间)

    //2014年4月17日 //2014年6月20日入"未完毕" //2014年6月21日 有两种方案:双定时器和单定时器 学长表示双定时器输出波形不好,还是单定时器好. 原理例如以 ...

  9. careercup-递归和动态规划 9.3

    9.3 在数组A[0...n-1]中,有所谓的魔术索引,满足条件A[i]=i.给定一个有序整数数组,元素值给不相同,编写一个方法,在数组A中找出一个魔术索引,若存在的话. 进阶: 如果数组元素有重复值 ...

  10. Android开发之线程池使用总结

    线程池算是Android开发中非常常用的一个东西了,只要涉及到线程的地方,大多数情况下都会涉及到线程池.Android开发中线程池的使用和Java中线程池的使用基本一致.那么今天我想来总结一下Andr ...