# TFTP Server 总结

# 前言

其实我菜的要死,然后在各位大佬的帮助下勉强完成了任务,然后过来水一篇博客,算是记录一下,也可以看作是蹭流量哈哈哈哈,可能主要讲一讲配置的问题。

# 前置环境

也就是两个软件,Tftpd64 和 clumsy 以及在 Windows 环境下的编译问题。

# Tftpd64

Tftpd64 的功能是当做 TFTP 通信中的客户端,各个参数的含义如下

Host 127.0.0.1 即为本机地址
Port 69 一般是69,如果端口冲突也可以选择其他的,需要与服务器的 port 一致
Local File 选择的是想要从客户端传送到服务器的文件的地址,从服务器传过来的文件也会放在同一目录下。
Remote File 则是想要请求的文件的名称
Block Size 512 与实验要求的一致
Get 就是从服务器索要文件
Put 就是将文件传送给服务器

# clumsy

clumsy 则是模拟传输过程的错误的软件。

值得的一提的是,在调试代码的时候一直开着这个软件的丢包,导致我开着 VPN 却用不了 GPT,甚至使用 Baidu/Google 都困难。用网易云听歌也是有电流声,一开始还怀疑是耳机的问题哈哈哈。

各个参数在文档中已经声明了

延迟(Lag),把数据包缓存一段时间后再发出,这样能够模拟网络延迟的状况。
掉包(Drop),随机丢弃一些数据。
节流(Throttle),把一小段时间内的数据拦截下来后再在之后的同一时间一同发出去。
重发(Duplicate),随机复制一些数据并与其本身一同发送。
乱序(Out of order),打乱数据包发送的顺序。
篡改(Tamper),随机修改小部分的包裹内容。

运行起来大概是下图这个样子,测试的时候用哪个就把哪个勾上然后 start 就可以了

# 编译问题

老师应该是下发一个 client 的 .c 文件的,但是大家会发现无法运行,因为老师下发的软件是需要在 linux 环境下运行。如果想要在 Win11 环境下运行需要更改一些头文件,然后代码里有些东西需要修改,这些内容不太重要,不如直接在下文讨论在写 Server 文件时该注意什么。

# 代码书写

首先需要声明我的代码的文件是写到了 t.cpp 当中,编译的话使用的命令是 g++ t.cpp -o tftp_server -lws2_32 ,会生成一个 tftp_server.exe 文件,直接运行就可以得到客户端,很遗憾本人菜菜没有做可视化界面 /kel。

代码的书写参照了 灰灰的博客,并且对某些部分做了自己的一些修改。

# 一些定义以及调用

一些用到的库,还有一些常量的声明,格外需要注意 #pragma comment(lib, "ws2_32.lib") 命令,这个命令在 Windows 中是必须的,其中 IP 写成自己主机的 IP。

#include <WinSock2.h>
#include <cstdio>
#include <psdk_inc/_ip_types.h>
#include <psdk_inc/_socket_types.h>
#include <stdlib.h>
#include <wingdi.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <bits/stdc++.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 516
#define DATA_SIZE 512
#define OPCODE_RRQ 1
#define OPCODE_WRQ 2
#define OPCODE_DATA 3
#define OPCODE_ACK 4
#define OPCODE_ERROR 5
#define MAX_RETRIES 10
#define TIMEOUT 5
static const char *tftp_error_msg[] = {
	"Undefined error",
	"File not found",
	"Access violation",
	"Disk full or allocation error",
	"Illegal TFTP operation",
	"Unknown transfer ID",
	"File already exists",
	"No such user"
};
const char IP[30]="";
const int PORT=6699;

# TFTP 的初始化

下面这段代码也是 Windows 环境下不同于 Linux 的部分。

