使用TCP或UDP时,调用socket()函数时需要为其第二个参数指定对应的类型。例如SOCK_STREAM表示使用TCP,SOCK_DGRAM表示使用UDP协议。除了指定这两种类型外,还可以指定为rawsocket类型,即SOCK_RAW。当socket()函数的第二个参数指定为SOCK_STREAM或SOCK_DGRAM时,可以默认第三个参数。而当socket()函数的第二个参数指定为SOCK_RAW时,第三个参数必须明确指定要使用的协议。当套接字类型指定为SOCK_RAW时,协议类型的常用值有IPPROTO_IP、IPPROTO_ICMP、IPPROTO_TCP、IPPROTO_UDP、IPPROTO_RAW。使用前四种,在发送数据时,系统会自动为数据添加IP头,并在IP头中设置上层协议字段(如果有IP_HDRINCL选项,系统不会自动添加IP头);接收数据时,系统不会去掉IP头,需要程序自己处理。如果使用IPPROTO_RAW,系统直接将数据包发送给网络层发送数据,需要程序自己构造IP头中的字段。本文通过引入rawsockets来实现经典的网络命令,即Ping命令。通过完成一个Ping命令,初步了解并掌握rawsockets的使用。一、Ping命令的使用Ping命令的目的是测试另一台主机是否可达。Ping命令向主机发送ICMP回显请求报文,等待ICMP回显响应返回。一般来说,如果无法ping通某台主机,则无法与该主机通信(例外情况是对方主机的防火墙屏蔽了进入该主机的echorequest报文。仍然可以正常通信)。Ping命令有很多参数。打开命令行直接输入Ping并回车,这样就可以看到Ping命令的参数列表,如图1所示。图1Ping命令的参数列表通常,用户只是简单地ping地址某个主机的。Ping命令的参数可以是主机名、域名和IP地址,后两者比较常用。下面是Ping的简单例子,如下:C:\>ping8.8.4.4Pinging8.8.4.4with32bytesofdata:Replyfrom8.8.4.4:bytes=32time=57msTTL=47Replyfrom8.8.4.4:bytes=32time=54msTTL=47Replyfrom8.8.4.4:bytes=32time=54msTTL=47Replyfrom8.8.4.4:bytes=32time=51msTTL=47Pingstatisticsfor8.8.4.4:Packets:Sent=4,Received=4,Lost=0(0%loss),Approximateroundtriptimesinmilli-seconds:Minimum=51ms,Maximum=57ms,Average=54ms以上是使用Ping命令回显IP8.8.4.4后的输出信息。这里解释一下请求后回显信息的含义。Pinging8.8.4.4with32bytesofdata:正在向远程主机8.8.4.4发送32字节的数据。如果Ping为域名或主机名,则将域名(主机名)转换为IP地址显示。回复from8.8.4.4:bytes=32time=57msTTL=47本地主机收到回显响应信息,bytes=32表示32字节,time=57ms表示57毫秒,TTL表示生存时间值,该值可以是设置,最大值为255。每个处理数据包的路由器需要将TTL值减1或减去数据包在路由器中的秒数。由于大多数路由器转发数据包的延迟小于1秒,所以TTL最终成为一个跳数计数器,它经过的每台路由器都会将其值减1,当该值减为0时,该数据包将被丢弃。Pingstatisticsfor8.8.4.4:Packets:Sent=4,Received=4,Lost=0(0%loss),Approximateroundtriptimesinmilli-seconds:Minimum=51ms,Maximum=57ms,Average=54msPing8.8.4.4的统计数据是:Sent=4表示发送了4个数据包,Received=4表示收到了4个数据包,Lost=0(0%丢失)表示丢失了0个数据包,丢包率为0%。发送时间一般情况:Mininum=51ms,最快51ms,Maximum=57ms,最慢57ms,Average=54ms,平均54ms。2.Ping命令的结构Ping命令不依赖于TCP或UDP,它依赖于ICMP。ICMP是IP层的协议之一,传递错误信息和其他需要注意的信息。ICMP消息通常由IP层或更高层协议使用。ICMP被封装在IP数据报中,如图2所示。图2ICMP被封装在IP数据报中ICMP报文的格式如图3所示。图3ICMP报文格式ICMP协议的类型代码和代码取不同值根据不同的情况。Ping命令类型码使用两个值,分别为0和8。code的值为0。当typecode的值为0时,code值为0表示回显响应;当typecode值为8时,code值为0表示回显请求。Ping命令发送ICMP数据报时,类型码为8,编码为0,表示请求回显给对方主机;回应回应。简单来说,Ping命令发送的数据中,type为8,code为0,如果对方响应,则对方发送的数据的type为0,code为0。实现时自己Ping命令,就是构造一个请求回显的ICMP数据报,然后发送。ICMP数据结构定义如下://ICMP协议结构定义structuralmp_header{unsignedcharicmp_type;//报文类型unsignedcharicmp_code;//codeunsignedshorticmp_checksum;//checksumunsignedshorticmp_id;//用于唯一标识本次请求的ID号,通常设置为processIDunsignedshorticmp_sequence;//序列号unsignedlongicmp_timestamp;//时间戳};ICMP数据结构在网络开发中经常用到,可以保存起来以备后用。在了解了ICMP协议的数据结构之后,现在使用抓包工具(也叫协议分析工具)Wireshark来分析一下ICMP结构的真实情况,如图4所示。图4ICMP数据结构分析图4,标为1的部分用于过滤协议。在这部分输入“ICMP”可以让Wireshark只显示ICMP数据记录。相应地,可以输入“TCP”、“UDP”、“HTTP”等协议进行过滤。标记2的部分用于显示过滤后的ICMP记录,从中可以清楚地看到源IP地址、目的IP地址和协议类型。标记3的部分用于显示ICMP数据结构的值和附加的数据内容。底部显示了数据的原始二进制文件,习惯了协议后也不是不可能看到原始二进制文件。3.Ping命令的实现有了前面的基础,就可以构造自己的ICMP数据报来构造自己的Ping命令了。首先定义两个常量,和一个计算校验和的函数,如下:structuralmp_header{unsignedcharicmp_type;//消息类型unsignedcharicmp_code;//codeunsignedshorticmp_checksum;//checksumunsignedshorticmp_id;//用于唯一标识本次请求ID号,通常设置为进程IDunsignedshorticmp_sequence;//序列号unsignedlongicmp_timestamp;//时间戳};#defineICMP_HEADER_SIZEsizeof(icmp_header)#defineICMP_ECHO_REQUEST0x08#defineICMP_ECHO_REPLY0x00//计算校验和unsignedshortchsum(structicmp_header*picmp,intlen){longsum=0;unsignedshort*pusicmp=(unsignedshort*)picmp;while(len>1){sum+=*(pusicmp++);if(sum&0x80000000){sum=(sum&0xffff)+(sum>>16);}len-=2;}if(len){sum+=(unsignedshort)*(unsignedchar*)pusicmp;}while(sum>>16){sum=(sum&0xffff)+(sum>>16);}return(unsignedshort)~sum;}ICMP校验值为16位unsignedinteger,会累加ICMP协议头中的数据,累加中如果溢出,也会累加溢出的部分。具体计算校验和的算法就不过多介绍了。如果看不懂校验和计算的代码,可以单步调试分析。我们来看看ICMP结构体的填充。具体代码如下:BOOLMyPing(char*szDestIp){BOOLbRet=TRUE;WSADATAwsaData;intnTimeOut=1000;charszBuff[ICMP_HEADER_SIZE+32]={0};icmp_header*pIcmp=(icmp_header*)szBuff;charicmp_data[32]={0};WSAStartup(MAKEWORD(2,2),&wsaData);//创建原始套接字SOCKETs=socket(PF_INET,SOCK_RAW,IPPROTO_ICMP)//设置接收超时setsockopt(s,SOL_SOCKET,SO_RCVTIMEO,(charconst*)&nTimeOut,sizeof(nTimeOut));//设置目的地址sockaddr_indest_addr;dest_addr.sin_family=AF_INET;dest_addr.sin_addr.S_un.S_addr=inet_addr(szDestIp);dest_addr.sin_port=htons(0);//构造ICMP数据包pIcmp->icmp_type=ICMP_ECHO_REQUEST;pIcmp->icmp_code=0;pIcmp->icmp_id=(USHORT)::GetCurrentProcessId();pIcmp->icmp_sequence=0;pIcmp->icmp_timestamp=0;pIcmp->icmp_checksum=0;//复制数据//这里的数据可以是任意的//这里使用abc,看起来和系统提供的一样memcpy((szBuff+ICMP_HEADER_SIZE),"abcdefghijklmnopqrstuvwabcdefghi",32);//计算checksumchecksumpIcmp->icmp_checksum=chsum((structicmp_header*)szBuff,sizeof(szBuff));sockaddr_infrom_addr;charszRecvBuff[1024];intnLen=sizeof(from_addr);sendto(s,szBuff,sizeof(szBuff),0,(SOCKADDR*)&dest_addr,sizeof(SOCKADDR));recvfrom(s,szRecvBuff,MAXBYTE,0,(SOCKADDR*)&from_addr,&nLen);//判断接收到的地址是否是自己请求的地址printf("%s\r\n",inet_ntoa(from_addr.sin_addr));}returnbRet;}这是Ping命令的全部代码。编写一个函数来调用它进行测试。在WindowsXP以上的操作系统中运行时,如Windows8,程序可能无法正常运行,这是操作系统权限导致的。在编译好的程序上单击鼠标右键,在弹出的菜单中选择“以管理员身份运行”,这样程序就可以正常执行了。
