在这篇文章中,我们将学习如何使用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
(登录不正确)PWD
530 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)PASV
227 Entering Passive Mode (h1,h2,h3,h4,p1,p2)
(进入被动模式),其中h1,h2,h3,h4
是IP地址,p1,p2
是被动模式下的数据端口。LIST
150 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.
(传输完成)QUIT
221 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命令,添加认证、加密、和更复杂的文件管理等高级功能。
本文作者:rick
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!