从0到1用java再造tcpip协议栈:实现ARP协议层

视频还在审查中,敬请期待。

经过前两节的准备,我们完成了数据链路层,已经具备了数据包接收和发送的基础设施,本机我们在此基础上实现上层协议,我们首先从实现ARP协议开始。先简单认识一下ARP协议,ARP是一种寻址协议,它要找寻目标的物理地址,连接在互联网上的设备有两种地址,一种叫IP,也就是我们常见的192.168.2.1这类地址,另一种叫物理地址,例如我们电脑上的mac地址。

为何要使用两种地址呢?这类似与个人的名字与身份证号的区别,名字是可以重复的,例如叫”张三“的肯定不止一个人,当一件包裹寄到一栋楼里如果楼内有多个人叫张三,那么此时就需要身份证号来辨别哪个张三才是包裹的收件人。在网络上传输数据时,ip对应一个区域网,该区域网内有多台连接设备,当数据包根据ip找到对应区域网后,如何知道区域网上的哪台设备是数据包的接收者呢?这时就得看硬件地址,只有与数据包附带的硬件地址相符合的设备才应该接收到来的数据包,如此一来数据包要正确发生并接收,那就得知道两个地址,一个是ip,一个是硬件地址,对应互联网设备而言,通常就是mac地址,而ARP协议就是专门用来获得接收对象的mac地址的。

网络协议的本质其实就是填表单。ARP协议的实现也是填写一系列表单,发给对方,对方根据表单要求也填写一张表单发回来,我们看看这张表单的结构:

这张表上头的0-32单位是比特位而不是字节,要注意。根据表单所示,前16个比特位也就是前2个字节对于的是硬件类型,也就是传送数据的网络,其取值情况如下: 1:10MB以太网;6:IEEE802网络;7:ARCNET;16:ATM… 它还有其他取值,为了简便我没有罗列出来,由于我们默认在互联网上收发数据,因此填表时这两个字节写死为1。

接下两字节也就是protocoal type,表示数据传输使用的网络协议,如果数据包使用IP定位接收目标所在的局域网,那么该值写死为0x0800,我们实现的协议也是把这两个字节写死。

接下来是两个单字节用于表示两种地址的长度,我们默认收发数据包的设备都是mac地址,因此Hardware Adress Length这个字节写死为6,同理我们默认设备都使用IP地址,因此protocal Address Length这个字节写死为4.

接着两字节是OpCode,用来表示消息目的。1表示请求,当A向B发出ARP请求希望获得B的mac地址时,A构造这张表单时在该字节填写1。2表示回应,当B收到请求,向A返回同样格式的表单,此时它在该字节填写2,同时把自己的硬件地址填写在表单里。

接下来是Sender Hardware Address,它用来存储发送者的硬件地址,其长度与Hardware Address Length中表示的一致,在我们实现中,它用来存储发生者的mac地址,因此占据6个字节。

接着的Sender Protocol Address表示发送者的IP地址,因此占据4字节。

Target Hardware Address是接收者的mac地址,占据6字节,最后是接收者的IP地址,占据4字节。

当表单填好后,数据链路层在发送出去前还会再加上一个包头,包头有14个字节,前6个字节表示接收者的mac地址,接着6个字节表示发送者的mac地址,然后有2字节表示包的类型,如果发送的是ARP包,那么这2字节的值为0x0806,如果发送的是IP包,那么值为0x0800,当网卡接收到数据包后,它会检测这两个字节,根据数值把前14字节的数据链路包头去除后,将剩下的数据提交给对应的网络协议层,因此ARP包经过链路层封装后发送时格式如下:

接下来我们看看代码实现,首先我们需要对上节模拟的数据链路层做一些修改:

package datalinklayer;

import jpcap.NetworkInterfaceAddress;
import jpcap.packet.EthernetPacket;
import jpcap.packet.Packet;
import utils.IMacReceiver;
import utils.PacketProvider;

import java.io.IOException;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;

import ARPProtocolLayer.ARPProtocolLayer;
import jpcap.JpcapCaptor;
import jpcap.JpcapSender;
import jpcap.NetworkInterface;

public class DataLinkLayer extends PacketProvider implements jpcap.PacketReceiver, IMacReceiver{   
        //change 1
        private static DataLinkLayer instance = null;
        private NetworkInterface device = null;
        private Inet4Address ipAddress = null;
        private byte[] macAddress = null;
        JpcapSender sender = null;

        private DataLinkLayer() {

        }

        public static DataLinkLayer getInstance() {
            if (instance == null) {
                instance = new DataLinkLayer();
            }

            return instance;
        }

