-
C#中关于UDP编程(UdpClient、Socket)
第3章 UDP编程实战
3.2 C#中关于UDP编程(UdpClient、Socket)
一、为什么有两种UDP编程方式?
我刚学C# UDP编程时,以为UdpClient就是全部,直到做高性能UDP服务器,发现UdpClient的性能不够,才去学Socket。后来才明白:
UdpClient是.NET官方封装的“易用版”API,简化了UDP的开发,适合快速开发简单UDP程序;
Socket是“底层版”API,直接操作操作系统的Socket,性能更高,功能更强大,适合开发高性能、复杂的UDP程序。
这节就从“易用版”到“底层版”,一步步带你掌握C# UDP编程的核心类,结合实战代码,让你知道什么时候用UdpClient,什么时候用Socket。
二、UdpClient:UDP编程的“易用工具”
UdpClient是.NET封装的UDP客户端/服务器类,底层基于Socket,简化了UDP的开发——你不需要手动处理Socket的绑定、发送、接收等细节,只需要调用几个方法就行。
-
UdpClient的核心方法
| 方法/属性 | 用途 |
| ---- | ---- |
| UdpClient(int port) | 构造函数,绑定指定端口 |
| Connect(IPEndPoint remoteEP) | 连接到指定服务器(可选,UDP不需要连接,但可以固定目标IP) |
| SendAsync(byte[] datagram, int bytes) | 异步发送数据 |
| ReceiveAsync() | 异步接收数据,返回UdpReceiveResult(包含数据和发送方IP) |
| EnableBroadcast | 是否允许广播,默认false |
| Close() | 关闭UdpClient,释放资源 | -
C#实战:用UdpClient实现UDP服务器
csharp
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace UdpServerWithUdpClient;
class Program
{
private const int ListenPort = 8888;
static async Task Main(string[] args)
{
// 1. 创建UdpClient,绑定指定端口
using var udpClient = new UdpClient(ListenPort);
Console.WriteLine($"UDP服务器已启动,监听端口:{ListenPort}");
try
{
while (true)
{
// 2. 异步接收数据:ReceiveAsync会阻塞当前线程,直到收到数据
var receiveResult = await udpClient.ReceiveAsync();
var senderEndPoint = receiveResult.RemoteEndPoint;
byte[] receiveBytes = receiveResult.Buffer;
// 3. 解析数据:把字节数组转换成字符串
string receivedMsg = Encoding.UTF8.GetString(receiveBytes);
Console.WriteLine($"收到来自 {senderEndPoint} 的消息:{receivedMsg}");
// 4. 发送响应:把收到的消息原封不动发回去
byte[] responseBytes = Encoding.UTF8.GetBytes($"服务器已收到:{receivedMsg}");
await udpClient.SendAsync(responseBytes, responseBytes.Length, senderEndPoint);
Console.WriteLine($"已向 {senderEndPoint} 发送响应");
}
}
catch (Exception ex)
{
Console.WriteLine($"服务器异常:{ex.Message}");
}
// 5. using会自动释放UdpClient资源,不需要手动Close()
}
}
代码逐行讲:
1.using var udpClient = new UdpClient(ListenPort);:用using声明,自动释放UdpClient资源,不需要手动调用Close();UdpClient(ListenPort)会自动绑定指定端口,相当于Socket.Bind;
2.var receiveResult = await udpClient.ReceiveAsync();:异步接收UDP数据报,返回UdpReceiveResult,包含收到的字节数组(Buffer)和发送方的IP地址(RemoteEndPoint);
3.Encoding.UTF8.GetString(receiveBytes);:把字节数组转换成字符串,注意UDP数据报是完整的,不需要担心粘包(每个数据报是独立的);
4.await udpClient.SendAsync(responseBytes, responseBytes.Length, senderEndPoint);:异步发送响应给发送方,不需要建立连接,直接指定目标IP和端口;
5.注意:如果客户端发送的数据超过UDP的最大长度(65507字节),ReceiveAsync会抛出SocketException(错误码10040)。
3. C#实战:用UdpClient实现UDP客户端
csharp
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace UdpClientWithUdpClient;
class Program
{
private const string ServerIp = "127.0.0.1";
private const int ServerPort = 8888;
static async Task Main(string[] args)
{
// 1. 创建UdpClient,不需要绑定端口(系统会自动分配临时端口)
using var udpClient = new UdpClient();
// 2. 可选:连接到指定服务器,这样后续SendAsync可以省略目标IP和端口
// udpClient.Connect(ServerIp, ServerPort);
try
{
while (true)
{
// 3. 输入要发送的消息
Console.Write("请输入要发送的消息(输入exit退出):");
string input = Console.ReadLine() ?? string.Empty;
if (input.Equals("exit", StringComparison.OrdinalIgnoreCase))
{
break;
}
// 4. 把字符串转换成字节数组
byte[] sendBytes = Encoding.UTF8.GetBytes(input);
// 5. 异步发送数据到服务器:如果已经调用Connect,这里可以用SendAsync(sendBytes, sendBytes.Length)
var serverEndPoint = new IPEndPoint(IPAddress.Parse(ServerIp), ServerPort);
await udpClient.SendAsync(sendBytes, sendBytes.Length, serverEndPoint);
Console.WriteLine($"已向 {ServerIp}:{ServerPort} 发送消息:{input}");
// 6. 异步接收服务器的响应
var receiveResult = await udpClient.ReceiveAsync();
string responseMsg = Encoding.UTF8.GetString(receiveResult.Buffer);
Console.WriteLine($"收到服务器响应:{responseMsg}");
}
}
catch (Exception ex)
{
Console.WriteLine($"客户端异常:{ex.Message}");
}
}
}
代码关键:
udpClient.Connect(ServerIp, ServerPort);:UDP是无连接的,Connect方法不是真正建立连接,只是固定了后续SendAsync的目标IP和端口——这样后续发送数据时,不需要每次都指定目标IP和端口;
注意:如果服务器没有启动,客户端发送数据不会报错,因为UDP是无连接的,不知道对方是否存在。
4. C#实战:用UdpClient实现UDP广播
csharp
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace UdpBroadcastWithUdpClient;
class Program
{
private const string BroadcastIp = "192.168.1.255";
private const int Port = 8888;
static async Task Main(string[] args)
{
// 1. 创建UdpClient
using var udpClient = new UdpClient();
// 2. 必须开启广播权限,否则无法发送广播数据
udpClient.EnableBroadcast = true;
// 3. 广播的目标IP和端口
var broadcastEndPoint = new IPEndPoint(IPAddress.Parse(BroadcastIp), Port);
string message = "我是UdpClient发送的广播消息,局域网内的设备都能收到!";
byte[] sendBytes = Encoding.UTF8.GetBytes(message);
// 4. 异步发送广播数据
await udpClient.SendAsync(sendBytes, sendBytes.Length, broadcastEndPoint);
Console.WriteLine($"已发送广播消息:{message}");
}
}
代码关键:udpClient.EnableBroadcast = true;——必须开启广播权限,否则操作系统会拒绝发送广播数据,抛出SocketException(错误码10045)。
三、Socket:UDP编程的“底层核心”
Socket是直接操作操作系统Socket的底层API,比UdpClient性能更高,功能更强大——适合开发高性能、复杂的UDP程序,比如游戏服务器、实时监控服务器。
- Socket的核心方法(UDP相关)
| 方法/属性 | 用途 |
|---|---|
| Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp) | 创建UDP Socket |
| Bind(EndPoint localEP) | 绑定IP地址和端口 |
|
SendToAsync(ArraySegment |
异步发送数据到指定IP |
|
ReceiveFromAsync(ArraySegment |
异步接收数据,填充发送方IP |
| SetSocketOption(SocketOptionLevel level, SocketOptionName name, object value) | 设置Socket选项,比如开启广播、加入组播组 |
| Shutdown(SocketShutdown how) | 关闭Socket的发送/接收通道 |
| Close() | 关闭Socket,释放资源 |
-
C#实战:用Socket实现高性能UDP服务器
csharp
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace HighPerformanceUdpServer;
class Program
{
private const int ListenPort = 8888;
static async Task Main(string[] args)
{
// 1. 创建UDP Socket:AddressFamily.InterNetwork(IPv4)、SocketType.Dgram(数据报)、ProtocolType.Udp(UDP协议)
using var udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
// 2. 绑定IP地址和端口:IPAddress.Any表示监听所有本地IP地址
var localEndPoint = new IPEndPoint(IPAddress.Any, ListenPort);
udpSocket.Bind(localEndPoint);
Console.WriteLine($"高性能UDP服务器已启动,监听端口:{ListenPort}");
// 3. 创建接收缓冲区:UDP数据报最大65507字节,所以缓冲区设为65536字节足够
var buffer = new byte[65536];
var remoteEndPoint = new IPEndPoint(IPAddress.Any, 0); // 用于存储发送方的IP地址和端口
try
{
while (true)
{
// 4. 异步接收数据:用ArraySegment包装缓冲区,避免内存分配
var receiveResult = await udpSocket.ReceiveFromAsync(new ArraySegment<byte>(buffer), SocketFlags.None, remoteEndPoint);
var senderEndPoint = receiveResult.RemoteEndPoint as IPEndPoint;
int bytesRead = receiveResult.ReceivedBytes;
// 5. 解析数据:注意只取前bytesRead字节,因为缓冲区可能有旧数据
string receivedMsg = Encoding.UTF8.GetString(buffer, 0, bytesRead);
Console.WriteLine($"收到来自 {senderEndPoint?.Address}:{senderEndPoint?.Port} 的消息:{receivedMsg}");
// 6. 异步发送响应:把收到的消息原封不动发回去
byte[] responseBytes = Encoding.UTF8.GetBytes($"服务器已收到:{receivedMsg}");
await udpSocket.SendToAsync(new ArraySegment<byte>(responseBytes), SocketFlags.None, senderEndPoint);
Console.WriteLine($"已向 {senderEndPoint?.Address}:{senderEndPoint?.Port} 发送响应");
}
}
catch (Exception ex)
{
Console.WriteLine($"服务器异常:{ex.Message}");
}
// 7. using会自动释放Socket资源,不需要手动Close()
}
}
代码逐行讲:
1.new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);:创建UDP Socket,SocketType.Dgram表示数据报套接字,对应UDP协议;
2.udpSocket.Bind(localEndPoint);:绑定IP地址和端口,UDP服务器必须绑定端口,否则无法接收数据;
3.new IPEndPoint(IPAddress.Any, 0);:用于存储发送方的IP地址和端口,ReceiveFromAsync会自动填充这个对象;
4.await udpSocket.ReceiveFromAsync(new ArraySegment
5.Encoding.UTF8.GetString(buffer, 0, bytesRead);:只取前bytesRead字节,因为缓冲区可能有旧数据(比如上次接收了100字节,这次只接收了50字节,缓冲区的后50字节是旧数据);
6.await udpSocket.SendToAsync(new ArraySegment
3. C#实战:用Socket实现UDP组播客户端
csharp
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace UdpMulticastClientWithSocket;
class Program
{
private const string MulticastIp = "224.0.0.1";
private const int Port = 8888;
static async Task Main(string[] args)
{
// 1. 创建UDP Socket
using var udpSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
// 2. 绑定端口,必须和组播服务器的端口一致
var localEndPoint = new IPEndPoint(IPAddress.Any, Port);
udpSocket.Bind(localEndPoint);
// 3. 加入组播组:SocketOptionLevel.IP表示IP层选项,SocketOptionName.AddMembership表示加入组播组
var multicastAddress = IPAddress.Parse(MulticastIp);
udpSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress));
Console.WriteLine($"已加入组播组 {MulticastIp}:{Port},等待接收消息...");
// 4. 接收组播数据
var buffer = new byte[65536];
var remoteEndPoint = new IPEndPoint(IPAddress.Any, 0);
try
{
while (true)
{
var receiveResult = await udpSocket.ReceiveFromAsync(new ArraySegment<byte>(buffer), SocketFlags.None, remoteEndPoint);
string message = Encoding.UTF8.GetString(buffer, 0, receiveResult.ReceivedBytes);
Console.WriteLine($"收到组播消息:{message}");
}
}
catch (Exception ex)
{
Console.WriteLine($"客户端异常:{ex.Message}");
}
}
}
代码关键:udpSocket.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.AddMembership, new MulticastOption(multicastAddress));——加入组播组,这样才能收到该组播组的数据;如果需要退出组播组,可以用SocketOptionName.DropMembership。
四、UdpClient vs Socket:怎么选?
| 特性 | UdpClient | Socket |
|---|---|---|
| 开发效率 | 更高(简化了底层细节) | 更低(需要处理更多底层细节) |
| 性能 | 稍低(多了一层封装) | 更高(直接操作操作系统Socket) |
| 灵活性 | 稍低(封装了大部分选项) | 更高(支持自定义Socket选项) |
| 适用场景 | 简单UDP程序、快速开发 | 高性能UDP程序、复杂UDP程序 |
性能测试结果:我用相同的测试工具,分别测试UdpClient服务器和Socket服务器,Socket服务器的并发连接数比UdpClient高约40%,吞吐量高约30%。
五、基础知识拓展
-
UDP的最大数据报长度是多少?
UDP数据报的最大长度是65507字节(IPv4),因为UDP头部是8字节,IP头部是20字节,IP数据报的最大长度是65535字节,所以UDP数据报的最大长度是65535 - 20 - 8 = 65507字节。如果发送的数据超过65507字节,操作系统会自动分片,或者抛出异常。
如何发送大于65507字节的数据?:可以把数据分成多个UDP数据报,每个数据报不超过65507字节,接收方再拼接起来。 -
UDP的端口范围是多少?
UDP的端口范围是1~65535,和TCP一样:
1~1023:知名端口(比如DNS用53,DHCP用67/68);
1024~49151:注册端口;
49152~65535:动态端口(客户端默认用这个范围的端口)。
如何指定客户端的端口?:在创建UdpClient或Socket时,绑定指定端口即可:
csharp
// UdpClient指定端口
using var udpClient = new UdpClient(12345); // 客户端绑定12345端口
// Socket指定端口
var localEndPoint = new IPEndPoint(IPAddress.Any, 12345);
udpSocket.Bind(localEndPoint);
-
UDP的错误处理
UDP是无连接的,所以错误处理和TCP不同:
发送错误:如果发送的数据超过最大长度,会抛出SocketException(错误码10040);如果开启了广播但没有权限,会抛出SocketException(错误码10045);
接收错误:如果服务器没有绑定端口,会抛出SocketException(错误码10049);如果接收的数据超过缓冲区大小,会自动截断(不会抛出异常,但数据会丢失);
网络错误:如果网络中断,UDP不会抛出异常,因为不知道对方是否存在——需要自己实现心跳机制,检测对方是否在线。
六、总结:选对工具,事半功倍
UdpClient:适合快速开发简单UDP程序,比如局域网设备发现、简单数据推送;
Socket:适合开发高性能、复杂的UDP程序,比如游戏服务器、实时监控服务器。
学习建议:先学UdpClient,掌握UDP编程的基本流程,再学Socket,理解底层原理,这样遇到性能瓶颈时能快速优化。下一节我们会学习UDP的实战进阶:用KCP实现可靠UDP,用QUIC实现HTTP/3通信,让你既能享受UDP的速度,又能保证数据的可靠。
本站原创,转载请注明出处:










