Unity网络通讯的一些理解——强联网socket

时间:2021-06-24 15:35:41
强联网 在我们的游戏开发中所占比重越来越大,尤其是开发MMO游戏时,更需要强联网来进行实时更新,所以我们就有了强联网的需要。
首先我们了解一下强联网的工作原理,说到强联网,我们就会想到 socket
socket 是对 TCP/IP 协议的封装和应用,是面向程序员的,给我们提供了操作网络的接口,但是我们也必须基本了解它的工作原理。

强联网我们主要使用的是TCPUDP,首先我们说一下TCP

Transmission Control Protocol/Internet Protocol的简写,中译名为传输控制协议/因特网互联协议,又名网络通讯协议,是Internet最基本的协议、Internet国际互联网络的基础,由网络层的IP协议和传输层的TCP协议组成。TCP/IP 定义了电子设备如何连入因特网,以及数据如何在它们之间传输的标准。协议采用了4层的层级结构,每一层都呼叫它的下一层所提供的协议来完成自己的需求。通俗而言:TCP负责发现传输的问题,一有问题就发出信号,要求重新传输,直到所有数据安全正确地传输到目的地。而IP是给因特网的每一台联网设备规定一个地址。

一说到 TCP ,必然会想到 三次握手 四次挥手


三次握手:客户端和服务端建立连接需要三次握手
    第一次:客户端向服务端发送报文,向服务器发送连接请求;
    第二次:服务端向客户端返回ACK报文,通知客户端可以连接;
    第三次:客户端收到服务端报文,正式连接服务端。

四次挥手:客户端要与服务器断开连接,需要四次挥手
    第一次:客户端向服务端发送FIN报文,向服务器发送中断连接请求;
    第二次:服务器收到客户端中断请求,向服务器发送已得知中断请求,但服务器还有资源未处理,需要等待;
    第三次:服务器处理完数据后,再次向客户端发送报文,告诉客户端可以断开连接了;
    第四次:客户端收到服务端断开连接的确认信息后,最后发送信息看是否真的断开连接了,如果服务器没有一段时间没有回应,则说明已经断开,中断过程完成。



如何使用强联网?
在c#中我们可以通过使用Socket来进行连接。


服务端:创建Socket->绑定IP端口->设置排队连接请求数量->启动监听->收发消息->关闭
客户端:创建Socket->连接对应IP端口->开始收发->关闭


TCP的连接,三个参数分别是地址簇(ipv4),传输模式(流模式),协议(TCP)
Socket =new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);

UDP的连接,三个参数分别是地址簇(ipv4),传输模式(数据报模式),协议(UDP)
Socket =new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);

注意:在网络传输数据的过程中会出现分包粘包的现象,让我们首先看一下这两个现象的具体情况。


分包传输数据不完整,一条信息被分成多次发送。
顾名思义,比如我们发送了一条信息:“你好”,如果分包现象发生,我们可能只收到了“你”,却没有收到“好”,这样就会导致数据的不完整。


粘包传输的多条数据粘在一起。
也像字面意思一样,比如我发了两句话“你好”和“很高兴认识你”,我们可能会收到“你好很”和“高兴认识你”这样两条信息。甚至会出现“你好很高”,却没有收到剩余数据的情况,这是分包粘包同时发生。


在应用中,我们肯定不期望这种现象发生,所以我们就有必要对我们发送的数据进行处理。

具体的解决方案就是,将我们所需要发送的数据作为一个数据包进行发送。也就是,我们每发送的一条数据就是一个数据包,我们是通过使用字节流来传输数据的,所以当我们每发送一个数据包,就顺便附带上数据包的长度,这样就形成了“数据包头”+“包体”的结构,包头用来存储数据包长度,包体用来存储具体的数据,每当我们接受数据时,首先读取数据包头,得到数据长度,再和已经传过来的长度对比,如果长度足够,说明至少传过来一个完整的包,我们就可以根据长度来取包体,如果不够,我们先不读取,直到长度满足时,我们再把这条数据进行读取。

这样就能方便的解决分包粘包的问题,但是我们之前接受到的不完整的信息放到哪呢?我们就需要一个缓存区,如果数据不完整,我们先放在缓存,当数据完整时,再取出读取。这里有一种建立缓存区的方案,通过内存流来读取。


