TCP通信协议的客户端
TCP协议的特点记录
1.面向连接
面向连接,是指发送数据之前必须在两端建立连接。建立连接的方法是“三次握手”,这样能建立可靠的连接。建立连接,是为数据的可靠传输打下了基础。
2.仅支持单播传输
每条TCP传输连接只能有两个端点,只能进行点对点的数据传输,不支持多播和广播传输方式。
3.面向字节流
TCP不像UDP一样那样一个个报文独立地传输,而是在不保留报文边界的情况下以字节流方式进行传输。
4.可靠传输
对于可靠传输,判断丢包,误码靠的是TCP的段编号以及确认号。TCP为了保证报文传输的可靠,就给每个包一个序号,同时序号也保证了传送到接收端实体的包的按序接收。然后接收端实体对已成功收到的字节发回一个相应的确认(ACK);如果发送端实体在合理的往返时延(RTT)内未收到确认,那么对应的数据(假设丢失了)将会被重传。
5.提供拥塞控制
当网络出现拥塞的时候,TCP能够减小向网络注入数据的速率和数量,缓解拥塞
TCP通信协议的客户端直接上代码,注释详细
using System;
using System.Collections;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;
//客户端Socket 的封装
public class ClientPeer
{
private Socket socket;
private string ip;
private int port;
/// <summary>
/// 构造连接对象
/// </summary>
/// <param name="ip">连接的服务器IP地址</param>
/// <param name="port">服务器端口号</param>
public ClientPeer(string ip, int port)
{
try
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
this.ip = ip;
this.port = port;
}
catch (Exception e)
{
Debug.LogError(e.Message);
}
}
public void Connect()
{
try
{
socket.Connect(ip, port);
Debug.Log("连接服务器成功!!!");
//开始接受数据
startReceice();
}
catch (Exception e)
{
Debug.LogError(e.Message);
}
}
#region 接收数据
/// <summary>
/// 接收数据缓存区
/// </summary>
private byte[] receiveBuffer = new byte[1024];
/// <summary>
/// 一旦接收到数据,就存到缓存区中
/// </summary>
private List<byte> dataCache = new List<byte>();
/// <summary>
/// 是否正在处理接收的数据
/// </summary>
private bool isProcessReceive = false;
/// <summary>
/// 接收到的网络消息数据
/// </summary>
public Queue<SocketMsg> socketMsgQueue = new Queue<SocketMsg>();
private void startReceice()
{
if (socket == null && !socket.Connected)
{
Debug.LogError("没有连接成功,无法接收数据!!!");
return;
}
Debug.Log(socket.Connected);
//开始异步接受数据
socket.BeginReceive(receiveBuffer, 0, 1024, SocketFlags.None, receiveCallBack, socket);
}
/// <summary>
/// 收到消息的回调
/// </summary>
/// <param name="result"></param>
private void receiveCallBack(IAsyncResult result)
{
//result.AsyncState ==> 就是最后传递过来的数据 socket【这里传不传都一样】
try
{
//接收到消息 结束接收,并获取接收到消息的长度
int lenght = socket.EndReceive(result);
byte[] tempByteArray = new byte[lenght];
Buffer.BlockCopy(receiveBuffer, 0 ,tempByteArray, 0, lenght);
//处理收到的消息数据
dataCache.AddRange(tempByteArray);
if (isProcessReceive == false)
processReceive();
}
catch (Exception e)
{
Debug.LogError( e.Message);
//Task.Run(() =>
//{
// while (!socket.Connected)
// {
// Task.Delay(5000);
// if (!socket.Connected)
// {
// Debug.Log("连接失败继续连接");
// Connect();
// }
// }
//});
}
}
/// <summary>
/// 处理收到的数据
/// </summary>
private void processReceive()
{
isProcessReceive = true;
//解析数据包【根据自己项目中 的通信协议进行解析 出一个完整的数据包】
byte[] data = EncodeTool.DecodePacket(ref dataCache);
if (data == null)
{
isProcessReceive = false;
dataCache.Clear();
startReceice();//继续接收数据
//socket.BeginReceive(receiveBuffer, 0, 1024, SocketFlags.None, receiveCallBack, socket);
return;
}
//解析 字节数据 为具体消息类型【将数据包解析为具体的类型】
SocketMsg msg = EncodeTool.DecodeMsg(data);
//存储消息等待处理
socketMsgQueue.Enqueue(msg);
//尾递归 处理接收的数据
processReceive();
}
#endregion
#region 发送数据
/// <summary>
/// 发送数据
/// </summary>
/// <param name="opCode"></param>
/// <param name="subCode"></param>
/// <param name="value"></param>
public void Send(int opCode, int subCode, object value)
{
SocketMsg msg = new SocketMsg(opCode, subCode, value);
Send(msg);
}
/// <summary>
/// 发送数据
/// </summary>
/// <param name="msg"></param>
public void Send(SocketMsg msg)
{
byte[] data = EncodeTool.EncodeMsg(msg);//序列化 消息
byte[] packet = EncodeTool.EncodePacket(data);//封装消息包
try
{
socket.Send(packet);
}
catch (Exception e)
{
Debug.LogError(e.Message);
}
}
#endregion
public void Close()
{
socket.Dispose();
socket.Close();
}
}
粘包拆包问题:
粘包拆包问题 基础 原理
//粘包:多个包 形成一个包
//拆包:一个包 形成多个包
//粘包拆包问题: 解决策略: 消息头和消息尾
//比如 发送的数据 12345 ==>byte[] bt = Encoding.Default.GetBytes("12345");
//消息头:发送数据 转换为字节数组 的 字节数组长度的字节数据[即将消息字节长度转换为字节] bt.Length ==> 转换为字节 btl
//消息尾:发送数据 转换为字节数组 的 字节数据 bt
//实际发送消息:消息头+消息尾 btl+bt ==> 拼接的发送消息字节 btt
//如何读取:
// 获取btt 的前四个字节 并转换为 int length = btt字节的前四个字节转换为int
//然后 读取 这个 长度 length 数据
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Threading.Tasks;
/// <summary>
/// 编码处理 工具类
/// </summary>
public static class EncodeTool
{
/// <summary>
/// 构造数据包:消息头+消息尾
/// </summary>
/// <param name="data">消息内容,实际消息数据</param>
/// <returns>构造的消息体 封装的消息数据,用于传输 解析数据</returns>
public static byte[] EncodePacket(byte[] data)
{
//创建内存流对象【可以获取 内存中的字节数据】[可理解为:存储数据 的缓存字节数据]
using (MemoryStream ms = new MemoryStream())
{//创建 二进制 写入对象【传递 内存流对象,写入到内存流中】
using (BinaryWriter bw = new BinaryWriter(ms))
{
//将数据写入到内存流中
//先写入长度
bw.Write(data.Length);
//再写入数据
bw.Write(data);
byte[] byteArray = new byte[(int)ms.Length];
//复制字节数组 源数组,复制的起始位置,目标数组,目标起始位置,复制的长度
Buffer.BlockCopy(ms.GetBuffer(), 0, byteArray, 0, (int)ms.Length);//复制 字节数组 【该方法 再内存中直接复制,效率较高】
return byteArray;
}
}
//关闭流 【使用using 就不需要自己关闭了 using 会自动释流】
//ms.Close();
//bw.Close();
}
/// <summary>
/// 解析数据包:从缓存中取出一个一个完整的数据包
/// </summary>
/// <param name="data">解析 封装后的数据包</param>
/// <returns>解析出来的 实际数据</returns>
public static byte[] DecodePacket(ref List<byte> dataCache)
{
//这里至少四个字节构成一个数据包【消息头四个字节构成一个int 长度,不足 不能构成一个完成的消息】
if (dataCache.Count < 4)
return null;
//throw new Exception("数据缓存长度不足4,不能够成一个完整的数据包");
//创建内存流对象【可以获取 内存中的字节数据】[可理解为:存储数据 的缓存字节数据]
using (MemoryStream ms = new MemoryStream(dataCache.ToArray()))
{//创建 二进制 读取对象【读取内存流】
using (BinaryReader br = new BinaryReader(ms))
{
//从当前流中读取4字节有符号整数,并使流的当前位置提升4个字节。 返回值:从当前流中读取的2字节有符号整数[即该数据的实际长度]
int length = br.ReadInt32();//数据长度(包头约定的长度)
int dataRemainLength = (int)(ms.Length - ms.Position);//剩余读取的长度【除去包头的 数据长度】
//检查数据完整性 大于 则缺少数据 拆包现象,等于 则数据完整,小于 则多出数据 粘包现象
//数据缺失
if (length > dataRemainLength)//ms.Length:流的长度。ms.Position:获取或设置 流中的 当前位置
return null;
//throw new Exception("数据长度不够包头约定的长度,不能够成一个完整的数据包");//发生拆包现象,数据包中的数据缺少
//否则 数据完整 或多余【只读取 自身的数据长度】
//byte[] datas = ms.GetBuffer();
byte[] datas = br.ReadBytes(length);//从当前流当前位置,读取指定长度字节数,并将当前位置移动到相应的字节数
//更新一下 数据缓存【因为可能发生粘包现象,包含其余数据,则需要在缓存数据中移除当前的数据,留下多余的数据,用于解析下个数据包的数据】
dataCache.Clear();
dataCache.AddRange(br.ReadBytes(dataRemainLength));
return datas;//解析出来的数据包
}
}
}
#endregion
#region 构造发送的SocketMsg 类【构造发送的数据】
/// <summary>
/// 将SocketMsg 数据类 装换为 字节数组 用于发送出去
/// </summary>
/// <param name="msg">数据类</param>
/// <returns></returns>
public static byte[] EncodeMsg(SocketMsg msg)
{
using (MemoryStream ms = new MemoryStream())
{
using (BinaryWriter bw = new BinaryWriter(ms))
{
bw.Write(msg.OpCode);
bw.Write(msg.SubCode);
//如果 不为空,则将object 转换为 字节数据 存储起来
if (msg.Value != null)
{
byte[] valuBytes = EncodeObj(msg.Value);//序列化对象
bw.Write(valuBytes);
}
byte[] datas = new byte[(int)ms.Length];
Buffer.BlockCopy(ms.GetBuffer(), 0, datas, 0, (int)ms.Length);
return datas;
}
}
}
/// <summary>
/// 将 字节数组数据 装换为 SocketMsg数据类 用于处理发送接收数据
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public static SocketMsg DecodeMsg(byte[] data)
{
using (MemoryStream ms = new MemoryStream(data))
{
using (BinaryReader br = new BinaryReader(ms))
{
SocketMsg socketMsg = new SocketMsg();
//读取数据顺序和写入数据顺序一样
socketMsg.OpCode = br.ReadInt32();
socketMsg.SubCode = br.ReadInt32();
int length = (int)(ms.Length - ms.Position);//获取当前内存流中剩余的 字节长度
//如果 还有 数据则 继续处理
if (length > 0)
{
byte[] valueBytes = br.ReadBytes(length);//获取 剩余的字节数据
object value = DecodeObj(valueBytes);//反序列化
socketMsg.Value = value;
}
return socketMsg;
}
}
}
#endregion
#region 将object 类型转换为byte[](字节数组) 【这里使用 C#内置的 BinaryFormatter 二进制 序列化与反序列化,也可以使用Protobuf 序列化为 字节】
/// <summary>
/// 序列化对象
/// </summary>
/// <param name="value">要序列化的对象</param>
/// <returns>对象 序列化后的字节数据</returns>
public static byte[] EncodeObj(object value)
{
using (MemoryStream ms = new MemoryStream())
{
//创建 以二进制格式序列化和反序列化对象或连接对象的整个图形
BinaryFormatter bf = new BinaryFormatter();
//将对象序列化到指定流中
bf.Serialize(ms, value);
byte[] valueBytes = new byte[(int)ms.Length];
Buffer.BlockCopy(ms.GetBuffer(), 0, valueBytes, 0, (int)ms.Length);
return valueBytes;
}
}
/// <summary>
/// 反序列化对象
/// </summary>
/// <param name="value">反序列化的字节数据</param>
/// <returns></returns>
public static object DecodeObj(byte[] value)
{
using (MemoryStream ms = new MemoryStream(value))
{
BinaryFormatter bf = new BinaryFormatter();
object obj = bf.Deserialize(ms);
return obj;
}
}
#endregion
}
其中使用的通信协议数据(SocketMsg):
public class SocketMsg
{
//操作码 如何处理数据的 规定
/// <summary>
/// 操作码【主键 哪个模块】
/// </summary>
public int OpCode { get; set; }
/// <summary>
/// 子操作【子健 哪个操作】
/// </summary>
public int SubCode { get; set; }
/// <summary>
/// 参数 【实际数据】
/// </summary>
public object Value { get; set; }
public SocketMsg() { }
public SocketMsg(int opCode, int subCode, object value)
{
OpCode = opCode;
SubCode = subCode;
Value = value;
}
}