3.用Raw Socket实现Ping

极其常用的Ping命令通过向计算机发送ICMP Echo请求报文并且监听回应报文的返回,以校验与远程计算机或本地计算机的连接。

3.1 使用ICMP.DLL实现Ping

在Windows平台编程中实现Ping的一个最简单方法是调用ICMP.DLL这个动态链接库,引用ICMP.DLL中的三个函数即可:

HANDLE IcmpCreateFile(void);

这个函数打开个ICMP Echo请求能使用的句柄;

BOOL IcmpCloseHandle(HANDLE IcmpHandle);

这个函数关闭由IcmpCreateFile打开的句柄;

DWORD IcmpSendEcho(

HANDLE IcmpHandle,  // IcmpCreateFile打开的句柄

IPAddr DestinationAddress, //Echo请求的目的地址

LPVOID RequestData,  //发送数据buffer

WORD RequestSize,   //发送数据长度

PIP_OPTION_INFORMATION RequestOptions,  // IP_OPTION_INFORMATION指针

LPVOID ReplyBuffer, //接收回复buffer

DWORD ReplySize, //接收回复buffer大小

DWORD Timeout   //等待超时

);

这个函数发送Echo请求并等待回复或超时。

把这个函数和相关数据封装成一个类CPing,CPing类的头文件如下:

class CPing

{

public:

CPing();

~CPing();

BOOL Ping(char* strHost);

private:

// ICMP.DLL 导出函数指针

HANDLE (WINAPI *pIcmpCreateFile)(VOID);

BOOL (WINAPI *pIcmpCloseHandle)(HANDLE);

DWORD (WINAPI *pIcmpSendEcho)

(HANDLE,DWORD,LPVOID,WORD,PIPINFO,LPVOID,DWORD,DWORD);

HANDLE hndlIcmp; // 加载ICMP.DLL库句柄

BOOL bValid; //是否构造(获得ICMP.DLL导出函数指针和初始化WinSock)成功

};

CPing类的构造函数获得ICMP.DLL中导出函数的指针并初始化WinSock:

CPing::CPing()

{

bValid = FALSE;

WSADATA wsaData;

int nRet;

// 动态加载ICMP.DLL

hndlIcmp = LoadLibrary("ICMP.DLL");

if (hndlIcmp == NULL)

{

::MessageBox(NULL, "Could not load ICMP.DLL", "Error:", MB_OK);

return;

}

// 获得ICMP.DLL中导出函数指针

pIcmpCreateFile  = (HANDLE (WINAPI *)(void))

GetProcAddress((HMODULE)hndlIcmp,"IcmpCreateFile");

pIcmpCloseHandle = (BOOL (WINAPI *)(HANDLE))

GetProcAddress((HMODULE)hndlIcmp,"IcmpCloseHandle");

pIcmpSendEcho = (DWORD (WINAPI *)

(HANDLE,DWORD,LPVOID,WORD,PIPINFO,LPVOID,DWORD,DWORD))

GetProcAddress((HMODULE)hndlIcmp,"IcmpSendEcho");

// 检查所有的指针

if (pIcmpCreateFile == NULL  ||

pIcmpCloseHandle == NULL ||

pIcmpSendEcho == NULL)

{

::MessageBox(NULL, "Error loading ICMP.DLL", "Error:", MB_OK);

FreeLibrary((HMODULE)hndlIcmp);

return;

}

// 初始化WinSock

nRet = WSAStartup(0x0101, &wsaData );

if (nRet)

{

::MessageBox(NULL, "WSAStartup() error:", "Error:", MB_OK);

WSACleanup();

FreeLibrary((HMODULE)hndlIcmp);

return;

}

// 检查WinSock的版本

if (0x0101 != wsaData.wVersion)

{

::MessageBox(NULL, "No WinSock version 1.1 support found", "Error:", MB_OK);

WSACleanup();

FreeLibrary((HMODULE)hndlIcmp);

return;

}

bValid = TRUE;

}

CPing类的析构函数完成相反的动作:

CPing::~CPing()

{

WSACleanup();

FreeLibrary((HMODULE)hndlIcmp);

}

CPing类的Ping函数是最核心的函数,实现真正的ping操作:

int CPing::Ping(char *strHost)