        // change 2
        public void initWithOpenDevice(NetworkInterface device) {
            this.device = device;    
            this.ipAddress = this.getDeviceIpAddress();
            this.macAddress = new byte[6];
            this.getDeviceMacAddress();

            JpcapCaptor captor = null;
            try {
                captor = JpcapCaptor.openDevice(device,2000,false,3000);
            } catch (IOException e) {
                e.printStackTrace();
            }

            this.sender = captor.getJpcapSenderInstance();

            //测试arp协议
            this.testARPProtocol();
        }

        private Inet4Address getDeviceIpAddress() {
            for (NetworkInterfaceAddress addr  : this.device.addresses) {
                  //网卡网址符合ipv4规范才是可用网卡
                  if (!(addr.address instanceof Inet4Address)) {
                      continue;
                  }

                  return (Inet4Address) addr.address;
            }

            return null;
        }

        private void getDeviceMacAddress() {
            int count = 0;
            for (byte b : this.device.mac_address) {
                this.macAddress[count] = (byte) (b & 0xff);
                count++;
            }
        }

        // change 3
        public  byte[] deviceIPAddress() {
            return this.ipAddress.getAddress();
        }

        public byte[] deviceMacAddress() {
            return this.macAddress;
        }


        @Override
        public void receivePacket(Packet packet) {
            //将受到的数据包推送给上层协议
            this.pushPacketToReceivers(packet);
        }

        public void sendData(byte[] data, byte[] dstMacAddress, short frameType) {
            /*
             * 给上层协议要发送的数据添加数据链路层包头,然后使用网卡发送出去
             */
            if (data == null) {
                return;
            }

            Packet packet = new Packet();
            packet.data = data;

            /*
             * 数据链路层会给发送数据添加包头:
             * 0-5字节:接受者的mac地址
             * 6-11字节: 发送者mac地址
             * 12-13字节:数据包发送类型,0x0806表示ARP包,0x0800表示ip包,
             */

            EthernetPacket ether=new EthernetPacket();
            ether.frametype = EthernetPacket.ETHERTYPE_ARP;
            ether.src_mac= this.device.mac_address;
            ether.dst_mac= dstMacAddress;
            packet.datalink = ether;

            sender.sendPacket(packet);
        }

     private void testARPProtocol() {
         ARPProtocolLayer arpLayer = new ARPProtocolLayer();
         this.registerPacketReceiver(arpLayer);

         byte[] ip;
        try {
            ip = InetAddress.getByName("192.168.2.1").getAddress();
            arpLayer.getMacByIP(ip, this);
        } catch (UnknownHostException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

     }

    @Override
    public void receiveMacAddress(byte[] ip, byte[] mac) {
        System.out.println("receive arp reply msg with sender ip: ");
        for (byte b: ip) {
            System.out.print(Integer.toUnsignedString(b & 0xff) + ".");
        }
        System.out.println("with sender mac :");
        for (byte b : mac)
            System.out.print(Integer.toHexString(b&0xff) + ":");

    }
}

首先它改成单子模式,一次只生成一个实力。它通过jpcap获得网卡对象,然后得到本机mac地址和ip地址,同时导出一个接口叫sendData,该接口从上层接收要发送的数据,然后封装一个数据链路层包头后,调用网卡将数据发送出去。它继承PacketProvider类,后者是一个观察者模式的实现,所有想获得网络数据包的对象都通过PacketProvider注册,一旦网卡收到数据后,PacketProvider就会把数据包推送给所有观察者,后者实现如下:

package utils;

public interface IPacketProvider {
    public void registerPacketReceiver(jpcap.PacketReceiver receiver);
}

package utils;

import java.util.ArrayList;

import jpcap.PacketReceiver;
import jpcap.packet.Packet;

public class PacketProvider implements IPacketProvider{
    private ArrayList<PacketReceiver> receiverList = new ArrayList<PacketReceiver>();

    @Override
    public void registerPacketReceiver(PacketReceiver receiver) {
        if (this.receiverList.contains(receiver) != true) {
            this.receiverList.add(receiver);
        }
    }

