TFTP(Trivial File Transfer Protocol)は、UDPベースのシンプルなファイル転送プロトコルであり、ブートストラップや組み込みシステムなどで広く使われています。本記事では、Windows環境下でのTFTPサーバー実装における環境設定、通信処理、およびエラー対応のポイントを解説します。
開発環境とテストツール
Windows上でTFTPサーバーを開発・検証するには、以下のツールが有用です。
Tftpd64
クライアントとして動作し、サーバーとの通信をシミュレートします。主な設定項目:
- Host: サーバーのIPアドレス(例: 127.0.0.1)
- Port: 通常69番ポート。競合時は任意に変更可能
- Local File: 転送元または保存先のローカルファイルパス
- Remote File: 要求するリモートファイル名
- Block Size: 実験仕様に合わせ512バイト固定
- Get / Put: ファイル取得/送信操作
clumsy
ネットワーク異常を模擬するためのツールです。以下のような障害を再現できます:
- 遅延: パケット送信を意図的に遅らせる
- 破棄: ランダムにパケットをドロップ
- 流量制限: 一時的に帯域を絞る
- 重複: 同じパケットを複数回送信
- 順序乱れ: パケット到着順をランダム化
- 改ざん: パケット内容の一部を書き換え
注意点:このツールを常時有効にすると、他のネットワークアプリ(VPN、ブラウザ、音楽ストリーミングなど)にも影響が出ることがあります。
ソケット初期化と基本構造
WindowsではWSAStartup()によるWinsockの初期化が必須です。また、UDPソケットを生成し、指定ポートにバインドします。
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
int initialize_network() {
WSADATA wsa;
if (WSAStartup(MAKEWORD(2, 2), &wsa) != 0) {
fprintf(stderr, "Winsock初期化失敗\n");
return -1;
}
return 0;
}
データ転送処理の設計
TFTPは「要求 → データ送信 → ACK確認」のサイクリックなやり取りで動作します。各操作はオペコードで識別され、512バイト単位で分割転送されます。
READ要求(RRQ)の処理
クライアントからのファイル要求に対し、サーバーはファイルを512バイトずつ読み出し、データパケットを送信。ACKが返るまで最大10回再送します。
void handle_read_request(SOCKET sock, struct sockaddr_in client, const char* fname) {
FILE* fp = fopen(fname, "rb");
if (!fp) { send_error(sock, client, 1); return; }
uint16_t blk = 1;
char packet[516];
struct timeval start, end;
gettimeofday(&start, nullptr);
while (true) {
size_t len = fread(packet + 4, 1, 512, fp);
*(uint16_t*)packet = htons(3); // OPCODE_DATA
*(uint16_t*)(packet + 2) = htons(blk);
int retry = 0;
bool acked = false;
while (!acked && retry < 10) {
sendto(sock, packet, len + 4, 0, (sockaddr*)&client, sizeof(client));
fd_set fds; FD_ZERO(&fds); FD_SET(sock, &fds);
struct timeval timeout = {5, 0};
int sel = select(sock + 1, &fds, nullptr, nullptr, &timeout);
if (sel > 0) {
char ack[4];
recvfrom(sock, ack, 4, 0, nullptr, nullptr);
if (ntohs(*(uint16_t*)ack) == 4 && ntohs(*(uint16_t*)(ack+2)) == blk) {
acked = true;
}
} else if (sel == 0) {
retry++;
}
}
if (!acked) break;
if (len < 512) break;
blk++;
}
gettimeofday(&end, nullptr);
double sec = (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec) / 1e6;
printf("転送速度: %.2f KB/s\n", ftell(fp) / sec / 1024);
fclose(fp);
}
WRITE要求(WRQ)の処理
クライアントから送られてくるデータブロックを受信し、期待するブロック番号と一致した場合のみファイルに書き込みます。不一致時はACK再送を行い、順序保証を実現します。
void handle_write_request(SOCKET sock, struct sockaddr_in client, const char* fname) {
FILE* fp = fopen(fname, "wb");
if (!fp) { send_error(sock, client, 2); return; }
uint16_t expect_blk = 1;
char buf[516];
struct timeval start, end;
gettimeofday(&start, nullptr);
while (true) {
*(uint16_t*)buf = htons(4); // OPCODE_ACK
*(uint16_t*)(buf + 2) = htons(expect_blk - 1);
sendto(sock, buf, 4, 0, (sockaddr*)&client, sizeof(client));
int len = recvfrom(sock, buf, 516, 0, nullptr, nullptr);
if (len < 4 || ntohs(*(uint16_t*)buf) != 3) break;
uint16_t recv_blk = ntohs(*(uint16_t*)(buf + 2));
if (recv_blk == expect_blk) {
fwrite(buf + 4, 1, len - 4, fp);
expect_blk++;
}
if (len < 516) break;
}
gettimeofday(&end, nullptr);
double sec = (end.tv_sec - start.tv_sec) + (end.tv_usec - start.tv_usec) / 1e6;
printf("受信速度: %.2f KB/s\n", ftell(fp) / sec / 1024);
fclose(fp);
}
エラー処理とタイムアウト管理
TFTPではエラーパケット(OPCODE=5)を送ることで異常を通知します。代表的なエラーコード:
- 1: ファイルが存在しない
- 2: アクセス権限違反
- 4: 不正な操作
- 6: 不明な転送ID
タイムアウトはselect()関数で監視し、指定時間内にACKが来なければ再送を行います。これにより、ネットワーク不安定時でも確実な転送を実現します。