需求
服务端使用多线程向多个客户端发送图片,客户端用队列接收图片。客户端用10个线程从队列里取出图片处理并返回结果给服务端
我们来构建一个满足你所有要求的 高性能 WPF TCP 图片处理系统。
这个系统将包含:
- 多线程服务端:能够同时接收多个客户端连接,并向它们并发发送图片任务。
- 队列化客户端 :使用队列(
ConcurrentQueue)来缓冲接收到的图片,避免处理不及时导致的数据丢失或阻塞。 - 多线程图片处理:客户端内部启动 10 个工作线程,从队列中取出图片进行处理(例如,我们可以进行一个简单的灰度转换作为示例)。
- 结果回传:客户端处理完一张图片后,会将处理结果(例如,处理后的图片数据或状态信息)发送回服务端。
一、整体架构设计
服务端 (TcpImageServer)
- 主监听线程:持续监听客户端连接。
- 客户端管理 :为每个新连接创建一个独立的
ClientHandler线程 / 任务。 - 图片发送 :
ClientHandler负责向对应的客户端发送图片数据。 - 结果接收 :
ClientHandler同时监听客户端返回的处理结果。
客户端 (TcpImageClient)
- 连接管理:负责与服务端建立和维护连接。
- 数据接收线程 :独立线程,持续从网络流读取数据,将完整的图片数据包解析后放入
ConcurrentQueue。 - 图片处理队列 :
ConcurrentQueue<ImageData>用于存储待处理的图片信息。 - 处理线程池 :10 个工作线程,循环从队列中
TryDequeue图片数据进行处理。 - 结果发送:处理完成后,将结果打包并发送回服务端。
二、核心数据结构与协议定义
为了方便通信,我们先定义一套简单的应用层协议和数据结构。
1. 协议枚举(双方共用)
csharp
运行
// Protocol.cs (可以放在一个共享类库中)
public enum MessageType : byte
{
ImageData = 0x01, // 服务端发送的图片数据
ProcessingResult = 0x02, // 客户端返回的处理结果
Heartbeat = 0x03 // 心跳包 (可选,用于连接保活)
}
2. 图片数据结构(服务端 -> 客户端)
csharp
运行
// ImageData.cs (共享)
[Serializable]
public class ImageData
{
public Guid TaskId { get; set; } // 任务唯一ID,用于匹配请求和响应
public byte[] ImageBytes { get; set; } // 图片的字节数组
public string ImageName { get; set; } // 图片名称
}
3. 处理结果结构(客户端 -> 服务端)
csharp
运行
// ProcessingResult.cs (共享)
[Serializable]
public class ProcessingResult
{
public Guid TaskId { get; set; } // 对应的任务ID
public bool IsSuccess { get; set; } // 是否处理成功
public string Message { get; set; } // 消息(成功/失败原因)
public byte[] ProcessedImageBytes { get; set; } // 处理后的图片字节数组 (可选)
}
三、服务端实现 (TcpImageServer)
1. 服务端核心逻辑 (MainWindow.xaml.cs)
csharp
运行
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
namespace TcpImageServer
{
public partial class MainWindow : Window
{
private TcpListener _tcpListener;
private readonly ConcurrentDictionary<Guid, TcpClient> _connectedClients = new ConcurrentDictionary<Guid, TcpClient>();
private CancellationTokenSource _cts;
public MainWindow()
{
InitializeComponent();
}
private async void StartServerBtn_Click(object sender, RoutedEventArgs e)
{
if (_tcpListener == null)
{
int port = int.Parse(PortTextBox.Text);
_tcpListener = new TcpListener(IPAddress.Any, port);
_cts = new CancellationTokenSource();
_tcpListener.Start();
StatusTextBlock.Text = "服务已启动,等待客户端连接...";
StartServerBtn.Content = "停止服务";
// 开始接受客户端连接
await AcceptClientsAsync(_cts.Token);
}
else
{
StopServer();
}
}
private async Task AcceptClientsAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
TcpClient client = await _tcpListener.AcceptTcpClientAsync();
Guid clientId = Guid.NewGuid();
_connectedClients.TryAdd(clientId, client);
Dispatcher.Invoke(() =>
{
StatusTextBlock.Text = $"客户端 {clientId} 已连接。当前连接数: {_connectedClients.Count}";
ClientsListBox.Items.Add(clientId);
});
// 为每个客户端创建一个独立的处理任务
_ = HandleClientAsync(client, clientId, token);
}
catch (Exception ex)
{
if (!token.IsCancellationRequested)
{
Dispatcher.Invoke(() => StatusTextBlock.Text = $"接受连接失败: {ex.Message}");
}
}
}
}
private async Task HandleClientAsync(TcpClient client, Guid clientId, CancellationToken token)
{
using (NetworkStream stream = client.GetStream())
{
byte[] buffer = new byte[4096];
try
{
while (!token.IsCancellationRequested && client.Connected)
{
// 这里服务端主要是发送图片,但也可以监听客户端的消息(如心跳或结果)
// 为了简单起见,我们假设服务端主动发送图片,这里可以留空或实现心跳接收逻辑
// 实际场景中,你可能需要一个更复杂的消息循环来处理双向通信
await Task.Delay(100, token); // 防止CPU空转
}
}
catch (Exception ex)
{
Dispatcher.Invoke(() => StatusTextBlock.Text = $"客户端 {clientId} 通信异常: {ex.Message}");
}
finally
{
if (client.Connected)
client.Close();
_connectedClients.TryRemove(clientId, out _);
Dispatcher.Invoke(() =>
{
StatusTextBlock.Text = $"客户端 {clientId} 已断开。当前连接数: {_connectedClients.Count}";
ClientsListBox.Items.Remove(clientId);
});
}
}
}
// 向所有连接的客户端广播一张图片
private async void SendImageToAllBtn_Click(object sender, RoutedEventArgs e)
{
if (_connectedClients.IsEmpty)
{
MessageBox.Show("没有连接的客户端!");
return;
}
OpenFileDialog openFileDialog = new OpenFileDialog
{
Filter = "图片文件 (*.jpg;*.jpeg;*.png;*.bmp)|*.jpg;*.jpeg;*.png;*.bmp"
};
if (openFileDialog.ShowDialog() == true)
{
try
{
byte[] imageBytes = File.ReadAllBytes(openFileDialog.FileName);
string imageName = Path.GetFileName(openFileDialog.FileName);
Dispatcher.Invoke(() => StatusTextBlock.Text = $"开始向 {_connectedClients.Count} 个客户端发送图片: {imageName}");
// 并行向所有客户端发送
var sendTasks = new List<Task>();
foreach (var clientPair in _connectedClients)
{
sendTasks.Add(SendImageToClientAsync(clientPair.Value, new ImageData
{
TaskId = Guid.NewGuid(),
ImageBytes = imageBytes,
ImageName = imageName
}));
}
await Task.WhenAll(sendTasks);
Dispatcher.Invoke(() => StatusTextBlock.Text = "图片发送任务已全部提交。");
}
catch (Exception ex)
{
MessageBox.Show($"发送图片失败: {ex.Message}");
}
}
}
// 向单个客户端发送图片
private async Task SendImageToClientAsync(TcpClient client, ImageData imageData)
{
if (!client.Connected) return;
try
{
using (NetworkStream stream = client.GetStream())
using (MemoryStream ms = new MemoryStream())
{
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
// 1. 写入消息类型
await stream.WriteAsync(new byte[] { (byte)MessageType.ImageData }, 0, 1);
// 2. 写入序列化后的图片数据长度 (4字节)
formatter.Serialize(ms, imageData);
byte[] dataBytes = ms.ToArray();
byte[] lengthBytes = BitConverter.GetBytes(dataBytes.Length);
await stream.WriteAsync(lengthBytes, 0, lengthBytes.Length);
// 3. 写入图片数据
await stream.WriteAsync(dataBytes, 0, dataBytes.Length);
await stream.FlushAsync();
}
}
catch (Exception ex)
{
Dispatcher.Invoke(() => StatusTextBlock.Text = $"向客户端发送图片失败: {ex.Message}");
}
}
private void StopServer()
{
_cts?.Cancel();
_tcpListener?.Stop();
foreach (var client in _connectedClients.Values)
{
if (client.Connected)
client.Close();
}
_connectedClients.Clear();
_tcpListener = null;
StatusTextBlock.Text = "服务已停止。";
StartServerBtn.Content = "启动服务";
ClientsListBox.Items.Clear();
}
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
StopServer();
base.OnClosing(e);
}
}
}
2. 服务端界面 (MainWindow.xaml)
xml
<Window x:Class="TcpImageServer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="TCP 图片服务端" Height="450" Width="800">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Content="监听端口:" Grid.Row="0" Grid.Column="0"/>
<TextBox x:Name="PortTextBox" Text="8888" Grid.Row="0" Grid.Column="1" Margin="5,0"/>
<Button x:Name="StartServerBtn" Content="启动服务" Click="StartServerBtn_Click" Grid.Row="0" Grid.Column="2" Margin="5,0" Width="100"/>
<Label Content="连接的客户端:" Grid.Row="1" Grid.Column="0" Margin="0,10,0,0"/>
<ListBox x:Name="ClientsListBox" Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" Margin="5,0"/>
<TextBlock x:Name="StatusTextBlock" Text="准备就绪" Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" Margin="5,10,0,0"/>
<Button x:Name="SendImageToAllBtn" Content="发送图片给所有客户端" Click="SendImageToAllBtn_Click" Grid.Row="3" Grid.Column="2" Margin="5,10,0,0" HorizontalAlignment="Right"/>
</Grid>
</Window>
四、客户端实现 (TcpImageClient)
1. 客户端核心逻辑 (MainWindow.xaml.cs)
csharp
运行
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
namespace TcpImageClient
{
public partial class MainWindow : Window
{
private TcpClient _tcpClient;
private NetworkStream _networkStream;
private readonly ConcurrentQueue<ImageData> _imageQueue = new ConcurrentQueue<ImageData>();
private readonly List<Thread> _workerThreads = new List<Thread>();
private CancellationTokenSource _cts;
private bool _isConnected = false;
public MainWindow()
{
InitializeComponent();
InitializeWorkerThreads();
}
// 初始化10个处理线程
private void InitializeWorkerThreads()
{
for (int i = 0; i < 10; i++)
{
Thread thread = new Thread(ProcessImageLoop)
{
IsBackground = true,
Name = $"WorkerThread-{i + 1}"
};
_workerThreads.Add(thread);
}
}
private void ConnectBtn_Click(object sender, RoutedEventArgs e)
{
if (!_isConnected)
{
string ip = IpTextBox.Text;
if (int.TryParse(PortTextBox.Text, out int port))
{
_cts = new CancellationTokenSource();
_ = ConnectAndListenAsync(ip, port, _cts.Token);
// 启动工作线程
_workerThreads.ForEach(t => { if (!t.IsAlive) t.Start(); });
}
else
{
MessageBox.Show("无效的端口号!");
}
}
else
{
Disconnect();
}
}
private async Task ConnectAndListenAsync(string ip, int port, CancellationToken token)
{
try
{
_tcpClient = new TcpClient();
await _tcpClient.ConnectAsync(ip, port);
_networkStream = _tcpClient.GetStream();
_isConnected = true;
Dispatcher.Invoke(() =>
{
StatusTextBlock.Text = $"成功连接到服务端 {ip}:{port}";
ConnectBtn.Content = "断开连接";
QueueCountTextBlock.Text = "0";
});
// 持续监听服务端消息
await ListenForMessagesAsync(token);
}
catch (Exception ex)
{
Dispatcher.Invoke(() => StatusTextBlock.Text = $"连接失败: {ex.Message}");
Disconnect();
}
}
private async Task ListenForMessagesAsync(CancellationToken token)
{
byte[] buffer = new byte[4096];
try
{
while (!token.IsCancellationRequested && _tcpClient.Connected)
{
// 1. 读取消息类型
int bytesRead = await _networkStream.ReadAsync(buffer, 0, 1, token);
if (bytesRead == 0) break; // 连接关闭
MessageType msgType = (MessageType)buffer[0];
// 2. 读取数据长度
bytesRead = await _networkStream.ReadAsync(buffer, 0, 4, token);
if (bytesRead == 0) break;
int dataLength = BitConverter.ToInt32(buffer, 0);
// 3. 读取数据内容
byte[] dataBytes = new byte[dataLength];
int totalRead = 0;
while (totalRead < dataLength)
{
int read = await _networkStream.ReadAsync(dataBytes, totalRead, dataLength - totalRead, token);
if (read == 0) break;
totalRead += read;
}
if (totalRead == dataLength)
{
ProcessReceivedData(msgType, dataBytes);
}
}
}
catch (OperationCanceledException)
{
// 正常断开
}
catch (Exception ex)
{
Dispatcher.Invoke(() => StatusTextBlock.Text = $"接收数据异常: {ex.Message}");
}
finally
{
Disconnect();
}
}
private void ProcessReceivedData(MessageType msgType, byte[] dataBytes)
{
switch (msgType)
{
case MessageType.ImageData:
using (MemoryStream ms = new MemoryStream(dataBytes))
{
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
ImageData imageData = (ImageData)formatter.Deserialize(ms);
// 将图片数据放入队列
_imageQueue.Enqueue(imageData);
Dispatcher.Invoke(() =>
{
StatusTextBlock.Text = $"收到图片: {imageData.ImageName},已加入队列。";
QueueCountTextBlock.Text = _imageQueue.Count.ToString();
});
}
break;
case MessageType.ProcessingResult:
// 客户端一般不接收这个,除非有特殊需求
break;
}
}
// 工作线程的处理循环
private void ProcessImageLoop()
{
while (!_cts?.IsCancellationRequested ?? false)
{
if (_imageQueue.TryDequeue(out ImageData imageData))
{
try
{
// --- 在这里执行你的图片处理逻辑 ---
Dispatcher.Invoke(() => StatusTextBlock.Text = $"线程 {Thread.CurrentThread.Name} 正在处理图片: {imageData.ImageName}");
// 示例:转换为灰度图
byte[] processedBytes = ConvertToGrayscale(imageData.ImageBytes);
// --- 处理完成 ---
// 准备返回结果
var result = new ProcessingResult
{
TaskId = imageData.TaskId,
IsSuccess = true,
Message = "处理成功",
ProcessedImageBytes = processedBytes // 可以选择是否返回处理后的图片
};
// 将结果发送回服务端
_ = SendResultToServerAsync(result);
Dispatcher.Invoke(() =>
{
StatusTextBlock.Text = $"线程 {Thread.CurrentThread.Name} 处理完成: {imageData.ImageName}";
QueueCountTextBlock.Text = _imageQueue.Count.ToString();
});
}
catch (Exception ex)
{
// 处理失败,也返回结果
var errorResult = new ProcessingResult
{
TaskId = imageData.TaskId,
IsSuccess = false,
Message = $"处理失败: {ex.Message}"
};
_ = SendResultToServerAsync(errorResult);
Dispatcher.Invoke(() =>
{
StatusTextBlock.Text = $"线程 {Thread.CurrentThread.Name} 处理失败: {imageData.ImageName} - {ex.Message}";
});
}
}
else
{
Thread.Sleep(10); // 队列空时,短暂休眠,减少CPU消耗
}
}
}
// 示例:将图片字节数组转换为灰度图字节数组 (使用WPF的BitmapSource)
private byte[] ConvertToGrayscale(byte[] imageBytes)
{
using (MemoryStream ms = new MemoryStream(imageBytes))
{
BitmapImage bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.StreamSource = ms;
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.EndInit();
FormatConvertedBitmap grayBitmap = new FormatConvertedBitmap(bitmapImage, PixelFormats.Gray8, null, 0);
using (MemoryStream grayMs = new MemoryStream())
{
PngBitmapEncoder encoder = new PngBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(grayBitmap));
encoder.Save(grayMs);
return grayMs.ToArray();
}
}
}
private async Task SendResultToServerAsync(ProcessingResult result)
{
if (!_isConnected || _networkStream == null) return;
try
{
using (MemoryStream ms = new MemoryStream())
{
System.Runtime.Serialization.Formatters.Binary.BinaryFormatter formatter = new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
// 1. 写入消息类型
await _networkStream.WriteAsync(new byte[] { (byte)MessageType.ProcessingResult }, 0, 1);
// 2. 写入序列化后的结果长度
formatter.Serialize(ms, result);
byte[] dataBytes = ms.ToArray();
byte[] lengthBytes = BitConverter.GetBytes(dataBytes.Length);
await _networkStream.WriteAsync(lengthBytes, 0, lengthBytes.Length);
// 3. 写入结果数据
await _networkStream.WriteAsync(dataBytes, 0, dataBytes.Length);
await _networkStream.FlushAsync();
}
}
catch (Exception ex)
{
Dispatcher.Invoke(() => StatusTextBlock.Text = $"发送处理结果失败: {ex.Message}");
}
}
private void Disconnect()
{
_cts?.Cancel();
_isConnected = false;
if (_networkStream != null)
{
_networkStream.Dispose();
_networkStream = null;
}
_tcpClient?.Close();
_tcpClient = null;
Dispatcher.Invoke(() =>
{
StatusTextBlock.Text = "已断开连接";
ConnectBtn.Content = "连接服务端";
QueueCountTextBlock.Text = "0";
});
}
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
Disconnect();
base.OnClosing(e);
}
}
}
2. 客户端界面 (MainWindow.xaml)
xml
<Window x:Class="TcpImageClient.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="TCP 图片客户端" Height="350" Width="525">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Content="服务端IP:" Grid.Row="0" Grid.Column="0"/>
<TextBox x:Name="IpTextBox" Text="127.0.0.1" Grid.Row="0" Grid.Column="1" Margin="5,0"/>
<Label Content="端口:" Grid.Row="0" Grid.Column="2" Margin="10,0,0,0"/>
<TextBox x:Name="PortTextBox" Text="8888" Grid.Row="0" Grid.Column="3" Margin="5,0" Width="80"/>
<Button x:Name="ConnectBtn" Content="连接服务端" Click="ConnectBtn_Click" Grid.Row="0" Grid.Column="4" Margin="10,0" Width="120"/>
<Label Content="任务队列长度:" Grid.Row="1" Grid.Column="0" Margin="0,10,0,0"/>
<TextBlock x:Name="QueueCountTextBlock" Text="0" Grid.Row="1" Grid.Column="1" Margin="5,10,0,0"/>
<TextBlock x:Name="StatusTextBlock" Text="准备就绪" Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="5" Margin="0,10,0,0"/>
</Grid>
</Window>
五、关键点说明与优化建议
-
并发与线程安全:
- 使用
ConcurrentQueue是线程安全的,确保了生产者(接收线程)和消费者(10 个处理线程)之间的数据交换不会产生竞态条件。 - UI 更新必须通过
Dispatcher.Invoke进行,因为 WPF UI 元素不是线程安全的。
- 使用
-
性能优化:
- 分块发送 / 接收:对于超大图片,示例中的一次性发送整个字节数组可能会占用大量内存。可以将其修改为分块传输(例如,每次发送 8KB)。
- 对象池 :如果图片处理任务的创建和销毁非常频繁,可以考虑使用对象池(
ObjectPool<T>)来复用ImageData或ProcessingResult对象,减少 GC 压力。 - 异步处理 :图片处理逻辑如果是 CPU 密集型的,使用
Thread是合适的。如果处理过程中有 I/O 等待(如读写文件),可以考虑使用Task.Run结合async/await来提高线程利用率。 - 限制队列大小 :可以为
ConcurrentQueue设置一个最大容量,当队列满时,可以选择丢弃最旧的任务或通知服务端暂缓发送,防止内存溢出。
-
可靠性:
- 消息确认 :当前的
ProcessingResult就是一种确认机制。服务端可以根据TaskId来追踪哪些任务已经被客户端成功处理。 - 心跳机制 :可以在
HandleClientAsync和ListenForMessagesAsync中加入心跳包的发送和接收逻辑,以检测死连接。 - 异常处理 :代码中包含了完善的
try-catch块,确保单个客户端的异常不会导致整个服务端或客户端程序崩溃。 - 优雅关闭 :在窗口关闭或用户点击断开时,通过
CancellationTokenSource和Disconnect方法,确保所有资源(TcpClient,NetworkStream,Thread)都被正确释放。
- 消息确认 :当前的
-
序列化:
- 示例中使用了
.NET自带的BinaryFormatter,它简单易用,但不推荐在不可信的网络环境中使用,因为它存在安全漏洞。 - 强烈建议 :在生产环境中,使用更安全、高效且跨平台的序列化库,如 Protobuf (Google.Protobuf) 或 MessagePack-CSharp 。你需要为
ImageData和ProcessingResult定义.proto或.mp契约文件,然后生成代码。这会稍微增加一些复杂度,但换来的是更高的性能和安全性。
- 示例中使用了