{

struct in_addr iaDest; // Internet地址结构体

LPHOSTENT pHost; // 主机入口结构体指针

DWORD *dwAddress; // IP地址

IPINFO ipInfo; // IP选项结构体

ICMPECHO icmpEcho; // ICMP Echo回复buffer

HANDLE hndlFile; // IcmpCreateFile函数打开的句柄

if (!bValid)

{

return FALSE;

}

//使用inet_addr()以判定ping目标为地址还是名称

iaDest.s_addr = inet_addr(strHost);

if (iaDest.s_addr == INADDR_NONE)

pHost = gethostbyname(strHost);

else

pHost = gethostbyaddr((const char*) &iaDest, sizeof(struct in_addr),

AF_INET);

if (pHost == NULL)

{

return FALSE;

}

// 拷贝IP地址

dwAddress = (DWORD*)(*pHost->h_addr_list);

// 获得ICMP Echo句柄

hndlFile = pIcmpCreateFile();

// 设置发送信息缺省值

ipInfo.Ttl = 255;

ipInfo.Tos = 0;

ipInfo.IPFlags = 0;

ipInfo.OptSize = 0;

ipInfo.Options = NULL;

icmpEcho.Status = 0;

// 请求一个ICMP echo

pIcmpSendEcho(hndlFile, *dwAddress, NULL, 0,  &ipInfo,  &icmpEcho, sizeof

(struct tagICMPECHO), 1000);

//设置结果

iaDest.s_addr = icmpEcho.Source;

if (icmpEcho.Status)

{

return FALSE;

}

// 关闭ICMP Echo句柄

pIcmpCloseHandle(hndlFile);

return TRUE;

}

其中所使用的相关结构体定义为:

typedef struct tagIPINFO

{

u_char Ttl; // TTL

u_char Tos; // 服务类型

u_char IPFlags; // IP标志

u_char OptSize; // 可选数据大小

u_char *Options; // 可选数据buffer

} IPINFO,  *PIPINFO;

typedef struct tagICMPECHO

{

u_long Source; // 源地址

u_long Status; // IP状态

u_long RTTime; // RTT

u_short DataSize; // 回复数据大小

u_short Reserved; // 保留

void *pData; // 回复数据buffer

IPINFO ipInfo; // 回复IP选项

} ICMPECHO, *PICMPECHO;

3.2 使用Raw Socket实现Ping

仅仅采用ICMP.DLL并不能完全实现ICMP灵活多变的各类报文,只有使用Raw Socket才是ICMP的终极解决之道。

使用Raw Socket发送ICMP报文前,我们要完全依靠自己的代码组装报文:

//功能:初始化ICMP的报头, 给data部分填充数据, 计算校验和

void init_ping_packet(ICMPHeader *icmp_hdr, int packet_size, int seq_no)

{

//设置ICMP报头字段

icmp_hdr->type = ICMP_ECHO_REQUEST;

icmp_hdr->code = 0;

icmp_hdr->checksum = 0;

icmp_hdr->id = (unsigned short)GetCurrentProcessId();

icmp_hdr->seq = seq_no;

icmp_hdr->timestamp = GetTickCount();

// 填充data域

const unsigned long int deadmeat = 0xDEADBEEF;

char *datapart = (char*)icmp_hdr + sizeof(ICMPHeader);

int bytes_left = packet_size - sizeof(ICMPHeader);

while (bytes_left > 0)

{

memcpy(datapart, &deadmeat, min(int(sizeof(deadmeat)), bytes_left));

bytes_left -= sizeof(deadmeat);

datapart += sizeof(deadmeat);

}

// 计算校验和

icmp_hdr->checksum = ip_checksum((unsigned short*)icmp_hdr, packet_size);

}

计算校验和(Checksum)的函数为:

//功能:计算ICMP包的校验和

unsigned short ip_checksum(unsigned short *buffer, int size)

{

unsigned long cksum = 0;

// 将所有的16数相加

while (size > 1)

{

cksum +=  *buffer++;

size -= sizeof(unsigned short);

}

if (size)   //加上最后一个BYTE

{

cksum += *(unsigned char*)buffer;

}

//和的前16位和后16位相加

cksum = (cksum >> 16) + (cksum &0xffff);

cksum += (cksum >> 16);

return (unsigned short)(~cksum);

}

在真正发送Ping报文前,需要先初始化Raw Socket:

// 功能:初始化RAW Socket, 设置ttl, 初始化目标地址

// 返回值:<0 失败

