FTP服务器与客户端实现(VC++)
FTP服务器和客户端的VC++实现,包含文件传输、目录操作和用户管理功能。
FTP服务器实现
服务器主程序 (FTPServer.cpp)
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <direct.h>
#include <sys/stat.h>
#include <fstream>
#include <vector>
#include <map>
#include <algorithm>
#pragma comment(lib, "ws2_32.lib")
#define PORT 21
#define DATA_PORT_START 1024
#define DATA_PORT_END 65535
#define BUFFER_SIZE 4096
#define MAX_CLIENTS 10
enum FTP_STATE {
NOT_LOGGED_IN,
LOGGED_IN
};
struct ClientInfo {
SOCKET controlSocket;
SOCKET dataSocket;
SOCKADDR_IN dataAddr;
FTP_STATE state;
char currentDir[MAX_PATH];
char username[50];
bool passiveMode;
int dataPort;
};
std::map<SOCKET, ClientInfo> clients;
std::map<std::string, std::string> users; // username -> password
void InitWinsock();
void CleanupWinsock();
void HandleClient(SOCKET clientSocket);
void ProcessCommand(SOCKET clientSocket, const char* command);
void SendResponse(SOCKET socket, const char* response);
void SendData(SOCKET dataSocket, const char* data);
void SendFile(SOCKET dataSocket, const char* filename);
void ReceiveFile(SOCKET dataSocket, const char* filename);
void ListDirectory(SOCKET dataSocket, const char* path);
void ChangeDirectory(const char* path, char* currentDir);
void CreateDirectory(const char* dirname);
void RemoveDirectory(const char* dirname);
void DeleteFile(const char* filename);
void RenameFile(const char* oldname, const char* newname);
void InitUsers();
bool AuthenticateUser(const char* username, const char* password);
void LogActivity(const char* message);
int main() {
WSADATA wsaData;
SOCKET listenSocket, clientSocket;
SOCKADDR_IN serverAddr, clientAddr;
int clientAddrLen = sizeof(clientAddr);
int result;
InitWinsock();
InitUsers();
// 创建监听套接字
listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (listenSocket == INVALID_SOCKET) {
printf("socket failed with error: %d\n", WSAGetLastError());
return 1;
}
// 配置服务器地址
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(PORT);
// 绑定套接字
result = bind(listenSocket, (SOCKADDR*)&serverAddr, sizeof(serverAddr));
if (result == SOCKET_ERROR) {
printf("bind failed with error: %d\n", WSAGetLastError());
closesocket(listenSocket);
return 1;
}
// 开始监听
result = listen(listenSocket, SOMAXCONN);
if (result == SOCKET_ERROR) {
printf("listen failed with error: %d\n", WSAGetLastError());
closesocket(listenSocket);
return 1;
}
printf("FTP Server started on port %d\n", PORT);
LogActivity("FTP Server started");
while (true) {
// 接受客户端连接
clientSocket = accept(listenSocket, (SOCKADDR*)&clientAddr, &clientAddrLen);
if (clientSocket == INVALID_SOCKET) {
printf("accept failed with error: %d\n", WSAGetLastError());
continue;
}
char ipStr[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(clientAddr.sin_addr), ipStr, INET_ADDRSTRLEN);
printf("Accepted connection from %s:%d\n", ipStr, ntohs(clientAddr.sin_port));
LogActivity(std::string("Accepted connection from ") + ipStr);
// 创建线程处理客户端
DWORD threadId;
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)HandleClient, (LPVOID)clientSocket, 0, &threadId);
if (hThread == NULL) {
printf("CreateThread failed\n");
closesocket(clientSocket);
} else {
CloseHandle(hThread);
}
}
closesocket(listenSocket);
CleanupWinsock();
return 0;
}
void InitWinsock() {
WSADATA wsaData;
int result = WSAStartup(MAKEWORD(2, 2), &wsaData);
if (result != 0) {
printf("WSAStartup failed: %d\n", result);
exit(1);
}
}
void CleanupWinsock() {
WSACleanup();
}
void InitUsers() {
users["admin"] = "admin123";
users["user"] = "password";
users["ftp"] = "ftppass";
}
bool AuthenticateUser(const char* username, const char* password) {
auto it = users.find(username);
if (it != users.end() && it->second == password) {
return true;
}
return false;
}
void LogActivity(const char* message) {
SYSTEMTIME st;
GetLocalTime(&st);
char logEntry[256];
sprintf(logEntry, "[%04d-%02d-%02d %02d:%02d:%02d] %s\n",
st.wYear, st.wMonth, st.wDay,
st.wHour, st.wMinute, st.wSecond,
message);
FILE* logFile = fopen("ftp_server.log", "a");
if (logFile) {
fputs(logEntry, logFile);
fclose(logFile);
}
}
void HandleClient(SOCKET clientSocket) {
ClientInfo client;
client.controlSocket = clientSocket;
client.dataSocket = INVALID_SOCKET;
client.state = NOT_LOGGED_IN;
client.currentDir[0] = '\0';
client.username[0] = '\0';
client.passiveMode = false;
client.dataPort = 0;
strcpy(client.currentDir, "."); // 当前目录
clients[clientSocket] = client;
// 发送欢迎消息
SendResponse(clientSocket, "220 Welcome to Simple FTP Server\r\n");
char buffer[BUFFER_SIZE];
int bytesReceived;
while (true) {
bytesReceived = recv(clientSocket, buffer, BUFFER_SIZE - 1, 0);
if (bytesReceived <= 0) {
break;
}
buffer[bytesReceived] = '\0';
printf("Received from client: %s", buffer);
// 处理命令
ProcessCommand(clientSocket, buffer);
}
// 清理客户端连接
closesocket(clientSocket);
if (client.dataSocket != INVALID_SOCKET) {
closesocket(client.dataSocket);
}
clients.erase(clientSocket);
printf("Client disconnected\n");
LogActivity("Client disconnected");
}
void ProcessCommand(SOCKET clientSocket, const char* command) {
char cmd[10], arg[256];
sscanf(command, "%s %[^\r\n]", cmd, arg);
// 转换为大写
for (int i = 0; cmd[i]; i++) {
cmd[i] = toupper(cmd[i]);
}
ClientInfo& client = clients[clientSocket];
if (strcmp(cmd, "USER") == 0) {
strncpy(client.username, arg, sizeof(client.username) - 1);
SendResponse(clientSocket, "331 User name okay, need password.\r\n");
}
else if (strcmp(cmd, "PASS") == 0) {
if (AuthenticateUser(client.username, arg)) {
client.state = LOGGED_IN;
SendResponse(clientSocket, "230 User logged in, proceed.\r\n");
LogActivity(std::string("User logged in: ") + client.username);
} else {
SendResponse(clientSocket, "530 Not logged in.\r\n");
}
}
else if (strcmp(cmd, "SYST") == 0) {
SendResponse(clientSocket, "215 Windows_NT\r\n");
}
else if (strcmp(cmd, "FEAT") == 0) {
SendResponse(clientSocket, "211-Features supported:\r\n");
SendResponse(clientSocket, " PASV\r\n");
SendResponse(clientSocket, " PORT\r\n");
SendResponse(clientSocket, "211 End\r\n");
}
else if (strcmp(cmd, "PWD") == 0) {
char response[MAX_PATH + 20];
sprintf(response, "257 \"%s\" is current directory.\r\n", client.currentDir);
SendResponse(clientSocket, response);
}
else if (strcmp(cmd, "CWD") == 0) {
if (client.state != LOGGED_IN) {
SendResponse(clientSocket, "530 Not logged in.\r\n");
return;
}
char newDir[MAX_PATH];
ChangeDirectory(arg, newDir);
if (strcmp(newDir, "") != 0) {
strncpy(client.currentDir, newDir, sizeof(client.currentDir) - 1);
char response[MAX_PATH + 20];
sprintf(response, "250 Directory changed to \"%s\".\r\n", client.currentDir);
SendResponse(clientSocket, response);
} else {
SendResponse(clientSocket, "550 Failed to change directory.\r\n");
}
}
else if (strcmp(cmd, "CDUP") == 0) {
if (client.state != LOGGED_IN) {
SendResponse(clientSocket, "530 Not logged in.\r\n");
return;
}
char parentDir[MAX_PATH];
ChangeDirectory("..", parentDir);
if (strcmp(parentDir, "") != 0) {
strncpy(client.currentDir, parentDir, sizeof(client.currentDir) - 1);
char response[MAX_PATH + 20];
sprintf(response, "250 Directory changed to \"%s\".\r\n", client.currentDir);
SendResponse(clientSocket, response);
} else {
SendResponse(clientSocket, "550 Failed to change directory.\r\n");
}
}
else if (strcmp(cmd, "LIST") == 0) {
if (client.state != LOGGED_IN) {
SendResponse(clientSocket, "530 Not logged in.\r\n");
return;
}
if (client.passiveMode) {
// 被动模式
SOCKET dataSocket = client.dataSocket;
if (dataSocket == INVALID_SOCKET) {
SendResponse(clientSocket, "425 Can't open data connection.\r\n");
return;
}
SendResponse(clientSocket, "150 Opening ASCII mode data connection for file list.\r\n");
ListDirectory(dataSocket, client.currentDir);
SendResponse(clientSocket, "226 Transfer complete.\r\n");
closesocket(dataSocket);
client.dataSocket = INVALID_SOCKET;
} else {
// 主动模式
SendResponse(clientSocket, "502 Active mode not implemented.\r\n");
}
}
else if (strcmp(cmd, "RETR") == 0) {
if (client.state != LOGGED_IN) {
SendResponse(clientSocket, "530 Not logged in.\r\n");
return;
}
if (client.passiveMode) {
SOCKET dataSocket = client.dataSocket;
if (dataSocket == INVALID_SOCKET) {
SendResponse(clientSocket, "425 Can't open data connection.\r\n");
return;
}
char filepath[MAX_PATH];
sprintf(filepath, "%s\\%s", client.currentDir, arg);
SendResponse(clientSocket, "150 Opening BINARY mode data connection for file transfer.\r\n");
SendFile(dataSocket, filepath);
SendResponse(clientSocket, "226 Transfer complete.\r\n");
closesocket(dataSocket);
client.dataSocket = INVALID_SOCKET;
} else {
SendResponse(clientSocket, "502 Active mode not implemented.\r\n");
}
}
else if (strcmp(cmd, "STOR") == 0) {
if (client.state != LOGGED_IN) {
SendResponse(clientSocket, "530 Not logged in.\r\n");
return;
}
if (client.passiveMode) {
SOCKET dataSocket = client.dataSocket;
if (dataSocket == INVALID_SOCKET) {
SendResponse(clientSocket, "425 Can't open data connection.\r\n");
return;
}
char filepath[MAX_PATH];
sprintf(filepath, "%s\\%s", client.currentDir, arg);
SendResponse(clientSocket, "150 Opening BINARY mode data connection for file transfer.\r\n");
ReceiveFile(dataSocket, filepath);
SendResponse(clientSocket, "226 Transfer complete.\r\n");
closesocket(dataSocket);
client.dataSocket = INVALID_SOCKET;
} else {
SendResponse(clientSocket, "502 Active mode not implemented.\r\n");
}
}
else if (strcmp(cmd, "PASV") == 0) {
if (client.state != LOGGED_IN) {
SendResponse(clientSocket, "530 Not logged in.\r\n");
return;
}
// 创建数据套接字
SOCKET dataSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (dataSocket == INVALID_SOCKET) {
SendResponse(clientSocket, "425 Can't create data socket.\r\n");
return;
}
// 绑定到任意可用端口
SOCKADDR_IN dataAddr;
dataAddr.sin_family = AF_INET;
dataAddr.sin_addr.s_addr = INADDR_ANY;
dataAddr.sin_port = 0;
if (bind(dataSocket, (SOCKADDR*)&dataAddr, sizeof(dataAddr)) == SOCKET_ERROR) {
closesocket(dataSocket);
SendResponse(clientSocket, "425 Can't bind data socket.\r\n");
return;
}
// 获取绑定的端口
int addrLen = sizeof(dataAddr);
getsockname(dataSocket, (SOCKADDR*)&dataAddr, &addrLen);
int port = ntohs(dataAddr.sin_port);
// 监听
if (listen(dataSocket, 1) == SOCKET_ERROR) {
closesocket(dataSocket);
SendResponse(clientSocket, "425 Can't listen on data socket.\r\n");
return;
}
// 保存数据套接字信息
client.dataSocket = dataSocket;
client.passiveMode = true;
client.dataPort = port;
// 发送响应
char ipStr[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &dataAddr.sin_addr, ipStr, INET_ADDRSTRLEN);
// 将IP地址和端口转换为PASV响应格式
int ip1, ip2, ip3, ip4;
sscanf(ipStr, "%d.%d.%d.%d", &ip1, &ip2, &ip3, &ip4);
char response[100];
sprintf(response, "227 Entering Passive Mode (%d,%d,%d,%d,%d,%d).\r\n",
ip1, ip2, ip3, ip4, port / 256, port % 256);
SendResponse(clientSocket, response);
}
else if (strcmp(cmd, "PORT") == 0) {
if (client.state != LOGGED_IN) {
SendResponse(clientSocket, "530 Not logged in.\r\n");
return;
}
int h1, h2, h3, h4, p1, p2;
sscanf(arg, "%d,%d,%d,%d,%d,%d", &h1, &h2, &h3, &h4, &p1, &p2);
SOCKADDR_IN dataAddr;
dataAddr.sin_family = AF_INET;
dataAddr.sin_addr.s_addr = inet_addr(std::to_string(h1).append(".").append(std::to_string(h2)).append(".").append(std::to_string(h3)).append(".").append(std::to_string(h4)).c_str());
dataAddr.sin_port = htons(p1 * 256 + p2);
client.dataAddr = dataAddr;
client.passiveMode = false;
SendResponse(clientSocket, "200 Port command successful.\r\n");
}
else if (strcmp(cmd, "MKD") == 0) {
if (client.state != LOGGED_IN) {
SendResponse(clientSocket, "530 Not logged in.\r\n");
return;
}
char dirpath[MAX_PATH];
sprintf(dirpath, "%s\\%s", client.currentDir, arg);
CreateDirectory(dirpath);
char response[MAX_PATH + 20];
sprintf(response, "257 \"%s\" directory created.\r\n", arg);
SendResponse(clientSocket, response);
}
else if (strcmp(cmd, "RMD") == 0) {
if (client.state != LOGGED_IN) {
SendResponse(clientSocket, "530 Not logged in.\r\n");
return;
}
char dirpath[MAX_PATH];
sprintf(dirpath, "%s\\%s", client.currentDir, arg);
RemoveDirectory(dirpath);
SendResponse(clientSocket, "250 Directory removed.\r\n");
}
else if (strcmp(cmd, "DELE") == 0) {
if (client.state != LOGGED_IN) {
SendResponse(clientSocket, "530 Not logged in.\r\n");
return;
}
char filepath[MAX_PATH];
sprintf(filepath, "%s\\%s", client.currentDir, arg);
DeleteFile(filepath);
SendResponse(clientSocket, "250 File deleted.\r\n");
}
else if (strcmp(cmd, "RNFR") == 0) {
if (client.state != LOGGED_IN) {
SendResponse(clientSocket, "530 Not logged in.\r\n");
return;
}
// 保存旧文件名
char oldpath[MAX_PATH];
sprintf(oldpath, "%s\\%s", client.currentDir, arg);
// 在实际实现中,需要存储这个路径
SendResponse(clientSocket, "350 File exists, ready for destination name.\r\n");
}
else if (strcmp(cmd, "RNTO") == 0) {
if (client.state != LOGGED_IN) {
SendResponse(clientSocket, "530 Not logged in.\r\n");
return;
}
// 在实际实现中,需要获取之前保存的旧文件名
char newpath[MAX_PATH];
sprintf(newpath, "%s\\%s", client.currentDir, arg);
// RenameFile(oldpath, newpath);
SendResponse(clientSocket, "250 File renamed.\r\n");
}
else if (strcmp(cmd, "QUIT") == 0) {
SendResponse(clientSocket, "221 Goodbye.\r\n");
closesocket(clientSocket);
}
else {
SendResponse(clientSocket, "502 Command not implemented.\r\n");
}
}
void SendResponse(SOCKET socket, const char* response) {
send(socket, response, strlen(response), 0);
printf("Sent: %s", response);
}
void SendData(SOCKET dataSocket, const char* data) {
send(dataSocket, data, strlen(data), 0);
}
void SendFile(SOCKET dataSocket, const char* filename) {
FILE* file = fopen(filename, "rb");
if (!file) {
return;
}
char buffer[BUFFER_SIZE];
size_t bytesRead;
while ((bytesRead = fread(buffer, 1, BUFFER_SIZE, file)) > 0) {
send(dataSocket, buffer, bytesRead, 0);
}
fclose(file);
}
void ReceiveFile(SOCKET dataSocket, const char* filename) {
FILE* file = fopen(filename, "wb");
if (!file) {
return;
}
char buffer[BUFFER_SIZE];
int bytesReceived;
while ((bytesReceived = recv(dataSocket, buffer, BUFFER_SIZE, 0)) > 0) {
fwrite(buffer, 1, bytesReceived, file);
}
fclose(file);
}
void ListDirectory(SOCKET dataSocket, const char* path) {
WIN32_FIND_DATA findData;
HANDLE hFind;
char searchPath[MAX_PATH];
sprintf(searchPath, "%s\\*", path);
hFind = FindFirstFile(searchPath, &findData);
if (hFind == INVALID_HANDLE_VALUE) {
return;
}
do {
// 跳过 "." 和 ".."
if (strcmp(findData.cFileName, ".") == 0 || strcmp(findData.cFileName, "..") == 0) {
continue;
}
char fileInfo[256];
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
sprintf(fileInfo, "drwxr-xr-x 1 user group %10llu %s\r\n",
(unsigned long long)findData.nFileSizeLow,
findData.cFileName);
} else {
sprintf(fileInfo, "-rw-r--r-- 1 user group %10llu %s\r\n",
(unsigned long long)findData.nFileSizeLow,
findData.cFileName);
}
SendData(dataSocket, fileInfo);
} while (FindNextFile(hFind, &findData));
FindClose(hFind);
}
void ChangeDirectory(const char* path, char* currentDir) {
char newDir[MAX_PATH];
if (strcmp(path, "..") == 0) {
// 上级目录
_chdir("..");
_getcwd(newDir, MAX_PATH);
} else if (path[0] == '/') {
// 绝对路径
_chdir(path);
_getcwd(newDir, MAX_PATH);
} else {
// 相对路径
char tempDir[MAX_PATH];
_getcwd(tempDir, MAX_PATH);
strcat(tempDir, "\\");
strcat(tempDir, path);
if (_chdir(tempDir) == 0) {
_getcwd(newDir, MAX_PATH);
} else {
newDir[0] = '\0';
}
}
if (strlen(newDir) > 0) {
strncpy(currentDir, newDir, MAX_PATH - 1);
}
}
void CreateDirectory(const char* dirname) {
_mkdir(dirname);
}
void RemoveDirectory(const char* dirname) {
_rmdir(dirname);
}
void DeleteFile(const char* filename) {
remove(filename);
}
void RenameFile(const char* oldname, const char* newname) {
rename(oldname, newname);
}
FTP客户端实现
客户端主程序 (FTPClientDlg.cpp)
#include "pch.h"
#include "framework.h"
#include "FTPClient.h"
#include "FTPClientDlg.h"
#include "afxdialogex.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
// CFTPClientDlg 对话框
CFTPClientDlg::CFTPClientDlg(CWnd* pParent /*=nullptr*/)
: CDialogEx(IDD_FTPCLIENT_DIALOG, pParent)
, m_host(_T(""))
, m_port(21)
, m_username(_T(""))
, m_password(_T(""))
, m_connected(false)
, m_loggedIn(false)
, m_passiveMode(true)
, m_dataSocket(INVALID_SOCKET)
{
m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}
void CFTPClientDlg::DoDataExchange(CDataExchange* pDX)
{
CDialogEx::DoDataExchange(pDX);
DDX_Control(pDX, IDC_LIST_COMMANDS, m_listCommands);
DDX_Control(pDX, IDC_EDIT_LOG, m_editLog);
DDX_Text(pDX, IDC_EDIT_HOST, m_host);
DDX_Text(pDX, IDC_EDIT_PORT, m_port);
DDX_Text(pDX, IDC_EDIT_USERNAME, m_username);
DDX_Text(pDX, IDC_EDIT_PASSWORD, m_password);
DDX_Control(pDX, IDC_BUTTON_CONNECT, m_btnConnect);
DDX_Control(pDX, IDC_BUTTON_DISCONNECT, m_btnDisconnect);
DDX_Control(pDX, IDC_BUTTON_SEND, m_btnSend);
DDX_Control(pDX, IDC_EDIT_COMMAND, m_editCommand);
DDX_Control(pDX, IDC_TREE_FILES, m_treeFiles);
DDX_Control(pDX, IDC_STATUS_TEXT, m_statusText);
}
BEGIN_MESSAGE_MAP(CFTPClientDlg, CDialogEx)
ON_WM_PAINT()
ON_WM_QUERYDRAGICON()
ON_BN_CLICKED(IDC_BUTTON_CONNECT, &CFTPClientDlg::OnBnClickedButtonConnect)
ON_BN_CLICKED(IDC_BUTTON_DISCONNECT, &CFTPClientDlg::OnBnClickedButtonDisconnect)
ON_BN_CLICKED(IDC_BUTTON_SEND, &CFTPClientDlg::OnBnClickedButtonSend)
ON_LBN_DBLCLK(IDC_LIST_COMMANDS, &CFTPClientDlg::OnLbnDblclkListCommands)
ON_EN_CHANGE(IDC_EDIT_COMMAND, &CFTPClientDlg::OnEnChangeEditCommand)
ON_NOTIFY(NM_DBLCLK, IDC_TREE_FILES, &CFTPClientDlg::OnNMDblclkTreeFiles)
ON_BN_CLICKED(IDC_BUTTON_UPLOAD, &CFTPClientDlg::OnBnClickedButtonUpload)
ON_BN_CLICKED(IDC_BUTTON_DOWNLOAD, &CFTPClientDlg::OnBnClickedButtonDownload)
END_MESSAGE_MAP()
// CFTPClientDlg 消息处理程序
BOOL CFTPClientDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
// 设置此对话框的图标
SetIcon(m_hIcon, TRUE); // 设置大图标
SetIcon(m_hIcon, FALSE); // 设置小图标
// 初始化命令列表
m_listCommands.AddString(_T("USER <username> - 设置用户名"));
m_listCommands.AddString(_T("PASS <password> - 设置密码"));
m_listCommands.AddString(_T("LIST - 列出当前目录内容"));
m_listCommands.AddString(_T("CWD <directory> - 改变工作目录"));
m_listCommands.AddString(_T("PWD - 显示当前工作目录"));
m_listCommands.AddString(_T("RETR <filename> - 下载文件"));
m_listCommands.AddString(_T("STOR <filename> - 上传文件"));
m_listCommands.AddString(_T("MKD <directory> - 创建目录"));
m_listCommands.AddString(_T("RMD <directory> - 删除目录"));
m_listCommands.AddString(_T("DELE <filename> - 删除文件"));
m_listCommands.AddString(_T("PASV - 进入被动模式"));
m_listCommands.AddString(_T("QUIT - 退出连接"));
// 初始化树控件
m_imageList.Create(16, 16, ILC_COLOR32, 0, 10);
m_imageList.Add(AfxGetApp()->LoadIcon(IDI_ICON_FOLDER));
m_imageList.Add(AfxGetApp()->LoadIcon(IDI_ICON_FILE));
m_treeFiles.SetImageList(&m_imageList, TVSIL_NORMAL);
// 禁用断开连接按钮
m_btnDisconnect.EnableWindow(FALSE);
m_btnSend.EnableWindow(FALSE);
// 设置默认主机
m_host = _T("127.0.0.1");
m_port = 21;
m_username = _T("user");
m_password = _T("password");
UpdateData(FALSE);
return TRUE;
}
void CFTPClientDlg::OnPaint()
{
if (IsIconic())
{
CPaintDC dc(this); // 用于绘制的设备上下文
SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);
// 使图标在工作区矩形中居中
int cxIcon = GetSystemMetrics(SM_CXICON);
int cyIcon = GetSystemMetrics(SM_CYICON);
CRect rect;
GetClientRect(&rect);
int x = (rect.Width() - cxIcon + 1) / 2;
int y = (rect.Height() - cyIcon + 1) / 2;
// 绘制图标
dc.DrawIcon(x, y, m_hIcon);
}
else
{
CDialogEx::OnPaint();
}
}
HCURSOR CFTPClientDlg::OnQueryDragIcon()
{
return static_cast<HCURSOR>(m_hIcon);
}
void CFTPClientDlg::LogMessage(const CString& message)
{
CString log;
m_editLog.GetWindowText(log);
log += message + _T("\r\n");
m_editLog.SetWindowText(log);
m_editLog.LineScroll(m_editLog.GetLineCount());
}
void CFTPClientDlg::OnBnClickedButtonConnect()
{
UpdateData(TRUE);
if (m_host.IsEmpty()) {
AfxMessageBox(_T("请输入服务器地址"));
return;
}
if (m_port <= 0 || m_port > 65535) {
AfxMessageBox(_T("端口号无效"));
return;
}
// 初始化Winsock
WSADATA wsaData;
if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) {
AfxMessageBox(_T("WSAStartup失败"));
return;
}
// 创建套接字
m_controlSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (m_controlSocket == INVALID_SOCKET) {
AfxMessageBox(_T("创建套接字失败"));
WSACleanup();
return;
}
// 解析服务器地址
sockaddr_in serverAddr;
serverAddr.sin_family = AF_INET;
serverAddr.sin_port = htons(m_port);
if (inet_pton(AF_INET, CT2A(m_host), &serverAddr.sin_addr) <= 0) {
AfxMessageBox(_T("无效的服务器地址"));
closesocket(m_controlSocket);
WSACleanup();
return;
}
// 连接服务器
if (connect(m_controlSocket, (sockaddr*)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR) {
CString error;
error.Format(_T("连接服务器失败: %d"), WSAGetLastError());
AfxMessageBox(error);
closesocket(m_controlSocket);
WSACleanup();
return;
}
m_connected = true;
m_loggedIn = false;
// 更新UI
m_btnConnect.EnableWindow(FALSE);
m_btnDisconnect.EnableWindow(TRUE);
m_btnSend.EnableWindow(TRUE);
// 启动接收线程
AfxBeginThread(ReceiveThread, this);
LogMessage(_T("已连接到服务器"));
m_statusText.SetWindowText(_T("已连接"));
}
UINT CFTPClientDlg::ReceiveThread(LPVOID pParam)
{
CFTPClientDlg* pDlg = (CFTPClientDlg*)pParam;
char buffer[4096];
while (pDlg->m_connected) {
int bytesReceived = recv(pDlg->m_controlSocket, buffer, sizeof(buffer) - 1, 0);
if (bytesReceived <= 0) {
pDlg->m_connected = false;
pDlg->LogMessage(_T("与服务器的连接已断开"));
pDlg->m_statusText.SetWindowText(_T("已断开连接"));
break;
}
buffer[bytesReceived] = '\0';
CString response(buffer);
pDlg->LogMessage(_T("服务器: ") + response);
// 检查登录状态
if (response.Left(3) == _T("230")) {
pDlg->m_loggedIn = true;
pDlg->m_statusText.SetWindowText(_T("已登录"));
}
}
return 0;
}
void CFTPClientDlg::OnBnClickedButtonDisconnect()
{
if (m_connected) {
// 发送QUIT命令
CString command = _T("QUIT\r\n");
send(m_controlSocket, CT2A(command), command.GetLength(), 0);
// 关闭套接字
closesocket(m_controlSocket);
m_connected = false;
m_loggedIn = false;
// 更新UI
m_btnConnect.EnableWindow(TRUE);
m_btnDisconnect.EnableWindow(FALSE);
m_btnSend.EnableWindow(FALSE);
LogMessage(_T("已断开与服务器的连接"));
m_statusText.SetWindowText(_T("已断开连接"));
}
}
void CFTPClientDlg::OnBnClickedButtonSend()
{
if (!m_connected) {
AfxMessageBox(_T("未连接到服务器"));
return;
}
CString command;
m_editCommand.GetWindowText(command);
if (command.IsEmpty()) {
return;
}
// 添加换行符
command += _T("\r\n");
// 发送命令
send(m_controlSocket, CT2A(command), command.GetLength(), 0);
// 记录命令
LogMessage(_T("客户端: ") + command);
// 清空输入框
m_editCommand.SetWindowText(_T(""));
}
void CFTPClientDlg::OnLbnDblclkListCommands()
{
int index = m_listCommands.GetCurSel();
if (index != LB_ERR) {
CString command;
m_listCommands.GetText(index, command);
// 提取命令部分
int spacePos = command.Find(' ');
if (spacePos != -1) {
command = command.Left(spacePos);
}
m_editCommand.SetWindowText(command);
}
}
void CFTPClientDlg::OnEnChangeEditCommand()
{
// TODO: 如果该控件是 RICHEDIT 控件,它将不
// 发送此通知,除非重写 CDialogEx::OnInitDialog()
// 函数并调用 CRichEditCtrl().SetEventMask(),
// 同时将 ENM_CHANGE 标志"或"运算到掩码中。
}
void CFTPClientDlg::OnNMDblclkTreeFiles(NMHDR *pNMHDR, LRESULT *pResult)
{
*pResult = 0;
if (!m_loggedIn) {
AfxMessageBox(_T("请先登录"));
return;
}
HTREEITEM hItem = m_treeFiles.GetSelectedItem();
if (hItem) {
CString name = m_treeFiles.GetItemText(hItem);
BOOL isFolder = m_treeFiles.GetItemData(hItem) == 0;
if (isFolder) {
// 进入目录
CString command;
command.Format(_T("CWD %s\r\n"), name);
send(m_controlSocket, CT2A(command), command.GetLength(), 0);
LogMessage(_T("客户端: ") + command);
} else {
// 下载文件
DownloadFile(name);
}
}
}
void CFTPClientDlg::OnBnClickedButtonUpload()
{
if (!m_loggedIn) {
AfxMessageBox(_T("请先登录"));
return;
}
CFileDialog dlg(TRUE, NULL, NULL, OFN_FILEMUSTEXIST, _T("所有文件 (*.*)|*.*||"));
if (dlg.DoModal() == IDOK) {
CString filePath = dlg.GetPathName();
CString fileName = dlg.GetFileName();
UploadFile(filePath, fileName);
}
}
void CFTPClientDlg::OnBnClickedButtonDownload()
{
if (!m_loggedIn) {
AfxMessageBox(_T("请先登录"));
return;
}
HTREEITEM hItem = m_treeFiles.GetSelectedItem();
if (hItem) {
CString name = m_treeFiles.GetItemText(hItem);
BOOL isFolder = m_treeFiles.GetItemData(hItem) == 0;
if (!isFolder) {
DownloadFile(name);
} else {
AfxMessageBox(_T("不能下载目录"));
}
} else {
AfxMessageBox(_T("请选择要下载的文件"));
}
}
void CFTPClientDlg::UploadFile(const CString& localPath, const CString& remoteName)
{
// 进入被动模式
CString command = _T("PASV\r\n");
send(m_controlSocket, CT2A(command), command.GetLength(), 0);
LogMessage(_T("客户端: ") + command);
// 等待服务器响应
Sleep(500);
// 发送STOR命令
command.Format(_T("STOR %s\r\n"), remoteName);
send(m_controlSocket, CT2A(command), command.GetLength(), 0);
LogMessage(_T("客户端: ") + command);
// 创建数据套接字
m_dataSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (m_dataSocket == INVALID_SOCKET) {
AfxMessageBox(_T("创建数据套接字失败"));
return;
}
// 连接数据端口
// 在实际实现中,需要解析PASV响应获取端口
sockaddr_in dataAddr;
dataAddr.sin_family = AF_INET;
dataAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
dataAddr.sin_port = htons(1025); // 示例端口
if (connect(m_dataSocket, (sockaddr*)&dataAddr, sizeof(dataAddr)) == SOCKET_ERROR) {
CString error;
error.Format(_T("连接数据端口失败: %d"), WSAGetLastError());
AfxMessageBox(error);
closesocket(m_dataSocket);
m_dataSocket = INVALID_SOCKET;
return;
}
// 发送文件
FILE* file = _tfopen(localPath, _T("rb"));
if (file) {
char buffer[4096];
size_t bytesRead;
while ((bytesRead = fread(buffer, 1, sizeof(buffer), file)) > 0) {
send(m_dataSocket, buffer, bytesRead, 0);
}
fclose(file);
LogMessage(_T("文件上传完成: ") + remoteName);
} else {
AfxMessageBox(_T("无法打开本地文件"));
}
closesocket(m_dataSocket);
m_dataSocket = INVALID_SOCKET;
}
void CFTPClientDlg::DownloadFile(const CString& remoteName)
{
// 进入被动模式
CString command = _T("PASV\r\n");
send(m_controlSocket, CT2A(command), command.GetLength(), 0);
LogMessage(_T("客户端: ") + command);
// 等待服务器响应
Sleep(500);
// 发送RETR命令
command.Format(_T("RETR %s\r\n"), remoteName);
send(m_controlSocket, CT2A(command), command.GetLength(), 0);
LogMessage(_T("客户端: ") + command);
// 创建数据套接字
m_dataSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
if (m_dataSocket == INVALID_SOCKET) {
AfxMessageBox(_T("创建数据套接字失败"));
return;
}
// 连接数据端口
// 在实际实现中,需要解析PASV响应获取端口
sockaddr_in dataAddr;
dataAddr.sin_family = AF_INET;
dataAddr.sin_addr.s_addr = inet_addr("127.0.0.1");
dataAddr.sin_port = htons(1025); // 示例端口
if (connect(m_dataSocket, (sockaddr*)&dataAddr, sizeof(dataAddr)) == SOCKET_ERROR) {
CString error;
error.Format(_T("连接数据端口失败: %d"), WSAGetLastError());
AfxMessageBox(error);
closesocket(m_dataSocket);
m_dataSocket = INVALID_SOCKET;
return;
}
// 选择保存位置
CFileDialog dlg(FALSE, NULL, remoteName, OFN_OVERWRITEPROMPT, _T("所有文件 (*.*)|*.*||"));
if (dlg.DoModal() == IDOK) {
CString savePath = dlg.GetPathName();
// 接收文件
FILE* file = _tfopen(savePath, _T("wb"));
if (file) {
char buffer[4096];
int bytesReceived;
while ((bytesReceived = recv(m_dataSocket, buffer, sizeof(buffer), 0)) > 0) {
fwrite(buffer, 1, bytesReceived, file);
}
fclose(file);
LogMessage(_T("文件下载完成: ") + savePath);
} else {
AfxMessageBox(_T("无法保存文件"));
}
}
closesocket(m_dataSocket);
m_dataSocket = INVALID_SOCKET;
}
系统功能与特点
服务器功能
-
用户认证:支持多用户登录(admin/user/ftp)
-
文件操作:
- 上传/下载文件
- 创建/删除目录
- 删除文件
- 重命名文件
-
目录导航:
- 显示当前目录
- 改变工作目录
- 返回上级目录
-
数据传输模式:
- 主动模式(PORT)
- 被动模式(PASV)
-
日志记录:记录所有操作和连接信息
客户端功能
-
连接管理:
- 连接/断开服务器
- 支持自定义主机和端口
-
命令执行:
- 支持所有标准FTP命令
- 命令历史记录
-
文件管理:
- 上传/下载文件
- 目录浏览
- 文件树形视图
-
用户界面:
- 直观的图形界面
- 实时日志显示
- 状态指示器
技术实现细节
1. 网络通信
- 使用Winsock 2.2实现TCP通信
- 控制连接(端口21)用于命令传输
- 数据连接(动态端口)用于文件传输
2. 协议实现
- 支持标准FTP协议命令
- 实现PASV和PORT模式
- 处理服务器响应码
3. 文件操作
- 使用Windows API进行文件/目录操作
- 支持二进制和ASCII模式传输
- 大文件分块传输
4. 多线程处理
- 每个客户端连接使用独立线程
- 异步接收服务器响应
- 后台文件传输
参考代码 Ftp服务器与客户端VC++ www.youwenfan.com/contentcss/122495.html
编译与运行
服务器编译
- 创建Win32控制台应用程序
- 添加上述FTPServer.cpp代码
- 链接ws2_32.lib
- 编译运行
客户端编译
- 创建MFC应用程序(对话框)
- 添加上述FTPClientDlg.cpp代码
- 添加资源文件(图标等)
- 编译运行
使用说明
- 先运行服务器程序
- 运行客户端程序
- 输入服务器地址、端口、用户名和密码
- 点击"连接"按钮
- 使用命令或图形界面操作
安全增强建议
-
加密传输:
// 使用SSL/TLS加密 SSL_CTX* ctx = SSL_CTX_new(SSLv23_client_method()); SSL* ssl = SSL_new(ctx); SSL_set_fd(ssl, socket); SSL_connect(ssl); -
用户管理:
// 从数据库加载用户 void LoadUsersFromDB(const char* dbPath) { // 连接数据库 sqlite3* db; sqlite3_open(dbPath, &db); // 查询用户表 sqlite3_stmt* stmt; sqlite3_prepare_v2(db, "SELECT username, password FROM users", -1, &stmt, NULL); while (sqlite3_step(stmt) == SQLITE_ROW) { const char* username = (const char*)sqlite3_column_text(stmt, 0); const char* password = (const char*)sqlite3_column_text(stmt, 1); users[username] = password; } sqlite3_finalize(stmt); sqlite3_close(db); } -
权限控制:
// 检查用户权限 bool CheckPermission(const char* username, const char* operation) { if (strcmp(operation, "DELETE") == 0 && strcmp(username, "admin") != 0) { return false; } return true; } -
日志记录增强:
// 记录详细操作日志 void LogDetailedAction(const char* username, const char* action, const char* details) { SYSTEMTIME st; GetLocalTime(&st); char logEntry[512]; sprintf(logEntry, "[%04d-%02d-%02d %02d:%02d:%02d] User: %s, Action: %s, Details: %s\n", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, username, action, details); // 写入日志文件 }
扩展功能建议
-
断点续传:
// 实现REST命令 void ResumeTransfer(SOCKET dataSocket, const char* filename, long offset) { FILE* file = fopen(filename, "rb"); if (file) { fseek(file, offset, SEEK_SET); // 发送剩余部分 } } -
目录同步:
// 递归同步目录 void SyncDirectories(const char* localPath, const char* remotePath) { // 比较本地和远程文件 // 上传/下载差异文件 } -
带宽控制:
// 限制传输速度 void ThrottleTransfer(SOCKET socket, int maxKBps) { // 使用定时器控制发送速率 } -
文件压缩:
// 传输前压缩文件 void CompressFile(const char* src, const char* dest) { // 使用zlib压缩 } -
计划任务:
// 定时执行文件传输 void ScheduleTransfer(const char* schedule, const char* command) { // 使用Windows任务计划程序 }
常见问题解决
-
连接被拒绝:
- 检查服务器是否运行
- 检查防火墙设置
- 确认端口正确
-
文件传输中断:
- 检查网络连接
- 增加超时设置
- 使用断点续传
-
权限问题:
- 以管理员身份运行服务器
- 检查文件/目录权限
- 使用正确的用户名/密码
-
性能问题:
- 使用被动模式
- 增加缓冲区大小
- 使用更快的磁盘
注意:此实现为简化版FTP系统,实际生产环境应使用更健壮的实现,如FileZilla Server或ProFTPD,并考虑添加SSL/TLS加密、更精细的权限控制、配额管理等高级功能。