文章目录
1 UDP 概述
1.1 通信流程
客户端和服务端的流程如下:
- 创建套接字 Socket。
- 用
Bind()
方法将套接字与本地地址进行绑定。 - 用
ReceiveFrom()
和SendTo()
方法在套接字上收发消息。 - 用
Shutdown()
方法释放连接。 - 关闭套接字。

1.2 TCP 与 UDP


1.3 UDP 分包
UDP 是不可靠的连接,消息传递过程中可能出现无序、丢包等情况。
为了避免其分包,建议在发送 UDP 消息时 控制消息的大小在 MTU(最大传输单元)范围内。
MTU(Maximum Transmission Unit)
最大传输单元,用来通知对方所能接受数据服务单元的最大尺寸,不同操作系统会提供用户一个默认值。
以太网和 802.3 对数据帧的长度限制,其最大值分别是 1500 字节和 1492 字节。
由于 UDP 包本身带有一些信息,因此建议:
局域网环境下:1472 字节以内(1500 减去 UDP 头部 28 为 1472)。
互联网环境下:548 字节以内(老的 ISP 拨号网络的标准值为 576 减去 UDP 头部 28 为 548)。
只要遵守这个规则,就不会出现自动分包的情况。
如果想要发送的消息确实比较大,可以进行手动分包,将其拆分成多个消息,每个消息不超过限制。但手动分包的前提是要解决 UDP 的丢包和无序问题。
1.4 UDP 黏包
UDP 本身作为无连接的不可靠的传输协议(适合频繁发送较小的数据包),不会对数据包进行合并发送。一端直接发送数据,不会对数据合并。
因此 UDP 当中不会出现黏包问题(除非手动进行黏包)。
2 同步通信
区别于 TCP,UDP 发送和接收消息的方式为 SendTo()
和 ReceiveFrom()
,需要传入指定的 EndPoint 以指明将消息发送到哪和从哪里接收消息。
SendTo()

参数
buffer
:要发送的数据缓冲区。size
:要发送的数据的字节数。socketFlags
:发送操作的控制标志。remoteEP
:远程终结点,指定数据要发送到的目标地址。
返回值
- 发送的字节数。
ReceiveFrom()

- 参数
buffer
:字节数组,用于存储接收到的数据。size
:指定从接收缓冲区中读取的最大字节数。socketFlags
:枚举值,用于指定接收操作的行为。remoteEP
:EndPoint
对象,用于存储发送方的网络地址。这个参数是引用类型,所以方法调用后,它将包含发送方的地址信息。
- 返回值
- 接收到的字节数。
2.1 服务端
// See https://aka.ms/new-console-template for more information
using System.Net;
using System.Net.Sockets;
using System.Text;
// 1.创建套接字
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
// 2.绑定本机地址
var ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8081);
socket.Bind(ipPoint);
Console.WriteLine("服务器开启,等待消息中...");
// 3.接受消息
var buffer = new byte[512];
EndPoint remoteIpPoint2 = new IPEndPoint(IPAddress.Any, 0);
var length = socket.ReceiveFrom(buffer, ref remoteIpPoint2);
Console.WriteLine("IP: " + (remoteIpPoint2 as IPEndPoint).Address +
" Port: " + (remoteIpPoint2 as IPEndPoint).Port +
" 发来了 " +
Encoding.UTF8.GetString(buffer, 0, length));
// 4.发送到指定目标
var remoteIpPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
socket.SendTo(Encoding.UTF8.GetBytes("hi"), remoteIpPoint);
// 5.释放关闭
socket.Shutdown(SocketShutdown.Both);
socket.Close();
Console.ReadKey();
2.2 客户端
using UnityEngine;
namespace Lesson
{
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
public class Lesson14 : MonoBehaviour
{
private void Start()
{
// 1.创建套接字
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
// 2.绑定本机地址
var ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
socket.Bind(ipPoint);
// 3.发送到指定目标
var remoteIpPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8081);
socket.SendTo(Encoding.UTF8.GetBytes("hello"), remoteIpPoint);
// 4.接受消息
var buffer = new byte[512];
EndPoint remoteIpPoint2 = new IPEndPoint(IPAddress.Any, 0);
var length = socket.ReceiveFrom(buffer, ref remoteIpPoint2);
print("IP: " + (remoteIpPoint2 as IPEndPoint).Address +
" Port: " + (remoteIpPoint2 as IPEndPoint).Port +
" 发来了 " +
Encoding.UTF8.GetString(buffer, 0, length));
// 5.释放关闭
socket.Shutdown(SocketShutdown.Both);
socket.Close();
}
}
}
2.3 测试
先运行服务器,再运行 Unity,可以看到双端互发消息。


3 异步通信
3.1 Bgin / End 方法
BeginSendTo()