int setup_for_ping(char *host, int ttl, SOCKET &sd, sockaddr_in &dest)

{

// 创建原始套接字

sd = WSASocket(AF_INET, SOCK_RAW, IPPROTO_ICMP, 0, 0, 0);

if (sd == INVALID_SOCKET)

{

cerr << "Failed to create raw socket: " << WSAGetLastError() << endl;

return  - 1;

}

if (setsockopt(sd, IPPROTO_IP, IP_TTL, (const char*) &ttl, sizeof(ttl)) ==

SOCKET_ERROR)

{

cerr << "TTL setsockopt failed: " << WSAGetLastError() << endl;

return  - 1;

}

// 初始化目标主机信息块

memset(&dest, 0, sizeof(dest));

// 将第1个参数转换为目标IP地址

unsigned int addr = inet_addr(host);

if (addr != INADDR_NONE)

{

// 为IP地址

dest.sin_addr.s_addr = addr;

dest.sin_family = AF_INET;

}

else

{

// 非IP地址,进行主机名和IP地址的转换

hostent *hp = gethostbyname(host);

if (hp != 0)

{

// 查找主机名对应的IP地址

memcpy(&(dest.sin_addr), hp->h_addr, hp->h_length);

dest.sin_family = hp->h_addrtype;

}

else

{

// 不能识别的主机名

cerr << "Failed to resolve " << host << endl;

return  - 1;

}

}

return 0;

}

下面可以利用Raw Socket发送生成的ICMP报文:

//功能:发送生成的ICMP包

//返回值:<0 发送失败

int send_ping(SOCKET sd, const sockaddr_in &dest, ICMPHeader *send_buf, int

packet_size)

{

// 发送send_buf缓冲区中的报文

cout << "Sending " << packet_size << " bytes to " << inet_ntoa(dest.sin_addr)

<< "..." << flush;

int bwrote = sendto(sd, (char*)send_buf, packet_size, 0, (sockaddr*) &dest,

sizeof(dest));

if (bwrote == SOCKET_ERROR)

{

cerr << "send failed: " << WSAGetLastError() << endl;

return  - 1;

}

else if (bwrote < packet_size)

{

cout << "sent " << bwrote << " bytes..." << flush;

}

return 0;

}

发送Ping报文后,我们需要接收Ping回复ICMP报文:

//功能:接收Ping回复

//返回值: <0 接收失败

int recv_ping(SOCKET sd, sockaddr_in &source, IPHeader *recv_buf, int

packet_size)

{

// 等待Ping回复

int fromlen = sizeof(source);

int bread = recvfrom(sd, (char*)recv_buf, packet_size + sizeof(IPHeader), 0,

(sockaddr*) &source, &fromlen);

if (bread == SOCKET_ERROR)

{

cerr << "read failed: ";

if (WSAGetLastError() == WSAEMSGSIZE)

{

cerr << "buffer too small" << endl;

}

else

{

cerr << "error #" << WSAGetLastError() << endl;

}

return  - 1;

}

return 0;

}

并使用如下函数对接收到的报文进行解析:

// 功能:解析接收到的ICMP报文

// 返回值: -2忽略, -1失败, 0 成功

int decode_reply(IPHeader *reply, int bytes, sockaddr_in *from)

{

// 偏移到ICMP报头

unsigned short header_len = reply->h_len *4;

ICMPHeader *icmphdr = (ICMPHeader*)((char*)reply + header_len);

// 报文太短

if (bytes < header_len + ICMP_MIN)

{

cerr << "too few bytes from " << inet_ntoa(from->sin_addr) << endl;

return  - 1;

}

// 解析回复报文类型

else if (icmphdr->type != ICMP_ECHO_REPLY)

{

//非正常回复

if (icmphdr->type != ICMP_TTL_EXPIRE)

{

//ttl减为零

if (icmphdr->type == ICMP_DEST_UNREACH)

{

//主机不可达

cerr << "Destination unreachable" << endl;

}

else

{

//非法的ICMP包类型

cerr << "Unknown ICMP packet type " << int(icmphdr->type) <<

" received" << endl;

}

return  - 1;

}

}

else if (icmphdr->id != (unsigned short)GetCurrentProcessId())

{

//不是本进程发的包, 可能是同机的其它ping进程发的

return  - 2;

}

// 指出往返时间TTL

int nHops = int(256-reply->ttl);

if (nHops == 192)

{

// TTL came back 64, so ping was probably to a host on the

// LAN -- call it a single hop.

nHops = 1;

}

else if (nHops == 128)

{

// Probably localhost

nHops = 0;

}

// 输出信息

cout << endl << bytes << " bytes from " << inet_ntoa(from->sin_addr) <<

", icmp_seq " << icmphdr->seq << ", ";

if (icmphdr->type == ICMP_TTL_EXPIRE)

{

cout << "TTL expired." << endl;

}

else

{

cout << nHops << " hop" << (nHops == 1 ? "" : "s");

cout << ", time: " << (GetTickCount() - icmphdr->timestamp) << " ms." <<

endl;

}

return 0;

}

为了在Visual C++中更加方便地使用发送和接收ICMP报文,我们可以使用由Jay Wheeler编写的CIcmp(An ICMP Class For MFC)类,在著名的开发网站的如下地址可以下载:

[url]http://www.codeguru.com/cpp/i-n/internet/network/article.php/c3395/[/url]

这个类的简要框架如下:

class CIcmp: public CSocket

{

// Attributes

public:

BOOL OpenNewSocket(HWND hWnd, unsigned int NotificationMessage, long

NotifyEvents);

BOOL OpenNewSocket(HWND hWnd, unsigned int NotificationMessage, long

NotifyEvents, int AFamily, int AType, int AProtocol);

int CloseIcmpSocket(void);

BOOL Connect(int ReceiveTimeout, int SendTimeout);

BOOL Connect(LPINT ReceiveTimeout, LPINT SendTimeout, int AFamily, int

AType, int AProtocol);

int SetTTL(int TTL);

int SetAsynchNotification(HWND hWnd, unsigned int Message, long Events);

int Receive(LPSTR pIcmpBuffer, int IcmpBufferSize);

unsigned long GetIPAddress(LPSTR iHostName);

int Ping(LPSTR pIcmpBuffer, int IcmpBufferSize);

unsigned short IcmpChecksum(unsigned short FAR *lpBuf, int Len);

void DisplayError(CString ErrorType, CString FunctionName);

// Operations

public:

CIcmp(void);

CIcmp(CIcmp &copy);

~CIcmp(void);

public:

//  I/O Buffer Pointers

LPIcmpHeader pIcmpHeader;

LPIpHeader pIpHeader;

SOCKET icmpSocket;

SOCKADDR_IN icmpSockAddr;

SOCKADDR_IN rcvSockAddr;

DWORD icmpRoundTripTime;

DWORD icmpPingSentAt;

DWORD icmpPingReceivedAt;

int icmpRcvLen;

int icmpHops;

int icmpMaxHops;

int icmpCurSeq;

int icmpCurId;

int icmpPingTimer;

int icmpSocketError;

int icmpSocketErrorMod;

unsigned long icmpHostAddress;

protected:

};

初始化网络连接的函数:

BOOL CIcmp::Connect(LPINT ReceiveTimeout, LPINT SendTimeout, int AFamily, int

AType, int AProtocol)

{

int Result;

icmpSocket = NULL;

icmpSocket = socket(AFamily, AType, AProtocol);

if (icmpSocket == INVALID_SOCKET)

{

icmpSocketError = WSAGetLastError();

icmpSocketErrorMod = 1;

return FALSE;

}

//

//  Set receive timeout

//

Result = setsockopt(icmpSocket, SOL_SOCKET, SO_RCVTIMEO, (char*)

ReceiveTimeout, sizeof(int));

if (Result == SOCKET_ERROR)

{

icmpSocketError = WSAGetLastError();

icmpSocketErrorMod = 2;

closesocket(icmpSocket);

icmpSocket = INVALID_SOCKET;

return FALSE;

}

//

//  Set send timeout

//

Result = setsockopt(icmpSocket, SOL_SOCKET, SO_SNDTIMEO, (char*)SendTimeout,

sizeof(int));

if (Result == SOCKET_ERROR)

{

icmpSocketError = WSAGetLastError();

icmpSocketErrorMod = 3;

closesocket(icmpSocket);

icmpSocket = INVALID_SOCKET;

return FALSE;

}

icmpCurSeq = 0;

icmpCurId = (USHORT)GetCurrentProcessId();

icmpHops = 0;

return TRUE;

}

接收的函数:

