1. TCP 协议是一种基于数据流的协议
-
Socket的Receive方法只是把接收缓冲区的数据提取出来,当系统的接收缓冲区为空,Receive方法会被阻塞,直到里面有数据。
-
Socket的Send方法只是把数据写入到发送缓冲区里,具体的发送过程由操作系统负责。当操作系统的发送缓冲区满了,Send方法会阻塞
2.解决粘包问题的方法
一般有三种方法 可以解决粘包和半包问题,分别是
- 长度信息法: 指在每个数据包前面加上长度信
- 固定长度法: 每次都以相同的长度发送数据
- 结束符号法
一般 的游戏开发会在每个数据包前面加上长度字节, 以方便解析
2.1 发送数据代码
//点击发送按钮
public void Send(string sendStr)
{
//组装协议
byte [] bodyBytes = System.Text.Encoding.Default.GetBytes(sendStr);
Int16 len = (Int16)bodyBytes.Length;
byte[] lenBytes = BitConverter.GetBytes (len);
byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray( );
// 为了精简代码:使用同步Send
// 不考虑抛出异常
socket.Send(sendBytes);
}
2.2 定义一个缓冲区(readBuff)和一个指示缓冲区有效数据长度变量(buffCount)。
// 接收缓冲区
byte[] readBuff= new byte[1024];
// 接收缓 冲区的数据 长度
int buffcount = 0;
public void ReceiveCallback (IAsyncResult ar) {
Socket socket = (Socket) ar.AsyncState;
// 获取接收数据长度
int count = socket.EndReceive(ar);
buffCount += count;
.....
}
3. 大小端
3.1 使用Reverse()兼容大小端编码
如果使用BitConverter.GetBytes将数字转换成二进制数据,转换出来的数据有可能基于大端模式, 也有可能基于小端模式。
因为我们规定必须使用小端编码,一个简单的办法 是,判断系统是否是 小端编码的系统,如果不是,就使用Reverse() 方法将大端编码转换为 小端编码。以Send 为例,代码如下:
//点击发送按钮
public void Send ()
{
string sendStr = InputFeld.text;
// 组装协议
byte[| bodyBytes = System.Text.Encoding.Default.GetBytes (sendStr);
Int16 len = (Int16)bodyBytes.Length;
byte[] lenBytes = BitConverter.GetBytes (len);
//大 小 端编 码
if(!BitConverter.IsLittleEndian) {
Debug. Log (" [Send] Reverse lenBytes");
lenBytes.Reverse();
}
// 拼接字节
byte[] sendBytes = lenBytes.Concat(bodyBytes) .ToArray ( );
socket.Send(sendBytes);
}
3.2 手动还原数值
BitConverter. Toint16中根据系统大小端采用不同的编码方式,如果是小端编码,返回 的 是(pbyte) | ((pbyte+1)<< 8),如果是大端编码,返回的是(pbyte<< 8) | ((pbyte+1)。以小端为例,由于采用指针,(pbyte)指向缓冲区中指定位置的第1 个字节,(pbyte + 1)指向缓冲区中指定位置的第2个字节,((pbyte+1)<<8)表示左移8位,相当于乘以 256,返回的数字便是“第1个字节+第2字节256”,与4. 4.1节中介绍的步骤相同。
public void OnReceiveData ( ) {
// 消息长 度
if (buffCount <= 2)
return;
// 消息长度
Int16 bodyLength = (short) ((readBuff[1] << 8) | readBuff[0]);
Debug.Log ( "[Recv] bodyLength=" + bodyLength);
// 消息体、更新 缓冲区
// 消 息 处 理 、 继 续读 取 消 息
.....
}
readBuff[0] 代表缓冲区的第1 个字节,readBuff[1] 代 表 缓 冲 区 的 第 2 个 字 节, ( readBuff[1] < < 8 ) 代 表 将 缓 冲 区 第 2 个 字 节 的 数 据 乘 以 2 5 6 , 中 间的“|” 代表逻辑与,在这里等同于相加。
- 完整的ByteArray
ByteArray作为一种通用的byte型缓冲区结构 ,它应该支持自动扩展,支持常用的读写操作 。 同时 , 为了做到极致的效率, ByteArray 的大部分成员变量都设为
public ,以提供灵活性
readldx 代表可 读位置,即缓冲区有效数据的起始位置,writeldx 代表可写位置,即缓冲区有效数据的末尾。 成员两数remain 代表缓冲区还可以容纳的字节数
using System;
public class ByteArray {
//默认大小
const int DEFAULT_SIZE = 1024;
//初始大小
int initSize = 0;
//缓冲区
public byte[] bytes;
//读写位置
public int readIdx = 0;
public int writeIdx = 0;
//容量
private int capacity = 0;
//剩余空间
public int remain { get { return capacity-writeIdx; }}
//数据长度
public int length { get { return writeIdx-readIdx; }}
//构造函数
public ByteArray(int size = DEFAULT_SIZE){
bytes = new byte[size];
capacity = size;
initSize = size;
readIdx = 0;
writeIdx = 0;
}
//构造函数
public ByteArray(byte[] defaultBytes){
bytes = defaultBytes;
capacity = defaultBytes.Length;
initSize = defaultBytes.Length;
readIdx = 0;
writeIdx = defaultBytes.Length;
}
//重设尺寸
public void ReSize(int size){
if(size < length) return;
if(size < initSize) return;
int n = 1;
while(n<size) n*=2;
capacity = n;
byte[] newBytes = new byte[capacity];
Array.Copy(bytes, readIdx, newBytes, 0, writeIdx-readIdx);
bytes = newBytes;
writeIdx = length;
readIdx = 0;
}
//写入数据
public int Write(byte[] bs, int offset, int count){
if(remain < count){
ReSize(length + count);
}
Array.Copy(bs, offset, bytes, writeIdx, count);
writeIdx+=count;
return count;
}
//读取数据
public int Read(byte[] bs, int offset, int count){
count = Math.Min(count, length);
Array.Copy(bytes, 0, bs, offset, count);
readIdx+=count;
CheckAndMoveBytes();
return count;
}
//检查并移动数据
public void CheckAndMoveBytes(){
if(length < 8){
MoveBytes();
}
}
//移动数据
public void MoveBytes(){
Array.Copy(bytes, readIdx, bytes, 0, length);
writeIdx = length;
readIdx = 0;
}
//读取Int16
public Int16 ReadInt16(){
if(length < 2) return 0;
Int16 ret = BitConverter.ToInt16(bytes, readIdx);
readIdx += 2;
CheckAndMoveBytes();
return ret;
}
//读取Int32
public Int32 ReadInt32(){
if(length < 4) return 0;
Int32 ret = BitConverter.ToInt32(bytes, readIdx);
readIdx += 4;
CheckAndMoveBytes();
return ret;
}
//打印缓冲区
public override string ToString(){
return BitConverter.ToString(bytes, readIdx, length);
}
//打印调试信息
public string Debug(){
return string.Format("readIdx({0}) writeIdx({1}) bytes({2})",
readIdx,
writeIdx,
BitConverter.ToString(bytes, 0, capacity)
);
}
}
4.2. 重设尺寸
在 某 此 情 况 下, 比 如 需 要 写 人 的 数 据 量 大 于 缓 冲 区 剩 余 长 度 (r e m a i n ) 时 , 就 需 要 扩 大 缓 冲 区。 例 如 要 在 图 4 - 4 2 所 示 缓 冲 区 后 面 添 加 数 据 〝0 5 h e l l o ” , 使 绥 冲 区 数 据 变 成 〝02hioshello”。此时缓冲区只剩余6 个字节,但“0shello” 是7个字节,放不下。此时的 做法是,重新申请一 个长度合适的byte 数组,然后把原byte 数组的数据复制过去,再重新 设置readldx、writeldx 等数值。
为了避免频繁重设尺寸,规定每次翻倍增加bytes数组长度,即长度是1、2、4、8、 1 6 、 32 、 64 、 128 、 256 、 512 、 1024 、 2048 等 值 。 如果参数 size 是1500 , 缓冲区长度会被设置成 2048 , 如果参数 size 是130 (假设size值合理), 缓冲区长度会被设 置成256。通过“ int n= 1; while(n<size) n*= 2〞 这两行代码,即可得出长度值。在图4-42 所示的缓冲区上执行Resize(6)后,缓冲区将变成图 4- 43 所 示的样式。
4.3 移动数据
在 某 些 情 形 下 , 例 如 有 效 数 据 长 度 很 小 ( 这 里 设 置 为 8 ), 或 者 数 据 全 部 被 读 取 时 ( readldx==writeldx),可以将数据前移,增加remain,避免bytes数组过长。由于数据很 少, 程 序 执 行 的 效 率 不 会 有 影 响 。 在 图 4 - 4 0 所 示 的 缓 冲 区 上 执 行 CheckAndMoveBytes 后 , 缓冲 区将 变成图4- 44 所示的样式。
- 把上面的ByteArray 应用到异步
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Net.Sockets;
using UnityEngine.UI;
using System;
using System.Linq;
public class Echo : MonoBehaviour {
//定义套接字
Socket socket;
//UGUI
public InputField InputFeld;
public Text text;
//接收缓冲区
ByteArray readBuff = new ByteArray();
//发送缓冲区
Queue<ByteArray> writeQueue = new Queue<ByteArray>();
bool isSending = false;
//显示文字
string recvStr = "";
//点击连接按钮
public void Connection()
{
//Socket
socket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
//为了精简代码:使用同步Connect
// 不考虑抛出异常
socket.Connect("127.0.0.1", 8888);
socket.BeginReceive( readBuff.bytes, readBuff.writeIdx,
readBuff.remain, 0, ReceiveCallback, socket);
}
//Receive回调
public void ReceiveCallback(IAsyncResult ar){
try {
Socket socket = (Socket) ar.AsyncState;
//获取接收数据长度
int count = socket.EndReceive(ar);
readBuff.writeIdx+=count;
//处理二进制消息
OnReceiveData();
//继续接收数据
if(readBuff.remain < 8){
readBuff.MoveBytes();
readBuff.ReSize(readBuff.length*2);
}
socket.BeginReceive( readBuff.bytes, readBuff.writeIdx,
readBuff.remain, 0, ReceiveCallback, socket);
}
catch (SocketException ex){
Debug.Log("Socket Receive fail" + ex.ToString());
}
}
//
public void OnReceiveData(){
Debug.Log("[Recv 1] length =" + readBuff.length);
Debug.Log("[Recv 2] readbuff=" + readBuff.ToString());
if(readBuff.length <= 2)
return;
//消息长度
int readIdx = readBuff.readIdx;
byte[] bytes =readBuff.bytes;
Int16 bodyLength = (Int16)((bytes[readIdx+1] << 8 )| bytes[readIdx]);
if(readBuff.length < bodyLength)
return;
readBuff.readIdx+=2;
Debug.Log("[Recv 3] bodyLength=" +bodyLength);
//消息体
byte[] stringByte = new byte[bodyLength];
readBuff.Read(stringByte, 0, bodyLength);
string s = System.Text.Encoding.UTF8.GetString(stringByte);
Debug.Log("[Recv 4] s=" +s);
;
Debug.Log("[Recv 5] readbuff=" + readBuff.ToString());
//消息处理
recvStr = s + "\n" + recvStr;
//继续读取消息
if(readBuff.length > 2){
OnReceiveData();
}
}
//点击发送按钮
public void Send()
{
string sendStr = InputFeld.text;
//组装协议
byte[] bodyBytes = System.Text.Encoding.Default.GetBytes(sendStr);
Int16 len = (Int16)bodyBytes.Length;
byte[] lenBytes = BitConverter.GetBytes(len);
//大小端编码
if(!BitConverter.IsLittleEndian){
Debug.Log("[Send] Reverse lenBytes");
lenBytes.Reverse();
}
//拼接字节
byte[] sendBytes = lenBytes.Concat(bodyBytes).ToArray();
ByteArray ba = new ByteArray(sendBytes);
lock(writeQueue){
writeQueue.Enqueue(ba);
}
//send
socket = new Socket(AddressFamily.InterNetwork,
SocketType.Stream, ProtocolType.Tcp);
if(!isSending){
socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, socket);
isSending = true;
}
Debug.Log("[Send] " + BitConverter.ToString(sendBytes));
}
//Send回调
public void SendCallback(IAsyncResult ar){
//获取state
Socket socket = (Socket) ar.AsyncState;
//EndSend的处理
int count = 0;
count = socket.EndSend(ar);
Debug.Log("Socket Send succ " + count);
ByteArray ba = writeQueue.First();
ba.readIdx+=count;
if(ba.length == 0){
lock(writeQueue){
writeQueue.Dequeue();
ba = writeQueue.First();
}
}
if(ba != null){
socket.BeginSend(ba.bytes, ba.readIdx, ba.length, 0, SendCallback, socket);
}
else{
isSending = false;
}
}
public void Update(){
text.text = recvStr;
}
}