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

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

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

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

屏幕快照 2018-12-13 下午4.36.23.png

这张表上头的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包经过链路层封装后发送时格式如下:

屏幕快照 2018-12-13 下午4.55.26.png

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

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包:

屏幕快照 2018-12-13 下午5.43.59.png

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

屏幕快照 2018-12-13 下午5.45.22.png

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

屏幕快照 2018-12-13 下午5.47.12.png

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

屏幕快照 2018-12-13 下午5.50.33.png

更详细的讲解和代码调试演示过程,请点击链接

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:
这里写图片描述

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值