Unity網絡編程(三)TCP 1VN聊天室 封包拆包

在之前的基礎上改成多人聊天

服務器

using System;

namespace TalkRoomTCP
{
    class Program
    {
        static void Main(string[] args)
        {
            new TalkSever().Init();
            // 接收一個鍵盤輸入的字符,目的是不讓命令行自動關閉
            Console.ReadKey();
        }
    }
}

using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace TalkRoomTCP
{
    //每一個客戶端的結構
    class Client
    {
        public const ushort Buffer_Length = 1024;
        public Socket socket;
        public byte[] buffer = new byte[Buffer_Length];
    }
    class TalkSever
    {
        //存放每一個客戶端
        Dictionary<Socket, Client> clientList = new Dictionary<Socket, Client>();
        public void Init()
        {
            //創(chuàng)建socket using 代替Close 用完不關閉會占用端口
            Socket socket = new Socket(SocketType.Stream, ProtocolType.Tcp);

            //綁定IP 端口號
            //IPAddress.Any:相當于"0.0.0.0"的IP地址偵聽本地所有網絡接口上的客戶端活動 有幾個偵聽幾個
            //IPAddress.Broadcast:相當于"255.255.255.255"的IP地址,通常用于Udp的數(shù)據(jù)包廣播。
            //IPAddress.Loopback:相當于"127.0.0.1"的IP地址,用于指代本機。監(jiān)聽"127.0.0.1"時,只能從本機連接到服務端。
            socket.Bind(new IPEndPoint(IPAddress.Any, 9999));
            //開啟監(jiān)聽 參數(shù)是最大接受隊列的長度 多于這個就只響應100個 其他拒絕
            socket.Listen(100);

            //開啟異步 第二個參數(shù)用于傳遞一些數(shù)據(jù)
            socket.BeginAccept(AcceptCallBack, socket);
            Console.WriteLine("服務器啟動");
        }

        private void AcceptCallBack(IAsyncResult ar)
        {
            var socket = ar.AsyncState as Socket;
            var clientSocket = socket.EndAccept(ar);
            Console.WriteLine($"{clientSocket.RemoteEndPoint}客戶端連接");

            Client client = new Client();
            client.socket = clientSocket;
            clientList.Add(clientSocket, client);

            //客戶端接收消息 如果客戶端不發(fā)送數(shù)據(jù) 服務器程序阻塞(掛起)這個位置
            var buffer = client.buffer;
            clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallBack, client);

            // 遞歸繼續(xù)Accept
            socket.BeginAccept(AcceptCallBack, socket);
        }

        private void ReceiveCallBack(IAsyncResult ar)
        {
            //
            Client client = ar.AsyncState as Client;
            Socket clientSocket = client.socket;
            byte[] buffer = client.buffer;
            try
            {
                int length = clientSocket.EndReceive(ar);
                //小于0客戶端就關閉了
                if (length > 0)
                {
                    Console.WriteLine($"接收到客戶端的消息:{Encoding.UTF8.GetString(buffer, 0, length)}");
                    foreach (var item in clientList)
                    {
                        item.Key.Send(buffer, length, SocketFlags.None);
                    }

                   
                    //遞歸重新開始接收
                    clientSocket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallBack, client);
                }
                else
                {
                    OnClientDisconnect(clientSocket);
                }
            }
            catch (SocketException ex)
            {
                // 如果服務端有向客戶端A未發(fā)送完的數(shù)據(jù),客戶端A主動斷開時會觸發(fā)10054異常,在此捕捉
                if (ex.SocketErrorCode == SocketError.ConnectionReset)
                    OnClientDisconnect(clientSocket);
            }

        }

        private void OnClientDisconnect(Socket clientSocket)
        {
            Console.WriteLine($"{clientSocket.RemoteEndPoint}斷開連接");
            clientList.Remove(clientSocket);
            clientSocket.Close();
        }
    }
}

image.png

客戶端

然后改造客戶端 客戶端不是實時的 而且順序有問題
先解決 不能實時刷新的問題
還有會有時候接收到連在一起的字符 那是粘包問題 后面改造
主要是我們點擊時候發(fā)送又輸入到屏幕上
其實應該是 點擊后 發(fā)給服務器等服務器返回才輸入屏幕上

/**
 *Copyright(C) 2019 by #COMPANY#
 *All rights reserved.
 *FileName:     #SCRIPTFULLNAME#
 *Author:       #AUTHOR#
 *Version:      #VERSION#
 *UnityVersion:#UNITYVERSION#
 *Date:         #DATE#
 *Description:   
 *History:
*/
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine.UI;
using System;
using System.Collections.Generic;
public class TalkClient : MonoBehaviour
{
    public InputField input;
    public Text text;
    public Button btn;
    byte[] buffer = new byte[1024];
    Socket socket;