int CIcmp::Receive(LPSTR pIcmpBuffer, int IcmpBufferSize)

{

LPSOCKADDR  pRcvSockAddr = (LPSOCKADDR)&rcvSockAddr;

int    Result;

int    RcvIpHdrLen;

icmpPingReceivedAt = GetTickCount();

icmpCurId = 0;

rcvSockAddr.sin_family = AF_INET;

rcvSockAddr.sin_addr.s_addr = INADDR_ANY;

rcvSockAddr.sin_port = 0;

RcvIpHdrLen = sizeof rcvSockAddr;

Result = recvfrom (icmpSocket,

pIcmpBuffer,

IcmpBufferSize,

0,

pRcvSockAddr,

&RcvIpHdrLen);

if (Result == SOCKET_ERROR)

{

icmpSocketError = WSAGetLastError();

icmpSocketErrorMod = 1;

DisplayError ("Receive","CIcmp::Receive");

return Result;

}

icmpRcvLen = Result;

pIpHeader = (LPIpHeader)pIcmpBuffer;

RcvIpHdrLen = pIpHeader->HeaderLength * 4;

if (Result < RcvIpHdrLen + ICMP_MIN)

{

//

// Too few bytes received

//

MessageBox(NULL,

"Short message!",

"CIcmp::Receive",

MB_OK|MB_SYSTEMMODAL);

icmpSocketErrorMod = 2;

return Result;

}

pIcmpHeader = (LPIcmpHeader)(pIcmpBuffer + RcvIpHdrLen);

icmpCurId = pIcmpHeader->IcmpId;

icmpRoundTripTime = icmpPingReceivedAt - pIcmpHeader->IcmpTimestamp;

if (pIcmpHeader->IcmpType != ICMP_ECHOREPLY)

{

//

// Not an echo response!

//

return Result;

}

icmpCurSeq = pIcmpHeader->IcmpSeq;

return Result;

}

异步通知主窗口:

int CIcmp::SetAsynchNotification(HWND hWnd, unsigned int Message, long Events)

{

int Result = WSAAsyncSelect (icmpSocket,

hWnd,

Message,

Events);

if (Result == SOCKET_ERROR)

{

icmpSocketError = WSAGetLastError();

icmpSocketErrorMod = 1;

icmpSocket = INVALID_SOCKET;

}

return Result;

}

设置TTL:

int CIcmp::SetTTL(int TTL)

{

int Result;

Result = setsockopt (icmpSocket, IPPROTO_IP, IP_TTL, (LPSTR)&TTL, sizeof(int));

if (Result == SOCKET_ERROR)

{

icmpSocketErrorMod = 1;

icmpSocketError = WSAGetLastError();

}

return Result;

}

Ping命令的函数:

int CIcmp::Ping (LPSTR pIcmpBuffer, int DataLen)

{

int Result;

int IcmpBufferSize = DataLen + IcmpHeaderLength;

pIcmpHeader = (LPIcmpHeader)pIcmpBuffer;

memset (pIcmpBuffer, 'E', IcmpBufferSize);

memset (pIcmpHeader, 0, IcmpHeaderLength);

pIcmpHeader->IcmpType = ICMP_ECHO;

pIcmpHeader->IcmpCode = 0;

pIcmpHeader->IcmpChecksum = 0;

pIcmpHeader->IcmpId = icmpCurId;

pIcmpHeader->IcmpSeq = icmpCurSeq;

pIcmpHeader->IcmpTimestamp = GetCurrentTime();

pIcmpHeader->IcmpChecksum = IcmpChecksum ((USHORT FAR *)pIcmpBuffer,

IcmpBufferSize);

icmpPingSentAt = GetCurrentTime();

Result = sendto (icmpSocket,

pIcmpBuffer,

IcmpBufferSize,

0,

(LPSOCKADDR)&icmpSockAddr,

sizeof icmpSockAddr);

if (Result == SOCKET_ERROR)

{

icmpSocketError = WSAGetLastError();

icmpSocketErrorMod = 1;

}

return Result;

}