int TFTP_init()
{
    WSADATA wsaData;
    int result = WSAStartup(MAKEWORD(2, 2), &wsaData); // 初始化 Winsock
    if (result != 0) {
        cerr<<"WSAStartup failed: "<<result<<endl;
        return 1;
    }
    printf("Winsock initialized.\n");
    return 0;
}

然后就是根据服务器的地址和接口绑定套接字(这段内容在代码的 Server 类的 init 部分),然后就是不断循环接收来自客户端的请求(Server 类的 work 部分)。

TFTP_Solve 类则是对于客户端的每个请求进行分类处理,而 ERRORthings 则是对错误信息进行处理后发送给客户端。接下来重点介绍 RRQ 和 WRQ 类。

由于实验有个要求是需要记录传输数据的速度,为此设计了一个 gettimeofday 函数用来记录每次传输的起始和截止时间用于计算速度。

# RRQ

首先需要知道的是,我们将一个文件分成了若干块,每块的大小是 512bytes ,不断循环读取文件直到读入的字符数小于 512 。

先将操作码和块的编号写到首部

memset(data_pac, 0, BUF_SIZE);
*(unsigned short*)data_pac = htons(OPCODE_DATA);  // 操作码:数据包
*(unsigned short*)(data_pac + 2) = htons(block);

然后开始读入文件中的信息,并且处理错误信息,接下来就是传输,只要没有收到确认包或者说重传的次数小于我们要求的最大重传次数时则重传。 while(!ack&&tot_retry<MAX_RETRIES)

使用 sendto 函数发送当前块到客户端

sendto(socketfd, data_pac, bytes_read + 4, 0, (sockaddr*)&client_addr, client_addr_len);

然后使用 select 函数检测

int select_result = select(socketfd + 1, &read_fds, NULL, NULL, &tv);

检查是否传输的数据有差错,并对错误类型进行分类处理决定是否重传和返回怎样的错误信息。如果 select 函数没有发现错误则使用 recvfrom 函数接收来自客户端的 ACK 包并进行检查判断。

recv_len=recvfrom(socketfd,buf,BUF_SIZE,0,(sockaddr*)&client_addr,&client_addr_len);

最后计算传输速率即可。

# WRQ

虽然是师从灰灰,但是也对一部分进行了修改和优化,也就是 WRQ 这一部分。灰灰的代码在这一部分,面对丢包和篡改的情况往往会发生服务器不断读取来自客户端的同一个 block 的数据,陷入死循环,一直将客户端某个 block 的数据写入目的文件。在调试后发现问题出现在 Server 在把选择新的块的时候并没有考虑客户端是否收到了 ACK 并且向下一个 block 移动,而是仅仅将 block++,对相关部分我们需要做出修改。

依旧是在确定文件的指针无误后进入循环,一块块地接收数据,先将上一个包的 ACK 发送给客户端,然后接受数据包。

在此处我们设置两个变量 received_blockexpected_block ,分别表示我们接受到的 block 编号和我们期望收到的 block 编号。如果两者一致的话则表明我们的数据传输没有任何问题,直接写入文件,并且期望下一个 block 。

if (received_block == expected_block) {
    printf("Received block NO.%hu\n", received_block);
    tot_recv += recv_len - 4;
    fwrite(buf + 4, 1, recv_len - 4, file);  // 写入文件
    expected_block++;  // 仅在收到正确的数据包时递增
}

否则的话输出错误提示信息并且尝试再次发送 ACK 包以及接受数据,直到收到的 block 编号和期望的一致。

最后就是统计传输速率以及发送最后一个块的 ACK 给客户端。

# code

本 blog 只讲述了一种最简单的实现方法(而且还没验收),需要代码详细解释的请移步灰灰的博客请教师祖大人捏。可视化的俺也没做,俺是菜狗。