    List<string> msg = new List<string>();
    // Start is called before the first frame update
    void Start()
    {
        socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
        //連接服務器
        socket.Connect("127.0.0.1", 9999);

        socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, null);
        btn.onClick.AddListener(() =>
        {
            //發(fā)送數(shù)據(jù)
            socket.Send(Encoding.UTF8.GetBytes(input.text));
        });
    }

    private void Update()
    {
        if (msg.Count>0)
        {
            foreach (var item in msg)
            {
                //因為unity不能在子線程調用unity大部分API Debug.Log可以 socket內部異步為我們開了線程
                //UniRx插件中有一個MainThreadDispatcher類,可以很方便地用來處理子線程到主線程的轉換
                text.text += item + "\n";
            }
            //清除處理過的消息
            msg.Clear();
        }
    }
    void ReceiveCallback(IAsyncResult ar)
    {
        try
        {
            //接受數(shù)據(jù)
            int length = socket.EndReceive(ar);
            if (length > 0)
            {
                var str = Encoding.UTF8.GetString(buffer, 0, length);
                Debug.Log($"接收到服務端的消息:{str}");
                msg.Add(str);

                //重新開始接受
                socket.BeginReceive(buffer, 0, buffer.Length, SocketFlags.None, ReceiveCallback, null);
            }
            else
            {
                OnClientDisconnect();
            }
        }
        catch (SocketException ex)
        {
            if (ex.SocketErrorCode == SocketError.ConnectionReset)
                OnClientDisconnect();
        }
    }

    void OnClientDisconnect()
    {
        Debug.Log("與服務端斷開連接");
        socket.Close();
    }
}

然后就OK了


image.png

區(qū)分玩家 解決粘包

這樣不知道哪個消息是誰發(fā)的
有三種解決方法
1、 加入注冊登錄功能
2、 在連接服務端成功后先給服務端發(fā)送消息設置昵稱
3、 在每次發(fā)送消息的時候發(fā)送昵稱
可以設計個數(shù)據(jù)包
用名字:信息
上面的放名字


image.png

這里一改就行

 btn.onClick.AddListener(() =>
        {
            //發(fā)送數(shù)據(jù)
            socket.Send(Encoding.UTF8.GetBytes(inputName.text+":"+input.text));
        });

然后是粘包
服務器壓力大的時候會出現(xiàn)
發(fā)送5次然后返回一次 黏在一起
原因 TCP是個"流"協(xié)議,所謂流,就是沒有界限的一串數(shù)據(jù)
會有以下4種情況 234都是粘包
先接收到data1,然后接收到data2。
先接收到data1的部分數(shù)據(jù),然后接收到data1余下的部分以及data2的全部。
先接收到了data1的全部數(shù)據(jù)和data2的部分數(shù)據(jù),然后接收到了data2的余下的數(shù)據(jù)。
一次性接收到了data1和data2的全部數(shù)據(jù)。
相比UDP UDP是個數(shù)據(jù)包協(xié)議 他要么完整要么全丟
服務器客戶端都可能發(fā)生粘包
1、由Nagle算法造成的發(fā)送端的粘包
我們提交一段數(shù)據(jù)給TCP發(fā)送時,TCP并不立刻發(fā)送此段數(shù)據(jù),而是等待一小段時間,看看在等待期間是否還有要發(fā)送的數(shù)據(jù),若有則會一次把這兩段數(shù)據(jù)發(fā)送出去
2.接收端接收不及TCP緩存區(qū)緩存了多個數(shù)據(jù)時造成的接收端粘包
解決方法 封包拆包


image.png

封包
1.數(shù)據(jù)轉為json字符串
2.把json轉為byte[]數(shù)組A
3.創(chuàng)建一個長度為數(shù)組A長度+2字節(jié)的字節(jié)數(shù)組B,依次將2字節(jié)的長度和json字節(jié)數(shù)組A先后輸入寫入到這個B中
4.將這個字節(jié)數(shù)組B(數(shù)據(jù)包)發(fā)送給服務端

拆包
1.將最新數(shù)據(jù)放入DataCache
2、嘗試從DataCache中解析數(shù)據(jù)包,具體代碼見上面的Decode
3、一直嘗試解析,直到數(shù)據(jù)不足

image.png

一般來說XML json會很大 一般用自定義的二進制格式
數(shù)據(jù)一般分為這兩種 定長的數(shù)據(jù)和不定長的數(shù)據(jù)
定長的數(shù)據(jù)比如:byte,short,int,long,char之類的簡單數(shù)據(jù),以及僅包含這些類型的類或結構體
不定長的數(shù)據(jù)比如:字符串string、列表List、字典Dictionary等等,這些都需要進行特殊處理,一般是在數(shù)據(jù)內容的開頭加上一個長度數(shù)據(jù)。比如寫入一個字符串string時,先寫入2字節(jié)的string的長度,再寫入string的具體內容,類似我們上面處理的json字符串。
比如現(xiàn)在我們的數(shù)據(jù)要改二進制的話


image.png

然后谷歌出了個protobuf比較好用 也不用自己寫這么多處理

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容