linux ping raw socket -(signal),***之旅――原始套接字(Raw Socket)透析(3)--用Raw Socket实现Ping...相关推荐

  1. linux串口编程实例_Linux 网络编程——原始套接字实例:发送 UDP 数据包

    以太网报文格式: IP 报文格式: UDP 报文格式: 校验和函数: /*******************************************************功能:校验和函数参 ...

  2. 基于原始套接字(raw socket)的网络抓包工具

    基于raw socket的网络抓包工具 1. 原始套接字(raw socket)简介 原始套接字可以接收本机网卡上的数据帧或者数据包,利用raw socket可以编写基于IP协议的程序.一般的TCP/ ...

  3. Linux原始套接字学习总结

    Linux网络编程:原始套接字的魔力[上] http://blog.chinaunix.net/uid-23069658-id-3280895.html 基于原始套接字编程        在开发面向连 ...

  4. Linux原始套接字实现分析---转

    http://blog.chinaunix.net/uid-27074062-id-3388166.html 本文从IPV4协议栈原始套接字的分类入手,详细介绍了链路层和网络层原始套接字的特点及其内核 ...

  5. linux 原始套接字 绑定网卡,Linux原始套接字实现分析

    之所以要转这篇文章,是因为这篇文章是我看到的同类博客中写得最好的,但非常可惜,这篇博客中只有一篇文章,没有什么收藏价值,故将其原文转载,以供今后学习查阅. 本文从IPV4协议栈原始套接字的分类入手,详 ...

  6. linux 原始套接字实现分析

    目录 1 原始套接字概述 1.1 链路层原始套接字 1.2 网络层原始套接字 1.2.1 接收报文 1.2.2 发送报文 2 原始套接字实现 2.1 原始套接字报文收发流程 2.2  链路层原始套接字 ...

  7. Teardrop原始套接字编程

    目录 一.含义介绍 二.Teardrop代码编程 参考 一.含义介绍 1.什么是原始套接字 原始套接字的含义就是在传输层之下使用的套接字,它提供了一些 TCP 和 UDP 套接字无法提供的功能,即: ...

  8. 原始套接字SOCK_RAW

    实际上,我们常用的网络编程都是在应用层的报文的收发操作,也就是大多数程序员接触到的流式套接字(SOCK_STREAM)和数据包式套接字(SOCK_DGRAM).而这些数据包都是由系统提供的协议栈实现, ...

  9. Raw_Socket原始套接字

    一.创建raw socket的权限:只有root权限才能够创建. 二.raw socket的用途:主要有三个方面 (1):通过raw socket来接收发向本机的ICMP,IGMP协议包,或者用来发送 ...

  10. 网络编程——原始套接字实现原理

    目录 1. 基础知识 1.1.概述 1.2.链路层原始套接字 1.3.网络层原始套接字 2.原始套接字的实现 2.1  原始套接字报文收发流程 2.2链路层原始套接字的实现 2.2.1  套接字创建 ...

最新文章

  1. C语言网络编程:close或者shutdown断开通信连接
  2. SQL中int类型与varchar类型的隐式转换
  3. 浅谈Volatile与多线程
  4. mysql执行一条语句会加锁吗_一条简单的更新语句,MySQL是如何加锁的?
  5. Python —— CPU vs. GPU
  6. 初等模型---交通流和道路通行能力
  7. Linux 设置Dlan服务器
  8. 周志华:关于机器学习的一点思考
  9. .js文件中的下划线
  10. 2020年哈尔滨工业大学C语言程序设计精髓 第四周练兵编程题
  11. 大学娱乐化值得高度警惕——胡乐乐
  12. jsp学生体育成绩管理系统
  13. @Scope注解设置创建bean的方式和生命周期
  14. MySQL----数据库概述
  15. c语言n1.n2%,C语言题库填空.doc
  16. 卡西欧计算机显示科学计数法怎么调回来,卡西欧计算器中的科学计数法键如何使用?请举例!急!...
  17. DRM驱动代码分析:展频 跳频
  18. PTA-就不告诉你(C语言)
  19. codeforces 732F
  20. 怎么用微信在蘑菇街结算

热门文章

  1. android下md5加密
  2. VS2005 My.Computer.Registry 对象 操作注册表 简单示例
  3. [j2me]二级菜单界面演练[三][0215update]
  4. 18. CSS 内边距
  5. Binwalk 后门(固件)分析利器
  6. 应用安全 - 代码审计 - PHP
  7. # bucketSort 箱排序 也称桶排序
  8. 从现实抽象出类的步骤
  9. unity3d的uGUI基本操作
  10. 50个新的汉化Demo!纯前端 Wijmo 放大招