学习《Unity3D网络游戏实战》(第二版)时的学习笔记
上一篇: Unity网络游戏编程学习(四)
解决沾包的方法
一般有三种方法可以解决沾包和半包问题,分别是长度信息法,固定长度法和结束符号法
长度信息法
长度信息法是在每个数据包前加上长度信息。每次接收数据后,先读取表示长度的自己,如果缓冲区的数据长度大于要读取的字节数,则取出相应的字节,否则等待下一次数据接收。
固定长度法
每次都以相同的长度发送数据,假设固定长度为10个字符,那么“Hello”可以发送成“Hello…”,其中的“.”为填充字符,是为了凑数,没有实际意义。接收方每次读取10个字符,作为一条消息去处理。如果读到的字符数大于10,比如读到的字符为“Hello…Worl”,只把前10个字符“Hello…”抽取出来,再把后面的“Worl”保存起来,等到下一次接收数据,拼接成第二条信息。(由于显示的原因,“.”最多只能显示3个,徐阅读者自己理解补充,请见谅)。
结束符号法
规定一个结束符号,作为消息间的分隔符。假设j结束符号为“#”,那么“Hello”可以发送成“Hello#”。接收方每次读取数据,直到“#”出现为止,并且使用“#”分割消息。如果读到的字符为“Hello#Worl”,只把结束符前的“Hello”抽取出来,再把“Worl”保存起来,等到下一次接收数据,拼接成第二条信息。
示例程序
下面是用长度信息法解决沾包的示例代码。
客户端代码
具体的方法解释请看代码中的注释
Echo.cs
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using UnityEngine;
using UnityEngine.UI;
public class Echo : MonoBehaviour
{
public InputField inputInfo;
public Text info;
//定义套接字
Socket socket;
//接收
byte[] recvBuffer = new byte[1024];
int recvBuffCount = 0;//接收缓冲区的长度
string recvStr;
//发送
byte[] sendBuffer = new byte[1024];
string sendStr;
public void Connect()
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.BeginConnect("127.0.0.1", 8888, ConnectCallBack, socket);
}
public void ConnectCallBack(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
socket.EndConnect(ar);
Debug.Log("Socket connect success");
socket.BeginReceive(recvBuffer, recvBuffCount, 1024 - recvBuffCount, 0, ReceiveCallBack, socket);
}
catch (SocketException ex)
{
Debug.Log("Socket connect fail" + ex.ToString());
}
}
/// <summary>
/// 接收回调
/// </summary>
/// <param name="ar"></param>
public void ReceiveCallBack(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
//获取接收数据的长度
int count = socket.EndReceive(ar);
recvBuffCount += count;
//处理二进制数据
OnReceiveData();
//继续接收数据
socket.BeginReceive(recvBuffer, recvBuffCount, 1024 - recvBuffCount, 0, ReceiveCallBack, socket);
}
catch (SocketException ex)
{
Debug.Log("Socket receive fail" + ex.ToString());
}
}
/// <summary>
/// 处理二进制数据
/// </summary>
public void OnReceiveData()
{
Debug.Log("[Recv 1] recvBuffCount = " + recvBuffCount);
Debug.Log("[Recv 2] recvBuffer = " + BitConverter.ToString(recvBuffer));
//如果缓冲区长度小于2,不去处理
if (recvBuffCount <= 2) return;
//得到消息体长度,只取消息体的前两个字节
Int16 bodyLength = BitConverter.ToInt16(recvBuffer, 0);
Debug.Log("[Recv 3] bodyLength = " + bodyLength);
//如果缓冲区的长度大于2,但是不足以组成一条消息,不去处理
//已知这条消息的长度为 2 + bodyLength,而当前的缓冲区长度为recvBuffCount
if (recvBuffCount < 2 + bodyLength) return;
string s = Encoding.UTF8.GetString(recvBuffer, 2, bodyLength);
Debug.Log("[Recv 4] s = " + s);
//更新缓冲区
int start = 2 + bodyLength;
int count = recvBuffCount - start;
//将已经读取的内容用为读取的覆盖
Array.Copy(recvBuffer, start, recvBuffer, 0, count);
recvBuffCount -= start;
Debug.Log("[Recv 1] recvBuffCount = " + recvBuffCount);
//消息处理
recvStr = s + "\n" + recvStr;
Debug.Log(recvStr);
//递归继续读取消息
OnReceiveData();
//递归的退出条件是消息体条件不足时的两个return
}
/// <summary>
/// 点击发送按钮
/// </summary>
/// <param name="sendStr"></param>
public void Send()
{
sendStr = inputInfo.text;
//使用长度信息法组装协议
byte[] bodyBytes = Encoding.UTF8.GetBytes(sendStr);
//获取字符长度,转换成16进制
Int16 len = (Int16)bodyBytes.Length;
byte[] lenBytes = BitConverter.GetBytes(len);
//Concat进行字节拼接
sendBuffer = lenBytes.Concat(bodyBytes).ToArray();
socket.BeginSend(sendBuffer, 0, sendBuffer.Length, 0, SendCallBack, socket);
Debug.Log("[Send]"+BitConverter.ToString(sendBuffer));
}
/// <summary>
/// Send回调
/// </summary>
/// <param name="ar"></param>
public void SendCallBack(IAsyncResult ar)
{
try
{
Socket socket = (Socket)ar.AsyncState;
int count = socket.EndSend(ar);
Debug.Log("Socket send success" + count);
}
catch (SocketException ex)
{
Debug.Log("Socket send fail" + ex.ToString());
}
}
void Update()
{
info.text = recvStr;
}
}
服务器代码
ClientState.cs
using System.Net.Sockets;
namespace _04_Chartroom_Select_Server
{
public class ClientState
{
public Socket socket;
public byte[] recvBuffer = new byte[1024];
public string recvStr = "";
}
}
MainClass.cs
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace _04_Chartroom_Select_Server
{
public class MainClass
{
static Socket listenfd;
static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();
public static void Main(string[] args)
{
//Socket
listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//Bind
IPAddress ipAdr = IPAddress.Parse("127.0.0.1");
IPEndPoint iPEndPoint = new IPEndPoint(ipAdr, 8888);
listenfd.Bind(iPEndPoint);
//Listen
listenfd.Listen(0);
Console.WriteLine("[服务器启动成功]");
//定义要监听的Socket链表
List<Socket> checkRead = new List<Socket>();
while (true)
{
//填充checkRead列表
checkRead.Clear();
checkRead.Add(listenfd);
foreach (ClientState cs in clients.Values)
{
checkRead.Add(cs.socket);
}
//Select,多路复用,同时检测多个Socket的状态
Socket.Select(checkRead, null, null, 1000);
//检查可读对象
foreach (Socket so in checkRead)
{
if (so == listenfd)
{
ReadListenfd(so);
}
else
{
ReadClientfd(so);
}
}
}
}
/// <summary>
/// 读取listenfd,开始Accept
/// </summary>
/// <param name="listenfd"></param>
public static void ReadListenfd(Socket listenfd)
{
Console.WriteLine("有一个客户端接入");
Socket clientfd = listenfd.Accept();
ClientState clientState = new ClientState();
clientState.socket = clientfd;
clients.Add(clientfd, clientState);
}
public static bool ReadClientfd(Socket clientfd)
{
ClientState clientState = clients[clientfd];
//接收
int count = 0;
try
{
count = clientfd.Receive(clientState.recvBuffer);
}
catch (SocketException ex)
{
clientfd.Close();
Console.WriteLine("Receive SocketException " + ex.ToString());
return false;
}
//客户端关闭
if (count == 0)
{
clientfd.Close();
clients.Remove(clientfd);
Console.WriteLine("有一个客户端断开连接");
return false;
}
clientState.recvStr = Encoding.UTF8.GetString(clientState.recvBuffer, 2, count - 2);
Console.WriteLine("Receive:" + clientState.recvStr);
//远端IP+接收到的内容
byte[] sendBuffer = new byte[count];
Array.Copy(clientState.recvBuffer, 0, sendBuffer, 0, count);
foreach (ClientState cs in clients.Values)
{
cs.socket.Send(sendBuffer);
}
return true;
}
}
}
下一篇:Unity网络游戏编程学习(六)