本文展示一個純 Winsock API 實作的 FTP 用戶端核心邏輯,涵蓋控制通道建立、伺服器回應解析、指令發送、匿名登入流程。雖然代碼基於舊版 Winsock 1.1 與阻塞式 Socket,但其協議解析、錯誤處理、狀態機設計對理解 FTP 協議與網路程式基礎仍具教學價值。
send/recv 同步呼叫,簡單但會阻塞 UI 執行緒。220 服務就緒、230 登入成功、530 認證失敗。UINT GetReplyCode(LPSTR lpszServerReply) {
char c = *(lpszServerReply + 3); // 暫存第 4 字元
*(lpszServerReply + 3) = '\0'; // 截斷前 3 碼
UINT nCode = atoi(lpszServerReply); // 轉整數
*(lpszServerReply + 3) = c; // 還原
return nCode;
}
UINT ReadFTPServerReply(SOCKET hControlChannel) {
char sReceiveBuffer[1024];
int iLength = recv(hControlChannel, sReceiveBuffer, sizeof(sReceiveBuffer), 0);
if (iLength == SOCKET_ERROR) {
// WSAGetLastError() 取得錯誤碼,MessageBox 顯示
return 999; // 自訂錯誤代碼
}
sReceiveBuffer[iLength] = '\0';
MessageBox(NULL, sReceiveBuffer, "ReadFTPServerReply", MB_OK | MB_ICONINFORMATION);
return GetReplyCode(sReceiveBuffer);
}
UINT SendFTPCommand(SOCKET hControlChannel, LPSTR szCommandBuffer) {
if (send(hControlChannel, szCommandBuffer, lstrlen(szCommandBuffer), 0) == SOCKET_ERROR) {
// 錯誤處理
return 999;
}
return ReadFTPServerReply(hControlChannel); // 發送後立即讀取回應
}
UINT AnonymousFTPLogIn(SOCKET hControlSocket) {
char *LoginCommand[] = {"USER anonymous\r\n", "PASS guest\r\n", NULL};
int nReplyCode, iMsg = 0;
do {
nReplyCode = SendFTPCommand(hControlSocket, LoginCommand[iMsg++]);
} while (LoginCommand[iMsg] && nReplyCode < 400); // 4xx/5xx 為失敗
return nReplyCode; // 230 表示成功
}
SOCKET ConnectFTPControlSocket(LPSTR lpszHost) {
LPHOSTENT lpHostEnt = gethostbyname(lpszHost);
if (!lpHostEnt) return INVALID_SOCKET;
SOCKET hControlSocket = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
if (hControlSocket == INVALID_SOCKET) return INVALID_SOCKET;
LPSERVENT lpServEnt = getservbyname("ftp", NULL);
short nProtocolPort = lpServEnt ? lpServEnt->s_port : htons(IPPORT_FTP); // 21
SOCKADDR_IN sockAddr = {};
sockAddr.sin_family = AF_INET;
sockAddr.sin_port = nProtocolPort;
sockAddr.sin_addr = *((LPIN_ADDR)*lpHostEnt->h_addr_list);
if (connect(hControlSocket, (LPSOCKADDR)&sockAddr, sizeof(sockAddr)) == SOCKET_ERROR) {
closesocket(hControlSocket);
return INVALID_SOCKET;
}
// 讀取歡迎訊息(220)
if (ReadFTPServerReply(hControlSocket) >= 400) {
closesocket(hControlSocket);
return INVALID_SOCKET;
}
return hControlSocket;
}
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int) {
WSADATA wsaData;
if (WSAStartup(0x0101, &wsaData)) { /* 載入失敗 */ return 0; }
SOCKET hControlChannel = ConnectFTPControlSocket("ftp.example.com");
if (hControlChannel != INVALID_SOCKET) {
if (AnonymousFTPLogIn(hControlChannel) == 230) { // 登入成功
SendFTPCommand(hControlChannel, "QUIT\r\n");
}
closesocket(hControlChannel);
}
WSACleanup();
MessageBox(NULL, "THE END!!", "Quick FTP", MB_OK | MB_ICONEXCLAMATION);
return 0;
}
| 原代碼特徵 | 問題/限制 | 現代最佳實踐 |
|---|---|---|
Winsock 1.1 (0x0101) | 舊版,缺乏 IPv6、重疊 I/O、條件接受等新功能 | WSAStartup(MAKEWORD(2,2), &wsaData) 使用 Winsock 2.2 |
阻塞式 send/recv | UI 凍結、無法並發多連線 | 非同步 I/O:WSAAsyncSelect/WSAEventSelect/IOCP/std::async + Boost.Asio |
| 硬編碼主機/帳號 | 無法複用、安全性差 | 設定檔/命令列參數/安全儲存憑證 |
MessageBox 直接在網路函式顯示錯誤 | 邏輯與 UI 耦合、無法單元測試 | 回傳錯誤碼/拋出例外,由上層 UI 決定呈現方式 |
單一緩衝區 recv 假設一次收完整行 | TCP 流無訊息邊界,可能黏包/拆包 | 實作緩衝區累積,解析 \r\n 結尾的完整行 |
| 無資料通道 (PORT/PASV) 實作 | 無法上傳/下載檔案、列表目錄 | 需實作主動/被動模式資料連線、傳輸型別 (ASCII/Binary) |
class FtpClient {
SOCKET ctrl_sock_ = INVALID_SOCKET;
std::string host_, user_, pass_;
std::vector<char> recv_buf_;
public:
FtpClient(std::string host, std::string user="anonymous", std::string pass="guest")
: host_(std::move(host)), user_(std::move(user)), pass_(std::move(pass)) {}
bool Connect() {
// Winsock 2.2 初始化、getaddrinfo (支援 IPv6)、非阻塞 connect + select
// 建立控制通道、讀取 220
}
bool Login() {
return SendCmd("USER " + user_) == 331 &&
SendCmd("PASS " + pass_) == 230;
}
int SendCmd(const std::string& cmd) {
std::string data = cmd + "\r\n";
send(ctrl_sock_, data.data(), data.size(), 0);
return ReadReplyCode(); // 解析多行回應、處理黏包
}
// 需額外實作:PASV/PORT、LIST、RETR、STOR、TYPE I/A、QUIT
// 建議改用成熟庫:libcurl、Boost.Beast、Poco::Net::FTPClientSession
};
此範例雖簡陋,但完整示範了 FTP 控制通道的連線建立、指令/回應協議、狀態流轉。實務開發中:
理解底層協議,才能在高層庫出問題時精準診斷、在特殊需求時正確擴充。