需求分析
假设我们需要实现一个基于网络的文件上传系统,用户可以通过客户端将本地文件上传到服务端。这种情况经常出现在文件存储和共享、云存储等应用场景中。使用Socket编程可以实现高效可靠的文件传输。
1、客户端需求:
- 用户可以选择本地文件进行上传。
- 用户需要输入服务端的IP地址和端口号。
- 客户端需要将选择的文件发送给服务端进行保存。
2、服务端需求:
- 服务端需要监听指定的端口,等待客户端连接请求。
- 接收到客户端连接后,服务端需要接收文件数据。
- 服务端需要将接收到的文件保存到指定位置。
3、文件传输需求:
- 传输协议:使用TCP协议确保可靠的数据传输。
- 文件分片:为了减小内存开销和网络负载,将大文件分成多个较小的数据包进行传输。
- 文件校验:传输过程中需要对文件进行校验,确保数据的完整性。
4、错误处理需求:
- 连接错误:需要处理连接失败、超时等错误情况。
- 文件错误:需要处理文件读取失败、传输中断等错误情况。
- 异常处理:需要处理网络异常、IO异常等情况,并提供相应的错误提示和处理机制。
基于以上需求,我们可以使用C#的Socket编程实现一个文件上传系统,包括客户端和服务端的程序。在程序中使用Socket进行网络连接和数据传输,同时对连接错误和文件错误进行适当处理和异常捕获。
客户端代码和实现过程
dotnet new wpf -n "FileUploaderClient"
MainWindow.xaml:
<Window x:Class="FileUploaderClient.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="文件上传客户端" Height="350" Width="500">
<Grid>
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="选择要上传的文件:" Margin="0 0 0 10" />
<Button Content="浏览..." Click="BrowseButton_Click" Width="80" Height="30" Margin="0 0 0 10" />
<TextBox x:Name="FilePathTextBox" Text="{Binding FilePath}" Width="300" Height="30" Margin="0 0 0 10" />
<TextBlock Text="输入服务端IP地址:" Margin="0 0 0 10" />
<TextBox x:Name="ServerIPTextBox" Text="{Binding ServerIP}" Width="200" Height="30" Margin="0 0 0 10" />
<TextBlock Text="输入服务端端口号:" Margin="0 0 0 10" />
<TextBox x:Name="ServerPortTextBox" Text="{Binding ServerPort}" Width="100" Height="30" Margin="0 0 0 10" />
<Button Content="上传" Click="UploadButton_Click" Width="80" Height="30" Margin="0 0 0 10" />
<TextBlock x:Name="ResultTextBlock" Text="{Binding ResultMessage}" Margin="0 10" TextWrapping="Wrap" />
</StackPanel>
</Grid>
</Window>
MainWindow.xaml.cs:
using System;
using System.ComponentModel;
using System.IO;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Windows;
namespace FileUploaderClient
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private string filePath;
public string FilePath
{
get { return filePath; }
set
{
filePath = value;
OnPropertyChanged("FilePath");
}
}
private string serverIP;
public string ServerIP
{
get { return serverIP; }
set
{
serverIP = value;
OnPropertyChanged("ServerIP");
}
}
private int serverPort;
public int ServerPort
{
get { return serverPort; }
set
{
serverPort = value;
OnPropertyChanged("ServerPort");
}
}
private string resultMessage;
public string ResultMessage
{
get { return resultMessage; }
set
{
resultMessage = value;
OnPropertyChanged("ResultMessage");
}
}
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void BrowseButton_Click(object sender, RoutedEventArgs e)
{
Microsoft.Win32.OpenFileDialog openFileDialog = new Microsoft.Win32.OpenFileDialog();
if (openFileDialog.ShowDialog() == true)
{
FilePath = openFileDialog.FileName;
}
}
private void UploadButton_Click(object sender, RoutedEventArgs e)
{
try
{
// 读取本地文件
byte[] fileData = File.ReadAllBytes(FilePath);
// 连接服务端并发送文件
using (TcpClient client = new TcpClient(ServerIP, ServerPort))
{
using (NetworkStream stream = client.GetStream())
{
// 发送文件名和文件长度
string fileName = Path.GetFileName(FilePath);
byte[] fileNameBytes = Encoding.UTF8.GetBytes(fileName);
byte[] fileNameLengthBytes = BitConverter.GetBytes(fileNameBytes.Length);
byte[] fileLengthBytes = BitConverter.GetBytes(fileData.Length);
stream.Write(fileNameLengthBytes, 0, 4);
stream.Write(fileNameBytes, 0, fileNameBytes.Length);
stream.Write(fileLengthBytes, 0, 4);
// 发送文件内容
int bufferSize = 1024;
int bytesSent = 0;
while (bytesSent < fileData.Length)
{
int remainingBytes = fileData.Length - bytesSent;
int bytesToSend = Math.Min(bufferSize, remainingBytes);
stream.Write(fileData, bytesSent, bytesToSend);
bytesSent += bytesToSend;
}
ResultMessage = "文件上传成功!";
}
}
}
catch (Exception ex)
{
ResultMessage = "文件上传失败:" + ex.Message;
}
}
}
}
使用该客户端程序,用户可以选择本地文件进行上传,并输入服务端的IP地址和端口号。客户端会将选择的文件发送给服务端进行保存。
这个示例实现了基于TCP协议的文件上传功能,使用TcpClient和NetworkStream进行连接和数据传输。文件被分成较小的数据包进行传输,发送前会计算文件名和文件长度,并通过4字节的长度前缀指示接收方应该接收多少数据。
服务端代码和实现过程
dotnet new wpf -n "FileUploaderServer"
MainWindow.xaml:
<Window x:Class="FileUploaderServer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="文件上传服务端" Height="350" Width="500">
<Grid>
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="输入要监听的端口号:" Margin="0 0 0 10" />
<TextBox x:Name="PortTextBox" Text="{Binding Port}" Width="100" Height="30" Margin="0 0 0 10" />
<Button Content="启动服务" Click="StartButton_Click" Width="80" Height="30" Margin="0 0 0 10" />
<TextBlock x:Name="ResultTextBlock" Text="{Binding ResultMessage}" Margin="0 10" TextWrapping="Wrap" />
</StackPanel>
</Grid>
</Window>
MainWindow.xaml.cs:
using System;
using System.ComponentModel;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
using System.Windows;
namespace FileUploaderServer
{
public partial class MainWindow : Window, INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int port;
public int Port
{
get { return port; }
set
{
port = value;
OnPropertyChanged("Port");
}
}
private string resultMessage;
public string ResultMessage
{
get { return resultMessage; }
set
{
resultMessage = value;
OnPropertyChanged("ResultMessage");
}
}
private TcpListener serverListener;
private Thread serverThread;
public MainWindow()
{
InitializeComponent();
DataContext = this;
}
private void OnPropertyChanged(string propertyName)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
private void StartButton_Click(object sender, RoutedEventArgs e)
{
try
{
// 启动服务端监听
IPAddress ipAddress = IPAddress.Any;
serverListener = new TcpListener(ipAddress, Port);
serverListener.Start();
// 启动服务端线程
serverThread = new Thread(new ThreadStart(ServerThreadProc));
serverThread.IsBackground = true;
serverThread.Start();
ResultMessage = "服务启动成功!";
}
catch (Exception ex)
{
ResultMessage = "服务启动失败:" + ex.Message;
}
}
private void ServerThreadProc()
{
while (true)
{
try
{
// 接受客户端连接请求
TcpClient client = serverListener.AcceptTcpClient();
// 处理客户端连接请求
Thread clientThread = new Thread(new ParameterizedThreadStart(ClientThreadProc));
clientThread.IsBackground = true;
clientThread.Start(client);
}
catch (Exception)
{
break;
}
}
}
private void ClientThreadProc(object parameter)
{
TcpClient client = (TcpClient)parameter;
try
{
using (client)
{
using (NetworkStream stream = client.GetStream())
{
// 读取文件名和文件长度
byte[] fileNameLengthBytes = new byte[4];
stream.Read(fileNameLengthBytes, 0, 4);
int fileNameLength = BitConverter.ToInt32(fileNameLengthBytes, 0);
byte[] fileNameBytes = new byte[fileNameLength];
stream.Read(fileNameBytes, 0, fileNameLength);
string fileName = Encoding.UTF8.GetString(fileNameBytes);
byte[] fileLengthBytes = new byte[4];
stream.Read(fileLengthBytes, 0, 4);
int fileLength = BitConverter.ToInt32(fileLengthBytes, 0);
// 接收文件内容
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int bytesRead = 0;
int totalBytesRead = 0;
byte[] fileData = new byte[fileLength];
while (totalBytesRead < fileLength && (bytesRead = stream.Read(buffer, 0, Math.Min(bufferSize, fileLength - totalBytesRead))) > 0)
{
Buffer.BlockCopy(buffer, 0, fileData, totalBytesRead, bytesRead);
totalBytesRead += bytesRead;
}
// 保存文件到本地
string savePath = Path.Combine(Environment.CurrentDirectory, "Uploads", fileName);
if (File.Exists(savePath))
{
savePath = Path.Combine(Environment.CurrentDirectory, "Uploads", Path.GetFileNameWithoutExtension(fileName) + "_" + DateTime.Now.ToString("yyyyMMddHHmmssfff") + Path.GetExtension(fileName));
}
using (FileStream fileStream = new FileStream(savePath, FileMode.CreateNew))
{
fileStream.Write(fileData, 0, fileLength);
}
ResultMessage = "文件保存成功:" + savePath;
}
}
}
catch (Exception ex)
{
ResultMessage = "文件保存失败:" + ex.Message;
}
}
}
}
使用该服务端程序,用户可以输入要监听的端口号,并启动服务端监听。当有客户端连接时,服务端会接收文件数据,并保存到指定位置。
这个示例实现了基于TCP协议的文件接收和保存功能,使用TcpListener和TcpClient进行监听和连接,使用NetworkStream进行数据传输。文件被分成较小的数据包进行传输,发送前会计算文件名和文件长度,并通过4字节的长度前缀指示接收方应该接收多少数据。
运行结果
启动服务端,开启端口12345。
启动客户端程序,配置服务端地址。