STM32NET学习笔记 IP ICMP部分
扫描二维码
随时随地手机看文章
1.前言
嵌入式以太网开发是一个很有挑战性的工作。通过几个月的学习,我个人觉得大致有两条途径。第一条途径,先通过高级语言熟悉socket编程,例如C#或C++,对bind,listen,connect,accept等函数熟悉之后,应用 lwIP。第二种途径,通过分析嵌入式以太网代码,结合TCPIP协议栈规范逐步实践代码。第一种途径效率高,开发周期短,编写出来的代码性能稳定,第二种途径花的时间长,开发出来的代码功能不完善,但是由于紧紧结合TCPIP规范,可以了解的内容较多,适合学习。本文通过分析和修改AVRNET源码,逐步实现TCPIP协议栈的各个子部分,包括ETHERNET部分,ARP部分,IP部分,ICMP部分,UDP部分,TCP部分和HTTP部分。
本文将实现IP部分和ICMP部分。
1.2 相关资料
【ENC28J60学习笔记】
【STM32NET学习笔记 ARP和Ethernet部分】
【AVRNET项目(国外)】
【AVR webserver项目(国外)】
1.3 代码仓库
【代码仓库】——CSDN Code代码仓库。
2.IP部分实现
IP层是TCP和UDP实现的基础。IP首部紧跟以太网首部,长度为20字节。IP首部具有最基本的两个任务,
【第一】定义IP包的具体协议类型,例如ICMP,TCP或UDP等;
【第二】定义IP报文从哪个IP地址来和到哪个IP地址去。
需要强调,在同一个子网中即同一个物理网络中,IP报文中的目标IP地址和以太网首部中的目标MAC地址相对应,若不在同一个物理网路中,目标IP地址和目标MAC地址不同,目标MAC地址被路由器的MAC地址替代,意味着通过路由器转发报文。在IP首部中还包括很多其他内容,需要注意的是IP标识符,该标识符主要用于区分IP报文,最简单的算法即每发送一个IP报文后IP标识符累加。具体通过以下代码实现IP首部的填充。
2.1 IP首部填充
//IP首部总长度
#defineIP_HEADER_LEN20
//协议类型
//ICMP协议
#defineIP_PROTO_ICMP_V0x01
//TCP协议
#defineIP_PROTO_TCP_V0x06
//UDP协议
#defineIP_PROTO_UDP_V0x11
//IPV4版本
#defineIP_V4_V0x40
#defineIP_HEADER_LENGTH_V0x05
//IP版本号位置以太网首部2+6+6
#defineIP_P0x0E
//首部长度
#defineIP_HEADER_VER_LEN_P0x0E
//服务类型
#defineIP_TOS_P0x0F
//IP总长度
#defineIP_TOTLEN_H_P0x10
#defineIP_TOTLEN_L_P0x11
//IP标识
#defineIP_ID_H_P0x12
#defineIP_ID_L_P0x13
//
#defineIP_FLAGS_H_P0x14
#defineIP_FLAGS_L_P0x15
//TTL生存时间
#defineIP_TTL_P0x16
//IP协议类型例如ICMPTCPUDP
#defineIP_PROTO_P0x17
//首部校验和
#defineIP_CHECKSUM_H_P0x18
#defineIP_CHECKSUM_L_P0x19
//源IP地址
#defineIP_SRC_IP_P0x1A
//目标IP地址
#defineIP_DST_IP_P0x1E
voidip_generate_header(BYTE*rxtx_buffer,WORD_BYTEStotal_length,BYTEprotocol,BYTE*dest_ip)
{
BYTEi;
//校验结果
WORD_BYTESck;
//版本号和首都长度
rxtx_buffer[IP_P]=IP_V4_V|IP_HEADER_LENGTH_V;
//服务类型
rxtx_buffer[IP_TOS_P]=0x00;
//总长度
rxtx_buffer[IP_TOTLEN_H_P]=total_length.byte.high;
rxtx_buffer[IP_TOTLEN_L_P]=total_length.byte.low;
//IP标识
rxtx_buffer[IP_ID_H_P]=ip_identfier>>8;
rxtx_buffer[IP_ID_H_P]=ip_identfier&0x00ff;
//累加
ip_identfier++;
//标志和分片偏移
rxtx_buffer[IP_FLAGS_H_P]=0x00;
rxtx_buffer[IP_FLAGS_L_P]=0x00;
//生存时间
rxtx_buffer[IP_TTL_P]=128;
//setippackettypetotcp/udp/icmp...
rxtx_buffer[IP_PROTO_P]=protocol;
//设定目标地址和源地址
for(i=0;i<4;i++)
{
rxtx_buffer[IP_DST_IP_P+i]=dest_ip[i];
rxtx_buffer[IP_SRC_IP_P+i]=avr_ip.byte[i];
}
//校验结果
rxtx_buffer[IP_CHECKSUM_H_P]=0;
rxtx_buffer[IP_CHECKSUM_L_P]=0;
ck.word=software_checksum(&rxtx_buffer[IP_P],sizeof(IP_HEADER),0);
rxtx_buffer[IP_CHECKSUM_H_P]=ck.byte.high;
rxtx_buffer[IP_CHECKSUM_L_P]=ck.byte.low;
}
2.2 IP报文查询
IP报文查询功能对应于ARP报文查询功能,通过以太网首部中的最后2个字节判断该报文是否为IP报文;如果是IP报文则继续和本机IP地址相比较。如果两步检查均通过则认为是合法的IP报文,当然这其中舍弃了IP版本号和首部校验和的检查,虽然存在某些隐患但并不妨碍实现基本功能。
BYTEip_packet_is_ip(BYTE*rxtx_buffer)
{
unsignedchari;
//检查该报文是否为IP报文
if(rxtx_buffer[ETH_TYPE_H_P]!=ETH_TYPE_IP_H_V||rxtx_buffer[ETH_TYPE_L_P]!=ETH_TYPE_IP_L_V)
return0;
//检查该报文的IP地址是否为本机IP地址,逐个检查
for(i=0;i
{
if(rxtx_buffer[IP_DST_IP_P+i]!=avr_ip.byte[i])
return0;
}
//若该报文为IP报文,且目标IP地址为本机地址,返回1
return1;
}
3.ICMP部分实现
虽然ICMP具有很多的子协议,但是其中最著名的要数ping程序,即ICMP回显请求和应答报文。通过使用ping命令来判断报文是否可以到达目标地址。ICMP的实现是一个逐步遵守规则的过程,即向固定的字节填充数据。
//回显应答
#defineICMP_TYPE_ECHOREPLY_V0
//回显请求
#defineICMP_TYPE_ECHOREQUEST_V8
//ICMP首部长度
#defineICMP_PACKET_LEN40
//ICMP类型
#defineICMP_TYPE_P0x22
//ICMP代码
#defineICMP_CODE_P0x23
//ICMP首部校验和
#defineICMP_CHECKSUM_H_P0x24
#defineICMP_CHECKSUM_L_P0x25
//ICMP标识符
#defineICMP_IDENTIFIER_H_P0x26
#defineICMP_IDENTIFIER_L_P0x27
//ICMP序号
#defineICMP_SEQUENCE_H_P0x28
#defineICMP_SEQUENCE_L_P0x29
#defineICMP_DATA_P0x2A
3.1 ICMP首部填充
ICMP首部填充需要根据协议类型填充不同的内容,对于回显请求而言只需在ICMP协议类型部分填充0即可,当然ICMP部分也包括ICMP首部校验和。
voidicmp_generate_packet(BYTE*rxtx_buffer,BYTEtype)
{
BYTEi;
WORD_BYTESck;
//ICMP回显请求
if(type==ICMP_TYPE_ECHOREQUEST_V)
{
rxtx_buffer[ICMP_TYPE_P]==ICMP_TYPE_ECHOREQUEST_V;
rxtx_buffer[ICMP_CODE_P]=0;
rxtx_buffer[ICMP_IDENTIFIER_H_P]=icmp_id;
rxtx_buffer[ ICMP_IDENTIFIER_L_P ] = 0;