参数
buffer
:要发送的数据缓冲区。offset
:缓冲区中开始发送数据的偏移量。size
:要发送的数据字节数。socketFlags
:用于指定发送操作的选项。例如,可以用来指定是否使用紧急数据。remoteEP
:远程终结点。它指定了要发送数据的目标地址。callback
:异步操作完成时要调用的回调方法。state
:一个用户定义的对象,它包含异步操作的状态信息。
返回值
- 返回
IAsyncResult
对象,表示异步操作的状态和结果。可以通过调用EndSendTo()
方法来获取异步操作的结果。
- 返回
BeginReceiveFrom()

参数
buffer
:字节数组,用于存储接收到的数据。offset
:在buffer
数组中开始存储接收数据的偏移量。size
:要接收的数据的字节数。socketFlags
:控制接收操作的标志。例如,SocketFlags.Partial
表示接收的数据可能不完整。remoteEP
:EndPoint
对象,用于存储发送方的地址。这个参数是引用类型,所以方法调用`后,它会被更新为发送方的地址。callback
:异步操作完成时要调用的回调方法。state
:用户定义的对象,包含与异步操作相关的状态信息。
返回值
- 返回
IAsyncResult
对象,表示异步操作的状态。通过这个对象,可以检查异步操作是否完成,或者等待操作完成。
- 返回
代码示例
using UnityEngine;
namespace Lesson
{
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
public class Lesson16 : MonoBehaviour
{
private byte[] _buffer = new byte[512];
private void Start()
{
// 创建一个UDP套接字
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
// 创建一个IP地址和端口号的EndPoint
EndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
// 将字符串转换为字节数组
byte[] bytes = Encoding.UTF8.GetBytes("123123lkdsajlfjas");
// 开始发送数据到指定的EndPoint
socket.BeginSendTo(bytes, 0, bytes.Length, SocketFlags.None, ipPoint, SendCallback, socket);
// 开始接收数据
socket.BeginReceiveFrom(_buffer, 0, _buffer.Length, SocketFlags.None, ref ipPoint, ReceiveCallback, (socket, ipPoint));
}
private void ReceiveCallback(IAsyncResult ar)
{
try
{
(Socket s, EndPoint ipPoint) info = ((Socket, EndPoint)) ar.AsyncState;
// 返回值 就是接收了多少个 字节数
int length = info.s.EndReceiveFrom(ar, ref info.ipPoint);
// 处理消息
// ...
// 处理完消息 又继续接受消息
info.s.BeginReceiveFrom(_buffer, 0, _buffer.Length, SocketFlags.None, ref info.ipPoint, ReceiveCallback, info);
}
catch (SocketException s)
{
print("接受消息出问题: " + s.SocketErrorCode + " : " + s.Message);
}
}
private void SendCallback(IAsyncResult ar)
{
try
{
Socket s = ar.AsyncState as Socket;
s.EndSendTo(ar);
print("发送成功");
}
catch (SocketException s)
{
print("发送失败: " + s.SocketErrorCode + " : " + s.Message);
}
}
}
}
3.2 Async 方法
SendToAsync()

ReceiveFromAsync()

代码示例
using UnityEngine;
namespace Lesson
{
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
public class Lesson16 : MonoBehaviour
{
private byte[] _buffer = new byte[512];
private void Start()
{
// 创建一个UDP套接字
var socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
// 创建一个IP地址和端口号的EndPoint
EndPoint ipPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 8080);
// 将字符串转换为字节数组
byte[] bytes = Encoding.UTF8.GetBytes("123123lkdsajlfjas");
var args = new SocketAsyncEventArgs();
// 设置发送数据的缓冲区
args.SetBuffer(bytes, 0, bytes.Length);
// 添加发送完成事件
args.Completed += SendToAsyncCompleted;
socket.SendToAsync(args);
var args2 = new SocketAsyncEventArgs();
// 设置接收数据的缓冲区
args2.SetBuffer(_buffer, 0, _buffer.Length);
// 添加接收完成事件
args2.Completed += ReceiveFromAsyncCompleted;
socket.ReceiveFromAsync(args2);
}
private void SendToAsyncCompleted(object s, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success)
{
print("发送成功");
}
else
{
print("发送失败");
}
}
private void ReceiveFromAsyncCompleted(object s, SocketAsyncEventArgs args)
{
if (args.SocketError == SocketError.Success)
{
print("接收成功");
// 具体收了多少个字节
// args.BytesTransferred
// 可以通过以下两种方式获取到收到的字节数组内容
// args.Buffer
// cacheBytes
// 解析消息
// ...
Socket socket = s as Socket;
//只需要设置 从第几个位置开始接 能接多少
args.SetBuffer(0, _buffer.Length);
socket.ReceiveFromAsync(args);
}
else
{
print("接收失败");
}
}
}
}