    @SuppressWarnings("unused")
    protected void pushPacketToReceivers(Packet packet) {
        for (int i = 0; i < this.receiverList.size(); i++) {
            PacketReceiver receiver = (PacketReceiver) this.receiverList.get(i);
            receiver.receivePacket(packet);
        }
    }

}

接着我们实现ARP协议层。我们在实现ARP协议时,除了按规定填表和读表外,我们还需要做的工作是提供缓存机制。由于发送数据包再等待回应是一种非常耗时的工作,因此完成后要把结果缓存起来,下次需要时不用再进行耗时的数据收发工作,因此我们在实现时会准备一个映射表,将ip和mac地址缓存起来,当查找指定ip设备的mac地址时,现在表中查找,如果找不到在进行数据包的发送接收,相关的代码实现如下:

package ARPProtocolLayer;

import java.net.Inet4Address;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;

import datalinklayer.DataLinkLayer;
import jpcap.PacketReceiver;
import jpcap.packet.ARPPacket;
import jpcap.packet.EthernetPacket;

import jpcap.packet.Packet;
import utils.IMacReceiver;

public class ARPProtocolLayer implements PacketReceiver {
    /*
     */
    private HashMap<byte[], byte[]> ipToMacTable = new HashMap<byte[], byte[]>();
    private HashMap<Integer, ArrayList<IMacReceiver>> ipToMacReceiverTable = new   HashMap<Integer, ArrayList<IMacReceiver>>();
    /*
     * 数据包含数据链路层包头:dest_mac(6byte) + source_mac(6byte) + frame_type(2byte)
     * 因此读取ARP数据时需要跳过开头14字节
     */
    private static int ARP_OPCODE_START = 20;
    private static int ARP_SENDER_MAC_START = 22;
    private static int ARP_SENDER_IP_START = 28;
    private static int ARP_TARGET_IP_START = 38;


    @Override
    public void receivePacket(Packet packet) {
        if (packet == null) {
            return;
        }

        //确保收到数据包是arp类型
        EthernetPacket etherHeader = (EthernetPacket)packet.datalink;
        /*
         * 数据链路层在发送数据包时会添加一个802.3的以太网包头,格式如下
         * 0-7字节:[0-6]Preamble , [7]start fo frame delimiter
         * 8-22字节: [8-13] destination mac, [14-19]: source mac 
         * 20-21字节: type
         * type == 0x0806表示数据包是arp包, 0x0800表示IP包,0x8035是RARP包
         */
        if (etherHeader.frametype != EthernetPacket.ETHERTYPE_ARP) {
            return;
        }
        byte[] header = packet.header;
         analyzeARPMessage(header);
    }

    private  boolean analyzeARPMessage(byte[] data) {
        /*
         * 解析获得的APR消息包,从中获得各项信息,此处默认返回的mac地址长度都是6
         */
        //先读取2,3字节,获取消息操作码,确定它是ARP回复信息
        byte[] opcode = new byte[2];
        System.arraycopy(data, ARP_OPCODE_START, opcode, 0, 2);
        //转换为小端字节序
        short op = ByteBuffer.wrap(opcode).getShort();
        if (op != ARPPacket.ARP_REPLY) {
            return false;
        }

        //获取接受者ip,确定该数据包是回复给我们的
        byte[] ip = DataLinkLayer.getInstance().deviceIPAddress();
        for (int i = 0; i < 4; i++) {
            if (ip[i] != data[ARP_TARGET_IP_START + i]) {
                return false;
            }
        }

        //获取发送者IP
        byte[] senderIP = new byte[4];
        System.arraycopy(data, ARP_SENDER_IP_START, senderIP, 0, 4);
        //获取发送者mac地址
        byte[] senderMac = new byte[6];
        System.arraycopy(data, ARP_SENDER_MAC_START, senderMac, 0, 6);
        //更新arp缓存表
        ipToMacTable.put(senderIP, senderMac);


        //通知接收者mac地址
        int ipToInteger = ByteBuffer.wrap(senderIP).getInt();
        ArrayList<IMacReceiver> receiverList = ipToMacReceiverTable.get(ipToInteger);
        if (receiverList != null) {
            for (IMacReceiver receiver : receiverList) {
                receiver.receiveMacAddress(senderIP, senderMac);
            }
        }
         return true;
    }


    public void  getMacByIP(byte[] ip, IMacReceiver receiver) {
        if (receiver == null) {
            return;
        }
        //查看给的ip的mac是否已经缓存
        int ipToInt = ByteBuffer.wrap(ip).getInt();
        if (ipToMacTable.get(ipToInt) != null) {
            receiver.receiveMacAddress(ip, ipToMacTable.get(ipToInt));
        }

        if (ipToMacReceiverTable.get(ipToInt) == null) {
            ipToMacReceiverTable.put(ipToInt, new ArrayList<IMacReceiver>());
            //发送ARP请求包
            sendARPRequestMsg(ip);
        }
        ArrayList<IMacReceiver> receiverList = ipToMacReceiverTable.get(ipToInt);
        if (receiverList.contains(receiver) != true) {
            receiverList.add(receiver);
        }

        return;
    }