//g++ t.cpp -o tftp_server -lws2_32
#include <WinSock2.h>
#include <cstdio>
#include <psdk_inc/_ip_types.h>
#include <psdk_inc/_socket_types.h>
#include <stdlib.h>
#include <wingdi.h>
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <bits/stdc++.h>
#pragma comment(lib, "ws2_32.lib")
#define BUF_SIZE 516
#define DATA_SIZE 512
#define OPCODE_RRQ 1
#define OPCODE_WRQ 2
#define OPCODE_DATA 3
#define OPCODE_ACK 4
#define OPCODE_ERROR 5
#define MAX_RETRIES 10
#define TIMEOUT 5
using namespace std;
static const char *tftp_error_msg[] = {
	"Undefined error",
	"File not found",
	"Access violation",
	"Disk full or allocation error",
	"Illegal TFTP operation",
	"Unknown transfer ID",
	"File already exists",
	"No such user"
};
const char IP[30]="0.0.0.0";
const int PORT=6699;
int client_addr_len;
void record(char *ch)
{
    FILE *file;
    file=fopen("record_log","w");
    fwrite(ch,1,strlen(ch),file);
}
int gettimeofday(struct timeval* tp, struct timezone* tzp)
{
    if (tp == nullptr) return -1;
    struct _timeb timebuffer;
    _ftime(&timebuffer);
    tp->tv_sec = static_cast<long>(timebuffer.time);      // 秒部分
    tp->tv_usec = timebuffer.millitm * 1000;              // 将毫秒转换为微秒
    return 0;
}
struct ERRORthings
{
    short opcode;
    short error_code;
    char message[BUF_SIZE];
    void init(short _error_code,const char* _message)
    {
        opcode = 5; 
        error_code = _error_code;
        strcpy(message+4, _message); 
    }
    void work(int socketfd, sockaddr_in &client_addr)
    {
        memset(message, 0, BUF_SIZE);
        *(unsigned short*)message = htons(opcode);
        *(unsigned short*)(message + 2) = htons(error_code);
        int packet_length = 4 + strlen(message) + 1;
        int send_len = sendto(socketfd, message, packet_length, 0, (sockaddr *)&client_addr, client_addr_len);
    }
};
class RRQ
{
    public:
    void work(const char *filename, const char *mode, int socketfd, struct sockaddr_in &client_addr)
    {
        FILE *file;
        if(strcmp(mode,"netascii")==0)
            file=fopen(filename,"r");
        else file=fopen(filename,"rb");
        if(!file)
        {
            cerr<<"ERROR: Cannot open file for reading: "<<filename<<endl;
            ERRORthings packet;
            packet.init(1,tftp_error_msg[1]);
            packet.work(socketfd, client_addr);
            return ;
        }
        unsigned short block=1;
        int bytes_read=0,tot_send=0;;
        char buf[BUF_SIZE];
        char data_pac[BUF_SIZE];
        timeval start,end;
        gettimeofday(&start, NULL);
        do
        {
            memset(data_pac, 0, BUF_SIZE);
            *(unsigned short*)data_pac = htons(OPCODE_DATA);  // 操作码:数据包
            *(unsigned short*)(data_pac + 2) = htons(block);
            bytes_read=fread(data_pac+4,1,DATA_SIZE,file);
            int tot_retry=0;
            bool ack=false;
            if(bytes_read<0)
            {
                cerr<<"ERROR: Reading file"<<endl;
                ERRORthings packet;
                packet.init(0,"Read error");
                packet.work(socketfd,client_addr);
                break;
            }
            int recv_len;
            while(!ack&&tot_retry<MAX_RETRIES)
            {
                sendto(socketfd, data_pac, bytes_read + 4, 0, (sockaddr*)&client_addr, client_addr_len);
                printf("sending NO.%hu packet", block);
                // 超时重传
                struct timeval tv;
                tv.tv_sec=TIMEOUT;
                tv.tv_usec=0;
                fd_set read_fds;
                FD_ZERO(&read_fds);
                FD_SET(socketfd, &read_fds);
                // 传入 ACK 或超时
                int select_result = select(socketfd + 1, &read_fds, NULL, NULL, &tv);
                if(select_result>0)
                {
                    recv_len=recvfrom(socketfd,buf,BUF_SIZE,0,(sockaddr*)&client_addr,&client_addr_len);
                    if(recv_len>=4&&ntohs(*(unsigned short*)buf)==OPCODE_ACK&&ntohs(*(unsigned short*)(buf + 2))==block)
                    {
                        printf(",recived NO.%hupacket\n", block);
                        ack=true;
                    }
                    else
                    {
                        printf(",ACK invalid,send NO.%hupacket again\n", block);
                        tot_retry++;
                    }
                }
                else if(select_result==0)
                {
                    printf("Timeout,retry sending NO.%hu packet\n",block);
                    tot_retry++;
                }
                else
                {
                    perror("select() error");
                    fclose(file);
                    return ;
                }
            }
            tot_send+=bytes_read;
            block++;
        }while(bytes_read==DATA_SIZE);
        fclose(file);
        gettimeofday(&end, NULL);
        double time_taken=(end.tv_sec-start.tv_sec)+(end.tv_usec-start.tv_usec)/1000000.0;
        double throughput=tot_send/time_taken;
        cerr<<"Download throughput: "<<throughput/1024<<" KB/s"<<endl;
    }
};
class WRQ
{
    public:
    void work(const char *filename, const char *mode, int socketfd, struct sockaddr_in &client_addr)
    {
        FILE *file;
        if(strcmp(mode,"netascii")==0)
            file=fopen(filename,"w");
        else file=fopen(filename,"wb");
        if(!file)
        {
            cerr<<"ERROR: Cannot open file for writing: "<<filename<<endl;
            ERRORthings error_packet;
            error_packet.init(2,tftp_error_msg[2]);
            error_packet.work(socketfd, client_addr);
            return ;
        }
        int tot_recv=0,recv_len;
        unsigned short block=0;
        struct timeval start,end;
        char buf[BUF_SIZE];
        gettimeofday(&start, NULL);
        unsigned short expected_block = 1;  // TFTP WRQ 开始时,第一个数据包的 block 是 1
        do {
            memset(buf, 0, BUF_SIZE);
            // 发送 ACK 包
            *(unsigned short *)buf = htons(OPCODE_ACK);
            *(unsigned short *)(buf + 2) = htons(expected_block - 1);  // ACK 上一块的 block
            if (sendto(socketfd, buf, 4, 0, (sockaddr *)&client_addr, client_addr_len) < 0) {
                perror("sendto failed");
                fclose(file);
                return;
            }
            // 接收数据包
            recv_len = recvfrom(socketfd, buf, BUF_SIZE, 0, (sockaddr *)&client_addr, &client_addr_len);
            if (recv_len < 4 || ntohs(*(unsigned short *)buf) != OPCODE_DATA) {
                printf("The packet received is not a valid data packet\n");
                ERRORthings error_packet;
                error_packet.init(4, tftp_error_msg[4]);
                error_packet.work(socketfd, client_addr);
                fclose(file);
                return;
            }
            // 获取接收数据包的 block 编号
            unsigned short received_block = ntohs(*(unsigned short *)(buf + 2));
            if (received_block == expected_block) {
                printf("Received block NO.%hu\n", received_block);
                tot_recv += recv_len - 4;
                fwrite(buf + 4, 1, recv_len - 4, file);  // 写入文件
                expected_block++;  // 仅在收到正确的数据包时递增
            } else {
                printf("Duplicate or out-of-order packet, expected block: %hu, received block: %hu\n", expected_block, received_block);
            }
        } while (recv_len == DATA_SIZE + 4);
         memset(buf,0,BUF_SIZE);
        *(unsigned short*)buf = htons(OPCODE_ACK);
        *(unsigned short*)(buf + 2) = htons(expected_block-1);
        sendto(socketfd,buf,4,0,(sockaddr*)&client_addr,client_addr_len);
        fclose(file);
        gettimeofday(&end, NULL);
        double time_taken=(end.tv_sec-start.tv_sec)+(end.tv_usec-start.tv_usec)/1000000.0;
        double throughput=tot_recv/time_taken;
        cerr<<"Download throughput: "<<throughput/1024<<" KB/s"<<endl;
        fclose(file);
    }
};
class TFTP_Solve
{
    private:
    int socketfd;
    sockaddr_in client_addr;
    char buf[BUF_SIZE];
    public:
    TFTP_Solve(int _socketfd,sockaddr_in &_client_addr): socketfd(_socketfd),client_addr(_client_addr){}
    void Work_for_RRQ(const char *filname,const char *mode)
    {
        RRQ RRQproject;
        RRQproject.work(filname,mode,socketfd,client_addr);
    }
    void Work_for_WRQ(const char *filname,const char *mode)
    {
        WRQ WRQproject;
        WRQproject.work(filname,mode,socketfd,client_addr);
    }
    void work()
    {
        int rev_len=recvfrom(socketfd,buf,BUF_SIZE,0,(sockaddr *)&client_addr,&client_addr_len);
        if(rev_len<4)
        {
            cerr<<"ERROR: Can't receive request"<<endl;
            ERRORthings packet;
            packet.init(4,tftp_error_msg[4]);
            packet.work(socketfd, client_addr);
            return ;
        }
        short opcode=ntohs(*(unsigned short *)buf);
        char *filename=buf+2;
        char *mode=filename+strlen(filename)+1;
        if (strcmp(mode, "netascii") != 0 && strcmp(mode, "octet") != 0)
        {
            ERRORthings packet;
            packet.init(4,"Unsupported transfer mode.");
            packet.work(socketfd, client_addr);
            return ;
        }
        if(opcode==OPCODE_RRQ) Work_for_RRQ(filename,mode);
        else if(opcode==OPCODE_WRQ) Work_for_WRQ(filename,mode);
        else if(opcode==OPCODE_ERROR);
        else
        {
            cerr<<"ERROR: Unknown request"<<endl;
            ERRORthings packet;
            packet.init(4,tftp_error_msg[4]);
            packet.work(socketfd,client_addr);
        }
    }
};
class Server
{
    private:
    int socketfd;
    