缓存区:我们使用内存流MemoryStream来进行读写操作,如果我们收到数据,我们就将数据写入内存流,当内存流的长度满足包头长度时,就将消息取出读取。

数据包:数据包头+包体是我们最基本的结构,但我们的数据不可能就这样来传输,因为一旦有人截获我们的数据,就能轻易的修改我们的数据值,对游戏造成影响,所以我们必须要对数据包进行加密。


接下来就是传输协议,我们传输的数据都是字节流,那么我们收到这些字节流后如何处理呢?这就需要我们对数据包进行进一步编辑,给数据包一个协议ID,即把包体分为两部分:包体=协议ID+内容;当我们收到数据后,首先进行拆包,获取到协议ID后,就进行对应ID的事件派发,派发事件后,对应的功能就会被执行,并且返回新的数据发送到另一端。

public class XmlSocket
{
//异步socket侦听从客户端传来的数据
public static string data=null;
//Threadsignal.线程用一个指示是否将初始状态设置为终止的布尔值初始化ManualResetEvent类的新实例。
public static ManualResetEvent allDone=new ManualResetEvent(false);
static void Main(string[]args)
{
StartListening();
}
public static void StartListening()
{
//传入数据缓冲
byte[] bytes = new Byte[1024];
//建立本地端口
IPAddress ipAddress;
String ipString = ConfigurationManager.AppSettings.Get("SocketIP");
if(ipString == null || ipString == String.Empty)
{
IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName());
ipAddress = ipHostInfo.AddressList[0];
}
else
{
ipAddress = IPAddress.Parse(ipString);
}
int port;
String portString = ConfigurationManager.AppSettings.Get("SocketPort");
if(portString == null || portString == String.Empty)
{
port=11001;
}
else
{
port=int.Parse(portString);
}
IPEndPoint localEndPoint = new IPEndPoint(ipAddress, port);
Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//绑定端口和数据
listener.Bind(localEndPoint);
listener.Listen(100);
while(true)
{//设置无信号状态的事件
allDone.Reset();
//重启异步连接
listener.BeginAccept(new AsyncCallback(AcceptCallback),listener);
//等待连接创建后继续
allDone.WaitOne();
}
public static void AcceptCallback(IAsyncResult ar)
{
//接受回调方法。该方法的此节向主应用程序线程发出信号,让它继续处理并建立与客户端的连接
allDone.Set();
//获取客户端请求句柄
Socket listener = (Socket)ar.AsyncState;
Socket handler = listener.EndAccept(ar);
StateObject state = new StateObject();
state.workSocket = handler;
handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReadCallback),state);
}
//与接受回调方法一样,读取回调方法也是一个AsyncCallback委托。该方法将来自客户端套接字的一个或多个字节读入数据缓冲区,然后再次调用BeginReceive方法,直到客户端发送的数据完成为止。从客户端读取整个消息后,在控制台上显示字符串,并关闭处理与客户端的连接的服务器套接字。
public static void ReadCallback(IAsyncResult ar)
{
String content = String.Empty;
//创建自定义的状态对象
StateObject state = (StateObject)ar.AsyncState;
Socket handler = state.workSocket;//处理的句柄
//读出
int bytesRead = handler.EndReceive(ar);
if(bytesRead>0)
{
String len = Encoding.UTF8.GetBytes(result).Length.ToString().PadLeft(8,'0');
log.writeLine(len);
Send(len + result, handler);
}
}
private static void Send(string data, Socket handler)
{
byte[] byteData = Encoding.UTF8.GetBytes(data);
handler.BeginSend(byteData, 0, byteData.Length, 0, new AsyncCallback(SendCallback), handler);
}
private static void SendCallback(IAsyncResult ar)
{
Socket handler = (Socket)ar.AsyncState;
//向远端发送数据
int bytesSent = handler.EndSend(ar);
StateObject state = new StateObject();
state.workSocket = handler;
handler.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0, new AsyncCallback(ReadCallback),state);
handler.Shutdown(SocketShutdown.Both);
handler.Close();
}
public static void StopListening()
{
allDone.Close();
log.close();
}

以上是一个demo,可供参考





下一次我会简单地说一下如何使用观察者模式在这里进行事件的派发,以及在网络模块中的一些其他应用。