    private void sendARPRequestMsg(byte[] ip) {
        if (ip == null) {
            return;
        }

        DataLinkLayer dataLinkInstance = DataLinkLayer.getInstance();
        byte[] broadcast=new byte[]{(byte)255,(byte)255,(byte)255,(byte)255,(byte)255,(byte)255};
        int pointer = 0;
        byte[] data = new byte[28];
        data[pointer] = 0;
        pointer++;
        data[pointer] = 1;
        pointer++;
        //注意将字节序转换为大端
        ByteBuffer buffer = ByteBuffer.allocate(2);
        buffer.order(ByteOrder.BIG_ENDIAN);
        buffer.putShort(ARPPacket.PROTOTYPE_IP);
        for (int i = 0; i < buffer.array().length; i++) {
            data[pointer] = buffer.array()[i];
            pointer++;
        }

        data[pointer] = 6;
        pointer++;
        data[pointer] = 4;
        pointer++;
        //注意将字节序转换为大端
        buffer = ByteBuffer.allocate(2);
        buffer.order(ByteOrder.BIG_ENDIAN);
        buffer.putShort(ARPPacket.ARP_REQUEST);
        for (int i = 0; i < buffer.array().length; i++) {
            data[pointer] = buffer.array()[i];
            pointer++;
        }

        byte[] macAddress = dataLinkInstance.deviceMacAddress();
        for (int i = 0; i < macAddress.length; i++) {
            data[pointer] = macAddress[i];
            pointer++;
        }

        byte[] srcip = dataLinkInstance.deviceIPAddress();
        for (int i = 0; i < srcip.length; i++) {
            data[pointer] = srcip[i];
            pointer++;
        }
        for (int i = 0; i < broadcast.length; i++) {
            data[pointer] = broadcast[i];
            pointer++;
        }
        for (int i = 0; i < ip.length; i++) {
            data[pointer] = ip[i];
            pointer++;
        }

        dataLinkInstance.sendData(data, broadcast, EthernetPacket.ETHERTYPE_ARP);
    }
}

getMacByIP是它提供给上层协议的接口,当上层协议需要获得指定ip设备的mac地址时就调用该接口,它先从缓存表中看指定ip对应的mac地址是否存在,如果不存在就调用sendARPRequestMsg发送ARP请求包。

sendARPRequestMsg的实现其实就是按照我们前面描述的填表规则进行填表。值得注意的是,我们把接收者的mac地址设置成[0xff, 0xff, 0xff, 0xff, 0xff, 0xff],这是一个广播硬件地址,于是所有设备都可以读取这个消息,如果接收设备的IP与数据包里对应的target ip相同,那么它就应该构造同一个表,把自己的硬件地址存储在表中,返回给消息的发起者。

ARPProtocolLayer继承了PacketReceiver接口,这意味着它希望链路层收到数据包后,把数据包推送给它。如果接收者收到我们发出的ARP请求包后,构造一个回复消息发送到我们网卡上,链路层就会调用ARPProtocolLayer的PacketReceiver接口来解读数据包。数据就存储在packet.head里面,我们调用analyzeARPMessage接口来读取返回的ARP包。

在解析数据包时,我们注意packet.head对应的内容包含着链路层包头,也就是前面讲到的14字节,因此我们要读取相应的字节时,在计算偏移时要跳过开始14字节,在代码里定义ARP_OPCODE_START这些常量时,注释中提到这一点。在接收到数据包时,它先从链路层包头确定该包是ARP包,然后再调用analyzeARPMessage解析包的内容。在后者的实现中,我们先取出opcode两字节,看看它是否是2,也就是ARP回应包,如果是那么再从target protocoal address对应4字节里读取数据包接收者的ip地址,如果该地址与我们的地址相同,那就能确定数据包是发给我们的,然后我们从sender hardware address中获得发送者的mac地址。

ARPProtocolLayer要求所有通过它获取mac地址的对象都必须实现IMacReceiver接口,有可能很多个上层协议对象都需要获得同一个ip对应设备的mac地址,它会把这些对象存储在一个队里中,一旦给定ip设备返回包含它mac地址的ARP消息后,ARPProtocolLayer从消息中解读出mac地址,它就会把该地址推送给所有需要的接收者,IMacReceiver的定义如下:

package utils;

public interface IMacReceiver {
    public void  receiveMacAddress(byte[] ip, byte[] mac);
}

在我们的代码中,DataLinkLayer就继承了这个接口,它在初始化ARPProtocolLayer时把自己进行了注册,病调用getMacByIP去获取对应设备的mac地址,代码如下:

private void testARPProtocol() {
         ARPProtocolLayer arpLayer = new ARPProtocolLayer();
         this.registerPacketReceiver(arpLayer);

         byte[] ip;
        try {
            ip = InetAddress.getByName("192.168.2.1").getAddress();
            arpLayer.getMacByIP(ip, this);
        } catch (UnknownHostException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

     }

    @Override
    public void receiveMacAddress(byte[] ip, byte[] mac) {
        System.out.println("receive arp reply msg with sender ip: ");
        for (byte b: ip) {
            System.out.print(Integer.toUnsignedString(b & 0xff) + ".");
        }
        System.out.println("with sender mac :");
        for (byte b : mac)
            System.out.print(Integer.toHexString(b&0xff) + ":");

    }

代码中192.168.2.1对应我家路由器ip,一旦DataLinkLayer接收到路由器回复的ARP数据包,从中解读出mac地址后,就调用上面的receiveMacAddress,把mac地址推送过来。

上面代码运行后,情况如下,我们用wireshark抓到了代码发送的数据包和接收到路由器返回的ARP包:

第一行时我们代码发出的数据包,第二行是路由器返回的数据包,我们点开第一行得到数据包内容如下:

其中sender mac address是我机器的mac地址,sender ip address是我机器的ip,opcode值是1表示它是一个arp请求包,我们点开第二行,起内容如下:

其中sender ip address正是路由器的ip,sender mac address 是路由器的mac地址,我们程序接收到这个数据包,并进行解读后得到结果如下:

原文发布于微信公众号 - Coding迪斯尼(gh_c9f933e7765d)

原文发表时间:2018-12-14

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏数值分析与有限元编程

Fortran知识 | 输出结果出现NaN

一旦输出结果出现NaN,编译器不会给出任何错误提示,这个时候该如何调试程序呢? ? 点击菜单栏的调试,最后一个为XXXX属性,打开对话框,左侧展开 Fortra...

3747
来自专栏大内老A

WCF技术剖析之三十三:你是否了解WCF事务框架体系内部的工作机制?[上篇]

WCF事务编程主要涉及到这么三个方面:通过服务(操作)契约确定TransactionFlow的策略;通过事务绑定实现事务流转;通过服务操作行为控制事务的自动登记...

2108
来自专栏coder修行路

Go 源码学习之--net/http

其实自己不是很会看源码,但是学习优秀的源码是提升自己代码能力的一种方式,也可以对自己以后写代码有一个很好的影响,所以决定在之后的时间内,要有一个很好的习惯,阅读...

7345
来自专栏向治洪

备忘录模式

概念 备忘录模式:又叫做快照模式,属于行为模式的一种,指在不破坏封装性的前提下,获取到一个对象的内部状态,并在对象之外记录或保存这个状态。在有需要的时候可将该对...

2018
来自专栏移动开发的那些事儿

BlockCanary源码解析

如上代码中的loop()方法是Looper中的,我们的目的是监测主线程的卡顿问题,因为UI更新界面都是在主线程中进行的,所以在主线程中做耗时操作可能会造成界面卡...

1422
来自专栏张善友的专栏

Contact Manager Web API 示例[4] 异常处理(Exception Handling)

联系人管理器web API是一个Asp.net web api示例程序,演示了通过ASP.NET Web API 公开联系信息,并允许您添加和删除联系人,示例地...

1957
来自专栏林冠宏的技术文章

XGoServer 一个基础性、模块完整且安全可靠的服务端框架

作者:林冠宏 / 指尖下的幽灵 掘金:https://juejin.im/user/587f0dfe128fe100570ce2d8 博客:htt...

39218
来自专栏恰童鞋骚年

自己动手写工具:百度图片批量下载器

开篇:在某些场景下,我们想要对百度图片搜出来的东东进行保存,但是一个一个得下载保存不仅耗时而且费劲,有木有一种方法能够简化我们的工作量呢,让我们在离线模式下也能...

4101
来自专栏james大数据架构

MVC中局部视图的使用

加载部分视图 $("#result").load("/home/message",function(){ //加载完之后隐藏进度条 });  public Ac...

2037
来自专栏Linux驱动

第1阶段——uboot分析之启动函数bootm命令 (9)

本节主要学习: 详细分析UBOOT中"bootcmd=nand read.jffs2 0x30007FC0 kernel;bootm 0x30007FC0...

1865

扫码关注云+社区

领取腾讯云代金券