在这篇文章中,我们将学习如何使用C#开发一个简单的FTP服务器。FTP(File Transfer Protocol)是一种用于在网络上的计算机之间传输文件的标准网络协议。C#提供了强大的网络编程功能,使得我们可以方便地创建自定义的FTP服务器。本文将引导你通过从基础开始一步一步地构建一个简单的FTP服务器应用程序。
在开始编码之前,确保你的项目已经正确设置。新建一个C#控制台应用程序项目,并确保添加了必要的引用。
在你的程序顶部,需要引用一些必要的命名空间来实现网络通信和文件操作:
C#using System;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
创建一个FTPServer类以处理FTP服务器的所有逻辑。
C#using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Globalization;
namespace AppFtpServer
{
class FtpServer
{
private TcpListener _listener;
private bool _isRunning;
private string _rootDirectory;
public FtpServer(string ip, int port, string rootDirectory)
{
_listener = new TcpListener(IPAddress.Parse(ip), port);
_rootDirectory = rootDirectory;
}
public async Task Start()
{
_isRunning = true;
_listener.Start();
Console.WriteLine($"FTP Server started. Listening for connections on port {((IPEndPoint)_listener.LocalEndpoint).Port}...");
while (_isRunning)
{
TcpClient client = await _listener.AcceptTcpClientAsync();
_ = HandleClientAsync(client);
}
}
private async Task HandleClientAsync(TcpClient client)
{
using (NetworkStream networkStream = client.GetStream())
using (StreamReader reader = new StreamReader(networkStream))
using (StreamWriter writer = new StreamWriter(networkStream) { AutoFlush = true })
{
await writer.WriteLineAsync("220 Welcome to Simple FTP Server");
string username = null;
bool isLoggedIn = false;
string currentDirectory = _rootDirectory;
TcpListener dataListener = null; // New field for managing the data connection
while (true)
{
string command = await reader.ReadLineAsync();
if (string.IsNullOrEmpty(command))
break;
string[] parts = command.Split(' ');
string cmd = parts[0].ToUpper();
switch (cmd)
{
case "USER":
username = parts[1];
await writer.WriteLineAsync("331 User name okay, need password");
break;
case "PASS":
if (username == "admin" && parts[1] == "password")
{
isLoggedIn = true;
await writer.WriteLineAsync("230 User logged in");
}
else
{
await writer.WriteLineAsync("530 Login incorrect");
}
break;
case "PWD":
if (!isLoggedIn)
{
await writer.WriteLineAsync("530 Not logged in");
break;
}
string relativePath = GetRelativePath(_rootDirectory, currentDirectory);
await writer.WriteLineAsync($"257 \"{relativePath}\" is current directory");
break;
case "CWD":
if (!isLoggedIn)
{
await writer.WriteLineAsync("530 Not logged in");
break;
}
if (parts.Length < 2)
{
await writer.WriteLineAsync("501 Syntax error in parameters or arguments");
break;
}
string requestedPath = parts[1];
string newPath;
newPath = _rootDirectory + requestedPath;
if (!newPath.StartsWith(_rootDirectory, StringComparison.OrdinalIgnoreCase))
{
await writer.WriteLineAsync("550 Requested action not taken. Access denied.");
break;
}
if (Directory.Exists(newPath))
{
currentDirectory = newPath;
await writer.WriteLineAsync("250 Directory successfully changed.");
}
else
{
await writer.WriteLineAsync("550 Requested action not taken. Directory not found.");
}
break;
case "RETR":
if (!isLoggedIn)
{
await writer.WriteLineAsync("530 Not logged in");
break;
}
if (parts.Length < 2)
{
await writer.WriteLineAsync("501 Syntax error in parameters or arguments");
break;
}
string filePath = _rootDirectory + "\\" + parts[1];
if (!filePath.StartsWith(_rootDirectory, StringComparison.OrdinalIgnoreCase))
{
await writer.WriteLineAsync("550 Requested action not taken. File access denied.");
break;
}
if (File.Exists(filePath))
{
await writer.WriteLineAsync("150 Opening data connection for file transfer.");
using (TcpClient dataClient = await dataListener.AcceptTcpClientAsync())
using (NetworkStream dataStream = dataClient.GetStream())
using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
await fileStream.CopyToAsync(dataStream);
}
await writer.WriteLineAsync("226 Transfer complete.");
}
else
{
await writer.WriteLineAsync("550 File not found.");
}
break;
case "TYPE":
await writer.WriteLineAsync("200 Type set to I");
break;
case "PASV":
// Dispose of the previous dataListener if it exists
dataListener?.Stop();
dataListener = new TcpListener(IPAddress.Any, 0);
dataListener.Start();
int pasvPort = ((IPEndPoint)dataListener.LocalEndpoint).Port;
byte[] pasvIpBytes = IPAddress.Parse(((IPEndPoint)client.Client.LocalEndPoint).Address.ToString()).GetAddressBytes();
await writer.WriteLineAsync($"227 Entering Passive Mode ({pasvIpBytes[0]},{pasvIpBytes[1]},{pasvIpBytes[2]},{pasvIpBytes[3]},{pasvPort / 256},{pasvPort % 256})");
break;
case "LIST":
await writer.WriteLineAsync("150 Here comes the directory listing.");
using (TcpClient dataClient = await dataListener.AcceptTcpClientAsync())
using (NetworkStream dataStream = dataClient.GetStream())
using (StreamWriter dataWriter = new StreamWriter(dataStream, Encoding.UTF8))
{
string[] entries = Directory.GetFileSystemEntries(currentDirectory);
foreach (string entry in entries)
{
FileAttributes attr = File.GetAttributes(entry);
string name = Path.GetFileName(entry);
DateTime lastWriteTime = File.GetLastWriteTime(entry);
string dateStr = lastWriteTime.ToString("MMM dd HH:mm");
string line = (attr & FileAttributes.Directory) == FileAttributes.Directory
? $"drwxr-xr-x 1 owner group 0 {dateStr} {name}"
: $"-rw-r--r-- 1 owner group {new FileInfo(entry).Length} {dateStr} {name}";
await dataWriter.WriteLineAsync(line);
}
}
await writer.WriteLineAsync("226 Directory send OK.");
break;
case "STOR":
if (!isLoggedIn)
{
await writer.WriteLineAsync("530 Not logged in");
break;
}
if (parts.Length < 2)
{
await writer.WriteLineAsync("501 Syntax error in parameters or arguments");
break;
}
string saveFilePath = _rootDirectory + parts[1];
if (!saveFilePath.StartsWith(_rootDirectory, StringComparison.OrdinalIgnoreCase))
{
await writer.WriteLineAsync("550 Requested action not taken. Access denied.");
break;
}
await writer.WriteLineAsync("150 Opening data connection for file upload.");
using (TcpClient dataClient = await dataListener.AcceptTcpClientAsync())
using (NetworkStream dataStream = dataClient.GetStream())
using (FileStream fileStream = new FileStream(saveFilePath, FileMode.Create, FileAccess.Write))
{
await dataStream.CopyToAsync(fileStream);
}
await writer.WriteLineAsync("226 Transfer complete.");
break;
case "QUIT":
await writer.WriteLineAsync("221 Goodbye");
return;
default:
await writer.WriteLineAsync("502 Command not implemented");
break;
}
}
dataListener?.Stop();
}
}
private async Task AcceptDataConnectionAsync(TcpListener dataListener, string currentDirectory, StreamWriter controlWriter)
{
using (TcpClient dataClient = await dataListener.AcceptTcpClientAsync())
using (NetworkStream dataStream = dataClient.GetStream())
using (StreamWriter dataWriter = new StreamWriter(dataStream, Encoding.UTF8))
{
string[] entries = Directory.GetFileSystemEntries(currentDirectory);
foreach (string entry in entries)
{
FileAttributes attr = File.GetAttributes(entry);
string name = Path.GetFileName(entry);
DateTime lastWriteTime = File.GetLastWriteTime(entry);
string dateStr = lastWriteTime.ToString("MMM dd HH:mm");
string line = (attr & FileAttributes.Directory) == FileAttributes.Directory
? $"drwxr-xr-x 1 owner group 0 {dateStr} {name}"
: $"-rw-r--r-- 1 owner group {new FileInfo(entry).Length} {dateStr} {name}";
await dataWriter.WriteLineAsync(line);
}
}
dataListener.Stop();
await controlWriter.WriteLineAsync("226 Directory send OK.");
}
private string GetRelativePath(string rootPath, string fullPath)
{
string relativePath = Path.GetRelativePath(rootPath, fullPath);
return relativePath == "." ? "/" : "/" + relativePath.Replace('\\', '/');
}
}
}
注意:关于目录文件的处理不同FTP客户端好像有些不同,我这用的是FileZilla做的测试,这中间还有中文乱码问题,需要修改一个连接中的配置。

