本節我們看看TFTP資料包的組裝方式,為我們代碼實作該協定奠定基礎。TFTP協定總共有5中不同資料包,分别對應讀請求,寫請求,資料塊,接收回應(ACK),以及錯誤。前兩種資料包格式一樣,隻不過某些值域設定有差别,剩下的三種資料包格式各不相同。但無論哪一種資料包,他們都包含一個值域叫操作碼,用來定義該資料包屬于那種類型。
我們先看讀請求和寫請求資料包的格式,首先是2位元組表示操作碼,它用來表示目前資料包的類型,取值1表示該資料包是個讀請求,2表示該資料包是;接下來是可變長字段,它用來表示要讀取或上傳的檔案名,它使用ASCII碼并以0表示結尾;第三個字段叫Mode,也是可變長字段,用來表示傳輸檔案的資料類型,如果傳輸的是字元串檔案,那麼它填寫字元串"netascii",如果傳輸的是二進制檔案,那麼它填寫字元串"octet",這些字元串都以0結尾,其結構用下圖表示:
我們看看對應的wireshak抓包:
接着我們看看傳輸資料塊的資料包,它頭2位元組也是操作碼,取值3用于表示資料包用于資料塊傳輸,接下來是2位元組,用于表示資料塊編号,最後是可變長字段Data,用于裝載資料塊,該資料包的格式如下:
我們看看對應的wireshark抓包:
然後是應答資料包,它開始2位元組也是操作碼,取值4,接下來2自己擁有表示接收到的資料塊編号,相應結構如下圖:
最後一個是錯誤資料報,它首2位元組表示操作碼,取值5;接下來2位元組表示錯誤碼,0表示未知錯誤,1表示檔案不存在,2表示權限不足,3表示磁盤已滿,具體的錯誤碼我們在實踐時再具體分析;接下來是可變長字段,它用字元串的形式描述具體錯誤,該資料包的結構如下圖:
它對應的wireshark抓包如下:
接下來我們看看如何代碼實作TFTP協定:
public class TFTPClient extends Application{
private byte[] sever_ip = null;
private static short OPTION_CODE_READ = 1; //讀請求操作碼
private static short OPTION_CODE_WRITE = 2; //寫請求操作碼
private static short OPTION_CODE_ACK = 4; //應答
private static final short OPTION_CODE_DATA = 3; //資料塊
private static final short OPTION_CODE_ERR = 5; //錯誤消息
private static short TFTP_ERROR_FILE_NOT_FOUND = 1;
private static short OPTION_CODE_LENGTH = 2; //操作碼字段占據2位元組
private short data_block = 1;
private static char TFTP_SERVER_PORT = 69;
private char server_port = 0;
private File download_file;
private String file_name;
FileOutputStream file_stream;
public TFTPClient(byte[] server_ip) {
this.sever_ip = server_ip;
//指定一個固定端口
this.port = (short)56276;
server_port = TFTP_SERVER_PORT;
}
public void getFile(String file_name) {
download_file = new File(file_name);
this.file_name = file_name;
try {
file_stream = new FileOutputStream(download_file);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
sendReadPacket();
}
....
}
首先我們定義了協定所需要的各項特定數值,建立的TFTPClient類将承當用戶端的角色,它将連接配接TFTP伺服器,從對方那裡下載下傳或上傳檔案。getFile是負責下載下傳檔案的接口,它擷取要下載下傳的檔案名,現在本地建立一個空檔案,然後向伺服器請求下載下傳對應檔案的資料塊,拿到資料塊後再寫入空檔案。
在getFile函數中,它調用了sendReadPacket函數,該函數的作用是構造一個讀請求資料包發送給伺服器:
private void sendReadPacket() {
//向伺服器發送讀請求包
String mode = "netascii";
//+1表示要用0表示結尾
byte[] read_request = new byte[OPTION_CODE_LENGTH + this.file_name.length() + 1 + mode.length() + 1];
ByteBuffer buffer = ByteBuffer.wrap(read_request);
buffer.putShort(OPTION_CODE_READ);
buffer.put(this.file_name.getBytes());
buffer.put((byte)0);
buffer.put(mode.getBytes());
buffer.put((byte)0);
byte[] udpHeader = createUDPHeader(read_request);
byte[] ipHeader = createIP4Header(udpHeader.length);
byte[] readRequestPacket = new byte[udpHeader.length + ipHeader.length];
buffer = ByteBuffer.wrap(readRequestPacket);
buffer.put(ipHeader);
buffer.put(udpHeader);
//将消息發送給路由器
try {
ProtocolManager.getInstance().sendData(readRequestPacket, sever_ip);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
該函數根據前面描述的資料包結構填寫相應字段,同時将要下載下傳的檔案名放在資料包内發送給伺服器,如果檔案存在,伺服器就會将資料塊發送回來。一旦伺服器傳回資料後,我們就得分析傳回的資料包,根據傳回内容采取行動:
public void handleData(HashMap<String, Object> headerInfo) {
byte[] data = (byte[])headerInfo.get("data");
if (data == null) {
System.out.println("empty data");
return;
}
short port = (short)headerInfo.get("src_port");
server_port = (char)port;
ByteBuffer buff = ByteBuffer.wrap(data);
short opCode = buff.getShort();
switch (opCode) {
case OPTION_CODE_ERR:
//處理錯誤資料包
handleErrorPacket(buff);
break;
case OPTION_CODE_DATA:
handleDataPacket(buff);
break;
}
}
一旦有資料從伺服器傳回後,上面函數就會被調用。它首先分析傳回的是資料塊還是錯誤資訊,如果是錯誤資訊就會調用handleErrorPacket函數進行處理,如果是資料塊,它會調用handleDataPacket進行處理。這裡需要注意的一點是,我們從伺服器傳回的資料包中重新擷取伺服器端口。前面我們說過TFTP伺服器使用兩個端口,一個固定端口69用來等待連接配接,然後啟動另外端口進行資料收發,是以我們與伺服器完成連接配接之後,就必須通過伺服器傳回的資料包獲得它用于資料交換的端口,我們先看看錯誤的處理流程:
private void handleErrorPacket(ByteBuffer buff) {
//擷取具體錯誤碼
short err_info = buff.getShort();
if (err_info == TFTP_ERROR_FILE_NOT_FOUND) {
System.out.println("TFTP server return file not found packet");
}
byte[] data = buff.array();
int pos = buff.position();
int left_len = data.length - pos;
byte[] err_msg = new byte[left_len];
buff.get(err_msg);
String err_str = new String(err_msg);
System.out.println("error message from server : " + err_str);
}
在上面函數中,我們根據前面講解的錯誤資料報解讀錯誤資料。錯誤資料報隻包含兩個字段,第一個操作碼含有值5,用于表示資料包包含錯誤資料;第二個字段包含一個字元串,用于描述錯誤的内容。接下來我們看看如何處理資料塊:
private void handleDataPacket(ByteBuffer buff) {
//擷取資料塊編号
data_block = buff.getShort();
System.out.println("receive data block " + data_block);
byte[] data = buff.array();
int content_len = data.length - buff.position();
//将資料塊寫入檔案
byte[] file_content = new byte[content_len];
buff.get(file_content);
try {
file_stream.write(file_content);
System.out.println("write data block " + data_block + " to file");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if (content_len == 512) {
sendACKPacket();
data_block++;
}
if (content_len < 512) {
sendACKPacket();
try {
file_stream.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
data_block = 1;
}
}
handleDataPacket負責接收資料塊并寫入檔案。同時要記錄下目前資料塊編号,并調用sendACKPacket向伺服器傳回确認資料包。如果目前資料塊有512位元組長,那麼資料塊編号就要增加已對應後面的資料塊,如果資料塊不到512位元組長度,那麼意味着最後一個資料塊到達,寫入後檔案就下載下傳完畢。
在這裡還有一個技術難點要解決,那就是我的TFTP伺服器運作在虛拟機裡,而我的用戶端程式運作在本地系統MacOs,也就是說伺服器所在的硬體與我的用戶端程式鎖運作的硬體相同。這造成一個問題是,伺服器發出來的資料包并沒有傳遞到實體網卡上,而是通過程序通訊的方式之間傳遞給Mac,由于我開發的用戶端無論是接收還是發生資料包都必須通過實體網卡,虛拟機發出的資料包不經過實體網卡而是直接交給Mac系統意味着我用戶端收不到伺服器資料包,是以我要做一些小手段促使伺服器将資料包發送到實體網卡上,相關代碼如下:
protected byte[] createIP4Header(int dataLength) {
IProtocol ip4Proto = ProtocolManager.getInstance().getProtocol("ip");
if (ip4Proto == null || dataLength <= 0) {
return null;
}
//建立IP標頭預設情況下隻需要發送資料長度,下層協定号,接收方ip位址
HashMap<String, Object> headerInfo = new HashMap<String, Object>();
headerInfo.put("data_length", dataLength);
ByteBuffer destIP = ByteBuffer.wrap(sever_ip);
headerInfo.put("destination_ip", destIP.getInt());
//假裝資料包是192.168.2.128發送的,目前主機ip是192.168.2.243,如果不僞造ip,虛拟機發出的資料包就不會走網卡于是我們就抓不到資料包
try {
InetAddress fake_ip = InetAddress.getByName("192.168.2.127");
ByteBuffer buf = ByteBuffer.wrap(fake_ip.getAddress());
headerInfo.put("source_ip", buf.getInt());
} catch (UnknownHostException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
byte protocol = UDPProtocolLayer.PROTOCOL_UDP;
headerInfo.put("protocol", protocol);
byte[] ipHeader = ip4Proto.createHeader(headerInfo);
return ipHeader;
}
在構造IP標頭時,我使用了一個虛假的IP位址,我本機位址時2.243,伺服器運作的虛拟機位址時2.140,由于虛拟機與我的用戶端程式同在一個機器裡,是以虛拟機像IP:2.243發送的資料包都不走實體網卡,而是通過程序通訊的方式直接發給系統,這樣我們原來設計的架構就不能處理,是以在給伺服器發包時,我使用另外一個IP:2.127,它是我手機IP,這樣就能讓伺服器以為資料包發送給其他裝置,是以資料包就會發給實體網卡,于是我們的程式架構就可以截獲資料包,這樣才能讓通訊正常進行,有關資料包欺騙的更多内容請參考視訊。
更詳細的講解和代碼調試示範過程,請點選連結
更多技術資訊,包括作業系統,編譯器,面試算法,機器學習,人工智能,請關照我的公衆号:
新書上架,請諸位朋友多多支援: