阅读 105

TCP/IP协议(一、自己动手实现udp)

对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包就可以了。


作者:善良的小油条
链接:https://juejin.cn/post/7021917132849938439


文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