## 1. 为什么用WinForm和Socket来模拟AGV通信?
如果你刚接触工业自动化或者AGV(自动导引运输车)开发,可能会觉得这玩意儿特别高大上,满脑子都是PLC、单片机、现场总线、Modbus协议这些听起来就头疼的词。确实,在真实的工厂里,AGV的核心控制器(比如PLC)通常会通过串口服务器(像MOXA这类设备)转换成485或232信号,再按照一套复杂的数据协议和上位机“对话”。但今天,咱们换个轻松点的玩法。我打算抛开那些厚重的工业协议,用一个你我都更熟悉的领域——网络编程,来模拟AGV的核心通信过程。
你可能会问,这靠谱吗?我的回答是:对于理解原理、快速搭建演示原型(Demo)或者进行教学测试,这方法简直太香了。想象一下,AGV小车和它的控制中心(比如调度系统)需要不断地交换信息:“小车,你在哪儿?”“报告,我在A点,状态空闲。”“请移动到B点。”“收到,正在执行。”这种一问一答、实时交互的模式,不就是典型的网络通信场景吗?而Socket(套接字),作为网络编程的基石,恰恰是模拟这种“对话”最直接、最本质的工具。
用WinForm(Windows窗体应用程序)来写这个Demo,优势就更明显了。首先,它开发速度快,拖拖控件、写写事件处理函数,一个带界面的工具就出来了,比用控制台黑窗口友好得多。其次,可视化效果好。你可以实时看到AGV的IP、端口、发送的指令、接收的回复,甚至模拟小车的移动轨迹,整个过程一目了然,非常适合初学者建立直观感受。最后,它足够轻量。我们不需要配置真实的硬件,不需要理解复杂的工业网络拓扑,一台电脑,一个Visual Studio(或者你喜欢的任何C# IDE),就能把AGV通信的“魂”给模拟出来。
所以,这个Demo的目标很明确:**不是复刻一个能下产线的工业级系统,而是为你打开一扇窗,让你用最熟悉的C#和网络知识,亲手触摸到工业通信的脉搏。** 当你理解了Socket如何承载“状态查询”和“移动指令”,再去学习Modbus TCP、Profinet等工业协议时,就会发现它们底层的思想是相通的,只是“语言”(数据格式)不同而已。接下来,我就带你一步步搭建这个模拟工具,从零开始,感受代码如何驱动“虚拟小车”。
## 2. 动手之前:理清Demo的设计思路
在敲代码之前,咱们得先画个“图纸”,搞清楚这个模拟Demo里谁是谁,它们之间怎么“说话”。别担心,这个设计非常简单,核心就两个角色:**服务端(Server)** 和**客户端(Client)**。
在这个模拟场景里,**WinForm程序将扮演AGV小车本身**,也就是服务端。为什么它是服务端?你可以把AGV小车想象成一个提供服务的设备。它静静地待在那里,监听某个网络端口(比如4001),等待控制中心(客户端)来连接它、询问它、命令它。而我们的**控制指令发送工具(可以是一个简单的控制台程序,或者另一个WinForm窗口)则扮演客户端**,主动去连接AGV小车(服务端),然后发送JSON格式的指令字符串。
那么,它们之间“说”什么呢?我们定义两种最简单的“暗号”(协议):
1. **状态读取指令**:客户端发送 `{"cmd":"Read","pathFrom":"","pathTo":""}`。AGV服务端收到后,需要回复一个字符串,比如 `"Point-0001;free"`。分号前面是当前所在点位名称,分号后面是状态(`free`代表空闲,`executing`代表执行中)。这样,客户端就知道小车在哪、忙不忙了。
2. **路径移动指令**:客户端发送 `{"cmd":"Path","pathFrom":"Point-0001","pathTo":"Point-0002"}`。AGV服务端收到后,如果参数合法,就回复一个 `"OK"`,表示指令已接收,并开始模拟从A点移动到B点的过程。
你看,协议非常简单,就是JSON字符串。在真实场景中,协议可能是二进制的,包含校验码、长度头等,但字符串协议最直观,调试起来也方便,用 `telnet` 或者网络调试助手都能直接测试。
我们的WinForm服务端需要做什么呢?它要完成以下几件事:
* **启动监听**:在指定的IP和端口上“竖起耳朵”。
* **接受连接**:允许客户端(控制中心)连上来。
* **接收数据**:读取客户端发来的JSON字符串。
* **解析与处理**:判断是“读状态”还是“走路径”,然后执行相应的模拟逻辑。
* **返回响应**:根据处理结果,生成回复字符串发回给客户端。
* **模拟状态变化**:如果是移动指令,还需要在一个后台线程里,定时更新自己的“当前位置”,让下一次状态查询能反映出移动效果。
理清了这些,我们的代码结构就清晰了。核心就是一个 `TcpListener` 负责监听,每个连进来的客户端用一个 `TcpClient` 和单独的线程或异步任务去处理,避免阻塞。界面上,我们会放一些文本框来显示日志、配置端口,再放个列表框实时展示连接进来的客户端,甚至可以用一个图片或者Label的移动来可视化模拟AGV的位移。思路通了,下面就开始准备“施工”环境。
## 3. 搭建开发环境与创建项目
工欲善其事,必先利其器。咱们这个Demo对开发环境要求极低,几乎任何能写C#的IDE都可以。我最习惯的还是用Visual Studio,社区版免费,功能强大。如果你用Rider、VS Code也没问题,确保能创建Windows窗体应用(.NET Framework 或 .NET Core/.NET 6+ 的 WinForms)就行。我这里以Visual Studio 2022社区版为例。
首先,打开VS,点击“创建新项目”。在项目模板搜索框里输入“Windows 窗体应用”,选择C#语言的这个模板。项目名称可以起个直观的,比如 `AgvSocketSimulator`。位置选个你找得到的地方。注意一下框架选择,如果你追求兼容性,选 `.NET Framework 4.7.2` 或更高版本都行;如果你想用最新技术,选 `.NET 6.0` 或 `.NET 8.0`(Windows桌面)也可以,WinForm在.NET Core及以后版本得到了很好的支持。点击“创建”,项目就生成了。
项目创建好后,你会看到一个叫 `Form1` 的设计界面。这就是我们程序的主窗口。我们先来简单设计一下界面布局,不需要很复杂,实用为主。从左侧的“工具箱”里拖拽以下控件到窗体上:
* **两个Label和两个TextBox**:分别用于输入IP地址和端口号。Label的Text属性改为“监听IP:”和“端口:”。TextBox可以命名一下,比如 `txtIP` 和 `txtPort`,方便后面代码里引用。`txtIP`里可以预填“127.0.0.1”(本机),`txtPort`预填“4001”。
* **两个Button**:一个Text属性改为“启动服务”,命名为 `btnStart`;另一个Text属性改为“停止服务”,命名为 `btnStop`,并且初始时将其 `Enabled` 属性设为 `false`(因为服务没启动时不能停止)。
* **一个ListBox**:用来显示当前连接的客户端信息,命名为 `listBoxClients`。
* **一个RichTextBox**:用来显示详细的运行日志,比如“服务已启动在 127.0.0.1:4001”、“客户端 xxx 已连接”、“收到指令:...”、“回复:...”。命名为 `rtbLog`。RichTextBox比TextBox能显示更多颜色和格式,更适合日志。
* **一个StatusStrip**(状态栏):拖到窗体底部,里面加一个 `ToolStripStatusLabel`,命名为 `lblStatus`,用来显示当前服务状态,比如“就绪”、“监听中...”。
界面大概布局可以是:顶部放IP、端口和按钮;中间左侧放ListBox显示客户端列表,右侧放RichTextBox显示日志;底部是状态栏。你可以根据喜好调整控件大小和位置,也可以使用 `TableLayoutPanel` 或 `SplitContainer` 让布局更规整。设计完界面,它应该看起来像一个有模有样的小工具了。接下来,就是给这些控件注入灵魂——编写后台逻辑代码。
## 4. 编写核心Socket服务端代码
现在,双击“启动服务”按钮,进入代码视图,开始编写核心逻辑。我们需要几个关键的类级变量来维护服务端的状态:
```csharp
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Collections.Generic;
namespace AgvSocketSimulator
{
public partial class Form1 : Form
{
// 监听器
private TcpListener _tcpListener;
// 用于控制监听线程的标记
private bool _isListening = false;
// 保存所有客户端连接的列表,注意线程安全
private List<TcpClient> _connectedClients = new List<TcpClient>();
// 模拟AGV的当前状态
private string _currentPoint = "Point-0001";
private string _currentStatus = "free"; // free, executing
// 模拟移动的目标点,用于后台线程
private string _targetPoint = "";
// 一个简单的锁对象,用于保护状态变量的并发访问
private readonly object _stateLock = new object();
public Form1()
{
InitializeComponent();
}
}
}
```
接下来,我们实现“启动服务”按钮的点击事件。这里的关键是启动一个后台线程来执行监听,避免阻塞UI线程导致界面卡死。
```csharp
private void btnStart_Click(object sender, EventArgs e)
{
string ipStr = txtIP.Text.Trim();
int port;
if (!int.TryParse(txtPort.Text.Trim(), out port))
{
AppendLog("端口号格式错误!", Color.Red);
return;
}
IPAddress ip;
if (!IPAddress.TryParse(ipStr, out ip))
{
ip = IPAddress.Any; // 如果解析失败,监听所有地址
}
try
{
_tcpListener = new TcpListener(ip, port);
_tcpListener.Start();
_isListening = true;
AppendLog($"AGV模拟服务已启动,监听于 {ip}:{port}", Color.Green);
lblStatus.Text = "监听中...";
btnStart.Enabled = false;
btnStop.Enabled = true;
// 启动一个后台线程来接受客户端连接
Thread listenThread = new Thread(new ThreadStart(ListenForClients));
listenThread.IsBackground = true; // 设为后台线程,主程序关闭时自动结束
listenThread.Start();
// 启动一个后台线程来模拟AGV状态更新(例如移动过程)
Thread agvSimThread = new Thread(new ThreadStart(AgvStateSimulator));
agvSimThread.IsBackground = true;
agvSimThread.Start();
}
catch (Exception ex)
{
AppendLog($"启动服务失败: {ex.Message}", Color.Red);
}
}
```
`ListenForClients` 方法在一个循环中等待客户端连接,并为每个连接创建单独的处理线程:
```csharp
private void ListenForClients()
{
while (_isListening)
{
try
{
TcpClient client = _tcpListener.AcceptTcpClient(); // 这里会阻塞,直到有客户端连接
AppendLog($"客户端已连接: {client.Client.RemoteEndPoint}", Color.Blue);
lock (_connectedClients) // 线程安全地添加客户端到列表
{
_connectedClients.Add(client);
}
// 更新UI上的客户端列表(需要跨线程调用)
UpdateClientList(client, true);
// 为这个客户端创建一个独立的处理线程
Thread clientThread = new Thread(new ParameterizedThreadStart(HandleClientComm));
clientThread.IsBackground = true;
clientThread.Start(client);
}
catch (SocketException)
{
// 通常是因为Stop()被调用,监听器被关闭,这是正常退出
break;
}
catch (Exception ex)
{
AppendLog($"接受连接时出错: {ex.Message}", Color.Red);
break;
}
}
}
```
`HandleClientComm` 方法是核心,它处理与单个客户端的全部对话:
```csharp
private void HandleClientComm(object clientObj)
{
TcpClient client = (TcpClient)clientObj;
NetworkStream stream = client.GetStream();
byte[] buffer = new byte[1024];
StringBuilder receivedData = new StringBuilder();
try
{
while (client.Connected && _isListening)
{
int bytesRead = stream.Read(buffer, 0, buffer.Length);
if (bytesRead == 0)
{
// 客户端断开连接
break;
}
string data = Encoding.UTF8.GetString(buffer, 0, bytesRead);
receivedData.Append(data);
// 简单判断:如果收到数据包含换行,则认为是一条完整指令(这是最简单的定界方式)
// 实际项目中可能需要更复杂的协议解析,比如根据长度头或特定结束符
if (receivedData.ToString().Contains(Environment.NewLine) || receivedData.ToString().EndsWith("\n"))
{
string fullCommand = receivedData.ToString().Trim();
AppendLog($"来自 {client.Client.RemoteEndPoint} 的指令: {fullCommand}", Color.DarkBlue);
receivedData.Clear();
// 处理指令并生成回复
string response = ProcessCommand(fullCommand);
byte[] responseBytes = Encoding.UTF8.GetBytes(response + Environment.NewLine);
stream.Write(responseBytes, 0, responseBytes.Length);
AppendLog($"回复: {response}", Color.DarkGreen);
}
}
}
catch (Exception ex)
{
AppendLog($"与客户端 {client.Client.RemoteEndPoint} 通信时出错: {ex.Message}", Color.Orange);
}
finally
{
// 客户端断开后的清理工作
stream?.Close();
client?.Close();
lock (_connectedClients)
{
_connectedClients.Remove(client);
}
UpdateClientList(client, false);
AppendLog($"客户端断开连接: {client.Client.RemoteEndPoint}", Color.Gray);
}
}
```
`ProcessCommand` 方法是我们协议解析和业务逻辑的心脏:
```csharp
private string ProcessCommand(string jsonCommand)
{
// 这里我们进行最简单的JSON解析。实际应用中应使用Newtonsoft.Json或System.Text.Json
// 为了Demo直观,我们直接进行字符串判断
try
{
if (jsonCommand.Contains("\"cmd\":\"Read\""))
{
lock (_stateLock)
{
return $"{_currentPoint};{_currentStatus}";
}
}
else if (jsonCommand.Contains("\"cmd\":\"Path\""))
{
// 非常简陋地提取目标点,仅用于演示!
// 例如从 {"cmd":"Path","pathFrom":"Point-0001","pathTo":"Point-0002"} 中提取Point-0002
int toIndex = jsonCommand.IndexOf("\"pathTo\":\"") + 10;
int endIndex = jsonCommand.IndexOf("\"", toIndex);
if (toIndex > 10 && endIndex > toIndex)
{
string target = jsonCommand.Substring(toIndex, endIndex - toIndex);
lock (_stateLock)
{
if (_currentStatus == "free")
{
_targetPoint = target;
_currentStatus = "executing";
AppendLog($"AGV开始从 {_currentPoint} 向 {_targetPoint} 移动。", Color.Purple);
return "OK";
}
else
{
return "ERROR:AGV is busy";
}
}
}
return "ERROR:Invalid Path Command";
}
else
{
return "ERROR:Unknown Command";
}
}
catch
{
return "ERROR:Process Failed";
}
}
```
最后,我们还需要一个 `AgvStateSimulator` 方法,它在后台线程中运行,定期检查是否需要更新AGV的位置(模拟移动过程):
```csharp
private void AgvStateSimulator()
{
while (_isListening)
{
Thread.Sleep(2000); // 每2秒模拟一次状态更新
lock (_stateLock)
{
if (_currentStatus == "executing" && !string.IsNullOrEmpty(_targetPoint))
{
// 模拟移动:这里我们简单地假设每次更新就移动到下一个“虚拟”点
// 例如 Point-0001 -> Point-0002
// 实际可以更复杂,比如根据路径分段移动
AppendLog($"AGV正在移动中... 当前位置: {_currentPoint}, 目标: {_targetPoint}", Color.Black);
// 假设移动完成
_currentPoint = _targetPoint;
_currentStatus = "free";
_targetPoint = "";
AppendLog($"AGV已到达 {_currentPoint},状态转为空闲。", Color.DarkCyan);
}
}
}
}
```
“停止服务”按钮的代码就相对简单了,主要是关闭监听器、断开所有客户端连接,并重置状态:
```csharp
private void btnStop_Click(object sender, EventArgs e)
{
_isListening = false;
try
{
_tcpListener?.Stop();
}
catch { }
// 断开所有客户端连接
lock (_connectedClients)
{
foreach (var client in _connectedClients.ToArray()) // 使用ToArray避免在遍历时修改集合
{
try { client.Close(); } catch { }
}
_connectedClients.Clear();
}
UpdateClientList(null, false); // 清空列表
AppendLog("服务已停止。", Color.Red);
lblStatus.Text = "已停止";
btnStart.Enabled = true;
btnStop.Enabled = false;
}
```
辅助方法 `AppendLog` 和 `UpdateClientList` 用于安全地更新UI控件(因为它们在非UI线程中被调用):
```csharp
private void AppendLog(string text, Color color)
{
if (rtbLog.InvokeRequired)
{
rtbLog.Invoke(new Action(() => AppendLog(text, color)));
return;
}
rtbLog.SelectionStart = rtbLog.TextLength;
rtbLog.SelectionColor = color;
rtbLog.AppendText($"[{DateTime.Now:HH:mm:ss}] {text}{Environment.NewLine}");
rtbLog.ScrollToCaret();
}
private void UpdateClientList(TcpClient client, bool isAdd)
{
if (listBoxClients.InvokeRequired)
{
listBoxClients.Invoke(new Action(() => UpdateClientList(client, isAdd)));
return;
}
if (isAdd && client != null)
{
listBoxClients.Items.Add(client.Client.RemoteEndPoint.ToString());
}
else
{
listBoxClients.Items.Clear();
lock (_connectedClients)
{
foreach (var c in _connectedClients)
{
listBoxClients.Items.Add(c.Client.RemoteEndPoint.ToString());
}
}
}
}
```
至此,一个具备基本功能的AGV Socket模拟服务端就完成了。它可以监听端口、接受连接、解析简单的JSON指令、模拟AGV状态和移动,并返回响应。代码虽然不复杂,但涵盖了Socket服务端编程的主要环节:监听、接受、读取、处理、写入、关闭以及多线程处理。
## 5. 测试与调试:让虚拟小车跑起来
代码写完了,不跑起来看看怎么知道好不好用呢?测试是我们开发过程中最有成就感的一环。我们将分两步走:先用通用工具测试通信链路,再用我们写的客户端代码进行集成测试。
**第一步:使用网络调试助手进行“黑盒”测试。**
我强烈推荐在开发Socket应用时,手边常备一个网络调试工具,比如“TCP/UDP Socket调试工具”或者“NetAssist”。这些工具可以让你快速验证服务端的基本功能,而不用等客户端写完。
1. 首先,编译并运行我们的WinForm程序。点击“启动服务”,看到日志显示“AGV模拟服务已启动,监听于 127.0.0.1:4001”。
2. 打开网络调试助手。选择“TCP客户端”模式。远程主机地址填“127.0.0.1”,远程端口填“4001”,点击“连接”。此时,我们的WinForm程序日志区应该会显示“客户端已连接: ...”,并且客户端列表里会出现该客户端的IP和端口。
3. 现在,在调试助手的发送区,输入我们的状态读取指令:`{"cmd":"Read","pathFrom":"","pathTo":""}`,记得后面**加上一个换行符(回车)**,因为我们的服务端代码是以换行符作为指令结束判断的。点击发送。
4. 观察。在调试助手的接收区,你应该会立刻收到回复:`Point-0001;free`。同时,WinForm的日志区也会显示收到的指令和回复的内容。这说明“状态读取”功能通了!
5. 测试移动指令。发送:`{"cmd":"Path","pathFrom":"Point-0001","pathTo":"Point-0002"}`(加换行)。你应该会收到回复 `OK`。此时,WinForm日志会显示AGV开始移动。等待大约2秒(我们模拟线程的周期),再发送一次状态读取指令。这时,你应该会收到 `Point-0002;free`(或者还在移动中就是 `Point-0001;executing`,取决于模拟逻辑的精确实现)。这说明“路径移动”和状态更新也通了!
如果在测试中遇到问题,比如连接失败、收不到回复、回复错误等,就要开启“侦探模式”。首先检查防火墙是否阻止了程序监听端口。然后,在代码的关键位置(如 `AcceptTcpClient` 后、`stream.Read` 后、`ProcessCommand` 内部)添加更详细的日志输出,看看程序执行到哪一步,数据是什么。网络调试助手的好处就在于,它能清晰地展示“发送的原始数据”和“接收的原始数据”,排除了客户端代码可能引入的干扰。
**第二步:编写一个简单的控制台客户端进行集成测试。**
用我们自己的代码来测试,更能模拟真实调用场景。新建一个控制台应用项目,引用必要的命名空间,写一个简单的循环,让用户输入指令并发送:
```csharp
using System;
using System.Net.Sockets;
using System.Text;
namespace AgvTestClient
{
class Program
{
static void Main(string[] args)
{
string serverIp = "127.0.0.1";
int port = 4001;
using (TcpClient client = new TcpClient())
{
try
{
client.Connect(serverIp, port);
NetworkStream stream = client.GetStream();
Console.WriteLine("已连接到AGV模拟服务器。输入指令 (或输入 'exit' 退出):");
while (true)
{
Console.Write("> ");
string input = Console.ReadLine();
if (input.ToLower() == "exit")
break;
if (!string.IsNullOrEmpty(input))
{
byte[] data = Encoding.UTF8.GetBytes(input + Environment.NewLine);
stream.Write(data, 0, data.Length);
// 接收回复
byte[] buffer = new byte[1024];
int bytesRead = stream.Read(buffer, 0, buffer.Length);
string response = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"服务器回复: {response.Trim()}");
}
}
}
catch (Exception ex)
{
Console.WriteLine($"发生错误: {ex.Message}");
}
}
Console.WriteLine("按任意键退出...");
Console.ReadKey();
}
}
}
```
运行这个客户端,你就可以像使用网络调试助手一样,手动输入JSON指令进行测试了。这能验证我们服务端协议处理的健壮性。通过这两轮测试,我们的AGV模拟服务端基本就稳定可用了。你可以尝试模拟多个客户端同时连接、连续发送指令等场景,看看服务端的表现如何。
## 6. 功能扩展与优化思路
一个基础的Demo跑起来后,我们往往会想,它还能做些什么?如何变得更像“真”的?这里分享几个我实践过的扩展和优化方向,你可以根据自己的兴趣和需求选择尝试。
**1. 模拟多台AGV小车**
目前我们只模拟了一台AGV。要模拟多台,一个简单的思路是让服务端监听一个端口,但为每台AGV分配一个逻辑ID。客户端连接后,发送的指令里需要包含目标AGV的ID,例如 `{"agvId":"AGV001","cmd":"Read"...}`。服务端维护一个字典,用ID映射到不同的状态对象(当前位置、状态等)。这样,一个服务端实例就能管理多个“虚拟小车”了。界面上可以用多个进度条或者图片来分别显示不同AGV的状态。
**2. 实现更真实的移动模拟**
我们现在的移动模拟是“瞬间传送”。更真实的模拟可以引入“速度”和“距离”概念。假设每个点位间有距离,AGV有恒定速度。在收到移动指令后,计算所需时间(距离/速度),然后启动一个计时器或后台任务,在这个时间段内,状态保持“executing”,并且可以定期更新一个“已走距离”或“剩余时间”的中间状态。到达时间后,再更新位置为目标点。这会让模拟看起来更连续。
**3. 定义更健壮的通信协议**
我们用的基于换行符的JSON字符串协议非常简陋。一个健壮的工业通信协议通常包含:
* **帧头/帧尾**:用于标识一个数据包的开始和结束,如 `0xAA 0x55`。
* **长度字段**:指明后面数据部分的长度,便于接收方准确读取完整一帧。
* **命令字**:一个字节或短整数,标识指令类型,比解析JSON字符串更高效。
* **数据域**:存放具体的参数,可以是结构化的二进制数据。
* **校验码**:如CRC16,用于验证数据在传输过程中是否出错。
你可以尝试设计一个这样的二进制协议,并修改服务端和客户端的解析逻辑。这会让你对工业协议有更深的理解。
**4. 增加图形化监控界面**
WinForm的GDI+或者使用更现代的WPF,可以绘制一个简单的工厂地图。在地图上用不同颜色的圆点表示点位,用一个图标或方块表示AGV。当AGV状态或位置更新时,实时更新图标在地图上的位置。这样,整个模拟过程就完全可视化了,非常炫酷且直观。你甚至可以添加路径规划的可视化,画出AGV将要行走的路线。
**5. 异常处理与日志完善**
目前的异常处理还比较基础。可以增加更多细节:记录每个客户端的完整对话日志到文件;处理客户端异常断开时的资源释放;服务端自身状态异常的恢复机制等。良好的日志系统是调试和后期维护的利器。
**6. 与开源调度系统集成(进阶)**
这可能是最有挑战也最有成就感的扩展。像原始文章提到的openTCS,是一个开源的交通控制系统。你可以深入研究openTCS的通信适配器(Comm Adapter)接口,然后把你现在写的这个WinForm模拟服务端,包装成一个openTCS可以识别的“车辆”。这意味着,你的虚拟AGV能够被一个专业的调度系统管理、派单、路径规划。这需要你理解openTCS的模型和API,但一旦成功,你就完成了一个从简易Demo到接近工业级框架的飞跃。
这些扩展方向每一个都可以作为一个独立的小项目去实践。不要试图一次性全部实现,挑一个最感兴趣的入手,一点点添加功能。编程的乐趣就在于这种不断创造和优化的过程。当你把这些功能一个个实现后,回头再看最初的Demo,你会惊讶于自己的成长。这个基于WinForm和Socket的AGV通信模拟项目,就像一颗种子,已经具备了长成一棵大树的潜力。