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协议的步骤

1struct 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是1if(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包就可以了。

  • 6
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值