    public:
    Server(): socketfd(-1){}
    sockaddr_in getAddr(const char *ip,int port)
    {
        sockaddr_in addr;
        addr.sin_family = AF_INET;
        addr.sin_port = htons(port);
        addr.sin_addr.S_un.S_addr = INADDR_ANY;
        return addr;
    }
    void init()
    {
        socketfd = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
        if(socketfd<0)
        {
            cerr<<"Error creating socket..."<<endl;
            exit(1);
        }
        sockaddr_in server_addr = getAddr(IP, PORT);
        if (bind(socketfd, (struct sockaddr *) &server_addr, sizeof(server_addr)) < 0) {
            cerr<<"Bind local port failed!"<<endl;
            exit(1);
        }
        cerr<<"TFTP is successfully started on port "<<PORT<<" at IP address "<<IP<<endl;
    }
    void work()
    {
        while(true)
        {
            sockaddr_in client_addr; client_addr_len=sizeof(client_addr);
            TFTP_Solve project(socketfd,client_addr);
            project.work();
        }
    }
    ~Server(){if(socketfd!=-1) closesocket(socketfd);}
};
int TFTP_init()
{
    WSADATA wsaData;
    int result = WSAStartup(MAKEWORD(2, 2), &wsaData); // 初始化 Winsock
    if (result != 0) {
        cerr<<"WSAStartup failed: "<<result<<endl;
        return 1;
    }
    printf("Winsock initialized.\n");
    return 0;
}
int main()
{
    TFTP_init();
    Server S; S.init(); S.work();
    return 0;
}