TFTP:简单文本传输协议的数据包格式解析以及文件读取代码实现

本节我们看看TFTP数据包的组装方式,为我们代码实现该协议奠定基础。TFTP协议总共有5中不同数据包,分别对应读请求,写请求,数据块,接收回应(ACK),以及错误。前两种数据包格式一样,只不过某些值域设置有差别,剩下的三种数据包格式各不相同。但无论哪一种数据包,他们都包含一个值域叫操作码,用来定义该数据包属于那种类型。

我们先看读请求和写请求数据包的格式,首先是2字节表示操作码,它用来表示当前数据包的类型,取值1表示该数据包是个读请求,2表示该数据包是;接下来是可变长字段,它用来表示要读取或上传的文件名,它使用ASCII码并以0表示结尾;第三个字段叫Mode,也是可变长字段,用来表示传输文件的数据类型,如果传输的是字符串文件,那么它填写字符串"netascii",如果传输的是二进制文件,那么它填写字符串"octet",这些字符串都以0结尾,其结构用下图表示:

屏幕快照 2019-05-28 下午5.50.03.png

我们看看对应的wireshak抓包:


屏幕快照 2019-05-28 下午5.57.58.png

接着我们看看传输数据块的数据包,它头2字节也是操作码,取值3用于表示数据包用于数据块传输,接下来是2字节,用于表示数据块编号,最后是可变长字段Data,用于装载数据块,该数据包的格式如下:

屏幕快照 2019-05-28 下午5.59.37.png

我们看看对应的wireshark抓包:

屏幕快照 2019-05-28 下午6.01.49.png

然后是应答数据包,它开始2字节也是操作码,取值4,接下来2自己拥有表示接收到的数据块编号,相应结构如下图:

屏幕快照 2019-05-28 下午6.03.18.png

最后一个是错误数据报,它首2字节表示操作码,取值5;接下来2字节表示错误码,0表示未知错误,1表示文件不存在,2表示权限不足,3表示磁盘已满,具体的错误码我们在实践时再具体分析;接下来是可变长字段,它用字符串的形式描述具体错误,该数据包的结构如下图:

屏幕快照 2019-05-28 下午6.07.06.png

它对应的wireshark抓包如下:

屏幕快照 2019-05-28 下午6.08.28.png

接下来我们看看如何代码实现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,这样就能让服务器以为数据包发送给其他设备,因此数据包就会发给物理网卡,于是我们的程序框架就可以截获数据包,这样才能让通讯正常进行,有关数据包欺骗的更多内容请参考视频。

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

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


这里写图片描述

新书上架,请诸位朋友多多支持:
WechatIMG1.jpeg
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,607评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,047评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,496评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,405评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,400评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,479评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,883评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,535评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,743评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,544评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,612评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,309评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,881评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,891评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,136评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,783评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,316评论 2 342

推荐阅读更多精彩内容