基于TCP通信协议的客户端以及粘包拆包问题处理

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;
    }

}

  • 6
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
TCP(传输控制协议)是一种面向连接的协议,它提供了可靠的、有序的、基于字节流的数据传输服务。下面是一个简单的TCP通信功能实现的示例代码。 1. 服务器端实现: ```python import socket # 创建socket对象 server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 绑定IP地址和端口号 server_socket.bind(('localhost', 8888)) # 监听客户端连接 server_socket.listen() # 等待客户端连接 print('Waiting for client connection...') client_socket, client_address = server_socket.accept() print(f'Client {client_address} connected.') # 接收客户端发送的数据 while True: data = client_socket.recv(1024) if not data: break print(f'Received data from client: {data.decode()}') # 发送数据给客户端 client_socket.send('Hello, client!'.encode()) # 关闭客户端连接和服务器socket client_socket.close() server_socket.close() ``` 2. 客户端实现: ```python import socket # 创建socket对象 client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 连接服务器 client_socket.connect(('localhost', 8888)) # 发送数据给服务器 client_socket.send('Hello, server!'.encode()) # 接收服务器发送的数据 data = client_socket.recv(1024) print(f'Received data from server: {data.decode()}') # 关闭客户端socket client_socket.close() ``` 在这个示例中,服务器端首先创建一个socket对象,并绑定IP地址和端口号,然后通过listen()方法开始监听客户端连接。当有客户端连接时,使用accept()方法接受客户端连接,并返回一个新的socket对象和客户端地址。然后服务器端就可以接收和发送数据了。 客户端创建一个socket对象,并使用connect()方法连接服务器。然后发送数据给服务器,接收服务器返回的数据,最后关闭客户端socket。 需要注意的是,在TCP通信中,发送和接收数据都是基于字节流的,因此需要使用encode()和decode()方法进行编码和解码。另外,在实际使用中,还需要考虑数据的粘包拆包问题

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值