Are you still experiencing headaches because old serial devices can't directly access the network? Are you working late into the night due to complex protocol conversions? Today, I'll use a complete C# project to teach you step-by-step how to build a high-performance Serial-to-Ethernet converter, transforming traditional devices into smart terminals in seconds!
This is not just a simple conversion tool, but a complete industrial-grade solution featuring multi-client management, asynchronous data processing, real-time status monitoring, and other enterprise-level functions. Whether you're an embedded engineer or a .NET developer, this article will open up a new world of industrial connectivity for you!
In factory automation, numerous PLCs, sensors, instruments, and other devices still use RS232/RS485 serial communication. These devices face core problems:
While serial servers on the market can solve basic needs, they have obvious drawbacks:
Our solution adopts a Producer-Consumer pattern, implementing efficient bidirectional data conversion through concurrent queues:

csharppublic partial class Form1 : Form
{
// Core components
private SerialPort _serialPort;
private TcpListener _tcpListener;
private readonly List<ClientHandler> _connectedClients = new List<ClientHandler>();
// Thread safety mechanisms
private readonly SemaphoreSlim _clientsSemaphore = new SemaphoreSlim(1, 1);
private readonly ConcurrentQueue<byte[]> _serialDataQueue = new ConcurrentQueue<byte[]>();
private readonly ConcurrentQueue<byte[]> _networkDataQueue = new ConcurrentQueue<byte[]>();
// State management
private CancellationTokenSource _cancellationTokenSource;
private bool _isRunning = false;
private long _totalBytesReceived = 0;
private long _totalBytesSent = 0;
}
💡 Design Highlights:
SemaphoreSlim ensures thread-safe access to the client listConcurrentQueue provides high-performance lock-free queue operationscsharpprivate async Task StartConverter()
{
// 🔧 Parameter validation
if (string.IsNullOrEmpty(cmbSerialPorts.Text))
throw new InvalidOperationException("Please select a serial port");
if (!int.TryParse(txtTcpPort.Text, out int tcpPort) || tcpPort <= 0 || tcpPort > 65535)
throw new InvalidOperationException("Please enter a valid TCP port (1-65535)");
// 🚀 Configure serial port
_serialPort = new SerialPort
{
PortName = cmbSerialPorts.Text,
BaudRate = int.Parse(cmbBaudRate.Text),
DataBits = int.Parse(cmbDataBits.Text),
Parity = (Parity)Enum.Parse(typeof(Parity), cmbParity.Text),
StopBits = (StopBits)Enum.Parse(typeof(StopBits), cmbStopBits.Text),
ReadTimeout = 500,
WriteTimeout = 500
};
_serialPort.DataReceived += SerialPort_DataReceived;
_serialPort.Open();
// 🌐 Start TCP listening
_tcpListener = new TcpListener(IPAddress.Any, tcpPort);
_tcpListener.Start();
_cancellationTokenSource = new CancellationTokenSource();
_isRunning = true;
// 🔄 Start async tasks
_ = Task.Run(() => AcceptClientsAsync(_cancellationTokenSource.Token));
_ = Task.Run(() => ProcessDataQueuesAsync(_cancellationTokenSource.Token));
LogMessage("Converter started", Color.Green);
}
⚠️ Key Points to Remember:
Task.Run avoids blocking the UI threadcsharpprivate async Task ProcessDataQueuesAsync(CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
try
{
// 📤 Serial to network: broadcast to all clients
while (_serialDataQueue.TryDequeue(out byte[] serialData))
{
await BroadcastToClientsAsync(serialData);
Interlocked.Add(ref _totalBytesSent, serialData.Length);
}
// 📥 Network to serial: write to serial device
while (_networkDataQueue.TryDequeue(out byte[] networkData))
{
if (_serialPort != null && _serialPort.IsOpen)
{
_serialPort.Write(networkData, 0, networkData.Length);
Interlocked.Add(ref _totalBytesSent, networkData.Length);
}
}
// ⏱️ Brief delay to avoid excessive CPU usage
await Task.Delay(1, cancellationToken);
}
catch (Exception ex)
{
LogMessage($"Data processing error: {ex.Message}", Color.Red);
await Task.Delay(100, cancellationToken);
}
}
}
🎯 Performance Optimization Tips:
TryDequeue avoids blocking waitsInterlocked.Add ensures atomic counter operationscsharpprivate async Task BroadcastToClientsAsync(byte[] data)
{
List<ClientHandler> clientsToRemove = new List<ClientHandler>();
List<ClientHandler> currentClients;
// 🔒 Get client list copy
await _clientsSemaphore.WaitAsync();
try
{
currentClients = new List<ClientHandler>(_connectedClients);
}
finally
{
_clientsSemaphore.Release();
}
// 🚀 Send data in parallel
var sendTasks = new List<Task>();
foreach (var client in currentClients)
{
sendTasks.Add(SendToClientSafeAsync(client, data, clientsToRemove));
}
await Task.WhenAll(sendTasks);
// 🧹 Clean up disconnected clients
if (clientsToRemove.Count > 0)
{
await _clientsSemaphore.WaitAsync();
try
{
foreach (var client in clientsToRemove)
{
_connectedClients.Remove(client);
client.Dispose();
}
}
finally
{
_clientsSemaphore.Release();
}
}
}
💎 Design Essence:
Task.WhenAll improves multi-client processing efficiency

