IP/TCP/UDP报文解析(1)IP报文
前言
关于IP报文的解析涉及到很多位运算的使用,如果不熟位运算规则的小伙伴,可以查看我的这篇博文《Java中的位运算》
正文
IP报文格式一览
一个完整的IP包通常被看做 首部 和 数据 两部分,首部中又包含 固定长度(20字节)和可变长度(最大60-20=40字节),IP报头包含以下数据:
- 版本号:在IP数据报中第1个字节的前4位,现在有IPv4和IPv6 两个IP协议版本,所以这里可能的值有0010(IPv4 十进制 4)和 0100(IPv6 十进制6),端与端之间只有同协议版本才能正常通信。
- 首部长:首部长的作用是标识IP报头的长度,在IP报文中第1个字节的后4位,其最大取值为15,其单位为4字节,即首部长最大为15 * 4 = 60字节,通常IP报头默认长度为5 * 4 = 20字节。
- 服务类型:服务类型的作用是标识IP报文的处理方式,在IP报文中第2个字节。前三位代表包的处理优先级,取值范围0-7,0为最低 7为最高。后4位是TOS(注意TOS之前还有一个位无效位),表示本数据报在传输过程中需要得到的服务,依次表示为 D(Minimum delay)最小延迟、T(Maximum throughput)最大传输率、R(Maximum reliability)最高可靠性、C(Minimum cost)最小成本。这里的服务类型只是代表用户希望得到的服务,并不一定要强制执行。
- 总长度:总长度的作用是标识IP报文首部分+数据部分的总长度,在IP报文中第3、4字节中,其最大取值为65535,其单位为1字节,所以一个IP报最大为65535 * 1 = 65535字节。
- 序列号:序列号的作用是标识该IP报文请求,由发起请求方随机生成且本地唯一,每次分包在上一个包基础上该值+1填入,在IP报文中第5、6字节中。
- 标志位:标志位的作用是标识该IP报是否发生分片行为DF(Don’t fragment)和是否还有分片MF(More Fragment) ,在IP报文中第7个字节的前3位,但只有后两位是有效的,依次为0 DF MF。有三个可能的值:未发生分片010、发生分片且该包后续没有分片000、发生分片且该包后续还有分片001。
- 片偏移: 片偏移的作用是在IP报发生分包时标识该包中的数据相对于原始数据的起始位置,在IP报文中第7个字后5位和第8个字节。最大取值8191,单位是8字节,所以片偏移的最大值为8191 * 8 = 65528字节。
- 生存时间:生存时间的作用是标识该请求能穿过路由的最大次数,以防止路由环路。该值每经过一次网络路由后减小1,信息送达或该值为0时停止传递。在IP报文中第9个字节中。
- 上层协议:上层协议标识IP包中数据部分是属于那种协议的封装,在数据报中第10个字节。列举几个常用的值 :
协议 | 二进制 | 十进制 |
---|---|---|
TCP | 00000110 | 6 |
UDP | 00010001 | 17 |
ICMP | 00000001 | 1 |
IGMP | 00000010 | 2 |
OSPF | 01011001 | 89 |
- 首部校验和:首部校验和用来校验IP报头是否准确。在IP数据报的第11、12个字节中,关于校验和的计算和使用等下再说。
- 源IP地址:源IP地址标识这个IP包的初始发送地址,在IP数据报的第13、14、15、16个字节中,所以IP地址通常被写为255.255.255.255这样的格式。
- 目的IP地址:目的IP地址标识这个IP包最终送达的地址。
- 选项和填充:作用是附加IP包处理信息,可变长度,最大为40字节。
IP校验和
校验和计算
计算首部校验和的过程如下
- 将首部校验和置零
- 如果首部的字节长度为奇数,则在计算时需要在首部末尾添加一个全为0的空字节
- 将首部中所有字节划分成16位长度的数进行相加求和
- 和的溢出位折叠求和。
- 将和做非运算得到最终校验和
- 将校验和填入首部校验和位。
具体实现请看文末的代码。
校验数据
校验数据相对简单,大体上和计算时差不多,只是校验计算的时候不需要重置校验和位,最后的结果为0时说明该IP报文正确,若不为0说明该报文错误将其丢弃。
片偏移的计算
由于数据链路层单次发送的数据单元最大为1500字节(1贞的大小 这个值貌似受源机硬件影响),所以当IP报文总长度大于这个值的时候,数据链路层会对IP包进行拆分发送,当产生分包后,每个包中的数据相对于原始IP包中的数据,就会有一个衔接位置,那么这个位置该如果计算呢?
这里举例说明,假如:一个原始IP包总长度4000,报头长20。
当发送第1个拆分包的时候,其数据起始位就是IP包起始位,所以其偏移为0,目前发送出去的数据就是1500 - 20 = 1480字节(下标0 - 1479),剩余数据2520,还有拆分包。
当发送第2个拆分包的时候,其数据起始位置应该在原始数据的1480位上,因为片偏移的单位是8字节,所以该包的片偏移值为1480/8 = 185,目前发送出去的数据是1480*2 = 2560,剩余1440,还有拆分包
当发送第3个拆分包的时候,其数据起始位置应该在原始数据的2560位上,所以该包的片偏移值为2560/8 = 370,剩余0,发送结束
这里具体的计算规则我也不是太熟
报文解析代码
public class IPPacket extends Packet{
private final static String TAG = IPPacket.class.getSimpleName();
private final static int VERSION_BIT = 0;
private final static int HEADER_LENGTH_BIT = 0;
private final static int TOS_BIT = 1;
private final static int TOTAL_LENGTH_BIT = 2;
private final static int SEQUENCE_BIT = 4;
private final static int TAG_BIT = 6;
private final static int OFFSET_BIT = 6;
private final static int LIVE_BIT = 8;
private final static int PROTOCL_BIT = 9;
public final static int PROTOCL_ICMP = 1;
public final static int PROTOCL_IGMP = 2;
public final static int PROTOCL_OSPF = 89;
public final static int PROTOCL_TCP = 6;
public final static int PROTOCL_UDP = 17;
private final static int HEADER_SUM_BIT = 10;
private final static int LOCAL_IP_BIT = 12;
private final static int REMOTE_IP_BIT = 16;
public IPPacket(byte[] bytes, int... parameters){
super(bytes,parameters);
}
/**
* 获取IP报文协议版本
* 协议版本
* 在IP报文第0个字节中前4位 占1/2字节 共4位
* @return
*/
public int getVersion(){
return (bytes[VERSION_BIT] & 0xFF) >> 4;
}
/**
* 获取IP报文头长度
* 报头长度
* 在IP报文第0个字节中后4位 占1/2字节 共4位
* 最小表示为 0000(十进制0) 最大表示为 1111(十进制15) 单位为4字节
* 也就是报头 最小为0 * 4字节0 * 4 * 8 = 0位,最大为15 * 4字节15 * 4 * 8 = 480位
* 一般报头长度是0101(十进制5),也就是5 * 4 = 20字节
* @return
*/
public int getHeaderLength(){
return (bytes[HEADER_LENGTH_BIT] & 0xF) * 4;
}
/**
* 获取TOS服务字段
* 服务类型
* 在IP报文中第1个字节 占1字节 共8位
* 一般情况不使用 也没查到具体有什么用
* @return
*/
public int getTos(){
return bytes[TOS_BIT] & 0xFF;
}
/**
* 获取IP报文总长度
* 数据报总长度
* 在IP报文中第2、3字节 占2字节 共16位 单位为1字节
* 标注没有分片前整个IP请求的总长度(包括 数据 和 报头)
* @return
*/
public int getTotalLength(){
return byteToInt(bytes[TOTAL_LENGTH_BIT],bytes[TOTAL_LENGTH_BIT + 1]);
}
/**
* 获取序列码
* 序列码
* 在IP报文中第4个和第5个字节 占2字节 共16位
* 分片请求时用作同IP请求标识
* 开始发送数据时由发送方生成。标识发送方发送的每一个数据报,如果发送的数据报未发生分片,则此值依次加1,如果发生了分片,分片后的各个数据报使用同一个序列号。
* @return
*/
public int getSequence(){
return byteToInt(bytes[SEQUENCE_BIT],bytes[SEQUENCE_BIT + 1]);
}
/**
* 获取标志位
* 分片标志
* 在IP报文中第6个字节的前3位 占3/8字节 共3位
* 第一位保留,未使用。
* 第二位是DF(Don’t Fragment),如果为1,表示未发生分片;如果为0,表示发生了分片。
* 第三位是MF(More Fragment),如果为1,表示发生了分片,并且除了分片出的最后一个报文中此标志为0,其余报文中此标志均为1
* 该标志位有以下几个可能的值
* 001(十进制1):发生分片且后续还有分片
* 000(十进制0):发生分片且这是最后一个分片
* 010(十进制2):不能分片
* @return
*/
public int getTag(){
return (bytes[TAG_BIT] & 0xFF) >> 5 ;
}
/**
* 是否发生分片
* 1 为未发生分片 返回false
* 0 为发生了分片 返回true
* DF Don't Fragment
* @return
*/
public boolean isDF(){
if((getTag() >> 1) == 1){
return false;
}else{
return true;
}
}
/**
* 是否还有分片
* 如果未发生分片直接返回false
* 如果发生分片且后面还有分片返回true
* 如果发生分片且后面没有分片了返回false
* MF More Fragment
* @return
*/
public boolean isMF(){
if(isDF() && (getTag() & 1) == 1){
return true;
}else{
return false;
}
}
/**
* 获取片偏移
* 片偏移量 单位 8 byte
* 在IP报文中第6个字节的后5位和第7个字节 占13/8字节 共13位
* 标识分片相对于原始IP数据报开始处的偏移。只在分片传输时起作用
* 链路层MTU(Maximum Transmission Unit 最大传输单元)为1500
* 如果IP报文总长度超过这个数值,就会产生IP分片
* 下面是计算公式
* 假设链路层传输时分片数量为 n
* IP报头长度为20字节(实际传输时默认为20)
* IP报文总长度为 m
* 分片后的片偏移为y (不管有没有分片,首个IP分片的片偏移肯定是0 如果有分片n肯定是大于1的)
* n = m > 1500 ? (m - m % 1500) / 1500 + 1 :1
* y = (1500 - 20) * (n - 1) / 8
* 可能会觉得懵逼 为什么还会除一个8 这是因为片偏移的单位是 8 byte 这里得到的值是最后写到报文中片偏移的值
* 我看过一个文档上面写的有个这样的描述:从0位到1479位是IP报数据位 当时不明白这个1479是怎么来的
* 后来仔细想想 这玩意是从下标读数来的 数组中读数是从0位开始到 数组长度 - 1 结束。
* 也就是 MTU - 报头长度 - 1 => 1500 - 20 - 1 = 1479
* 从0位数到1479位 正好是1500个位置。
* @return
*/
public int getOffsetByte(){
return byteToInt(bytes[OFFSET_BIT],bytes[OFFSET_BIT + 1]) * 8;
}
/**
* 获取生命周期
* 生存时间
* 在IP报文中第8个字节 占1字节 共8位
* 表示生存周期,每经过一个路由值减一,防止路由环路。(windows系统下默认值为128 二进制 10000000)
* 例如 路由A发数据给B B又转发给A 值依次衰减,值为0时或主动停止时停止转发
* @return
*/
public int getLive(){
return bytes[LIVE_BIT] & 0xFF;
}
/**
* 获取上层协议
* 上层协议
* 在IP报文中第9个字节 占1字节 共8位
* 该值标识上层协议
* 其中常见的值有
* ICMP协议 十进制 1 二进制 00000001
* IGMP协议 十进制 2 二进制 00000010
* OSPF协议 十进制 89 二进制 01011001
* TCP协议 十进制 6 二进制 00000110
* UDP协议 十进制 17 二进制 00010001
* @return
*/
public int getProtocl(){
return bytes[PROTOCL_BIT] & 0xFF;
}
/**
* 获取头部校验和
* 头部校验和
* 在IP报文中第10、11个字节 占2字节 共16位
* 该值是对整个数据包的包头进行的校验
* @return
*/
public int getHeaderSum(){
return byteToInt(bytes[HEADER_SUM_BIT],bytes[HEADER_SUM_BIT + 1]);
}
/**
* 获取本地IP地址 String类型
* x.x.x.x格式
* 源IP地址
* 在IP报文中第12、13、14、15个字节 占4个字节(32位)
* 表示本地IP地址(发送方MAC地址 也就是发送方的物理硬件地址) 那么地址掩码又是怎么回事?
* 因为一个字节最大表示数为255 所以我们看到的IP地址通常写为x.x.x.x的格式(x为0-255的十进制数 转换为二进制数为00000000~11111111)
* @return
*/
public String getLocalIpString(){
return (bytes[LOCAL_IP_BIT] & 0xFF)
+ "." + (bytes[LOCAL_IP_BIT + 1] & 0xFF)
+ "." + (bytes[LOCAL_IP_BIT + 2] & 0xFF)
+ "." + (bytes[LOCAL_IP_BIT + 3] & 0xFF);
}
public int getLocalIpInt(){
return byteToInt(bytes[LOCAL_IP_BIT],bytes[LOCAL_IP_BIT + 1],bytes[LOCAL_IP_BIT + 2],bytes[LOCAL_IP_BIT + 3]);
}
/**
* 获取远程IP地址 String类型
* x.x.x.x格式
* 目的IP地址
* 在IP报文中第16、17、18、19个字节 占4个字节(32位)
* 远程IP地址(接收方MAC地址 也就是接收方的物理硬件地址)
* 因为一个字节最大表示数为255 所以我们看到的IP地址通常写为x.x.x.x的格式(x为0-255的十进制数 转换为二进制数为00000000~11111111)
* @return
*/
public String getRemoteIpString(){
return (bytes[REMOTE_IP_BIT] & 0xFF)
+ "." + (bytes[REMOTE_IP_BIT + 1] & 0xFF)
+ "." + (bytes[REMOTE_IP_BIT + 2] & 0xFF)
+ "." + (bytes[REMOTE_IP_BIT + 3] & 0xFF);
}
public int getRemoteIpInt(){
return byteToInt(bytes[REMOTE_IP_BIT],bytes[REMOTE_IP_BIT + 1],bytes[REMOTE_IP_BIT + 2],bytes[REMOTE_IP_BIT + 3]);
}
/**
* 检查头部校验和
* 很无语这个校验和的算法模式为什么会这样设计
* 直接取每个字节中表示的十进制数相加不就得了
* 还非得用16位来表示肯定会溢出的校验和
* 这个方法还没有搞明白
*/
public boolean checkSum(){
return (( ~getSum() ) & 0xFFFF) == 0;
}
/**
* 校验求和
* @return
*/
private int getSum(){
int headerLength = getHeaderLength();
int sum = 0;
// 两字节一组依次计算16位和得到累加值
for(int i = offset; i < offset + headerLength; i += 2){
sum += byteToInt(bytes[i],bytes[i + 1]);
}
// 如果报头为奇数 那么最后一个字节的和也需要加上
if((offset + headerLength) % 2 > 0 ){
sum += (bytes[offset + headerLength - 1] & 0xFF) << 8;
}
//这里计算校验和16位之上是否有进位 若有进位则循环折叠求和 直到16位之上没有进位
while ((sum >> 16) > 0){
sum = (sum >> 16) + (sum & 0xFFFF);
}
return sum;
}
/**
* 设置完参数后重新计算校验和并设置
*/
public void refreshCheckSum(){
bytes[HEADER_SUM_BIT] = 0;
bytes[HEADER_SUM_BIT + 1] = 0;
int sum = ~getSum();
bytes[HEADER_SUM_BIT] = (byte) (sum >> 8);
bytes[HEADER_SUM_BIT + 1] = (byte)(sum);
}
public void setLocalIp(String ip){
String[] strings = ip.split("\\.");
if(strings.length != 4) return;
int y = 255;
for(int i = 0;i < 4; i++){
try {
y = Integer.valueOf(strings[0]);
}catch (NumberFormatException e){}
bytes[REMOTE_IP_BIT + i] = (byte) y;
}
}
public void setLocalIp(int ip){
for(int i = 0; i < 4;i ++){
bytes[LOCAL_IP_BIT + i] = (byte) (ip >> ((3 - i) * 8));
}
}
public void setRemoteIp(String ip){
String[] strings = ip.split("\\.");
if(strings.length != 4) return;
int y = 255;
for(int i = 0;i < 4; i++){
try {
y = Integer.valueOf(strings[0]);
}catch (NumberFormatException e){}
bytes[REMOTE_IP_BIT + i] = (byte) y;
}
}
public void setRemoteIp(int ip){
for(int i = 0; i < 4;i ++){
bytes[REMOTE_IP_BIT + i] = (byte) (ip >> ((3 - i) * 8));
}
}
}
关于文中的Packet类的代码如下:
public class Packet {
// 数据报文
protected byte[] bytes;
// 偏移值
protected int offset;
// 有效数据长度
protected int validLength;
// 下一层的包
protected Packet packet;
/**
*
* @param bytes 收到的byte数据集合
* @param parameters 一些参数 如:数据偏移 有效长度
*/
public Packet(byte[] bytes,int... parameters){
this.bytes = bytes;
if(parameters != null){
if(parameters.length > 0){
offset = parameters[0];
if(parameters.length > 1){
validLength = parameters[1];
}
}
}
if(validLength == 0){
validLength = bytes.length;
}else if(validLength > bytes.length){
validLength = bytes.length;
}
}
/**
* 获取下级协议
* @return
*/
public Packet getPacket() {
return packet;
}
/**
* 设置下级协议 从上至下依次为 HTTP/HTTPS -> TCP/UDP -> IP -> 网路路由
* @param packet
*/
public void setPacket(Packet packet) {
this.packet = packet;
}
/**
* 获取数据
* @return
*/
public byte[] getBytes(){
return bytes;
}
/**
* 获取数据偏移
* @return
*/
public int getOffset(){
return offset;
}
/**
* 获取数据有效长度
* @return
*/
public int getValidLength(){
return validLength;
}
/**
* 获取报头长度
* @return
*/
public int getHeaderLength(){
return 0;
}
public int byteToInt(byte... bytes){
int sum = 0;
if(bytes == null) return 0;
for (int i = 0; i < bytes.length && bytes.length <= 4; i ++){
sum |= (bytes[i] & 0xFF) << ((bytes.length - 1 - i) * 8);
}
return sum;
}
public byte intToByte(int i){
return (byte) i;
}
public byte[] intToBytes(int i){
byte[] bytes = new byte[4];
bytes[0] = (byte) (i >> 24);
bytes[1] = (byte) (i >> 16);
bytes[2] = (byte) (i >> 8);
bytes[3] = (byte) i;
return bytes;
}
}
代码中的注释很简洁,全部看完应该能理解是什么意思
总结
以上就是我对IP报文的理解,有问题请多指教。