在Avalonia这类跨平台桌面框架中,网络功能并不由UI层提供,而是直接复用.NET标准的System.Net.Sockets库。这意味着无论目标是Windows、Linux还是macOS,我们都能用同一套C#代码完成UDP或TCP通信,只在数据回到界面时通过Avalonia的线程调度器做同步。

一、UDP通信的实现方式
UDP是一种无连接协议,发送端只管往外发,接收端监听固定端口即可。在Avalonia里,通常把UDP逻辑放在一个独立的服务类里,避免阻塞UI线程。使用UdpClient可以大幅简化收发代码,不需要手动处理Socket的Bind和ReceiveFrom细节。
下面示例展示了一个简单的UDP接收服务,它在后台任务中持续监听,并把收到的文本通过事件暴露出来。注意UdpClient的Receive方法会阻塞线程,因此必须放在Task.Run中执行,不能直接在UI线程调用。
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
public class UdpService
{
private UdpClient _client;
private CancellationTokenSource _cts;
// 收到消息时触发
public event Action<string, string> MessageReceived;
public void Start(int port)
{
_cts = new CancellationTokenSource();
_client = new UdpClient(port);
Task.Run(() => ReceiveLoop(_cts.Token));
}
private async Task ReceiveLoop(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
var result = await _client.ReceiveAsync();
string msg = Encoding.UTF8.GetString(result.Buffer);
string ip = result.RemoteEndPoint.Address.ToString();
MessageReceived?.Invoke(ip, msg);
}
catch (Exception ex)
{
// 网络异常时简单记录
Console.WriteLine(ex.Message);
}
}
}
public void Send(string ip, int port, string text)
{
var bytes = Encoding.UTF8.GetBytes(text);
_client.Send(bytes, bytes.Length, ip, port);
}
public void Stop()
{
_cts?.Cancel();
_client?.Close();
}
}
上面的代码在ReceiveLoop中使用await ReceiveAsync,比旧版的同步接收更不容易卡死线程。但即便如此,MessageReceived事件仍然是在后台线程触发的,如果直接在事件里修改Avalonia的TextBox或ListBox,会抛出跨线程访问异常。
在Avalonia中,可以使用Avalonia.Threading.Dispatcher.UIThread.Post把界面更新封送回主线程。实际项目里,更推荐配合ReactiveUI的ReactiveObject,用Observable来驱动界面,这样既能解耦又能自动处理线程切换。
二、TCP通信的实现方式
TCP面向连接,适合需要确保数据完整到达的场景,比如远程控制指令、文件传输。与UDP不同,TCP一端要先监听(TcpListener),另一端主动连接(TcpClient),连接建立后双方用NetworkStream读写。
下面给出一个最简化的TCP服务端片段,它接受客户端连接后,循环读取换行符分隔的文本消息。生产环境还需要加入心跳、超时和重连机制,否则客户端掉线服务端可能长时间阻塞在ReadLine上。
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
public class TcpServer
{
public async Task StartAsync(int port)
{
TcpListener listener = new TcpListener(IPAddress.Any, port);
listener.Start();
Console.WriteLine("TCP服务已启动");
while (true)
{
TcpClient client = await listener.AcceptTcpClientAsync();
_ = HandleClientAsync(client);
}
}
private async Task HandleClientAsync(TcpClient client)
{
using (client)
{
var stream = client.GetStream();
byte[] buffer = new byte[1024];
while (true)
{
int len = await stream.ReadAsync(buffer, 0, buffer.Length);
if (len == 0) break; // 客户端断开
string msg = Encoding.UTF8.GetString(buffer, 0, len);
Console.WriteLine("收到: " + msg);
byte[] resp = Encoding.UTF8.GetBytes("OKn");
await stream.WriteAsync(resp, 0, resp.Length);
}
}
}
}
在Avalonia客户端中,可以用TcpClient发起连接,并用一个长期运行的后台任务读取服务端推送。由于TCP是字节流,发送端和接收端必须约定消息边界,例如用特殊分隔符或先发长度再发内容,否则会出现拆包和粘包问题。
对于界面集成,同样建议在ViewModel里持有TcpClient的封装对象,通过Dispatcher或ReactiveCommand把接收到的字符串写入一个ObservableCollection,从而让ListBox自动刷新,而不用手动操作控件。
三、UDP与TCP在Avalonia项目中的选型对比
很多初学者会盲目选TCP,但其实在局域网设备状态广播、多人游戏位置同步等场景,UDP的轻量特性更有优势。下面的表格列出了两者在常见维度上的差异,方便你在架构阶段做判断。
| 维度 | UDP | TCP |
|---|---|---|
| 连接建立 | 无连接,直接发 | 需三次握手 |
| 可靠性 | 可能丢包、乱序 | 保证送达和顺序 |
| 适用场景 | 广播、实时音视频、状态同步 | 指令、文件、登录鉴权 |
| 界面集成复杂度 | 低,单向推送简单 | 中,需管理连接生命周期 |
如果业务同时需要两者,可以把UDP用于发现局域网内的服务,再用TCP建立可靠控制通道。这种混合模式在Avalonia工控软件里非常常见:上位机先听UDP广播找到下位机,再连TCP发配方数据。
另外要注意,Avalonia程序在Linux或macOS打包后,若用到绑定1024以下端口,需要提权或使用cap_net_bind_service,否则UdpClient构造时会抛SocketException。开发阶段用高端口即可规避。
四、线程与生命周期管理要点
无论是UDP还是TCP,网络对象都不该随Window关闭而被强制GC,否则会出现端口仍被占用的情况。推荐把通信服务注册为单例,在App.axaml.cs里随程序退出调用Stop。
在ViewModel中订阅MessageReceived事件时,记得在页面销毁时取消订阅,否则会造成内存泄漏。Avalonia的ReactiveUI提供了WhenActivated块,可以精确控制订阅周期,比手动加减事件稳妥得多。
using ReactiveUI;
using System;
public class MainViewModel : ReactiveObject
{
private readonly UdpService _udp;
public MainViewModel(UdpService udp)
{
_udp = udp;
_udp.MessageReceived += OnMsg;
}
private void OnMsg(string ip, string msg)
{
// 错误示范:直接改界面字段
// 正确做法用Dispatcher或Rx把msg推到UI线程
}
// 页面退出时调用
public void Detach()
{
_udp.MessageReceived -= OnMsg;
}
}
最后提醒,Avalonia的Dispatcher和WPF的相似,但命名空间在Avalonia.Threading下。若你从WPF迁移代码,别漏改using,否则编译期就会报找不到类型。
把网络层与界面层通过事件或响应式流隔离后,后续要换用WebSocket或串口,只需替换服务实现,ViewModel几乎不用动,这也是Avalonia配合.NET做分层架构的好处。