Socket连接
Socket连接介绍
这里Socket先使用Tcp协议同步连接,Tcp协议作为稳定协议,在消息发送前必须完成客户端连接,且客户端连接在Tcp协议中只能是一对一的,即如果有ABC三个连接,那个A连接与B连接如果相互连接,则A与C之间则无法互相通信,只能由A接受到消息时创建出额外的D连接,然后由D与C相互通信
同步作为与异步区分的概念,同步即线程执行到发送或等待接受消息的指令时会进入阻塞状态,即暂停执行,直到接收到消息时,线程才会再次开始工作
Socket客户端
Socket客户端介绍
客户端的概念是作为客机向主机连接的通信通道,客户端不需要绑定IP和端口,客户端在创建之后直接向服务器发送连接申请,成功连接后即可作为单向的消息的收发通道,客户端的代码中不需要存在自己的IP及端口号,只需要存在需要连接的主机和端口号即可
创建Socket客户端
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
代码的功能为创建一个Tcp协议下的客户端
构造函数的第一个参数为采用IPv4的方式进行网络传输,保持不变即可,偶尔可用IPv6方式
构造函数的第二个参数为采用流的方式传递数据,保持不变即可
构造函数的第三个参数为使用tcp网络传输协议,保持不变即可
Socket客户端连接服务器
socket.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 12345));
代码的功能为连接服务器主机,这个方法只需要在客户端执行,服务器中不需要使用Connect连接功能,需要注意的是连接的Socket必须要是服务器中已经调用Bind和Listen的Socket
传入的参数为IPEndPoint,声明IpEndPoint则需要传入IpAddress和端口号,IpAddress代表Ip地址,IpAddress.Parse(string)方法为获取指定字符串所指向的IP地址,连接时必须指定特定IP,不能使用IpAddress,Any或其他,IpAddress和开放端口号组成IpEndPoint
Socket客户端接收消息
byte [] message = new byte[1024];
socket.Receive(message);
string str = Encoding.UTF8.GetString(message);
连接服务器完成后,即可调用Receive方法开始接受,需要注意的是执行的Receive语句时,无论是主线程或是分支线程,都会开始阻塞(暂停执行),直到收到服务器消息为止,所以Receive语句需要写到线程当中,然后在主线程中开启分支线程,在分支线程中执行Receive语句
Receive方法中的参数为byte数组,使用前需要先声明并赋予任意初始值,在Receive方法执行完后byte数组中将包含从服务器中接受到的数据流,需要通过下面的Encoding.UTF8.GetString方法将byte数组传入并返回由数据流转换好的字符串
Socket客户端发送消息
socket.Send(Encoding.UTF8.GetBytes("客户端发送内容"));
Send方法的参数类型为byte数组,功能是将参数中的byte数组数据流发送到服务器,Encoding.UTF8.GetBytes方法为将传入的String类型的参数专函为byte数组并返回,在这里直接将方法的返回值传入的Send方法的参数中
Socket服务器
Socket服务器介绍
Socket服务器并不需要特殊的系统或特殊的硬件
服务器相当于不主动发送连接请求而被动接受连接请求的主机端
与客户端不同的是客户端向服务器申请连接,而服务器则是被动等待客户端的申请
服务器必须要在声明时绑定固定的Ip地址和端口号,这样才能被客户端寻找到
服务器在接收到新的连接申请时,并不会由服务器与其进行连接,而是会生成一个本地客户端与访问服务器的客户端进行一对一连接,客户端发送消息时将发送到服务器生成的本地客户端上
创建Socket服务器
Socket mainSocket;
mainSocket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
mainSocket.Bind(new IPEndPoint(IPAddress.Any,12345));
Socket服务器的声明与客户端的声明相同,但是区别于客户端的方式是服务器必须调用Bind方法绑定固定的IP地址和端口,服务器在声明时IP地址可以传入IPAddress.Any,等同于传入"127.0.0.1"或者自己的Ipv4地址,12345代表服务器部署的端口
Socket服务器开启监听连接请求
mainSocket.Listen(10);
服务器必须调用Listen代码之后,才能收到来自客户端的连接申请,Listen方法的参数为同时最多可连接到服务器的客户端数量,超出数量的客户端发送连接请求时将返回连接失败
Socket监听连接请求并生成本地客户端
Socket socket = mainSocket.Accept();
服务器调用Accept方法后,无论是主线程还是分支线程,都将进入阻塞状态(暂停执行),直到收到连接请求后才会继续执行,所以需要将Accept方法写入到分支线程中,然后在主线程开启的分支线程中调用Accept方法.
Accept方法调用后,服务器将为接下来第一个连接进来的客户端生成一个一对一专属的通信客户端作为方法的返回值返回,客户端发送消息时将会由通信客户端进行通信,与服务器将无关
所以通过Accept生成的客户端需要利用List或Dictionary等方式储存起来,供发送信息时使用
Accept方法接受到连接申请并创建本地客户端后,需要新开线程,在线程中用Receive方法队本地客户端进行消息监听处理
客户端示例代码
代码仅供学习演示,未进行异常处理,仅供展示方法功能
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;public class SocketScript : MonoBehaviour
{Socket mainSocket;/// <summary>/// 声明客户端并连接服务器/// </summary>void Start(){mainSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);mainSocket.Connect(new IPEndPoint(IPAddress.Parse("127.0.0.1"), 12345));Thread thread = new Thread(Listen);thread.Start();}byte [] message = new byte[1024];/// <summary>/// 监听服务器消息并处理/// </summary>void Listen(){while (true){mainSocket.Receive(message);Debug.Log(Encoding.UTF8.GetString(message));}}/// <summary>/// 客户端向服务器发送消息/// </summary>/// <param name="str"></param>void Send(string str){mainSocket.Send(Encoding.UTF8.GetBytes(str));}private void Update(){if (Input.GetKeyDown(KeyCode.Space)){Send("客户端发送测试数据");}}
}
服务器示例代码
代码仅供学习演示,未进行异常处理,仅供展示方法功能
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using UnityEngine;public class SeverSocketScript : MonoBehaviour
{Dictionary<string,Socket> connectSockets = new Dictionary<string,Socket>();Socket mainSocket;/// <summary>/// 声明服务器并开启接受监听线程/// </summary>void Start(){mainSocket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);mainSocket.Bind(new IPEndPoint(IPAddress.Any,12345));mainSocket.Listen(10);Thread thread = new Thread(CreatConnectSocket);thread.Start();}/// <summary>/// 接受监听后保存生成的通信客户端,并开启线程监听通信客户端消息/// </summary>void CreatConnectSocket(){while (true){Socket socket = mainSocket.Accept();if (!connectSockets.ContainsKey(socket.RemoteEndPoint.ToString())){connectSockets.Add(socket.RemoteEndPoint.ToString(), socket);Thread thrad = new Thread(() => { ListenNewConnectSocket(socket); });thrad.Start();}}}byte[] bytes = new byte[1024];/// <summary>/// 接受通信客户端消息并对消息进行处理/// </summary>/// <param name="socket"></param>void ListenNewConnectSocket(Socket socket){while (true){socket.Receive(bytes);Debug.Log("接受消息" + Encoding.UTF8.GetString(bytes));socket.Send(Encoding.UTF8.GetBytes("服务器收到测试数据"));Debug.Log("发送消息回执");}}/// <summary>/// 由服务器向全部客户端进行数据广播/// </summary>void Send(){foreach (var socket in connectSockets){socket.Value.Send(Encoding.UTF8.GetBytes("服务器发送测试数据"));Debug.Log("服务器向" + socket.Key + "发送测试数据");}}private void Update(){if (Input.GetKeyDown(KeyCode.Space)){Send();}}
}
Tcp连接
Tcp内容介绍
Tcp分为四个部分,分别是Tcp客户端,Tcp服务器,NetworkStream数据流通道,Tcp异步方法四个部分,其中代码默认使用同步方法,异步方法在Tcp异步连接部分单独区分
Tcp客户端
声明Tcp客户端
TcpClient client = new TcpClient();
Tcp客户端应用到的类是TcpClient的类,声明Tcp客户端类只需要调用无参构造函数,不需要传入Ip地址和端口,调用构造函数声明类的时候Ip和端口会自动生成并保存到TcpClient中
Tcp客户端连接服务器
client.Connect("127.0.0.1",12345);
方法的功能是:客户端连接地址为127.0.0.1,端口为12345的服务器
声明Tcp客户端之后第二部就是用客户端连接建立好的Tcp服务器,这里的Connect是同步方法,在调用的时候线程会进入阻塞,Connect连接的异步方式会在后面展示
从Tcp客户端生成数据流通道
NetworkStream stream = client.GetStream();
NetworkStream类代表数据流通道,需要从已经连接服务器的Tcp客户端中生成,且一个Tcp客户端只能生成一个,后面与服务器的通信功能包括接受信息和发送信息都在数据流通道中进行
Tcp服务器
声明Tcp服务器
TcpListener listener = new TcpListener(new IPEndPoint(IPAddress.Any,12345));
和Socket不一样的地方在于,声明Tcp的服务器采用的是和声明客户端完全不同的关键字,在声明服务器的时候构造函数必须传入IPEndPoint参数,包含了Ip地址和端口,其中Ip地址可以使用IpAddress.Any来自动获取,但是端口必须明确指定端口号
开启Tcp服务器
listener.Start();
Tcp服务器声明之后必须使用Start参数开启后,才能使用。与Socket不同的是服务器开启时不需要指定可以连接的客户端的最大数量
服务器创捷通信客户端
TcpClient client = listener.AcceptTcpClient();
NetworkStream stream = client.GetStream();
与Socket相同,服务器接收到客户端的连接申请时,服务器会额外创建出一个通信客户端与申请连接的客户端相连
方法的第二句时创建数据流通道,因为通信是由数据流通道进行的,所以大部分情况在获取通信客户端之后都会立刻创建数据流通道对数据进行监听
TcpListener.AcceptTcpClient()是同步方法,方法执行后线程将进入阻塞状态,直到有出现客户端向服务器发送的连接申请位置,所以这个方法尽量写在线程当中,这个方法的异步版本在文章的异步部分
TcpListener.AcceptTcpClient()方法是一次性的,调用后在接收到第一个连接申请创建客户端后执行将会结束,如果有多个客户端需要连接时,需要将这个方法放在拥有While(true)无限循环语句的线程中
NetworkStream数据流通道
NetworkStream数据流通道介绍
无论是Tcp客户端还是Tcp服务器的代码都非常简短,并且最终结果都是创建NetworkStream数据流通道
在实际的代码当中,Tcp客户端在生成,连接,生成通道之后,后续的数据通信也都是由NetworkStream数据流通道完成
Tcp服务器也是在生成,启动,监听客户端连接请求,创建通信客户端,创建通信数据流通道,然后后续的数据通信也是由创建的通信数据流通道完成
作为这么重要的NetworkStream数据流,其实核心也就只有两个方法,也就是发送和接受
数据流通道发送消息
byte[] bytes = Encoding.UTF8.GetBytes("客户端消息");
stream.Write(bytes,0,bytes.Length);
Encoding.UTF8.GetBytes(string)方法的功能是传入一个string类型的参数,将参数的字符串转换为byte数组并返回
NetworkStream.Write() 方法有三个参数
第一个参数为需要发送的byte数组,byte数组一般都是由第一个方法传入字符串转换而来
第二个参数为发送数据的起始索引,一般都是0
第三个参数为发送数据的长度,一般都是把byte数组中的数据全部发送,也是就bytes.Length
需要注意的是这个方法为同步方法,在发送消息成功之前线程都将处于阻塞状态,这个方法的异步版本在后面异步的部分
数据流通道接受消息
byte[] bytes = new byte[1024];
stream.Read(bytes, 0, bytes.Length);
string message = Encoding.UTF8.GetString(bytes);
NetworkStream.Read()方法有三个参数,不过写法基本固定为
参数一:接受数据的Byte数组
参数二:开始接受的位置,也就是0
参数三:接受的总数量,也就是Byte数组的长度
方法运行的结果是,将数据流通道中接受的数据保存到bytes中
Encoding.UTF8.GetString(byte[])方法的功能是传入一个byte数组的参数,将参数的byte数组转换为字符串并返回
上面代码中的message就保存了数据流中接收到的来自客户端的字符串信息
需要注意的是这个方法为同步方法,方法执行后线程将持续阻塞直到数据流通道的对方发送数据流为止,在对方发送数据流之前线程将一直处于阻塞状态,这个方法的异步版本在后面的异步部分
NetworkStream.Read()方法是一次性的,调用后在接收到第一个条消息后执行将会结束,如果需要多次接受消息,需要将这个方法放在拥有While(true)无限循环语句的线程中
Tcp异步方法
tcp异步方法介绍
在网络连接中使用:1.客户端连接服务器,2.服务器等待客户端连接,3.数据发送,4.数据接收 这四个部分的内容时,线程均会进入阻塞状态,客户端在主线程中出现程序阻塞时将会导致程序无响应,而且如果服务器为每个连接的客户端均开一条新线程,在连接请求数量较大的情况下硬件负担又相当严重,所以出现了异步方法,通过回调的方式处理数据,不会造成线程的阻塞和程序的卡死,同时由于开启线程的数量降低了,硬件负担也显著下降
异步和新线程同步原理上的区别(待验证)
异步方法是通过事件函数回调的方式实现的,属于在主线程中临时插入需要处理的数据,即在一条流水线中,工作分出了轻重缓急,在主线程处理完当前这一帧(理解为工人处理完今天的工作)的全部数据后,临时在这一帧额外增加新的需要执行的逻辑(主线程加班),通过主线程先正常工作,任务来了再临时加班的方式完成额外的工作,而线程则相当于多开一条流水线,预备一个工人随时待命,什么时候来活儿了什么时候干,没活儿就歇着,使用异步能节省计算机(工厂)开支(一核有难,八核围观(不是))
异步方法的使用方式
异步方法的调用不会引起线程阻塞,可以将异步方法放在主线程中直接调用,只需在回调方法中正确处理回调后需要执行的逻辑
客户端连接服务器的异步方法
客户端连接服务器的同步方法为
client.Connect("127.0.0.1",12345);
这个方法的异步方法为
client.BeginConnect("127.0.0.1", 12345, ConnectCallBack , "服务器连接成功");
其中的ConnectCallBack为回调方法,这个方法具体内容为
void ConnectCallBack(IAsyncResult asyncResult)
{ Debug.Log(asyncResult.AsyncState.ToString());
}
同步和异步方法的区别是,关键字由Connect变成了BeginConnect,同时新增了第三和第四个参数,其中第三个参数就是回调方法,第四个参数是回调方法的参数。
回调方法ConnectCallBack中有一个IAsyncResult类型的变量作为参数,因为所有的异步回调方法中都需要这个参数,这个参数不重要但是必须存在,因为没有这个参数方法将无法作为回调方法传入异步方法中,IAsyncResult 类型变量比较重要的就是可以通过获取变量中的.AsyncState属性,来获取异步方法中和回调方法一起传入的第四个参数。
服务器等待客户端连接的异步方法
服务器等待客户端连接的同步方法为
TcpClient client = listener.AcceptTcpClient();
这个方法的异步版本为
listener.BeginAcceptTcpClient(CreatConnectSocket, null);
其中的CreatConnectSocket为回调方法,这个方法具体内容为
void CreatConnectSocket(IAsyncResult asyncResult){TcpClient client = listener.EndAcceptTcpClient(asyncResult);NetworkStream stream = client.GetStream();string point = client.Client.RemoteEndPoint.ToString();//添加处理数据的逻辑,其中point为收到客户端发送的字符串listener.BeginAcceptTcpClient(CreatConnectSocket, null);}
这里面涉及到了两个完全陌生的方法,分别是BeginAcceptTcpClient和EndAcceptTcpClient
BeginAcceptTcpClient()方法的功能是服务器开启监听客户端连接请求,然后在连接请求出现之后调用参数一中的回调方法,参数二为IAsyncResult.AsyncState能访问到的值,在这里不需要访问这个值所以可以传一个null,注意这里的回调方法也是只能是有一个IAsyncResult类型参数没有返回值的方法
EndAcceptTcpClient(IAsyncResult)方法的功能是获取服务器为连接创建的通信客户端,需要一个参数,这个参数正好是BeginAcceptTcpClient回调方法中唯一的参数,也就是说EndAcceptTcpClient这个方法基本都是写在BeginAcceptTcpClient的回调方法中,然后将回调方法的唯一参数传入EndAcceptTcpClient方法,再从方法中获得通信客户端
其中client.Client.RemoteEndPoint方法返回一个IpEndPoint,用来获取连接到服务器的客户端的Ip地址和端口号,比较常用。
回调方法的最后一句为再次开启监听并添加回调,这样才能达到监听多个客户端连接的功能
数据流通道发送数据的异步方法
数据流通道发送数据的同步方法为
byte[] bytes = Encoding.UTF8.GetBytes("客户端消息");
stream.Write(bytes,0,bytes.Length);
这个方法的异步方法为
byte[] bytes = Encoding.UTF8.GetBytes("客户端消息");
stream.BeginWrite(b, 0, b.Length, SendCallBack, "连接回执已发送");
其中SendCallBack为回调方法,方法内容为
void SendCallBack(IAsyncResult asyncResult){Debug.Log(asyncResult.AsyncState.ToString());}
异步方法的方法名由Write改为了BeginWrite,在同步方法的基础上额外传入了两个参数,第一个额外参数为回调方法,第二个额外参数为回调方法中的参数的AsyncState属性访问到的值,需要注意回调方法必须是只有一个IAsyncResult类型参数且没有返回值的方法
数据流通道接受数据的异步方法
数据流通道接受数据的同步方法为
byte[] bytes = new byte[1024];
stream.Read(bytes, 0, bytes.Length);
这个方法的异步方法为
byte[] bytes = new byte[1024];
stream.BeginRead(bytes,0, bytes.Length, ListenMessage, stream);
其中ListenMessage为回调方法,方法内容为
void ListenMessage(IAsyncResult asyncResult){NetworkStream stream = (NetworkStream)asyncResult.AsyncState;string str = Encoding.UTF8.GetString(bytes);Debug.Log("服务器接受消息:" + str);stream.BeginRead(bytes, 0, bytes.Length, ListenMessage, stream);}
异步方法的方法名由Read改为了BeginRead,在同步方法的基础上额外传入了两个参数,第一个额外参数为回调方法,第二个额外参数为回调方法中的参数的AsyncState属性访问到的值,需要注意回调方法必须是只有一个IAsyncResult类型参数且没有返回值的方法
第二个传入的参数虽然格式不固定,但是非常推荐传入监听消息的数据流通道NetworkStream,因为一个服务器可能同时拥有多个数据流通道,但是回调方法中无法获取到具体是哪个数据通道收到的消息,所以需要将接受消息的数据通道通过参数的方式传递到回调方法中
回调方法执行时,bytes数组保存的内容已经是数据流通道接收到的数据,所以可以直接将bytes数组通过Encoding.UTF8.GetString方法转换为字符串
回调方法的最后一句为数据流通道再次开启接受数据并添加回调,这样才能达到多次接受消息的效效果
示例代码
示例代码仅用作展示方法功能,没有做异常处理,仅供学习使用
客户端代码
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System;public class SocketScript : MonoBehaviour
{TcpClient client;NetworkStream stream;byte[] byt = new byte[1024];/// <summary>/// 声明客户端连接服务器创建数据流通道并添加消息监听/// </summary>void Start(){client = new TcpClient();client.Connect("127.0.0.1",12345);stream = client.GetStream();byte[] b = Encoding.UTF8.GetBytes("客户端连接成功");stream.BeginWrite(b, 0, b.Length, s => { Debug.Log("客户端连接通知发送成功"); }, null);stream.BeginRead(byt,0,byt.Length,Listen,null);}/// <summary>/// 数据流通道接受消息回调/// </summary>void Listen(IAsyncResult asyncResult){string str = Encoding.UTF8.GetString(byt);Debug.Log("客户端收到消息:" + str);stream.BeginRead(byt, 0, byt.Length, Listen, byt);}/// <summary>/// 客户端向服务器发送消息/// </summary>void Send(string str){byte[] b = Encoding.UTF8.GetBytes(str);stream.BeginWrite(b,0,b.Length,s => { Debug.Log("客户端发送消息" + str.ToString()); },str);}private void Update(){if (Input.GetKeyDown(KeyCode.Space)){Send("客户端消息");}}
}
服务器代码
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;public class SeverSocketScript : MonoBehaviour
{TcpListener listener;Dictionary<string,NetworkStream> allClient = new Dictionary<string, NetworkStream>();/// <summary>/// 声明服务器并开启监听/// </summary>void Start(){listener = new TcpListener(new IPEndPoint(IPAddress.Any,12345));listener.Start();listener.BeginAcceptTcpClient(CreatConnectSocket, null);}/// <summary>/// 服务器监听到客户端连接申请的回调方法/// </summary>void CreatConnectSocket(IAsyncResult asyncResult){TcpClient client = listener.EndAcceptTcpClient(asyncResult);NetworkStream stream = client.GetStream();string point = client.Client.RemoteEndPoint.ToString();if (!allClient.ContainsKey(point)){Debug.Log("新用户" + point + "已连接到服务器");allClient.Add(point, stream);}byte[] b = Encoding.UTF8.GetBytes("服务器已连接");stream.BeginWrite(b, 0, b.Length, ar => { Debug.Log(ar.AsyncState.ToString()); }, "连接回执已发送");stream.BeginRead(bytes,0, bytes.Length, ListenMessage, stream);listener.BeginAcceptTcpClient(CreatConnectSocket, null);}byte[] bytes = new byte[1024];/// <summary>/// 数据流通道接受消息回调方法/// </summary>void ListenMessage(IAsyncResult asyncResult){NetworkStream stream = (NetworkStream)asyncResult.AsyncState;string str = Encoding.UTF8.GetString(bytes);Debug.Log("服务器接受消息:" + str);byte[] b = Encoding.UTF8.GetBytes("服务器发送消息回执");stream.BeginWrite(b, 0, b.Length, (obj) => { Debug.Log("消息回执已发送"); }, null);stream.BeginRead(bytes, 0, bytes.Length, ListenMessage, stream);}/// <summary>/// 由服务器向全部客户端进行数据广播/// </summary>void Send(string str){byte[] b = Encoding.UTF8.GetBytes(str);foreach (var pair in allClient) {pair.Value.BeginWrite(b, 0, b.Length, obj => { Debug.Log("服务器向" + pair.Key + "发送消息"); }, null);}}private void Update(){if (Input.GetKeyDown(KeyCode.Space)){Send("服务器消息");}}
}
Udp连接
Udp连接介绍
udp连接的特点是在发送消息时不需要和目标客户端进行长连接,例如拥有ABCD四个udp客户端时,A可以给B发消息,然后紧接着给C再发一条消息,然后收到D的消息,这种情况使用tcp是肯定做不到的,所以相比之下udp比tcp要更加灵活
但是udp的确定对比tcp也显而易见,udp无法时刻与目标客户端保持通信状态,只能在向目标客户端发送消息时获取对方状态,这种情况下的连接属于不稳定链接,无法时时监控对方状态,同事信息传输的效率也不如需要建立稳定连接的tcp协议效率高
Udp部分介绍
udp没有明确的客户端与服务器的区分,客户端既服务器,但是udp在同步异步的基础上,还有:单播,组播,广播,三种方式,这里不深入讲广播,正常一对一收发消息属于单播,所以需要额外讲解一下组播。
所以udp部分的内容为:upd客户端(同步),udp组播,udp异步方法
Upd客户端
声明Udp客户端
UdpClient client = new UdpClient(12345);
udp客户端的关键字为UdpClient,在创建的时候需要调用构造函数,构造函数的参数是客户端指定的端口,端口是必须传入的,客户端的Ip将会从本地的Ipv4地址中自动获取,不需要传入
Udp客户端发送消息
IpEndPoint sendEndPoint = new IPEndPoint(IPAddress.Parse("127.0.0.1"),12345)
byte[] bytes = Encoding.UTF8.GetBytes("消息内容");
client.Send(bytes,bytes.Length, sendEndPoint);
Udp客户端使用UdpClient.Send方法发送消息,方法的三个参数分别是
第一个参数为需要发送的byte数组
第二个参数为发送数据的长度,一般都会发送数组的全部的内容,所以传入数组的长度
第三个参数为消息发送的目标,传入IpEndPoint类型
对比Tcp的发送方法,参数中可以少传入一个发送的起始索引,多了一个发送的目标Ip,因为Udp客户端没有稳定链接的通信,所以每次发送消息时都需要传入目标的EndPoint,并在EndPoint中必须指定对方的Ip地址和端口号
Udp客户端发送消息方法的方法发送的目标是另一个声明的Udp客户端,而非Tcp概念上的Tcp服务器,Udp不存在单独的服务器关键字,Udp的消息发送和接受都是由客户端到客户端的过程
UdpClient.Send()方法为同步方法,调用后线程会进入阻塞状态,这个方法的异步版本在后面
Udp客户端接受消息
byte[] getByte = new byte[1024];
IPEndPoint listenEndPoint;
getByte = client.Receive(ref listenEndPoint);
string str = Encoding.UTF8.GetString(getByte);
Udp客户端使用UdpClient.Receive方法接受消息
方法的功能为:Udp客户端开始接受来自其他Udp客户端发送的消息,接收到任何的网络消息时都会将消息发送的发送方的IpEndPoint覆盖掉方法带有ref的参数中,然后将消息的内容作为返回值
UdpClient.Receive()方法为同步方法,调用后线程会进入阻塞状态,这个方法的异步版本在后面
Udp客户端部分总结
udp单播功能客户端的同步方法只有这三个,也就是只用单播功能和同步方法的话最重要的三个部分只有:声明客户端,发送,接受,这三个部分,而且客户端之间可以相互发送消息不需要经过服务器,对比Tcp连接来说写法上要更为简单。
Udp组播
Udp组播介绍
组播为Udp协议独有的消息收发机制,可以将Udp客户端注册并加入一个消息组,然后客户端可以向这个聊天组内发送消息,所有的聊天组内成员都会收到,并且得到消息发送者的Ip地址信息。
即使加入消息组,客户端自身的端口也必须和向消息组发送消息的方法中传入的端口一致才行,否则无法接受到消息
Udp客户端加入消息组
UdpClient client = new UdpClient(12345);
client.JoinMulticastGroup(IPAddress.Parse("224.100.0.1"));
client.EnableBroadcast = true;
这段代码中有三行逻辑
第一行逻辑为声明一个Udp客户端
第二行的逻辑为指定Udp客户端加入到一个消息组当中,参数为这个消息组的Ip,需要注意的是消息组为一个Ip地址,这个地址必须由224开头,后面的数字可以变化。
同一个网络下可以有多个客户端进入同一个消息组,这个消息组的Ip收到消息后消息组内的所有客户端都将收到消息
加入消息组时客户端的端口仍然有作用,客户端声明时传入的端口必须和发送消息的目标端口一致,否则无法收到消息
第三行为激活客户端的组播功能,这个功能默认时关闭的,Udp客户端希望激活组播功能时需要将EnableBroadcast属性设置为true
Udp客户端向消息组发送信息
IpEndPoint sendEndPoint = new IPEndPoint(IPAddress.Parse("224.100.0.1"),12345);
byte[] bytes = Encoding.UTF8.GetBytes(str);
client.Send(bytes,bytes.Length, sendEndPoint);
组播发送消息的逻辑和正常的单播完全相同,唯一的区别就是发送消息的目标Udp客户端的Ip地址改为了消息组的Ip地址的端口
加入消息组的Udp客户端自身的端口需要和发送消息方法中传入的端口一致,否则无法收到消息
Udp客户端从消息组接受消息
byte[] getByte = new byte[1024];
IPEndPoint listenEndPoint;
getByte = client.Receive(ref listenEndPoint);
string str = Encoding.UTF8.GetString(getByte);
客户端从消息组接受消息的方法和从客户端接受消息的方法完全相同,甚至代码我都是复制过来的
不过其中Receive方法中ref的参数可以返回向消息组发送消息的客户端的具体Ip地址和端口
也可以通过自己加入消息组,自己向消息组发送消息,自己接受自己的消息,来获取自己的Ipv4地址和端口
示例代码:
Udp组播不区分客户端和服务器,所以只有一个代码,需要两个以上的电脑,每个电脑运行一份代码,然后就可以组内互相发送消息了
示例代码未做任何异常处理,仅供学习方法功能使用
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;public class SocketScript : MonoBehaviour
{IPEndPoint sendEndPoint;IPEndPoint listenEndPoint;UdpClient client;byte[] getByte = new byte[1024];void Start(){sendEndPoint = new IPEndPoint(IPAddress.Parse("224.100.0.1"),12345);client = new UdpClient(12345);client.JoinMulticastGroup(IPAddress.Parse("224.100.0.1"));client.EnableBroadcast = true;Thread thread = new Thread(Listen);thread.Start();}void Listen(){while (true){getByte = client.Receive(ref listenEndPoint);Debug.Log("收到消息:" + Encoding.UTF8.GetString(getByte) + "\n" + "消息来自" + listenEndPoint.ToString());}}void Send(string str){byte[] bytes = Encoding.UTF8.GetBytes(str);client.Send(bytes,bytes.Length, sendEndPoint);Debug.Log("发送消息:" + str);}private void Update(){if (Input.GetKeyDown(KeyCode.Space)){Send("发送广播消息");}}
}
Udp异步方法
Udp客户端发送消息的异步方法
Udp客户端发送消息的同步方法为:
client.Send(bytes,bytes.Length, sendEndPoint);
这个方法的异步方法为:
client.BeginSend(bytes,bytes.Length,sendEndPoint,SendCallBack,"消息发送成功");
其中SendCallBack方法为回调方法,方法内容为:
void SendCallBack(IAsyncResult asyncResult){Debug.Log(asyncResult.AsyncState.ToString());}
对比同步方法,异步方法的关键字由Send改变为BeginSend的同时,需要额外传入两个参数
第一个参数为消息发送成功的回调方法
第二个参数为回调方法的参数IAsyncResult的AsyncState属性访问到的值
其中需要注意的是能在BeginSend中使用的回调方法,必须只有一个IAsyncResult类型的参数且没有返回值
Udp客户端接受消息的异步方法
Udp客户端接受消息的同步方法为:
getByte = client.Receive(ref listenEndPoint);
这个方法的异步方法为:
client.BeginReceive(ListenCallBack, null);
其中,ListenCallBack为回调方法,方法的内容为:
void ListenCallBack(IAsyncResult asyncResult)
{getByte = client.EndReceive(asyncResult,ref listenEndPoint);string str = Encoding.UTF8.GetString(getByte);Debug.Log("收到消息" + str + "\n消息来自" + listenEndPoint.ToString());client.BeginReceive(ListenCallBack, client);
}
Udp客户端接受消息的方法为BeginReceive,这个方法需要两个参数
第一个参数为接受消息是调用的回调方法
第二个参数为回调方法的参数IAsyncResult的AsyncState属性访问到的值
在回调方法中,最为重要的方法是EndReceive方法
EndReceive方法是仅写在BeginReceive方法中的回调方法里面的内容,有两个参数一个返回值
第一个参数为BeginReceive的回调方法中必须存在的参数
第二个参数为带有ref关键字,方法执行完后参数将保存接受消息时发送消息的客户端
返回值为消息的内容,类型为byte数组
如果不想只接受一次消息而是持续接受多次消息,则需要在回调方法的最后再次调用BeginReceive函数开启接收消息的异步方法。
其中需要注意的是能在BeginReceive中使用的回调方法,必须只有一个IAsyncResult类型的参数且没有返回值
示例代码
在上个部分的组播示例代码中,将发送和接受改为异步方法,其余均不变
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System;public class SocketScript : MonoBehaviour
{IPEndPoint sendEndPoint;IPEndPoint listenEndPoint;UdpClient client;byte[] getByte = new byte[1024];void Start(){sendEndPoint = new IPEndPoint(IPAddress.Parse("224.100.0.1"),12345);client = new UdpClient(12345);client.JoinMulticastGroup(IPAddress.Parse("224.100.0.1"));client.EnableBroadcast = true;client.BeginReceive(ListenCallBack, null);}void Send(string str){byte[] bytes = Encoding.UTF8.GetBytes(str);client.BeginSend(bytes,bytes.Length,sendEndPoint,SendCallBack,"消息发送成功");Debug.Log("发送消息:" + str);}void SendCallBack(IAsyncResult asyncResult){Debug.Log(asyncResult.AsyncState.ToString());}void ListenCallBack(IAsyncResult asyncResult){getByte = client.EndReceive(asyncResult,ref listenEndPoint);string str = Encoding.UTF8.GetString(getByte);Debug.Log("收到消息" + str + "\n消息来自" + listenEndPoint.ToString());client.BeginReceive(ListenCallBack, client);}private void Update(){if (Input.GetKeyDown(KeyCode.Space)){Send("发送广播消息");}}
}