转自https://blog.csdn.net/flysnow010/article/details/136934950
1 TFTP协议
- TFTP协议是基于UDP的简单文件传输协议,协议双方为Client和Server.Client和Server之间通过5种消息来传输文件,消息前两个字节Code是消息类型,消息内容随消息类型不同而不同。传输模式有三种:octet,netascii和mail,octet为二进制模式,netascii为文本模式,mail为文本模式,不过收到的文本不是保存到文件,而是打印出来,现在已不常用。DATA消息种数据长度是512字节,最后一个数据包可能会小于512。
-
消息类型如下:
Code Type Desc
0x00 0x01 RRQ Read message
0x00 0x02 WRQ Write message
0x00 0x03 DATA Data message
0x00 0x04 ACK Ack message
0x00 0x05 ERROR Error message
- 消息格式
- 3.1 RRQ消息
Code FileName NULL MODE NULL
0x00 0x01 filename.bin 0x00 octet 0x00
0x00 0x01 filename.txt 0x00 netascii 0x00
0x00 0x01 filename.txt 0x00 mail 0x00
- 3.2 WRQ消息
Code FileName NULL MODE NULL
0x00 0x02 filename.bin 0x00 octet 0x00
0x00 0x02 filename.txt 0x00 netascii 0x00
0x00 0x02 filename.txt 0x00 mail 0x00
- 3.3 DATA消息
Code BlockNumber Data(128/512bytes)
0x00 0x03 0x00 0x01 data
- 3.4 ACK消息
Code BlockNumber
0x00 0x04 0x00 0x01
- 3.5 ERROR消息
Code ErrorCode ErrorMsg NULL
0x00 0x05 0x00 0x01 File Not Found 0x00
- 错误类型
ErrorCode ErrorMsg
0x00 0x01 File Not Found
0x00 0x02 Access Violation
0x00 0x03 Disk Full
0x00 0x04 Illegal Operation
0x00 0x05 Unknown TransferID
0x00 0x06 File Exists
0x00 0x07 No Such User
————————————————————————————————————————–
1. TFTP 扩展选项(Option Extensions,RFC 2347)
TFTP 原始协议没有提供选项协商机制,而是直接通过 RRQ(读取请求)或 WRQ(写入请求)开始文件传输。
RFC 2347 引入了一种新的选项协商机制,允许客户端和服务器在传输文件前协商以下关键参数:
1.1 扩展选项的协商过程
扩展选项通过在 RRQ 或 WRQ 数据包的末尾附加选项(Option)字段实现。格式如下:
<opcode=1|2> <filename> 0 <mode> 0 <opt1> 0 <val1> 0 <opt2> 0 <val2> 0
例如:
RRQ "example.txt" "octet" "blksize" "1024" "tsize" "0"
服务器响应可能会接受这些选项,返回一个 OACK
(Option Acknowledgement)包,或拒绝直接进入原始的 TFTP 模式。
2. 块大小选项(Blocksize Option,RFC 2348)
问题
原始 TFTP 块大小固定为 512 字节,这在大文件传输时会导致更多的网络往返(ACK 包频繁)、增加传输时间。
解决方案
通过 blksize
选项,客户端和服务器可以协商更大的块大小,范围为 8 到 65464 字节。
使用更大的块大小可以减少 ACK 次数,从而提高传输效率,但需要注意避免超过网络 MTU(一般为 1500 字节),否则可能导致数据包碎片化或丢包。
实现示例
客户端请求:
RRQ "example.txt" "octet" "blksize" "1468"
服务器响应:
OACK "blksize" "1468"
此后,数据包的块大小将为 1468 字节,而不是默认的 512 字节。
3. 文件大小选项(Transfer Size Option,RFC 2349)
问题
原始 TFTP 协议无法提前得知传输文件的总大小,这在传输大文件或评估传输时间时非常不便。
解决方案
通过 tsize
选项,客户端可以在请求时让服务器返回文件的总大小(以字节为单位)。
- 在 RRQ(读取请求)中,客户端发送
tsize=0
表示询问文件大小。 - 在 WRQ(写入请求)中,客户端可以提供文件大小,服务器用于验证或日志记录。
实现示例
读取请求:
客户端请求:
RRQ "example.txt" "octet" "tsize" "0"
服务器响应:
OACK "tsize" "1024000" # 文件大小为 1024000 字节
写入请求:
客户端请求:
WRQ "upload.txt" "octet" "tsize" "500000"
服务器响应:
OACK "tsize" "500000"
优势
- 客户端可以根据文件大小选择是否继续传输。
- 便于 UI 显示进度条。
4. 超时选项(Timeout Option,RFC 2349)
问题
原始 TFTP 的超时值是固定的(默认 5 秒),在高延迟或不稳定的网络中可能导致误判超时并中断传输。
解决方案
通过 timeout
选项,客户端和服务器可以协商更合适的超时值(以秒为单位,范围 1 到 255 秒)。
实现示例
客户端请求:
RRQ "example.txt" "octet" "timeout" "10"
服务器响应:
OACK "timeout" "10"
此后,服务器和客户端都将使用 10 秒作为超时值。
注意
如果双方未协商成功,则默认使用原始的 5 秒超时。
5. TFTP 多文件传输支持
TFTPD32 支持在同一连接中处理多个文件传输请求,而原始 TFTP 每次只能传输一个文件。
实现方式并未标准化,但通过并行会话或优化线程池可以高效支持多个客户端。
6. PXE 引导支持的改进(续)
在 PXE 引导场景中,TFTPD32 针对以下场景进行了优化:
- 支持短超时值: 在 PXE 环境中,TFTP 的超时通常需要设置得非常短(1 秒或更少),以便快速处理引导程序的请求。TFTPD32 提供了调整超时值的功能,使其更加适应 PXE 的需求。
- 支持大文件传输: 某些 PXE 引导镜像文件可能较大(如超过 32 MB),TFTPD32 通过支持扩展选项(如
blksize
和tsize
)优化了这些场景的文件加载效率。
7. 日志和统计扩展(续)
这些日志和统计功能在调试、性能分析和审计中非常有用,特别是在企业网络中使用时:
- 实时会话监控: 在 TFTPD32 的界面上可以实时查看当前的传输会话,包括每个客户端的传输进度。
- 错误详细信息: 支持捕获和显示 TFTP 错误代码(如文件未找到、访问被拒绝等)的详细信息,以便快速诊断问题。
8. 高并发优化
TFTPD32 针对高并发环境进行了优化:
- 多线程模型: 支持同时为多个客户端处理文件传输请求,每个请求分配一个线程,从而避免阻塞问题。
- 内存缓存: 提供对常用文件的内存缓存支持,从而加快文件读取速度,减少磁盘 I/O。
9. 支持非标准端口
TFTPD32 支持在非标准端口上运行 TFTP 服务(默认端口为 69)。这在某些防火墙规则严格或需要运行多个 TFTP 实例的环境中非常有用。
10. 目录浏览与虚拟路径支持
TFTPD32 扩展了 TFTP 的文件访问机制:
- 目录浏览: 允许客户端请求服务器上的文件目录清单,从而支持动态的文件选择。
- 虚拟路径映射: 管理员可以配置虚拟路径,使客户端看到的路径与实际文件系统结构不同,从而提高安全性和灵活性。
11. 安全性扩展
为了提高 TFTP 的安全性,TFTPD32 增加了一些限制和增强:
- IP 地址过滤: 可以限制哪些客户端的请求被接受。
- 文件访问权限: 支持只读模式或限制特定文件的访问权限,避免客户端修改服务器文件。
- 日志审计功能: 对所有传输操作进行详细记录,便于管理员跟踪和分析潜在的安全问题。
总结
TFTPD32 的这些扩展显著增强了 TFTP 协议的性能、灵活性和安全性,特别是在需要高效文件传输和高并发支持的场景(如嵌入式设备引导、PXE 引导等)中,表现出极大的优势。这些扩展中,大多数基于官方的 RFC 标准,但某些功能(如目录浏览、虚拟路径映射)为特定实现的独有扩展,不属于标准协议的一部分。
跟TFTP相关的RFC有哪些?
与 TFTP(Trivial File Transfer Protocol)相关的 RFC 主要有以下几个:
-
RFC 1350 – Trivial File Transfer Protocol (TFTP)
- 这是 TFTP 协议的基础 RFC,定义了 TFTP 协议的规范。TFTP 是一种简单的文件传输协议,设计用于有限的设备(例如嵌入式设备和路由器)之间的文件传输。该协议基于 UDP(用户数据报协议)进行通信,适用于没有复杂功能需求的环境。
-
RFC 2347 – TFTP Option Extension
- 该 RFC 扩展了 TFTP 协议,定义了选项扩展,使 TFTP 支持文件传输时的可选参数,例如传输模式(如二进制模式和ASCII模式)和文件大小等。它允许客户端和服务器在文件传输过程中协商一些额外选项。
-
RFC 2348 – TFTP Blocksize Option
- 这个 RFC 定义了 TFTP 的一个扩展,即 Blocksize 选项。它允许客户端和服务器在传输过程中协商块大小,以提高传输效率。默认的块大小为 512 字节,但通过这个扩展,客户端和服务器可以选择不同的块大小。
-
RFC 2349 – TFTP Timeout and Transfer Size Options
- 这个 RFC 扩展了 TFTP 协议,定义了 Timeout(超时)和 Transfer Size(传输大小)选项。它们帮助调整传输超时参数以及定义传输的数据块最大尺寸,以适应不同的网络环境。
-
RFC 7440 – TFTP Option for MD5-based Integrity Checking
- 该 RFC 引入了一个选项,用于基于 MD5 哈希算法进行完整性检查。它使 TFTP 在传输文件时可以使用 MD5 校验和验证文件的完整性,防止传输过程中的数据损坏。
-
RFC 1782 – Trivial File Transfer Protocol (TFTP) Option for Local and Remote File Inclusion
- 该 RFC 引入了 TFTP 的扩展,定义了 本地和远程文件包含(Local and Remote File Inclusion)选项。通过这些选项,TFTP 协议能够更灵活地支持远程文件的引入,扩展了传统 TFTP 的功能。
-
RFC 1783 – Trivial File Transfer Protocol (TFTP) Option for Extended Block Size
- 该 RFC 提供了对 扩展块大小(Extended Block Size)的支持。它允许 TFTP 协议在传输文件时支持大于默认的 512 字节的块大小,从而提高文件传输的效率。该扩展对于大文件传输和高带宽网络非常有用。
-
RFC 1784 – Trivial File Transfer Protocol (TFTP) Option for Timestamps
- 该 RFC 引入了 时间戳(Timestamps)选项,用于在 TFTP 协议中传输文件时为每个文件块添加时间戳。这有助于文件传输的顺序控制和传输过程中的追踪。
-
RFC 1785 – Trivial File Transfer Protocol (TFTP) Option for File Authentication
- 该 RFC 提供了 TFTP 的 文件认证(File Authentication)选项,允许客户端和服务器在传输文件时通过认证机制确保文件的完整性和真实性。这个扩展增加了对文件认证的支持,增强了 TFTP 的安全性。
对于 TFTP 协议中扩展块大小的定义,通常最为广泛接受的标准是 RFC 2347 – TFTP Option Extension,它介绍了 TFTP 的 扩展选项,包括 扩展块大小(block size) 的定义。
RFC 1783(Trivial File Transfer Protocol (TFTP) Option for Extended Block Size) 也是对 RFC 2347 的扩展和补充,针对 TFTP 中块大小的扩展进行了更详细的说明。因此,如果你在处理扩展块大小时,可以参考 RFC 2347 来作为标准,它是 TFTP 协议扩展的基础和广泛实现。
总结:
- RFC 2347 定义了 TFTP 的扩展选项,包括 扩展块大小(默认 512 字节的块大小可以增大),它是基础标准。
- RFC 1783 进一步扩展了块大小选项,可以作为补充参考。
因此,你应该主要参考 RFC 2347 来处理扩展块大小,同时如果需要了解更多关于如何实现更大块的细节或兼容性,可以查看 RFC 1783。
————————————————————————————————————————-
2 概要设计
TFTP客户端通过GetFile来下载文件,通过PutFile上传文件。
2.1 序列图
GetFile序列图
PutFile序列图
2.2 类图
如图所示整个软件有8个类:
类型说明:
- TFTP类实现TFTP协议。
- TFtpServerFile类实现服务端文件收发。
- TFtpClientFile类实现客户端文件收发。
- BaseUdp类定义UDP写接口。
- ServerSocket类实现服务端UDP写接口。
- ClientSocket类实现客户端UDP写接口。
- TFtpServer类启动服务,等待客户端上传/下载文件。
- TFtpClient类连服务端来上传/下载文件。
2.3 模块划分
软件划分为如下模块:
- Core是一个静态库
- Server是一个EXE程序,调用Core库实现TFTP服务端功能。
- Client是一个EXE程序,调用Core库实现TFTP客户端功能。
2.3.1 Core
Core包括下面4个类:
- FTP
- BaseUdp
- TFtpClientFile
- TFtpServerFile
2.3.2 Client
Client包括下面3个类:
ClientSocke
TFtpClient
TFtpClientWidget
2.3.3 Server
Server包括下面3个类:
- ServerSocket
- TFtpServer
- TFtpServerWidget
3 实现
3.1 Core
Core模块包括下面4个类:
- TFTP
- BaseUdp
- TFtpClientFile
- TFtpServerFile
3.1.1 TFTP
TFTP类实现了TFTP协议。
3.1.1.1 TFTP定义
class TFtp
{
public:
TFtp();
enum Code {
RRQ = 0x0001,//Read request
WRQ = 0x0002,//Write request
DATA = 0x0003,//Data request
ACK = 0x0004,//Acknowledgement
ERROR = 0x0005 //Error
};
enum Mode { BINARY, ASCII, MAIL };
enum Error {
NotDefined = 0x0000,
FileNotFound = 0x0001,
AccessViolation = 0x0002,
DiskFull = 0x0003,
IllegalOperation = 0x0004,
UnknownTransferID = 0x0005,
FileExists = 0x0006,
NoSuchUser = 0x0007,
};
enum Size {
CODE_SIZE = 2,
HEADER_SIZE = 4,
BLOCK_SIZE = 512
};
enum Type { None, Read, Write };
bool process(uint8_t const *data, uint32_t size);
bool is_finished() const { return finished_; }
bool is_error() const { return !error_msg_.empty(); }
Error error() const { return error_; }
std::string error_msg() const { return error_msg_; }
protected:
virtual void on_read_req(std::string const& filename, Mode mode) {}
virtual void on_write_req(std::string const& filename, Mode mode) {}
virtual void on_data(uint16_t block_number, uint8_t const*data, uint32_t size) = 0;
virtual void on_ack(uint16_t block_number) = 0;
virtual void on_error(uint16_t error, std::string const& error_msg) = 0;
virtual uint32_t write(uint8_t const *data, size_t size) = 0;
void read_req(std::string const& filename, Mode mode);
void write_req(std::string const& filename, Mode mode);
void send(uint16_t block_number, size_t size);
void resend();
void ack(uint16_t block_number);
void error(Error error, std::string const& error_msg);
char *data() { return (char *)(data_ + HEADER_SIZE); }
void set_error(Error error, std::string const& error_msg)
{
error_ = error;
error_msg_ = error_msg;
}
void finished() { finished_ = true; }
size_t get_filesize(const char*filename);
private:
uint16_t op_code(uint8_t const *data) { return static_cast<uint16_t>((data[0] << 8) | data[1]); }
uint16_t block_num(uint8_t const *data) { return static_cast<uint16_t>((data[2] << 8) | data[3]); }
uint16_t error_code(uint8_t const *data) { return static_cast<uint16_t>((data[2] << 8) | data[3]); }
Mode getMode(std::string const& text);
std::string getText(Mode mode);
private:
uint8_t data_[HEADER_SIZE + BLOCK_SIZE];
bool finished_ = false;
TFtp::Error error_ = NotDefined;
std::string error_msg_;
size_t block_size_ = 0;
};
成员函数说明:
- process 解析数据包为TFtp::Code的请求包
- on_read_req 读请求虚函数,TFtpServerFile重载此函数
- on_write_req 写请求虚函数,TFtpServerFile重载此函数
- on_data 数据请求虚函数,TFtpServerFile/TFtpClientFile重载此函数
- on_ack 应答请求虚函数,TFtpServerFile/TFtpClientFile重载此函数
- on_error 出错处理虚函数,TFtpServerFile/TFtpClientFile重载此函数
- write 写数据虚函数,TFtpServerFile/TFtpClientFile重载此函数
- read_req 构造读请求数据包并发送
- write_req 构造写请求数据包并发送
- send 构造数据包请求并发送
- resend 重发数据包
- ack 构造应答请求数据包并发送
- error 构造出错数据包并发送
3.1.1.2 TFTP实现
- process/getMode
#define MIN_PACKET_LEN 4
bool TFtp::process(uint8_t const *data, uint32_t size)
{
if(size < MIN_PACKET_LEN)
return false;
uint16_t code = op_code(data);
if(code == RRQ || code == WRQ)
{
uint8_t const*d = data + sizeof(uint16_t);
uint8_t const*e = data + size;
uint8_t const*s = d;
while(s < e && *s)
s++;
std::string filename((char *)d, s - d);
s++;
d = s;
while(s < e && *s)
s++;
std::string mode_text((char *)d, s - d);
if(code == RRQ)
on_read_req(filename, getMode(mode_text));
else
on_write_req(filename, getMode(mode_text));
return true;
}
else if(code == DATA)
{
on_data(block_num(data), &data[HEADER_SIZE], size - HEADER_SIZE);
return true;
}
else if(code == ACK)
{
on_ack(block_num(data));
return true;
}
else if(code == ERROR)
{
uint8_t const* d = data + HEADER_SIZE;
uint8_t const *e = data + size;
uint8_t const *s = d;
while(s < e && *s)
s++;
on_error(error_code(data), std::string((char *)d, s - d));
return true;
}
return false;
}
TFtp::Mode TFtp::getMode(std::string const& text)
{
if(text == "octet")
return BINARY;
else if(text == "netascii")
return ASCII;
else
return MAIL;
}
函数流程:
- 判断数据包长度小于最小长度4,解析失败返回
- 获取数据包的code。
- 根据code类型解析数据调用对应的接口函数。
- read_req/write_req/send/resend/ack/error
#define WITE_CODE(data, code) \
data[0] = uint8_t(code >> 8); \
data[1] = uint8_t(code >> 0);
#define WITE_HEAD(data, code, value) \
data[0] = uint8_t(code >> 8); \
data[1] = uint8_t(code >> 0); \
data[2] = uint8_t(value >> 8);\
data[3] = uint8_t(value >> 0);
void TFtp::read_req(std::string const& filename, Mode mode)
{
std::string text = getText(mode);
std::vector<uint8_t> data(CODE_SIZE + filename.size() + text.size() + 2, 0);
WITE_CODE(data, RRQ)
memcpy(&data[CODE_SIZE], filename.c_str(), filename.size());
memcpy(&data[CODE_SIZE + filename.size() + 1], text.c_str(), text.size());
write(&data[0], data.size());
}
void TFtp::write_req(std::string const& filename, Mode mode)
{
std::string text = getText(mode);
std::vector<uint8_t> data(CODE_SIZE + filename.size() + text.size() + 2, 0);
WITE_CODE(data, WRQ)
memcpy(&data[CODE_SIZE], filename.c_str(), filename.size());
memcpy(&data[CODE_SIZE + filename.size() + 1], text.c_str(), text.size());
write(&data[0], data.size());
}
void TFtp::send(uint16_t block_number, size_t size)
{
WITE_HEAD(data_, DATA, block_number)
block_size_ = size;
write(data_, size + HEADER_SIZE);
}
void TFtp::resend()
{
write(data_, block_size_ + HEADER_SIZE);
}
void TFtp::ack(uint16_t block_number)
{
std::vector<uint8_t> data(HEADER_SIZE);
WITE_HEAD(data, ACK, block_number)
write(&data[0], data.size());
}
void TFtp::error(Error error, std::string const& error_msg)
{
std::vector<uint8_t> data(HEADER_SIZE + error_msg.size() + 1);
error_ = error;
error_msg_ = error_msg;
finished();
WITE_HEAD(data, ERROR, error)
memcpy(&data[HEADER_SIZE], error_msg.c_str(), error_msg.size());
data[data.size() - 1] = 0;
write(&data[0], data.size());
}
函数说明:根据请求类型构造对应的请求包并发送。
3.1.2 BaseUdp
class BaseUdp
{
public:
virtual ~BaseUdp(){}
virtual uint32_t write(const char* data, size_t size) = 0;
};
类型说名:
- 定义udp的写接口,该接口需要TFtpServer和TFtpClient去实现。
3.1.3 TFtpClientFile
TFtpClientFile类实现客户端文件收发
3.1.3.1 TFtpClientFile定义
class TFtpClientFile : public TFtp
{
public:
TFtpClientFile(BaseUdp *udp)
: udp_(udp)
, type_(None)
{}
~TFtpClientFile();
bool getFile(std::string const& local_filename,
std::string const& remote_filename, Mode mode);
bool putFile(std::string const& local_filename,
std::string const& remote_filename, Mode mode);
size_t filesize() const { return filesize_; }
size_t file_bytes() const { return file_bytes_; }
using Ptr = std::shared_ptr<TFtpClientFile>;
protected:
void on_data(uint16_t block_number, uint8_t const* data, uint32_t size) override;
void on_ack(uint16_t block_number) override;
void on_error(uint16_t error, std::string const& error_msg) override;
uint32_t write(uint8_t const *data, size_t size) override;
private:
void send_data(uint16_t block_number);
private:
BaseUdp* udp_;
Type type_;
std::ifstream read_file;
std::ofstream write_file;
uint16_t block_number_ = 0;
uint32_t block_size_ = 0;
size_t filesize_ = 0;
size_t file_bytes_ = 0;
};
成员函数说明:
- getFile 下载文件
- putFile 上传文件
- on_data 实现数据请求
- on_ack 实现应答请求
- on_error 实现出错处理
- write 实现写功能
- send_data 从文件读取数据包并发送。
3.1.3.2 TFtpClientFile实现
- getFile
bool TFtpClientFile::getFile(std::string const& local_filename,
std::string const& remote_filename,
Mode mode)
{
if(mode == TFtp::BINARY)
write_file.open(local_filename.c_str(),
std::ifstream::out | std::ifstream::binary);
else
write_file.open(local_filename.c_str());
if(!write_file.is_open())
return false;
read_req(remote_filename, mode);
type_ = Write;
return true;
}
函数流程:
-
以写方式打开本地文件
-
发送读文件请求
-
将类型设置为写
-
putFile
if(mode == TFtp::BINARY)
read_file.open(local_filename.c_str(),
std::ifstream::in | std::ifstream::binary);
else
read_file.open(local_filename.c_str());
if(!read_file.is_open())
return false;
filesize_ = get_filesize(local_filename.c_str());
write_req(remote_filename, mode);
type_ = Read;
return true;
函数流程:
-
以读方式打开本地文件
-
发送写文件请求
-
将类型设置为读
-
on_data
void TFtpClientFile::on_data(uint16_t block_number, uint8_t const* data, uint32_t size)
{
if(type_ != Write)
{
error(IllegalOperation, "Illegal TFTP Operation in Data");
return;
}
if(block_size_ == 0)
block_size_ = size;
write_file.write((char *)data, size);
file_bytes_ += size;
ack(block_number);
if(size < block_size_)
{
filesize_ = file_bytes_;
finished();
write_file.close();
}
}
函数流程:
-
保存数据包
-
发送应答
-
处理最后一个包
-
下载结束
-
on_ack
void TFtpClientFile::on_ack(uint16_t block_number)
{
if(type_ != Read)
{
error(IllegalOperation, "Illegal TFTP Operation in ACK");
return;
}
if(read_file.eof())
{
std::cout << "send data is finished" << std::endl;
finished();
return;
}
if(block_number_ != block_number)
resend();
else
{
block_number_++;
send_data(block_number_);
}
}
函数流程:
-
如果文件上传完毕,结束上传。
-
BlockNumber不同,则重传。
-
上传下一Block。
-
on_error
void TFtpClientFile::on_error(uint16_t error, std::string const& error_msg)
{
set_error((Error)error, error_msg + std::string(" come from remote"));
finished();
}
- send_data
void TFtpClientFile::send_data(uint16_t block_number)
{
char* d = data();
read_file.read(d, TFtp::BLOCK_SIZE);
file_bytes_ += read_file.gcount();
send(block_number, read_file.gcount());
}
- write
uint32_t TFtpClientFile::write(uint8_t const *data, size_t size)
{
return udp_->write((const char*)data, size);
}
3.1.4 TFtpServerFile
TFtpServerFile类实现服务端文件收发
class TFtpServerFile : public TFtp
{
public:
TFtpServerFile(BaseUdp *udp, std::string const& path, std::string const& id)
: udp_(udp)
, type_(None)
, file_path_(path)
, transfer_id_(id)
, block_number_(0)
{}
~TFtpServerFile();
using Ptr = std::shared_ptr<TFtpServerFile>;
std::string transfer_id() const { return transfer_id_; }
Type type() const { return type_; }
std::string filename() const { return filename_; }
uint16_t block_number() const { return block_number_; }
uint16_t block_numbers() const { return static_cast<uint16_t>((filesize_ + BLOCK_SIZE - 1) / BLOCK_SIZE); }
size_t filesize() const { return filesize_; }
size_t file_bytes() const { return file_bytes_; }
protected:
void on_read_req(std::string const& filename, Mode mode) override;
void on_write_req(std::string const& filename, Mode mode) override;
void on_data(uint16_t block_number, uint8_t const* data, uint32_t size) override;
void on_ack(uint16_t block_number) override;
void on_error(uint16_t error, std::string const& error_msg) override;
uint32_t write(uint8_t const *data, size_t size) override;
private:
void send_data(uint16_t block_number);
std::string full_fileaname(std::string const& filename) const {
return file_path_ + filename;
}
TFtpServerFile(TFtpServerFile const&);
TFtpServerFile(TFtpServerFile &&);
TFtpServerFile operator == (TFtpServerFile const&);
TFtpServerFile operator == (TFtpServerFile &&);
private:
BaseUdp* udp_;
Type type_;
std::string filename_;
std::string file_path_;
std::string transfer_id_;
std::ifstream read_file;
std::ofstream write_file;
uint16_t block_number_;
uint32_t block_size_ = 0;
size_t filesize_ = 0;
size_t file_bytes_ = 0;
};
3.1.4.1 TFtpServerFile定义
成员函数说明:
- transfer_id 返回唯一传输ID,用来管理多个TFtpServerFile实例。
- on_read_req 实现读请求。
- on_write_req 实现写请求。
- on_data 实现数据请求。
- on_ack 实现应答请求。
- on_error 实现出错处理。
- write 实现写功能。
- send_data 从文件读取数据包并发送。
3.1.4.2 TFtpServerFile实现
on_read_req
void TFtpServerFile::on_read_req(std::string const& filename, Mode mode)//read
{
if(type_ != None)
{
error(IllegalOperation, "Illegal TFTP Operation in RRQ");
return;
}
type_ = Read;
filename_ = full_fileaname(filename);
if(mode == TFtp::BINARY)
read_file.open(filename_.c_str(),
std::ifstream::in | std::ifstream::binary);
else
read_file.open(filename_.c_str());
if(!read_file.is_open())
error(FileNotFound, std::string("File(") + filename + std::string(") Not Found"));
else
{
block_number_ = 1;
filesize_ = get_filesize(filename_.c_str());
send_data(block_number_);
}
}
函数流程:
-
以读方式打开文件
-
设置类型为读
-
发送第一个Block数据
-
on_write_req
void TFtpServerFile::on_write_req(std::string const& filename, Mode mode)//write
{
if(type_ != None)
{
error(IllegalOperation, "Illegal TFTP Operation in WRQ");
return;
}
filename_ = full_fileaname(filename);
if(get_filesize(filename_.c_str()) > 0)
{
error(FileExists, "File Exists in WRQ");
return;
}
type_ = Write;
if(mode == TFtp::BINARY)
write_file.open(filename_.c_str(),
std::ifstream::out | std::ifstream::binary);
else
write_file.open(filename_.c_str());
if(!write_file.is_open())
error(AccessViolation, "Access Violation");
else
ack(block_number_);//ack(0)
}
函数流程:
-
以写方式打开文件。
-
设置类型为写
-
请求第一块数据
-
on_data
void TFtpServerFile::on_data(uint16_t block_number, uint8_t const* data, uint32_t size) //write
{
if(type_ != Write)
{
error(IllegalOperation, "Illegal TFTP Operation in Data");
return;
}
if(block_number != block_number_ + 1)
ack(block_number_);
else
{
if(block_size_ == 0)
block_size_ = size;
write_file.write((char *)data, size);
file_bytes_ += size;
ack(block_number);
block_number_ = block_number;
if(size < block_size_)
{
filesize_ = file_bytes_;
write_file.close();
finished();
}
}
}
函数流程:
-
Block号不一致,则请求前一个块号。
-
保存数据包
-
保存最后数据包,
-
上传结束
-
on_ack
void TFtpServerFile::on_ack(uint16_t block_number) // read
{
if(type_ != Read)
{
error(IllegalOperation, "Illegal TFTP Operation in ACK");
return;
}
if(read_file.eof())
{
finished();
return;
}
if(block_number != block_number_)
resend();
else
{
block_number_++;
send_data(block_number_);
}
}
函数流程:
-
如果文件发送完毕,则结束
-
BlockNumber不一致,则重传
-
增加BlockNumber,发送Block数据
-
on_error
void TFtpServerFile::on_error(uint16_t error, std::string const& error_msg) //read/write
{
set_error((Error)error, error_msg + std::string(" come from remote"));
finished();
}
- write
uint32_t TFtpServerFile::write(uint8_t const *data, size_t size)
{
return udp_->write((const char*)data, size);
}
- send_data
void TFtpServerFile::send_data(uint16_t block_number)
{
char* d = data();
read_file.read(d, TFtp::BLOCK_SIZE);
file_bytes_ += read_file.gcount();
send(block_number, read_file.gcount());
}
3.2 Client
Client包括下面3个类:
- ClientSocke
- TFtpClient
- TFtpClientWidget
3.2.1 ClientSocke
ClientSocke从BaseUdp派生实现write接口.
3.2.1.1 ClientSocke定义
#include "baseudp.h"
class QUdpSocket;
class ClientSocket : public BaseUdp
{
public:
ClientSocket(QUdpSocket* socket)
: socket_(socket)
{}
uint32_t write(const char* data, size_t size) override;
private:
QUdpSocket* socket_;
};
成员函数说明:
- write 重载函数,实现父类BaseUdp中定义的write接口。
3.2.1.2 ClientSocke实现
#include "clientsocket.h"
#include <QUdpSocket>
uint32_t ClientSocket::write(const char* data, size_t size)
{
return socket_->write(data, size);
}
函数实现说明:
- 直接调用QUdpSocket对象的write接口。
3.2.2 TFtpClient
TFtpClient类通过TFtpClientFile类实现一个TFTP客户端,实现上下载文件。
3.2.2.1 TFtpClient定义
class QUdpSocket;
class TFtpClient : public QObject
{
Q_OBJECT
public:
explicit TFtpClient(QObject *parent = nullptr);
void setHostPort(QString const& host, quint16 port);
void getFile(QString const& localFileName, QString const& remoteFileName);
void putFile(QString const& localFileName, QString const& remoteFileName);
bool isPut() const { return isPut_; }
public slots:
void stopFileTransfer();
signals:
void started();
void progress(quint64 bytes, quint64 total);
void error(QString const& text);
void finished();
private slots:
void connected();
void connectError(QAbstractSocket::SocketError error);
void readPendingDatagrams();
private:
QUdpSocket* socket;
QString host_;
quint16 port_;
QString localFileName_;
QString remoteFileName_;
TFtpClientFile::Ptr tftpFile_;
bool isPut_ = false;
};
成员函数说明:
- setHostPort 配置TFTP服务器的IP地址和端口.
- getFile 从TFTP服务器下载文件.
- putFile 向TFTP服务器上传文件.
- isPut 是否是上传
- stopFileTransfer 终止文件传输
信号说明:
- started 文件传输开始信号
- progress 传输进度信号
- error 错误信号
- finished 传输完成信号
槽函数说明:
- connected 与TFTP服用器连接成功后的处理函数
- connectError 与TFTP服用器连接失败后的处理函数
- readPendingDatagrams 从TFTP服用器读数处理函数
3.2.2.2 TFtpClient实现
构造函数
TFtpClient::TFtpClient(QObject *parent)
: QObject(parent)
, socket(new QUdpSocket(this))
{
connect(socket, &QUdpSocket::readyRead,
this, &TFtpClient::readPendingDatagrams);
connect(socket, &QUdpSocket::connected,
this, &TFtpClient::connected);
connect(socket, SIGNAL(error(QAbstractSocket::SocketError)),
this, SLOT(connectError(QAbstractSocket::SocketError)));
}
函数说明:
-
创建QUdpSocket对象socket
-
连接socket的信号和对应槽函数。
-
setHostPort/getFile/putFile
void TFtpClient::setHostPort(QString const& host, quint16 port)
{
host_ = host;
port_ = port;
}
void TFtpClient::getFile(QString const& localFileName, QString const& remoteFileName)
{
isPut_ = false;
localFileName_ = localFileName;
remoteFileName_ = remoteFileName;
socket->connectToHost(host_, port_);
}
void TFtpClient::putFile(QString const& localFileName, QString const& remoteFileName)
{
isPut_ = true;
localFileName_ = localFileName;
remoteFileName_ = remoteFileName;
socket->connectToHost(host_, port_);
}
函数说明:
- setHostPort 保存主机地址和端接口
- getFile 保存本地文件名和远程文件名,设置为下载,连接TFTP服务器
- putFile 保存本地文件名和远程文件名,设置为上传,连接TFTP服务器
- stopFileTransfer/connected/connectError/readPendingDatagrams
void TFtpClient::stopFileTransfer()
{
socket->disconnectFromHost();
}
void TFtpClient::connected()
{
ClientSocket* udp = new ClientSocket(socket);
tftpFile_ = TFtpClientFile::Ptr(new TFtpClientFile(udp));
bool isOK = true;
if(isPut_)
isOK = tftpFile_->putFile(localFileName_.toStdString(),
remoteFileName_.toStdString(), TFtp::BINARY);
else
isOK = tftpFile_->getFile(localFileName_.toStdString(),
remoteFileName_.toStdString(), TFtp::BINARY);
if(!isOK)
emit error("Local File not Found");
else
emit started();
}
void TFtpClient::connectError(QAbstractSocket::SocketError)
{
emit error("Connect host is failed");
}
void TFtpClient::readPendingDatagrams()
{
while (socket->hasPendingDatagrams()) {
QNetworkDatagram datagram = socket->receiveDatagram();
QByteArray d = datagram.data();
if(tftpFile_)
{
tftpFile_->process((uint8_t *)d.data(), d.size());
emit progress(tftpFile_->file_bytes(), tftpFile_->filesize());
if(tftpFile_->is_finished())
{
if(tftpFile_->is_error())
emit error(QString::fromStdString(tftpFile_->error_msg()));
else
emit finished();
}
}
}
}
函数说明:
- stopFileTransfer 与TFTP服务器断开连接,终止文件传输。
- connected 连接上TFTP服务器后,创建TFtpClientFile对象,开始上传/下载文件
- connectError 与TFTP服务器连接失败,发送连接失败信号量。
- readPendingDatagrams 从TFTP服务器读取数据给TFtpClientFile对象处理,如果传输结束,有错误发送error信号,没错误发送finished信号。
3.2.3 TFtpClientWidget
TFtpClientWidget从QWidget派生一个窗口类,负责发起上下载文件,并显示文件传输进度等界面操作。
3.2.3.1 TFtpClientWidget定义
class TFtpClient;
class TFtpClientWidget : public QWidget
{
Q_OBJECT
public:
TFtpClientWidget(QWidget *parent = nullptr);
~TFtpClientWidget();
private slots:
void onGetFile();
void onPutFile();
void onStarted();
void onProgress(quint64 bytes, quint64 total);
void onStop();
void onFinished();
void onError(QString const& error);
void onSelectLocalFile();
private:
void enableButtons(bool enable);
private:
Ui::TFtpClientWidget *ui;
TFtpClient* tftpClient;
};
3.2.3.2 TFtpClientWidget实现
TFtpClientWidget::TFtpClientWidget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::TFtpClientWidget)
, tftpClient(new TFtpClient(this))
{
ui->setupUi(this);
connect(ui->btnBrowse, SIGNAL(clicked()), this, SLOT(onSelectLocalFile()));
connect(ui->btnGet, SIGNAL(clicked()), this, SLOT(onGetFile()));
connect(ui->btnPut, SIGNAL(clicked()), this, SLOT(onPutFile()));
connect(ui->btnBreak, SIGNAL(clicked()), this, SLOT(onStop()));
connect(tftpClient, &TFtpClient::started, this, &TFtpClientWidget::onStarted);
connect(tftpClient, &TFtpClient::progress, this, &TFtpClientWidget::onProgress);
connect(tftpClient, &TFtpClient::finished, this, &TFtpClientWidget::onFinished);
connect(tftpClient, &TFtpClient::error, this, &TFtpClientWidget::onError);
}
TFtpClientWidget::~TFtpClientWidget()
{
delete ui;
}
void TFtpClientWidget::onGetFile()
{
tftpClient->stopFileTransfer();
tftpClient->setHostPort(ui->lineEditHost->text(), ui->spinBoxPort->value());
tftpClient->getFile(ui->lineEditLocalFile->text(), ui->lineEditRemoteFile->text());
ui->progressBar->setValue(0);
}
void TFtpClientWidget::onPutFile()
{
tftpClient->stopFileTransfer();
tftpClient->setHostPort(ui->lineEditHost->text(), ui->spinBoxPort->value());
tftpClient->putFile(ui->lineEditLocalFile->text(), ui->lineEditRemoteFile->text());
ui->progressBar->setValue(0);
}
void TFtpClientWidget::onStarted()
{
enableButtons(false);
}
void TFtpClientWidget::onProgress(quint64 bytes, quint64 total)
{
if(total > 0)
ui->progressBar->setValue(bytes * 100 / total);
else
{
int value = ui->progressBar->value();
ui->progressBar->setValue(QRandomGenerator(value).bounded(value, 99));
}
}
void TFtpClientWidget::onStop()
{
enableButtons(true);
tftpClient->stopFileTransfer();
}
void TFtpClientWidget::onFinished()
{
if(tftpClient->isPut())
QMessageBox::information(this, "TFtpClient", "Put is done!");
else
QMessageBox::information(this, "TFtpClient", "Get is done!");
enableButtons(true);
}
void TFtpClientWidget::onError(QString const& error)
{
QMessageBox::critical(this, "TFtpClient", error);
enableButtons(true);
}
void TFtpClientWidget::onSelectLocalFile()
{
static QString filePath;
QFileDialog dialog(this, tr("Select File"), filePath, tr("All files (*.*)"));
if(dialog.exec() == QDialog::Rejected)
return;
QStringList fileNames = dialog.selectedFiles();
if(fileNames.isEmpty())
return;
QString fileName = fileNames.first();
filePath = QFileInfo(fileName).filePath();
ui->lineEditLocalFile->setText(fileName);
}
void TFtpClientWidget::enableButtons(bool enable)
{
ui->btnGet->setEnabled(enable);
ui->btnPut->setEnabled(enable);
ui->btnBreak->setDisabled(enable);
}
3.2.4 PutFile序列图
3.2.4 GetFile序列图
3.3 Server
Server包括下面3个类:
- ServerSocket
- TFtpServer
- TFtpServerWidget
3.3.1 ServerSocket
ServerSocket从BaseUdp派生实现write接口.
3.3.1.1 ServerSocket定义
class QUdpSocket;
class ServerSocket : public BaseUdp
{
public:
ServerSocket(QUdpSocket* socket, QHostAddress const& host,
uint16_t port)
: socket_(socket)
, host_(host)
, port_(port)
{}
uint32_t write(const char* data, size_t size) override;
private:
QUdpSocket* socket_;
QHostAddress host_;
uint16_t port_;
};
成员函数说明:
- write 重载函数,实现父类BaseUdp中定义的write接口。
uint32_t ServerSocket::write(const char* data, size_t size)
{
return socket_->writeDatagram(data, size, host_, port_);
}
3.3.1.2 ServerSocket实现
uint32_t ServerSocket::write(const char* data, size_t size)
{
return socket_->writeDatagram(data, size, host_, port_);
}
函数实现说明:
- 直接调用QUdpSocket对象的writeDatagram接口。
3.3.2 TFtpServer
TFtpServer类通过TFtpServerFile类实现一个TFTP服务端,接受上下载文件请求。
3.3.2.1 TFtpServer定义
class QUdpSocket;
class TFtpServer: public QObject
{
Q_OBJECT
public:
TFtpServer(QObject *parent = nullptr);
void setFilePath(QString const& filePath);
void start();
void stop();
signals:
void bindError();
void startFile(QString const&transferId, QString const& fileName);
void progress(QString const&transferId, quint64 bytes, quint64 total);
void statusText(QString const& text);
void stopFile(QString const&transferId);
private slots:
void readPendingDatagrams();
private:
private:
QUdpSocket* socket;
QString filePath_;
TFtpFileManager::Ptr fileManager_;
const uint16_t TFTP_PORT = 69;
};
成员函数说明:
- setFilePath 配置TFTP服务器的下载文件路径.
- start 启动TFTP服务.
- stop 停止TFTP服务.
信号说明:
- bindError 绑定UDP端口错误信号
- startFile 文件开始传输信号
- progress 文件传输进度信号
- statusText 状态文本变化信号
- stopFile 停止文件传输信号
槽函数说明:
- readPendingDatagrams 从TFTP客户端读数据处理函数
3.3.2.1 TFtpServer实现
构造函数
TFtpServer::TFtpServer(QObject *parent)
: QObject(parent)
, socket(new QUdpSocket(this))
, fileManager_(new TFtpFileManager())
{
connect(socket, &QUdpSocket::readyRead,
this, &TFtpServer::readPendingDatagrams);
}
函数说明:
-
创建QUdpSocket对象socket
-
连接socket的信号和对应槽函数。
-
setFilePath/start/stop/readPendingDatagrams
void TFtpServer::setFilePath(QString const& filePath)
{
if(!filePath.endsWith("/"))
filePath_ = filePath + "/";
}
void TFtpServer::start()
{
if(!socket->bind(TFTP_PORT))
emit bindError();
}
void TFtpServer::stop()
{
socket->close();
}
void TFtpServer::readPendingDatagrams()
{
while (socket->hasPendingDatagrams()) {
QNetworkDatagram datagram = socket->receiveDatagram();
QByteArray d = datagram.data();
QString transferId = QString("%1:%2").arg(datagram.senderAddress().toString())
.arg(datagram.senderPort());
TFtpServerFile::Ptr file = fileManager_->find(transferId.toStdString());
if(file)
file->process((uint8_t *)d.data(), d.size());
else
{
ServerSocket* udp = new ServerSocket(socket, datagram.senderAddress(), datagram.senderPort());
file = TFtpServerFile::Ptr(new TFtpServerFile(udp, filePath_.toStdString(), transferId.toStdString()));
fileManager_->add(file);
file->process((uint8_t *)d.data(), d.size());
emit startFile(transferId, QString::fromStdString(file->filename()));
}
if(!file->is_finished())
{
if(file->type() == TFtpServerFile::Read)
emit statusText(QString("Downloding file: %1, progress: %4% blockNumber(%2/%3)")
.arg(QString::fromStdString(file->filename()))
.arg(file->block_number())
.arg(file->block_numbers())
.arg(file->block_number() * 100 / file->block_numbers()));
else
emit statusText(QString("Uploading file: %1, blockNumber(%2)")
.arg(QString::fromStdString(file->filename()))
.arg(file->block_number()));
emit progress(transferId, file->file_bytes(), file->filesize());
}
else
{
if(file->is_error())
emit statusText(QString("%1:%2").arg((int)file->error()).arg(QString::fromStdString(file->error_msg())));
else
emit statusText(QString());
emit progress(transferId, file->file_bytes(), file->filesize());
emit stopFile(transferId);
fileManager_->remove(file->transfer_id());
}
}
}
函数说明:
setFilePath 保存文件路径地址
start 绑定TFTP端口,启动服务,绑定失败发送bindError信号。
stop 关闭socket,停止服务
readPendingDatagrams 从socket读取数据包,构造transferId,根据transferId判断是新连接还是旧连接,旧连接则找到对应TFtpServerFile对象处理文件上下载;新连接则TFtpServerFile对象处理文件上下载,并保存TFtpServerFile对象;处理数据完毕后,根据结束与否进行相应的处理。
3.3.3 TFtpServerWidget
TFtpServerWidget从QWidget派生一个窗口类,负责设置下载文件路径,并显示文件传输进度等界面操作。
3.3.3.1 TFtpServerWidget定义
class TFtpServer;
class TFtpServerWidget : public QWidget
{
Q_OBJECT
public:
TFtpServerWidget(QWidget *parent = nullptr);
~TFtpServerWidget();
private slots:
void selectTFtpDir();
void setCurrentDir(QString const& path);
void onBindError();
void onStartFile(QString const&transferId, QString const& fileName);
void onProgress(QString const&transferId, quint64 bytes, quint64 total);
void onStopFile(QString const&transferId);
private:
void saveSettinggs();
void loadSettings();
private:
Ui::TFtpServerWidget *ui;
TFtpServer* tftpServer;
int MAX_PATH_SIZE = 5;
};
3.3.3.2 TFtpServerWidget实现
TFtpServerWidget::TFtpServerWidget(QWidget *parent)
: QWidget(parent)
, ui(new Ui::TFtpServerWidget)
, tftpServer(new TFtpServer(this))
{
ui->setupUi(this);
loadSettings();
connect(ui->btnBrowse, SIGNAL(clicked()), this, SLOT(selectTFtpDir()));
connect(ui->currentDir, SIGNAL(currentIndexChanged(QString)), this, SLOT(setCurrentDir(QString)));
connect(tftpServer, SIGNAL(startFile(QString,QString)), this, SLOT(onStartFile(QString,QString)));
connect(tftpServer, SIGNAL(progress(QString,quint64,quint64)), this, SLOT(onProgress(QString,quint64,quint64)));
connect(tftpServer, SIGNAL(stopFile(QString)), this, SLOT(onStopFile(QString)));
connect(tftpServer, SIGNAL(bindError()), this, SLOT(onBindError()));
tftpServer->start();
}
TFtpServerWidget::~TFtpServerWidget()
{
saveSettinggs();
tftpServer->stop();
delete ui;
}
void TFtpServerWidget::selectTFtpDir()
{
QString filePath = QFileDialog::getExistingDirectory(this,
"Select Dir", ui->currentDir->currentText());
if(filePath.isEmpty())
return;
int index = ui->currentDir->findText(filePath);
if(index != -1)
ui->currentDir->setCurrentIndex(index);
else
{
if(ui->currentDir->count() >= MAX_PATH_SIZE)
ui->currentDir->removeItem(0);
ui->currentDir->addItem(filePath);
ui->currentDir->setCurrentIndex(ui->currentDir->count() - 1);
}
}
void TFtpServerWidget::setCurrentDir(QString const& path)
{
tftpServer->setFilePath(path);
}
void TFtpServerWidget::onStartFile(QString const&transferId, QString const& fileName)
{
ui->clientTables->addTopLevelItem(new QTreeWidgetItem(QStringList()
<< transferId << fileName << QTime::currentTime().toString("hh:mm:ss")));
}
void TFtpServerWidget::onProgress(QString const&transferId, quint64 bytes, quint64 total)
{
QList<QTreeWidgetItem*> items = ui->clientTables->findItems(transferId, Qt::MatchCaseSensitive);
for(int i = 0; i < items.size(); i++)
{
if(total == 0)
items[i]->setText(5, QString::number(bytes));
else
{ items[i]->setText(3, QString("%1%").arg(bytes * 100 / total));
items[i]->setText(5, QString::number(total));
}
items[i]->setText(4, QString::number(bytes));
}
}
void TFtpServerWidget::onStopFile(QString const&transferId)
{
QList<QTreeWidgetItem*> items = ui->clientTables->findItems(transferId, Qt::MatchCaseSensitive);
for(int i = 0; i < items.size(); i++)
{
int index = ui->clientTables->indexOfTopLevelItem(items[i]);
ui->clientTables->takeTopLevelItem(index);
}
}
void TFtpServerWidget::saveSettinggs()
{
QSettings settings(QCoreApplication::applicationName(), QCoreApplication::applicationVersion());
QStringList dirs;
for(int i = 0; i < ui->currentDir->count(); i++)
dirs << ui->currentDir->itemText(i);
settings.setValue("dirs", dirs);
settings.setValue("currentDir", ui->currentDir->currentText());
}
void TFtpServerWidget::loadSettings()
{
QSettings settings(QCoreApplication::applicationName(), QCoreApplication::applicationVersion());
QStringList dirs = settings.value("dirs", QStringList()).toStringList();
QString currentDir = settings.value("currentDir", QString()).toString();
ui->currentDir->addItems(dirs);
int index = ui->currentDir->findText(currentDir);
if(index != -1)
{
tftpServer->setFilePath(currentDir);
ui->currentDir->setCurrentIndex(index);
}
else
{
tftpServer->setFilePath(QApplication::applicationDirPath());
ui->currentDir->addItem(QApplication::applicationDirPath());
}
}
void TFtpServerWidget::onBindError()
{
QMessageBox::critical(this, "TFtpServer", "Port(69) is already occupied!");
ui->btnBrowse->setDisabled(true);
ui->currentDir->setDisabled(true);
setWindowTitle("TFtpServer is not running");
}
没有回复内容