对TCP/IP协议都只是听过,没有仔细研究过,一些知识体系也比较零散,什么三次握手,四次挥手,滑动窗口,零拷贝技术等等,都是知识有这么个东西,而不知道具体是啥,这几天还是根king老师学习TCP/IP协议栈,受益匪浅,所以把这几天学习的TCP/IP协议的知识整理一下,形成一个自己的知识体系。
1.1 7层5层模型
每次说到tcp/ip都要上这一张图,这样才显得自己很专业(其实并不专业)。网络上介绍这5层模型的挺多的,我这里就引用一下
OSI七层模型与TCP/IP五层模型
顺便盗两张图:
第一张是描述不同的设备工作在那一层:
第二张是每一层的主要协议是啥:
从最后一张图就可以看到,之前实现的http协议就是在应用层的(博客还没总结,抽个时间写写,自己实现的http),然后我们在应用层就是直接调用api,比如send和recv,这个send和recv发送的数据到哪里去了呢?其实通过图就可以看出,我们应用层调用了send和recv之后,数据发送到传输层(也就是tcp/udp),这里就是我们今天要自己动手实现UDP协议,也就是造轮子,我一直以为要学习一个开源代码,还是自己动手实现一个比较靠谱,所以今天我们来实现一个简单的UDP协议。
1.2 动手实现udp
1.2.1 udp数据帧的包头
这是king老师自己画的,这里又盗图了。
右边是tcp/ip模型,我们从应用程序中调用sendto的时候,用户层会把用户数据传到传输层,传输层在用户数据的基础上添加传输层的数据包头(其实就是端口),传输层再往下发数据,网络层也会在传输层数据的基础上添加上自己的网络层的包头(其实就是IP),网络层继续往下走,数据链路层也会封装自己的包头(其实就是mac地址),这样经过层层包装,才送到物理层去发送,另一端接收到的数据,就是一层一层就解包,就跟收快递一样,这里就不细说了。
1.2.2 数据链路层包头
数据链路层的包头14个字节,包括目的mac地址,源目的mac地址,和类型
封装的代码如下:
#define ETH_LENGTH 6
struct ethhdr {
unsigned char dest_mac[ETH_LENGTH]; //源mac地址
unsigned char src_mac[ETH_LENGTH]; //目的mac地址
unsigned short proto; //网络层协议类型,通常是IP协议,0x0800
}
1.2.3 IP包头
版本:包含IP数据报的版本号:IPv4为14,IPv6为6.
首部长度:标明IP数据包头部长度,单位是字也就是4个字节,它是一个4位的字段,所以IPv4的头部为15*4=60个字节的数据,这个字段是正常的值是5(没当没有选项时)。IPv6不存在这个字段,其头部长度固定为40字节。
服务类型(Tos):区别不同服务
总长度:IPv4数据集包的总长度。由于它是一个16位的字段,所以IPv4数据报的最大长度(包括包头)为65535字节。
标识符:这个域的作用是当一个大的数据报被拆分时,拆分成的小的数据段的这个域都是一样的。
标记和段偏移以后再说
TTL:生存时间,设置一个数据报可经过的路由器数量的上限。发送方将它初始化为某一个值建议为64,每台路由器在转发数据报时将该值减1,表示在网络中经过了几个网关。(当这个字段值为0时,该数据被丢弃,防止出现不希望的路由环路而导致数据报在网络中永远循环)。
协议号:表示应用层使用的协议,比如UDP=17,TCP=6。
首部校验和:IP头部的校验和(为什么每层都加校验,是因为在以前双绞线的时候,数据容易出错,每一层加校验就可以知道那一层的数据出现了问题,容易定位问题)
源IP地址:数据报来源主机的IP地址
目的IP地址:数据报目的主机的IP地址
封装的代码如下:
struct iphdr {
unsigned char version : 4;
unsigned char hdrlen : 4; //首部长度
unsigned char tos;
unsigned short totlen;
unsigned short id; //分片标识
unsigned short flag : 3;
unsigned short offset : 13;
unsigned short ttl; //每经过一个网关,交换机就减1,默认值是64,跨一次网络都减1
unsigned char proto; //应用层协议
unsigned short check;
unsigned int sip; //源ip地址
unsigned int dip; //目标ip地址
};
1.2.4 udp包头
udp的包头就比较简单了,没有ip包头那么多。
就不解释了,直接上代码:
struct udphdr
{
unsigned short sport;
unsigned short dport;
unsigned short length;
unsigned short crc;
};
1.2.5 udp帧
udp数据帧其实就是包前面的包头全部叠加,在加上用户数据,这里用户数据用柔性数组来定义:
//udp 包 = ethhdr + iphdr + udphdr + userdata
struct udppkt {
struct ethhdr eh; // 14
struct iphdr ip; // 20
struct udphdr udp; // 8
unsigned char body[0];
};
1.2.6 用户层实现协议基础
怎么实现用户层程序来实现协议,在github上有一个开源代码netmap,是把网卡的数据直接映射到内存中,不经过内核,有点像DMA技术,所以我们需要用这种技术来完成网卡接收到数据直接放到内存中,代码github连接。
就是这个驱动编译搞了好几天,用了几个系统,最后没办法了,只能换回Ubuntu16的系统,因为这个驱动好像是基于Ubuntu16写的,所以只能乖乖的换回来,以后有空再分析一下不同系统安装的步骤。
1.2.7 简单实现udp协议
简述一个udp协议的步骤
1、struct nm_desc *nmr = nm_open("netmap:eth0", NULL, 0, NULL);
//使用netmap打开一个虚拟网卡设备。
2、pdf.fd = nmr->fd;
pfd.events = POLLIN;
int ret = poll(&pfd, 1, -1);
//使用poll来管理nmr->fd的设备文件
3、如果poll有数据过来了,就可以处理数据了,首先先转换成ethhdr包,判断一下协议是否是IP协议,IP协议的值是0x0800.
if(ntohs(eh->h_proto) == PROTO_IP )
4、如果网络层是使用ip协议,这时候就可以获取到ip包,并解析,得出传输层是使用什么协议的,如果是UDP就是17,icmp是1。
if(ntohs(eh->h_proto) == PROTO_IP )
5.如果传输层是udp,这时候就可以取数据了,这里需要注意,我们是使用柔性数据接收的,所以需要拿到数据长度,这个数据长度是udp包已经有的,
if(udp->ip.proto == PROTO_UDP) {
unsigned short udplen = ntohs(udp->udp.length);
udp->body[udp->udp.length-8] = '\0';
}
其实比较简单的,做过单片机的,对这种协议都不陌生,当初手撕I2c的时候,只不过这个是分层处理了,用了之后,才越来越觉得分层的好处。
测试的的结果:
1.2.8 实现arp
上述程序实现的结果是只能发现一段时间,过了一会就发送了不了。
这是缺少实现arp协议了,(其实我之前也不知道arp是啥),现在要用到了,就要普及一下了。
arp协议详解,我看这这篇文章就讲的不错,ARP协议详解
讲的很详细,我这里就总结一下:
ARP是地址解析协议,在以太网中,一台主机和一台主机通信,是通过mac地址通信的,但是在一个局域网内,我们只知道ip地址,而不知道mac地址,这时候就需要ARP协议,IP地址和mac地址的一个映射表,通过IP地址查找到对应mac地址。
ARP映射主要是动态方式,如果其他主机要想知道ARP映射表,会轮询发送arp数据包,所以我们实现的udp协会也需要回复一个arp包。
1.2.8.1 arp包
这里借用了那篇博客的arp包的图,
代码封装如下:
//ip层协议
struct arphdr {
unsigned short hw_type;
unsigned short proto_type;
unsigned char hw_addr_len;
unsigned char proto_addr_len;
unsigned short op;
unsigned char s_mac[ETH_LENGTH]; //mac地址
unsigned int sip;
unsigned char d_mac[ETH_LENGTH];
unsigned int dip;
};
arp包是网络层的协议,已经是最高层协议了,上面没有其他层了。
arp包也是基础数据链路层的,所以整个arp包:
//ip层协议
struct arphdr {
unsigned short hw_type;
unsigned short proto_type;
unsigned char hw_addr_len;
unsigned char proto_addr_len;
unsigned short op;
unsigned char s_mac[ETH_LENGTH]; //mac地址
unsigned int sip;
unsigned char d_mac[ETH_LENGTH];
unsigned int dip;
};
1.2.8.2 arp包回复
程序思路:
1、因为arp协议是网络层的,所以需要判断数据链路层发过来的协议:
if(ntohs(eh->h_proto) == PROTO_ARP)
2.收到ARP数据包的时候,需要判断是不是自己的IP地址
if(arp->arp.dip == inet_addr("192.168.121.155")
3.如果匹配上的话,就回复一个arp包
void echo_arp_pkt(struct arppkt *arp, struct arppkt *arp_rt, char *hmac)
{
memcpy(arp_rt, arp, sizeof(struct arppkt));
memcpy(arp_rt->eh.h_dest, arp->eh.h_src, ETH_LENGTH);
str2mac(arp_rt->eh.h_src, hmac); //源mac ffffffff
arp_rt->eh.h_proto = arp->eh.h_proto;
arp_rt->arp.hw_addr_len = 6;
arp_rt->arp.proto_addr_len = 4;
arp_rt->arp.op = htons(2);
str2mac(arp_rt->arp.s_mac, hmac);
arp_rt->arp.sip = arp->arp.dip;
memcpy(arp_rt->arp.d_mac, arp->arp.s_mac, ETH_LENGTH);
arp_rt->arp.dip = arp->arp.sip;
}
主要是把源mac和源ip换成目的mac和目的ip,然后再把自己的mac和ip填充到源mac和目的ip
在加上这个arp协议之后,我们实现的udp协议就比较稳定了。
1.2.9 实现ping
如果我们在电脑端ping这个虚拟机的ip地址,发现是ping不通,为什么呢?是因为我们没有实现ping协议。
ping其实是icmp协议中的,icmp协议是在网络层的协议,就是iP报文中的一部分,一直顶这ip报文这个大哥的大腿,做一些不可描述的事情。协议详解我目前不善长,所以还是引用了别人的协议详解,ICMP协议全解析,这个就讲的很清楚,可以好好看一看。
1.2.9.1 icmp协议包
看到这个协议包是不是很开心,比较简单,
struct icmppkt {
unsigned char type;
unsigned char code;
unsigned short sum; //前面才是icmp的数据包,这次封装的不好
unsigned short id; //这个是ping包的id
unsigned short num; //是ping包的num
};
ping包的图也不好找,直接贴出代码:
struct pingpkt {
struct ethhdr eh; //14
struct iphdr ip; //20
struct icmppkt icmp; //8
unsigned char data[0]; //柔性数组存储数据
};
1.2.9.2 ping请求包
所以就是我们接受到这个ping包之后,需要恢复一个ping包,通过wireshark抓包工具:
ICMP是在IP层的,并且协议是1,还有带上目的ip,所以首先我们做的是判断协议类型和ip地址:
if(udp->ip.proto == PROTO_ICMP) {
if(udp->ip.dip == inet_addr("192.168.121.155"))
如果这两个满足就可以恢复ping包了,我们再看看icmp包的抓包图:
type:8就是请求的意思,可以看协议详解,就是不太明白为什么要带一串数据,不明白就先留着,以后可以看看。
1.2.9.2 ping回复包
代码简单思维:
1、先通过接受回来的ping包,计算出data数据的长度(就是莫名其妙的那个数据),通过这个长度申请一个ping包的内存,这里是柔性数据,记得申请data数据长度。
2、把整个ping包数据拷贝到要发送的数据包中
3、准备ethhdr包,把mac地址的源和目的交换
4、准备ip的数据包,把源ip和目的ip交换,比较计算总长度,这个很重要,记得加上icmp的数据长度
5、准备ping包,type=0是应答8的请求,并且计算校验和,这个校验和是抄king老师的
6、调用发送接口,发送数据包
nm_inject(nmr, ping_rt, len);
完整代码如下,打印信息我还没去掉,你们可以去掉打印信息
struct pingpkt* echo_ping_pkt(struct pingpkt *ping, unsigned short *len)
{
if(ping == NULL) {
printf("ping is null\n");
return NULL;
}
//第一步
unsigned short icmp_data_len = ntohs(ping->ip.totlen)-sizeof(struct iphdr)-sizeof(struct icmppkt); //strlen(ping->data)
printf("icm_data_len %d\n", icmp_data_len);
struct pingpkt* ping_rt = NULL;
ping_rt = malloc(sizeof(struct pingpkt)+icmp_data_len);
if(ping_rt == NULL) {
printf("echo_ping_pkt malloc error\n");
return NULL;
}
//第二步
memcpy(ping_rt, ping, sizeof(struct pingpkt)+icmp_data_len);
printf("ping->data %s %d %d\n", ping->data, icmp_data_len, sizeof(ping_rt->data));
//memcpy(ping_rt->data, ping->data, icmp_data_len);
printf("ping_rt->data %s\n", ping_rt->data);
//第三步
memcpy(ping_rt->eh.h_dest, ping->eh.h_src, 6);
memcpy(ping_rt->eh.h_src, ping->eh.h_dest, 6);
ping_rt->eh.h_proto = ping->eh.h_proto;
//第四步
ping_rt->ip.sip = ping->ip.dip;
ping_rt->ip.dip = ping->ip.sip;
printf("ip tolen = %d %d\n", ntohs(ping->ip.totlen), icmp_data_len);
unsigned short ippkt_len = sizeof(struct iphdr)+sizeof(struct icmppkt)+icmp_data_len;
ping_rt->ip.totlen = htons(ippkt_len);
//第五步
ping_rt->icmp.type = 0;
ping_rt->icmp.code = 0;
printf("ping->id %d ping->seq %d\n", ping->icmp.id, ping->icmp.num);
ping_rt->icmp.sum = 0;
//ping_rt->icmp.sum = cimp_pkt_sum(&ping_rt->icmp, sizeof(struct icmppkt)+icmp_data_len);
ping_rt->icmp.sum = in_cksum(&ping_rt->icmp, sizeof(struct icmppkt)+icmp_data_len);
printf("dd\n");
*len = sizeof(struct pingpkt)+icmp_data_len;
return ping_rt;
}
校验和代码比较难算,这里就抄抄king老师的:
unsigned short in_cksum(unsigned short *addr, int len) {
register int nleft = len;
register unsigned short *w = addr;
register int sum = 0;
unsigned short answer = 0;
while (nleft > 1) {
sum += *w++;
nleft -= 2;
}
if (nleft == 1) {
*(u_char *)(&answer) = *(u_char *)w ;
sum += answer;
}
sum = (sum >> 16) + (sum & 0xffff);
sum += (sum >> 16);
answer = ~sum;
return (answer);
}
1.2.9.3 实现结果
左边是ping数据,右边是抓包出来的数据,一个简单实现的udp包就可以了。