USER [用户名]331 User name okay, need password(用户名正确,需要密码)PASS [密码]230 User logged in(用户已登录)530 Login incorrect(登录不正确)PWD530 Not logged in(未登录)257 "[当前目录]" is current directory(当前目录)CWD [目录]530 Not logged in(未登录)501 Syntax error in parameters or arguments(参数或参数语法错误)550 Requested action not taken. Access denied.(请求的操作未执行,访问被拒绝)250 Directory successfully changed.(目录成功更改)550 Requested action not taken. Directory not found.(请求的操作未执行,未找到目录)RETR [文件]530 Not logged in(未登录)501 Syntax error in parameters or arguments(参数或参数语法错误)550 Requested action not taken. File access denied.(请求的操作未执行,文件访问被拒绝)150 Opening data connection for file transfer.(正在为文件传输打开数据连接)226 Transfer complete.(传输完成)550 File not found.(文件未找到)TYPE [类型] (在代码中,仅处理TYPE I,即二进制模式)200 Type set to I(类型设置为I)PASV227 Entering Passive Mode (h1,h2,h3,h4,p1,p2)(进入被动模式),其中h1,h2,h3,h4是IP地址,p1,p2是被动模式下的数据端口。LIST150 Here comes the directory listing.(这里是目录列表)226 Directory send OK.(目录发送成功)STOR [文件]530 Not logged in(未登录)501 Syntax error in parameters or arguments(参数或参数语法错误)550 Requested action not taken. Access denied.(请求的操作未执行,访问被拒绝)150 Opening data connection for file upload.(正在为文件上传打开数据连接)226 Transfer complete.(传输完成)QUIT221 Goodbye(再见)502 Command not implemented(命令未实现)在Main方法中实例化并启动你的FTP服务器:
C#public static async Task Main(string[] args)
{
string ip = "127.0.0.1";
int port = 21;
string rootDirectory;
if (args.Length > 0 && Directory.Exists(args[0]))
{
rootDirectory = Path.GetFullPath(args[0]);
}
else
{
rootDirectory = "d:\\book";
while (!Directory.Exists(rootDirectory))
{
Console.WriteLine("Directory does not exist. Please enter a valid path:");
rootDirectory = Console.ReadLine();
}
rootDirectory = Path.GetFullPath(rootDirectory);
}
Console.WriteLine($"Using root directory: {rootDirectory}");
FtpServer server = new FtpServer(ip, port, rootDirectory);
await server.Start();
}

本文介绍了如何用C#开发一个基本的FTP服务器。我们的服务器目前支持基本的FTP命令并且能够响应客户端的请求。虽然这是一个简化的版本,但它为更复杂和功能完整的FTP服务器打下了基础。可以尝试扩展这个服务器,支持更多的FTP命令,添加认证、加密、和更复杂的文件管理等高级功能。
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!