1. 程式人生 > >從0到1用java再造tcpip協議棧:使用jpacap模擬資料鏈路層

從0到1用java再造tcpip協議棧:使用jpacap模擬資料鏈路層

我們上一節成功使用jpcap獲得了網絡卡硬體,我們要重新構造tcp/ip協議棧,那麼就需要做兩部分工作。一部分由上層協議完成,他們的工作是將要傳送的資料進行封裝,主要是在資料包上新增包頭資料結構,包頭裡有很多控制位元組,用於不同節點間進行資料傳送時對傳送過程的控制和調整,瞭解,掌握,實現每層資料協議的包頭結構以及資料控制流程是我們系列課程的重點和難點。

有了上層資料封裝後,剩下的就需要下層硬體將資料準確的傳送到指定目的地。負責將資料包轉換為電訊號傳輸到另一端的,就是資料鏈路層。我們無需瞭解它的實現原理,只要把它作為一個黑盒子,當上層資料經過各層協議封裝好後,傳入這個黑盒子,然後確保它能將資訊正確的傳送出去即可,本節我們看看這個黑盒子如何使用。

螢幕快照 2018-11-27 下午5.38.41.png

我們本節要模擬實現的就是上圖所表示的network interface。上一節我們使用jpcap列舉了機器當前具備的網絡卡,其中有很多是虛擬網絡卡,也就是它們不具備資料的接受和傳送功能,因此我們要從中找到可以使用的真正硬體網絡卡,辨別網絡卡是否可用的一個標準是,看他是否具備ipv4的地址格式,下面程式碼就用於從jpcap列舉的所有網絡卡中獲取硬體網絡卡:

NetworkInterface[] devices = JpcapCaptor.getDeviceList();
		NetworkInterface device = null;
		
		System.out.println("there are " + devices.length +  " devices");

        for (int i = 0; i < devices.length; i++) {
        	boolean findDevice = false;
        	
            for (NetworkInterfaceAddress addr  : devices[i].addresses) {
                //網絡卡網址符合ipv4規範才是可用網絡卡
                if (!(addr.address instanceof Inet4Address)) {
                	continue;
                }
                
               findDevice = true;
               break;
            }
            
            if (findDevice) {
            	device = devices[i];
            	break;
            }
            
        }

上面程式碼通過jpcap遍歷當前所有網絡卡,然後看哪個網絡卡的ip地址符合ipv4格式,符合的就是可以用於傳送和接收資料的硬體網絡卡。接下來我們看看如何從網絡卡上擷取到來的資料包。

要通過jpcap從網絡卡獲取資料,首先需要繼承一個介面叫PacketReceiver,然後實現receivePacket介面。我們在工程下新建一個檔案叫DataLinkLayer.java,其實現內容如下:

import jpcap.packet.DatalinkPacket;
import jpcap.packet.EthernetPacket;
import jpcap.packet.ICMPPacket;
import jpcap.packet.IPPacket;
import jpcap.packet.Packet;
import jpcap.packet.TCPPacket;
import jpcap.packet.UDPPacket;

public class DataLinkLayer implements jpcap.PacketReceiver {
    String protocoll[] = {"HOPOPT", "ICMP", "IGMP", "GGP", "IPV4", "ST", "TCP", "CBT", "EGP", "IGP", "BBN", "NV2", "PUP", "ARGUS", "EMCON", "XNET", "CHAOS", "UDP", "mux"};
    
	@Override
	public void receivePacket(Packet packet) {
	   
        boolean show_tcp = false, show_icmp = true, show_udp = false;
        
	    IPPacket tpt=(IPPacket)packet;
	    if (packet != null) {
            int ppp=tpt.protocol;
            String proto=protocoll[ppp];
          
    	    if (proto.equals(("TCP")) && show_tcp) {
    	           System.out.println("\nthis is TCP packet");
    	           TCPPacket tp = (TCPPacket) packet;
    	           System.out.println("this is destination port of tcp :" + tp.dst_port);
    	           if (tp.ack) {
    	                 System.out.println("\n" + "this is an acknowledgement");
    	           } else {
    	                 System.out.println("this is not an acknowledgment packet");
    	           }

    	           if (tp.rst) {
    	                 System.out.println("reset connection ");
    	           }
    	           System.out.println(" \n protocol version is :" + tp.version);
    	           System.out.println("\n this is destination ip " + tp.dst_ip);
    	           System.out.println("this is source ip"+tp.src_ip);
    	           if(tp.fin){
    	                 System.out.println("sender does not have more data to transfer");
    	           }
    	           if(tp.syn){
    	                 System.out.println("\n request for connection");
    	           }

    	     }else if(proto.equals("ICMP") && show_icmp){
    	           ICMPPacket ipc=(ICMPPacket)packet;
    	                
    	           System.out.println("\nThis ICMP Packet");
    	           System.out.println(" \n this is alive time :"+ipc.alive_time);
    	           System.out.println("\n number of advertised address :"+(int)ipc.addr_num);
    	           System.out.println("mtu of the packet is :"+(int)ipc.mtu);
    	           System.out.println("subnet mask :"+ipc.subnetmask);
    	           System.out.println("\n source ip :"+ipc.src_ip);
    	           System.out.println("\n destination ip:"+ipc.dst_ip);
    	           System.out.println("\n check sum :"+ipc.checksum);
    	           System.out.println("\n icmp type :"+ipc.type);
    	           System.out.println("");
    	     }
    	     else if(proto.equals("UDP")  && show_udp){
    	          UDPPacket pac=(UDPPacket)packet;
    	          System.out.println("this is udp packet \n");
    	          System.out.println("this is source port :"+pac.src_port);
    	          System.out.println("this is destination port :"+pac.dst_port);

    	     }
	    }
	    else{
	        System.out.println("dft bi is not set. packet will  be fragmented \n");
	    }
	}
}

在上面程式碼中,當監聽的網絡卡有資料包抵達時,jpcap會呼叫上面類所實現的receviePacket介面,將接收到的資料包傳入。在程式碼中我們注意監控三種網路資料包,他們分別是tcp, icmp, 和udp,我們用三個布林變數來控制是否列印相應包的資訊,上面程式碼實現中,我們只打印icmp協議資料包。

icmp協議其實就是我們常用的ping命令,用來看看網路通不通。上面程式碼完成後,我們需要開啟要監控的網絡卡物件,構建上面物件一個例項後,將它傳入jpcap框架,以便介面被回撥,然後獲得資料包,回到專案的主入口,新增如下程式碼:

public class ProtocolEntry   {
	public static void main(String[] args) throws IOException {
        ....
            System.out.println("open divice: " + device.name);
        
        JpcapCaptor jpcap = JpcapCaptor.openDevice(device, 2000, true, 20);

       jpcap.loopPacket(-1, (jpcap.PacketReceiver) new DataLinkLayer());
	}
}

我們前面的程式碼已經找到可以收發資料包的網絡卡物件了,此時我們通過openDevice呼叫獲得網絡卡硬體的使用權,然後構造DataLinkLayer例項,傳入到loopPacket呼叫裡,-1表示持續不停的監聽對應網絡卡上的資料包,於是程式進入一個死迴圈,一旦網絡卡有資料包抵達時,DataLinkLayer例項的receivePacket函式就會被呼叫,同時資料包物件會被傳入。我們先執行起程式碼,得到如下情況:

螢幕快照 2018-12-04 下午5.04.18.png

由於我們此時健康網絡卡上的ping資料,由於當前沒有ping資料包出現在網絡卡上,所以我們的程式進入等待狀況,此時開啟控制檯,執行一個ping命令,如下:

螢幕快照 2018-12-04 下午5.06.09.png

這是我們在看java程式控制臺就會發現ping包的相關資料被打印出來:

螢幕快照 2018-12-04 下午5.07.18.png

後面我們將會使用DataLinkLayer作為資料鏈路層實現資料包的傳送和接收。當它接收到資料包後,會把它提交給我們自己實現的相關協議,在協議裡,我們自己安裝協議封包的流程解包,並根據協議棧把處理的資料包一層層往上傳。同理我們自己實現的協議在把資料進行封包後,也會一層層往下傳,最後傳到現在實現的DataLinkLayer層,讓它把資料發生出去,下一節我們將實現ARP協議層,到時候可以看到我們是如何實現資料封包及發生的