csharp// Scenario: PLC data acquisition
// PLC connects to converter via RS485, multiple host computers monitor production data simultaneously
// Converter configuration: Baud rate 9600, Data bits 8, Stop bits 1, No parity
csharp// Scenario: Environmental monitoring stations
// Temperature and humidity sensor data uploaded to cloud platform through converter
// Support multiple monitoring centers receiving data simultaneously
csharp// ❌ Wrong: Improper resource release
_serialPort.Close(); // Only closes, doesn't release
// ✅ Correct: Complete resource release
if (_serialPort != null && _serialPort.IsOpen)
{
_serialPort.DataReceived -= SerialPort_DataReceived; // Unsubscribe event
_serialPort.Close();
_serialPort.Dispose(); // Release resources
_serialPort = null;
}
csharp// ❌ Wrong: Directly updating UI from worker thread
lblStatus.Text = "Connected"; // May cause exceptions
// ✅ Correct: Using Invoke mechanism
if (InvokeRequired)
{
Invoke(new Action(() => lblStatus.Text = "Connected"));
}
else
{
lblStatus.Text = "Connected";
}
csharp// ✅ Log length control to prevent memory overflow
if (rtbLog.Lines.Length > 1000)
{
var lines = rtbLog.Lines.Skip(200).ToArray();
rtbLog.Lines = lines;
}
csharp// 💡 Batch processing improves efficiency
private readonly List<byte[]> _batchBuffer = new List<byte[]>();
private const int BATCH_SIZE = 10;
// Batch process data packets to reduce network IO operations
if (_batchBuffer.Count >= BATCH_SIZE)
{
var combinedData = _batchBuffer.SelectMany(x => x).ToArray();
await BroadcastToClientsAsync(combinedData);
_batchBuffer.Clear();
}
csharp// 💡 Client connection limitation
private const int MAX_CLIENTS = 50;
if (_connectedClients.Count >= MAX_CLIENTS)
{
LogMessage("Maximum connections reached, rejecting new connection", Color.Orange);
newClient.Close();
return;
}
In actual deployment at an electronics manufacturing factory:
Through this complete C# Serial-to-Ethernet project, we not only solved the practical problem of industrial device networking but also demonstrated the powerful capabilities of modern .NET technology in the industrial IoT field.
This solution has been validated in multiple industrial projects, saving over 60% in costs compared to commercial products. If you're developing similar projects, this complete code framework will save you significant development time.
What technical challenges have you encountered in industrial IoT projects? Welcome to share your practical experience in the comments, let's explore more optimization solutions together! If you find this article helpful, please share it with more engineering colleagues!
Follow me for more C# industrial application development insights!
相关信息
通过网盘分享的文件:AppSerialToEthernet.zip 链接: https://pan.baidu.com/s/1jYmkw1_sHaSwMFnXyvnLbQ?pwd=qwp4 提取码: qwp4 --来自百度网盘超级会员v9的分享
本文作者:技术老小子
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!