在某台机器上获取活动的 TCP/UDP 连接






4.81/5 (32投票s)
2003年6月10日
7分钟阅读

339651

11563
本文介绍 IP 辅助 API 的主要 TCP/UDP 函数的实现,该 API 用于获取活动连接的信息,包括连接所关联的进程。
引言
该库的主要目的是监视 PC 上活动的 UDP/TCP 连接(类似于 netstat 的功能);它主要是 IpHelperApi.dll 的一个包装器,实现了四个通用函数,用于获取 TCP 和 UDP 连接的统计信息(GetUdpStats()/
GetTcpStats()
),以及 UDP 和 TCP 连接的表(GetUdpConnexions()
/ GetTcpConnexions()
)。
它还实现了 IpHelperApi.dll 的两个未记录函数,这些函数与 GetUdpConnexions
/GetTcpConnexions
类似,只是它们会获取与连接关联的 processID
;在 Win 32 API 中,它们名为:AllocateAndGetTcpExTableFromStack
和 AllocateAndGetUdpExTableFromStack
。
包装 IpHlpAPI.dll
该库名为 IPHelper
,它只是 .NET 框架 P/Invoke 机制对 IpHelperAPI.dll 的一个包装器。在 IPHlpAPI32.cs 文件中包含了 IPHlpApi.dll 中所有函数和结构的声明;它使用了 System.Runtime.InteropServices
命名空间中的标准属性。
[DllImport("iphlpapi.dll",SetLastError=true)]
public extern static int GetUdpStatistics (ref MIB_UDPSTATS pStats );
SetLastError=true
标志允许我们获取 P/Invoke 机制中引发的任何错误的有关信息,通常错误代码由 API 函数返回,但此代码并不非常直观,因此我包含了一个函数,使用 kernell32.dll 的 FormatMessage
函数来获取错误消息描述。
public static string GetAPIErrorMessageDescription(int ApiErrNumber )
{
System.Text.StringBuilder sError = new System.Text.StringBuilder(512);
int lErrorMessageLength;
lErrorMessageLength = FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM,
(IntPtr)0, ApiErrNumber, 0, sError, sError.Capacity, (IntPtr)0) ;
if(lErrorMessageLength > 0) {
string strgError = sError.ToString();
strgError=strgError.Substring(0,strgError.Length-2);
return strgError+" ("+ApiErrNumber.ToString()+")";
}
return "none";
}
此函数仅从系统中获取消息描述,如果找不到描述,则返回“none”。
在 MSDN 文档中,所有 iphlpapi 函数都通过传递的参数填充自己的结构;对于没有数组、指针或嵌套结构的简单结构,很容易正确填充结构,但对于复杂结构,这可能会更困难。
让我们看看如何做到这一点。
从 API 调用中获取结果
简单结构包装
首先,让我们看看如何获取一个简单函数 GetTcpStats()
的结果;此函数用于获取 TCP 连接的各种信息,例如活动连接的数量。
在原始的 C++ 库中,它的声明如下:
typedef struct _MIB_TCPSTATS { //The structure holding the results
DWORD dwRtoAlgorithm;
//.. Other DWORD fields
DWORD dwNumConns;
}MIB_TCPSTATS, *PMIB_TCPSTATS
DWORD GetTcpStatistics(PMIB_TCPSTATS pStats);
此结构非常简单,所有 DWORD
字段在 c# 中都可以替换为 integer。
[StructLayout(LayoutKind.Sequential)]
public struct MIB_TCPSTATS
{
public int dwRtoAlgorithm;
//.. Other int fields
public int dwNumConns ;
}
[DllImport("iphlpapi.dll",SetLastError=true)]
public extern static int GetTcpStatistics (ref MIB_TCPSTATS pStats );
请注意,需要 ref
关键字,因为在 C++ 调用中,函数需要结构的指针作为参数。
因此,在 IPHelper
类中,有一个公共方法可以填充 MIB_TCPSTATS
结构。
public void GetTcpStats()
{
TcpStats = new MIB_TCPSTATS();
IPHlpAPI32Wrapper.GetTcpStatistics(ref TcpStats);
}
非常直接。
请注意,GetUdpStats()
函数的工作方式完全相同。
更复杂的结构包装
GetTcpTable()
函数获取所有活动 TCP 连接的数组,包括本地端点、远程端点和连接状态;这些结果存储在两个嵌套结构中。
typedef struct _MIB_TCPTABLE { // The top level structure
//that hold an array of the second structure
DWORD dwNumEntries;
MIB_TCPROW table[ANY_SIZE]; //an array of undefined size
} MIB_TCPTABLE, *PMIB_TCPTABLE;
typedef struct _MIB_TCPROW { //the structure that represent
//a single row in the tcp table
DWORD dwState;
DWORD dwLocalAddr;
DWORD dwLocalPort;
DWORD dwRemoteAddr;
DWORD dwRemotePort;
} MIB_TCPROW, *PMIB_TCPROW
DWORD GetTcpTable(PMIB_TCPTABLE pTcpTable,PDWORD pdwSize,BOOL bOrder);
GetTcpTable
函数有三个参数:一个指向将保存结果的结构的指针、pTcpTable
参数指向的缓冲区的大小(请注意,如果缓冲区太小,函数将在输出时将此参数设置为所需缓冲区大小),以及一个 bool
标志,用于指定我们是否希望结果排序。
这里的主要问题是,我们无法直接为这两个结构进行封送处理,因为存在一个大小不确定的数组,但有几种解决方案可以解决这个问题;我选择使用一个简单的通用方法,将第一个参数替换为字节数组。
[DllImport("iphlpapi.dll",SetLastError=true)]
public static extern int GetTcpTable(byte[] pTcpTable,
out int pdwSize, bool bOrder);
然后,我们必须在 IPHelper
类中使用此函数,在 GetTcpConnexion
成员方法中。
首先要做的就是获取所需的缓冲区大小(pTcpTable
),以便获取所有结果。这是通过使用一个无效缓冲区调用函数来完成的,该函数将返回所需缓冲区的大小(在 pdwSize
参数中);请注意,这是可能的,因为函数已声明 pdwSize
参数前带有 out
关键字。
public void GetTcpConnections()
{
int pdwSize = 20000;
byte[] buffer = new byte[pdwSize]; // Start with 20.000 bytes
//left for information about tcp table
int res = IPHlpAPI32Wrapper.GetTcpTable(buffer, out pdwSize, true);
if (res != NO_ERROR)
{
buffer = new byte[pdwSize];
res = IPHlpAPI32Wrapper.GetTcpTable(buffer, out pdwSize, true);
if (res != 0)
return; // Error. You should handle it
}
//.....
}
一旦有了正确的缓冲区大小,我们再次调用函数,以正确填充缓冲区,然后我们只需解析字节以获取正确的值。
TcpConnexion = new IpHlpApidotnet.MIB_TCPTABLE();
int nOffset = 0;
// number of entry in the
TcpConnexion.dwNumEntries = Convert.ToInt32(buffer[nOffset]);
nOffset+=4;
TcpConnexion.table = new MIB_TCPROW[TcpConnexion.dwNumEntries];
for(int i=0; i<TcpConnexion.dwNumEntries;i++)
{
// state
int st = Convert.ToInt32(buffer[nOffset]);
// state in string
((MIB_TCPROW)(TcpConnexion.table[i])).StrgState=convert_state(st);
// state by ID
((MIB_TCPROW)(TcpConnexion.table[i])).iState = st;
nOffset+=4;
// local address
string LocalAdrr = buffer[nOffset].ToString()+"."+
buffer[nOffset+1].ToString()+"."+
buffer[nOffset+2].ToString()+"."+buffer[nOffset+3].ToString();
nOffset+=4;
//local port in decimal
int LocalPort = (((int)buffer[nOffset])<<8) +
(((int)buffer[nOffset+1])) +
(((int)buffer[nOffset+2])<<24) + (((int)buffer[nOffset+3])<<16);
nOffset+=4;
// store the remote endpoint
((MIB_TCPROW)(TcpConnexion.table[i])).Local =
new IPEndPoint(IPAddress.Parse(LocalAdrr),LocalPort);
// remote address
string RemoteAdrr = buffer[nOffset].ToString()+"."+
buffer[nOffset+1].ToString()+"."+
buffer[nOffset+2].ToString()+"."+buffer[nOffset+3].ToString();
nOffset+=4;
// if the remote address = 0 (0.0.0.0) the remote port is always 0
// else get the remote port in decimal
int RemotePort;
//
if(RemoteAdrr == "0.0.0.0")
{
RemotePort = 0;
}
else
{
RemotePort = (((int)buffer[nOffset])<<8) +
(((int)buffer[nOffset+1])) +
(((int)buffer[nOffset+2])<<24) + (((int)buffer[nOffset+3])<<16);
}
nOffset+=4;
((MIB_TCPROW)(TcpConnexion.table[i])).Remote =
new IPEndPoint(IPAddress.Parse(RemoteAdrr),RemotePort);
}
这样,我们就得到了一个名为 TcpConnexion
的结构,它保存了所有活动连接。
请注意,我们不得不将本地和远程端口从 DWORD
(UInt32
) 转换为 Int16
,这是通过按位运算符完成的。GetUdpConnexion()
的工作方式完全相同。
IpHelperApi 未记录函数
GetTcpConnexion()
和 GetUdpConnexion()
函数很有用,但不能用于控制活动连接,这意味着我们无法关闭任何连接,甚至无法获取有关哪个软件正在使用该连接的更多信息。要能够控制连接,意味着我们必须知道哪个进程与该连接关联,但这两个函数仅在 Windows XP 上可用。
经过数天寻找一种方法来获取与连接关联的 processId
,我发现 ipHlpApi 有一些未记录的函数:AllocateAndGetTcpExTableFromStack
和 AllocateAndGetUdpExTableFromStack
- 这两个函数的功能与 GetTcpTable
和 GetUdpTable
完全相同,只是它们获取与连接关联的 processID
;不要问它们为什么不在 MSDN 上记录,我真的不知道。
以下是使用它们的方法,让我们看看 AllocateAndGetTcpExTableFromStack
(AllocateAndGetUdpExTableFromStack
工作方式相同)在 C++ 中的声明。
typedef struct _MIB_TCPTABLE_EX
{
DWORD dwNumEntries;
MIB_TCPROW_EX table[ANY_SIZE];
} MIB_TCPTABLE_EX, *PMIB_TCPTABLE_EX;
typedef struct _MIB_TCPROW_EX{
DWORD dwState;
DWORD dwLocalAddr;
DWORD dwLocalPort;
DWORD dwRemoteAddr;
DWORD dwRemotePort;
DWORD dwProcessId;
} MIB_TCPROW_EX, *PMIB_TCPROW_EX;
AllocateAndGetTcpExTableFromStack(PMIB_TCPTABLE_EX*,
BOOL,HANDLE,DWORD,DWORD);
第一个参数是指向保存结果的结构的指针,第二个参数是一个 bool
标志,用于排序结果,第三个参数是指向进程堆的指针,另外两个是我不知道含义的标志。
此函数遵循 GetTcpTable
的模式,结果存储在一个具有大小不确定数组的结构中。我在 C# 中实现此功能使用了与 GetTcpTable
略有不同的解决方案:指针的安全 C# 版本。
不要害怕,这并不可怕 :) 对于不熟悉指针的人来说,让我们(简要地)了解一下它。
指针是一种存储内存地址的数据结构;例如,如果你有一个保存数字“100”的整数,那么指向该整数的指针将存储“100”值的地址,而不是值本身,希望这很清楚。
因此,在 C# 中,在安全上下文中,我们使用 IntPtr
结构。
[DllImport("iphlpapi.dll",SetLastError=true)]
public extern static int AllocateAndGetTcpExTableFromStack(
ref IntPtr pTable, bool bOrder, IntPtr heap ,int zero,int flags );
[DllImport("kernel32" ,SetLastError= true)] // this function is
// used to get the pointer on the process
// heap required by AllocateAndGetTcpExTableFromStack
public static extern IntPtr GetProcessHeap();
为了使用它,我将基本遵循与 GetTcpTable
相同的路径:先调用函数一次以获取连接数(行数)从而获取正确的缓冲区大小,然后再次调用函数以获取正确的结果。
public void GetExTcpConnections()
{
// the size of the MIB_EXTCPROW struct = 6*DWORD
int rowsize = 24;
int BufferSize = 100000;
// allocate a dumb memory space in order to retrieve nb of connexion
IntPtr lpTable = Marshal.AllocHGlobal(BufferSize);
//getting infos
int res = IPHlpAPI32Wrapper.AllocateAndGetTcpExTableFromStack(
ref lpTable, true, IPHlpAPI32Wrapper.GetProcessHeap(),0,2);
if(res!=NO_ERROR)
{
Debug.WriteLine(
"Erreur : "+IPHlpAPI32Wrapper.GetAPIErrorMessageDescription(res)
+" "+res);
return; // Error. You should handle it
}
int CurrentIndex = 0;
//get the number of entries in the table
int NumEntries= (int)Marshal.ReadIntPtr(lpTable);
lpTable = IntPtr.Zero;
// free allocated space in memory
Marshal.FreeHGlobal(lpTable);
//...........
}
因此,我们必须将一个 IntPtr
传递给函数。第一件事是定义一个 IntPtr
,它指向一个足够大的内存空间来存储所有结果。因此,正如我所说的,我们必须分配一个任意的内存空间来调用函数一次,然后我们可以使用:int NumEntries = (int)Marshal.ReadIntPtr(lpTable);
来获取连接数。这里我们使用 Marshal
类的一个函数来读取指针指向的值;最后,不要忘记释放先前分配的内存以避免内存泄漏。
现在,让我们获取所有数据。
///////////////////
// calculate the real buffer size nb of entrie *
// size of the struct for each entrie(24) + the dwNumEntries
BufferSize = (NumEntries*rowsize)+4;
// make the struct to hold the resullts
TcpExConnexions = new IpHlpApidotnet.MIB_EXTCPTABLE();
// Allocate memory
lpTable = Marshal.AllocHGlobal(BufferSize);
res = IPHlpAPI32Wrapper.AllocateAndGetTcpExTableFromStack(
ref lpTable, true,IPHlpAPI32Wrapper.GetProcessHeap() ,0,2);
if(res!=NO_ERROR)
{
Debug.WriteLine("Erreur : "+
IPHlpAPI32Wrapper.GetAPIErrorMessageDescription(res)+
" "+res);
return; // Error. You should handle it
}
// New pointer of iterating throught the data
IntPtr current = lpTable;
CurrentIndex = 0;
// get the (again) the number of entries
NumEntries = (int)Marshal.ReadIntPtr(current);
TcpExConnexions.dwNumEntries = NumEntries;
// Make the array of entries
TcpExConnexions.table = new MIB_EXTCPROW[NumEntries];
// iterate the pointer of 4 (the size of the DWORD dwNumEntries)
CurrentIndex+=4;
current = (IntPtr)((int)current+CurrentIndex);
// for each entries
for(int i=0; i< NumEntries;i++)
{
// The state of the connexion (in string)
TcpExConnexions.table[i].StrgState =
this.convert_state((int)Marshal.ReadIntPtr(current));
// The state of the connexion (in ID)
TcpExConnexions.table[i].iState = (int)Marshal.ReadIntPtr(current);
// iterate the pointer of 4
current = (IntPtr)((int)current+4);
// get the local address of the connexion
UInt32 localAddr = (UInt32)Marshal.ReadIntPtr(current);
// iterate the pointer of 4
current = (IntPtr)((int)current+4);
// get the local port of the connexion
UInt32 localPort = (UInt32)Marshal.ReadIntPtr(current);
// iterate the pointer of 4
current = (IntPtr)((int)current+4);
// Store the local endpoint in the struct and
// convert the port in decimal (ie convert_Port())
TcpExConnexions.table[i].Local = new IPEndPoint(localAddr,
(int)convert_Port(localPort));
// get the remote address of the connexion
UInt32 RemoteAddr = (UInt32)Marshal.ReadIntPtr(current);
// iterate the pointer of 4
current = (IntPtr)((int)current+4);
UInt32 RemotePort=0;
// if the remote address = 0 (0.0.0.0) the remote port is always 0
// else get the remote port
if(RemoteAddr!=0)
{
RemotePort = (UInt32)Marshal.ReadIntPtr(current);
RemotePort=convert_Port(RemotePort);
}
current = (IntPtr)((int)current+4);
// store the remote endpoint in the struct and
// convert the port in decimal (ie convert_Port())
TcpExConnexions.table[i].Remote = new IPEndPoint(
RemoteAddr,(int)RemotePort);
// store the process ID
TcpExConnexions.table[i].dwProcessId =
(int)Marshal.ReadIntPtr(current);
// Store and get the process name in the struct
TcpExConnexions.table[i].ProcessName =
this.get_process_name(TcpExConnexions.table[i].dwProcessId);
current = (IntPtr)((int)current+4);
}
// free the buffer
Marshal.FreeHGlobal(lpTable);
// re init the pointer
current = IntPtr.Zero;
因此,我们再次使用正确的缓冲区大小调用函数,然后我们将使用分配内存的开头作为起始地址在内存中“导航”。
前 4 个字节是条目数,然后我们进入一个循环,对连接表中的每一行进行处理,该行从起始地址 + 4 开始;在第一个循环中,我们将采用相同的机制:获取每个值,其顺序与 MIB_TCPROW_EX
中的相同,对于每个值,将指针移动 4 个字节,执行此操作的次数等于行数。
好了,我们已经获得了所有连接以及连接附带的进程 ID。我添加了一些辅助函数,例如,通过给定进程 ID 来获取进程名称,但它们非常简单且自明。
请记住,这两个函数仅在 WinXP 上可用。
如何使用库
我编写了一个小应用程序来测试此库,它只是在一个列表中显示库中所有函数的结果。
以下是库的方法:
GetTcpStats()
填充MIB_TCPSTATS
结构。GetUdpStats()
填充MIB_UDPSTATS
结构。GetTcpConnexions()
填充MIB_TCPTABLE
结构。GetUdpConnexions()
填充MIB_UDPTABLE
结构。GetExTcpConnexions()
填充MIB_EXTCPTABLE
结构。GetExUdpConnexions()
填充MIB_EXUDPTABLE
结构。
就是这样,希望它会有用。