Section I Problem Specification
本次实验主要是写一个程序:统计本机的符合IP协议的流量包的个数。这些流量包都是本机与外网的交互的信息,也就是说,这些包的接受IP或者源IP就是本机的ip地址。属于网络层的IP协议主要是为属于传输层的TCP协议和UDP协议服务,而TCP协议又为属于应用层的Telent、FTP、SMTP服务,UDP协议又为DNS、TFTP、SNMP服务。本次实验是捕获所有协议为IP的包,与前几次实验同属于arp协议一样,IP协议也有自己的EtherType,如下图所示,值得注意的:Ipv4与Ipv6拥有不同的EtherType
此外,IP数据包的格式也值得复习一下:
\
下面逐一简要说明:
Version:占4位,指IP协议的版本。通信双方使用的IP协议版本必须一致。目前广泛使用的IP协议版本号为4(即IPv4)。关于IPv6,目前还处于草案阶段。
IHL:占4位,可表示的最大十进制数值是15
Differentialted services:区分服务 占8位,用来获得更好的服务。这个字段在旧标准中叫做服务类型,但实际上一直没有被使用过。1998年IETF把这个字段改名为区分服务DS(Differentiated Services)。只有在使用区分服务时,这个字段才起作用。
Total length:总长度 总长度指首部和数据之和的长度,单位为字节。总长度字段为16位,因此数据报的最大长度为2^16-1=65535字节。
Identification:标识占16位。IP软件在存储器中维持一个计数器,每产生一个数据报,计数器就加1,并将此值赋给标识字段。但这个“标识”并不是序号,因为IP是无连接服务,数据报不存在按序接收的问题。当数据报由于长度超过网络的MTU(
最大传输单元(Maximum Transmission Unit
))而必须分片时,这个标识字段的值就被复制到所有的数据报的标识字段中。相同的标识字段的值使分片后的各数据报片最后能正确地重装成为原来的数据报。
在这里老师举了个例子:比如一火车的人中途必须分开坐汽车,那么给每个汽车一个相同的编号,证明是从同一个火车上下来的,到了最终目的地再根据编号重新组装。
DF与MF:标志(flag) 占3位,但目前只有2位有意义。
● 标志字段中的最低位记为MF(More Fragment)。MF=1即表示后面“还有分片”的数据报。MF=0表示这已是若干数据报片中的最后一个。
● 标志字段中间的一位记为DF(Don’t Fragment),意思是“不能分片”。只有当DF=0时才允许分片。
● 标志字段中的最低位记为MF(More Fragment)。MF=1即表示后面“还有分片”的数据报。MF=0表示这已是若干数据报片中的最后一个。
● 标志字段中间的一位记为DF(Don’t Fragment),意思是“不能分片”。只有当DF=0时才允许分片。
Fragment offset:片偏移 占13位。片偏移指出:较长的分组在分片后,某片在原分组中的相对位置。
Time to Live:生存时间 占8位,生存时间字段常用的的英文缩写是TTL(Time To Live),表明是数据报在网络中的寿命。由发出数据报的源点设置这个字段。其目的是防止无法交付的数据报无限制地在因特网中兜圈子,因而白白消耗网络资源。虽然名字是时间,但是实际上是 跳数限制 。路由器在转发数据报之前就把TTL值减1.若TTL值减少到零,就丢弃这个数据报,不再转发。因此,现在TTL的单位不再是秒,而是跳数。TTL的意义是指明数据报在网络中至多可经过多少个路由器。显然,数据报在网络上经过的路由器的最大数值是255.若把TTL的初始值设为1,就表示这个数据报只能在本局域网中传送。
protocol:协议,指的就是传输层的协议,占8位,协议字段指出此数据报携带的数据是使用何种协议,以便使目的主机的IP层知道应将数据部分上交给哪个处理过程。
具体就是:
- ICMP:00000001
- IGMP:00000010
- TCP: 00000110
- UDP: 00010001
Header checksum:首部检验和 占16位。这个字段只检验数据报的首部,但不包括数据部分。这是因为数据报每经过一个路由器,路由器都要重新计算一下首部检验和(一些字段,如生存时间、标志、片偏移等都可能发生变化)。不检验数据部分可减少计算的工作量。
源地址和目的地址不言而喻。
option:选择域,长度为0到40b,主要用于支持纠错,测量以及安全等措施。
Section II Solution Method and Design
总结书中样例:
今日忽然觉得虽然每次都完成了任务,但代码实在是难看极了。是认真分析、学习优秀作品的时候了。我相信这样才能写出好的代码。
代码位于云盘的:计算机网络高级软件编程技术+光盘。相应章节。
仔细看了之后,略微有些伤感,因为例子写得太好了,我何时才有这么一天啊。
例子中总过用到了2个类、一个结构体和一份写逻辑的cpp文件(也许官方并不这样称呼,只是我这样叫而已,main函数就在这里面)。
每个类有一个头文件和实现文件,结构体是一个份头文件,总共6份文档。
利用结构体格式化捕获的数据包:
先看结构体,结构体给我最大的启示就是如何 格式化 捕获的数据包,之前我总是一个字节一个字节的去使用,比如打印ip地址,我会使用类似这样的语句:
printf(
"%d.%d.%d.%d"
,data[28],data[29],data[30],data[31]);
显得非常的愚笨。
请看结构体的代码:
typedef struct IPHeader
{
unsigned char Version_HeaderLength; // 版本(4位)+首部长度(4位)
unsigned char TypeOfService; // 服务类型
unsigned short TotalLength; // 总长度
unsigned short Identification; // 标识
unsigned short Flags_FragmentOffset; // 标志(3位)+分片偏移(13位)
unsigned char TimeToLive; // 生存时间
unsigned char Protocal; // 协议
unsigned short HeaderChecksum; // 首部校验和
unsigned long SourceAddress; // 源IP地址
unsigned long DestAddress; // 目的IP地址
}IPHEADER;
这样非常巧妙的应用了 long short之类的基本数据类型,因为ip地址占32个bits,也就是4个字节,而long也是(注意这是由编译器决定的)。
这样就可以用long来记录ip地址,
当拿到抓取得包后,只需要:
// 通过指针把缓冲区中的内容强制转换为IPHEADER数据结构
pIpHdr = (IPHEADER *)pBuffer;
那么pIpHdr->SourceAddress就是源地址,当然这不是常见的192.168.1.1这样的,是一个long型。
此后,我们就可以用下面代码,将ip地址打印出来:
unsigned long dwSourTemp = pTemp->getSourIPAddr();
out << inet_ntoa(*(in_addr *)&(dwSourTemp)) << '\t';
中文解释:先讲unsigned的long型用指针做成in_addr类型,再调用inet_ntoa函数,即可打印出这样的
常见的192.168.1.1形式。
此外,我想在这里穿插一下,一直混淆我的htons()函数
msdn的解释:The htons function takes a 16-bit number in host byte order and returns a 16-bit number in network byte order used in TCP/IP networks(将一个16位主机字顺序的数转换成16位网络字顺序的数)
两种情况下要用:
// 填充sockaddr_in结构
sockaddr_in addr_in;
addr_in.sin_addr = *(in_addr *)pHostIP->h_addr_list[0]; // 设定IP地址
addr_in.sin_family = AF_INET; // 设定地址类型
addr_in.sin_port = htons(8000); // 设定端口
和
设置
ethernetType的类型的时候:比如我只之前的代码
senderFrame.ethernet