Unity-TCP-网络聊天功能(三): 公共/私人聊天、添加好友、好友上下线、新消息提醒、点击好友聊天、退出登录

news2024/11/17 22:25:57

7.多人公共聊天、私人聊天、添加好友、好友上下线、新消息提醒、点击好友开始聊天、退出登录

搭建好ChatView的UI和ChatItem

编写Unity-Scripts/View/ChatView.cs脚本,加入了私人聊天和公共聊天

逻辑,chatView界面处理接收(ChatHandle委托)和发送信息方法,同时ChatView界面可以添加好友,好友上下线提示,新消息提示,点击好友名称开始聊天,退出登录。

public class ChatView : MonoBehaviour
{
    private GameObject chatItem;
    private Transform chatItemParent;
    private Scrollbar scrollbar;

    private Button sendBtn;
    private InputField sendInput;

    private Button publicChanelBtn;
    private Transform scrollViewPublic;
    
    //用来点击头像退出账号、添加好友
    private Button portraitBtn;
    private GameObject portraitPanel;
    private Text username;
    private GameObject friendsPanel;
    private Button friendListBtn;

    //用来放置其他的聊天页面
    private Transform scrollViewPageParent;//放全部聊天页面
    private Transform tagListParent;//聊天框标签
    private Transform friendBtnParent;//放好友位
    private GameObject scroolViewPersonal;//私人聊天框
    private GameObject personelChatPageBtn;//私人聊天框标签
    private GameObject chatWithFriendBtn;//好友位按钮

    private Button lastPressedTag;
    private Color tagNormalColor = new Color(4/255f, 67/255f, 99/255f, 255/255f);
    private Color tagPressedColor = new Color(0/255f,127/255f,191/255f, 255/255f);

    private Button sendAddFriendBtn;
    private InputField addFriendInput;
    private Text addFriendTips;

    private Button logoutBtn;

    //聊天频道
    private int chatType = 0;
    private string chatTargetPlayer = "";
    
    // Start is called before the first frame update
    void OnEnable()
    {
        //发送信息
        chatItem = Resources.Load<GameObject>("ChatItem");
        //chatItemParent = this.transform.Find("ScrollViewPage/ScrollViewPublic/Viewport/Content");
        scrollbar = this.transform.Find("ScrollViewPage/ScrollViewPublic/Scrollbar Vertical").GetComponent<Scrollbar>();

        sendInput = this.transform.Find("SendInput").GetComponent<InputField>();//输入框

        sendBtn = this.transform.Find("SendBtn").GetComponent<Button>();
        sendBtn.onClick.AddListener(SendChatMsg); //发送聊天数据

        //点击头像弹窗
        portraitBtn = this.transform.Find("Popup_Dark/Portrait").GetComponent<Button>();
        portraitPanel = this.transform.Find("PortraitPanel").gameObject;
        portraitBtn.onClick.AddListener(() =>
        {
            portraitPanel.SetActive(!portraitPanel.activeInHierarchy);
            friendsPanel.SetActive(false);
        });
        username = this.transform.Find("Popup_Dark/Username").gameObject.GetComponent<Text>();
        username.text = PlayerData.Instance.LoginMsgS2C.account;

        //朋友列表按钮
        friendListBtn = this.transform.Find("Popup_Dark/FriendList").GetComponent<Button>();
        friendsPanel = this.transform.Find("FriendsPanel").gameObject;
        friendListBtn.onClick.AddListener(() =>
        {
            friendsPanel.SetActive(!friendsPanel.activeInHierarchy);
            portraitPanel.SetActive(false);
        });

        //初始化朋友聊天界面相关设置。
        scrollViewPageParent = this.transform.Find("ScrollViewPage");
        scroolViewPersonal = Resources.Load<GameObject>("ScrollViewPersonal");
        tagListParent = this.transform.Find("TagList/Viewport/Content");
        personelChatPageBtn = Resources.Load<GameObject>("PersonalChatPageBtn");
        friendBtnParent = this.transform.Find("FriendsPanel/Scroll View/Viewport/Content");
        chatWithFriendBtn = Resources.Load<GameObject>("ChatWithFriendBtn");

        //公共聊天
        scrollViewPublic = scrollViewPageParent.transform.Find("ScrollViewPublic");
        publicChanelBtn = tagListParent.transform.Find("PublicChanelBtn").GetComponent<Button>();
        publicChanelBtn.onClick.AddListener(() =>
        {
            scrollViewPublic.SetAsLastSibling();
            chatType = 0;
            chatTargetPlayer = "";//设置公共聊天
            //设置按钮按下颜色
            lastPressedTag.gameObject.GetComponent<Image>().color = tagNormalColor;
            lastPressedTag = publicChanelBtn;
            publicChanelBtn.gameObject.GetComponent<Image>().color = tagPressedColor;
            //关闭新消息提醒
            publicChanelBtn.gameObject.transform.GetChild(1).gameObject.SetActive(false);
        });
        //初始化公共聊天按钮颜色
        lastPressedTag = publicChanelBtn;
        publicChanelBtn.gameObject.GetComponent<Image>().color = tagPressedColor;

        //退出功能
        logoutBtn = this.transform.Find("PortraitPanel/LogoutBtn").GetComponent<Button>();
        logoutBtn.onClick.AddListener(() =>
        {
            //退出账号
            MessageHelper.Instance.SendOnOfflineMsg(PlayerData.Instance.LoginMsgS2C.account, 0);
            //打开登录界面
            var loginPrefab = Resources.Load<GameObject>("LoginView");
            var loginView = GameObject.Instantiate<GameObject>(loginPrefab);
            loginView.AddComponent<LoginView>();
            Destroy(this.gameObject);
        });

        sendAddFriendBtn = this.transform.Find("AddFriendPanel/AddBtn").GetComponent<Button>();
        sendAddFriendBtn.onClick.AddListener(SendAddFriendMsg);
        addFriendInput = this.transform.Find("AddFriendPanel/InputField").GetComponent<InputField>();
        addFriendTips = this.transform.Find("AddFriendPanel/Tips").GetComponent<Text>();

        //初始化朋友列表
        foreach (var friend in PlayerData.Instance.LoginMsgS2C.friends)
        {
            InitChatWithFriend(friend.Key, friend.Value);
        }
        scrollViewPublic.transform.SetAsLastSibling();//默认优先展示公共聊天
        
        //初始化过往聊天记录
        List<ChatMsgS2C> localMsgs = new List<ChatMsgS2C>();
        if (MessageHelper.Instance.TryLoadMsgFromLocal(out localMsgs))
        {
            foreach (var msg in localMsgs)
            {
                if (msg.type == 0 ||
                    (msg.type == 1 && msg.player == PlayerData.Instance.LoginMsgS2C.account) ||
                    (msg.type == 1 && msg.targetPlayer == PlayerData.Instance.LoginMsgS2C.account))
                {
                    AddMessage(msg.player, msg.msg, msg.time, msg.type, msg.targetPlayer, false);
                }
            }
        }

        MessageHelper.Instance.chatHandle += ChatHandle;
        MessageHelper.Instance.addFriendHandle += AddFriend;
        MessageHelper.Instance.friendOnOfflineHandle += FriendOnOffline;
    }

    private void ChatHandle(ChatMsgS2C obj)
    {
        AddMessage(obj.player, obj.msg, obj.time, obj.type, obj.targetPlayer, true);
        MessageHelper.Instance.SaveMsgToLocal(obj);//缓存本地聊天。
    }

    //添加消息条,,,,最后一条针对的是本地缓存的消息还是网络接收的信息,网络接收的信息为新消息,本地为旧消息
    public void AddMessage(string title, string content, string time, int chatType, string chatTarget, bool newMsg)
    {
        var go = GameObject.Instantiate<GameObject>(chatItem);
        if (chatType == 1)//私人聊天
        {
            if (title == PlayerData.Instance.LoginMsgS2C.account)//消息发送者是本人,表明这自己给别人发的消息,自己要能看到
            {
                chatItemParent = this.transform.Find("ScrollViewPage/" + chatTarget + "/Viewport/Content");
                //自己发的消息不需要消息提示
            }
            else//消息发送者不是本人,但收到了这条消息,表明这别人给自己发的消息
            {
                chatItemParent = this.transform.Find("ScrollViewPage/" + title + "/Viewport/Content");
                //设置消息提醒
                var newMsgTagTip = this.transform.Find("TagList/Viewport/Content/" + title).GetChild(3).gameObject;
                newMsgTagTip.SetActive(newMsg);
                var newMsgBtnTip = this.transform.Find("FriendsPanel/Scroll View/Viewport/Content/" + title)
                    .GetChild(2).gameObject;
                newMsgBtnTip.SetActive(newMsg);
            }
        }
        else if (chatType == 0)//公共聊天
        {
            chatItemParent = this.transform.Find("ScrollViewPage/ScrollViewPublic/Viewport/Content");
            if (title != PlayerData.Instance.LoginMsgS2C.account)//不是自己发的消息在显示消息提醒
            {
                //设置消息提醒
                var newMsgTagTip = this.transform.Find("TagList/Viewport/Content/PublicChanelBtn").GetChild(1).gameObject;
                newMsgTagTip.SetActive(newMsg);
            }
        }
        //将单条消息实例化到对应的聊天框,并修改用户名、聊天信息、消息发送时间等
        go.transform.SetParent(chatItemParent, false);
        var titleText = go.transform.Find("MessagePanel/Title").GetComponent<Text>();
        titleText.text = title;
        var chat = go.transform.Find("MessagePanel/Message").GetComponent<Text>();
        chat.text = content;
        var chatTime = go.transform.Find("MessagePanel/Time").GetComponent<Text>();
        chatTime.text = time;
        //修改默认头像
        if (title != PlayerData.Instance.LoginMsgS2C.account)
        {
            var portrait = go.transform.Find("UserPortrait").GetComponent<Image>();
            portrait.sprite = Resources.Load<Sprite>("Sprites/头像");
        }

        StartCoroutine(ResetScrollbar());
    }

    //刷新对话框最新位置
    public IEnumerator ResetScrollbar()
    {
        yield return new WaitForEndOfFrame();
        scrollbar.value = 0;
    }

    //发送消息
    private void SendChatMsg()
    {
        if (string.IsNullOrEmpty(sendInput.text))
        {
            return;
        }
        //如果希望减少带宽,用用户id也可以,不需要用用户账号
        MessageHelper.Instance.SendChatMsg(PlayerData.Instance.LoginMsgS2C.account, sendInput.text, chatType, chatTargetPlayer);
        sendInput.text = "";
    }
    
    //发送添加好友消息
    private void SendAddFriendMsg()
    {
        if (string.IsNullOrEmpty(addFriendInput.text) && addFriendInput.text != PlayerData.Instance.LoginMsgS2C.account)
        {
            return;
        }
        MessageHelper.Instance.SendAddFriendMsg(PlayerData.Instance.LoginMsgS2C.account, addFriendInput.text);
        addFriendInput.text = "";
    }
    
    //添加好友
    private void AddFriend(AddFriendMsgS2C obj)
    {
        if (obj.result == 0)
        {
            PlayerData.Instance.LoginMsgS2C.friends = obj.friends;
            InitChatWithFriend(obj.newFriend, obj.friendState);
            scrollViewPublic.SetAsLastSibling();
			addFriendTips.text = "添加成功";
        }
        else
        {
            addFriendTips.text = "添加失败";
        }
    }

    //初始化与朋友聊天的好友列表按钮、好友标签、好友聊天框,每个好友按钮点击都会生成好友标签和好友聊天框
    private void InitChatWithFriend(string friendName, int friendState)
    {
        //聊天列表按钮
        var go = GameObject.Instantiate<GameObject>(chatWithFriendBtn);
        go.transform.SetParent(friendBtnParent, false);
        go.gameObject.name = friendName;
        go.transform.GetChild(0).GetComponent<Text>().text = friendName;
        go.transform.GetChild(1).GetComponent<Image>().color = friendState == 1 ? Color.green : Color.gray;//更新好友在线状态
        var btn = go.GetComponent<Button>();
        btn.onClick.AddListener(() =>
        {
            if (tagListParent.transform.Find(friendName))//查看是否聊天标签有好友标签
            {
                //仅调整聊天框置最前
                var friendChatScrollView = scrollViewPageParent.transform.Find(friendName);
                friendChatScrollView.transform.SetAsLastSibling();
                tagListParent.transform.Find(friendName).GetChild(2).GetComponent<Image>().color= friendState == 1 ? Color.green : Color.gray;//更新好友在线状态
            }
            else
            {
                //添加一个好友聊天框
                var friendChatScrollView = GameObject.Instantiate<GameObject>(scroolViewPersonal);
                friendChatScrollView.transform.SetParent(scrollViewPageParent, false);
                friendChatScrollView.transform.SetAsLastSibling();
                friendChatScrollView.gameObject.name = friendName;
                //添加好友聊天标签
                var friendChatTagBtn = GameObject.Instantiate<GameObject>(personelChatPageBtn);
                friendChatTagBtn.transform.SetParent(tagListParent, false);
                friendChatTagBtn.gameObject.name = friendName;
                friendChatTagBtn.transform.GetChild(0).gameObject.GetComponent<Text>().text = friendName;
                friendChatTagBtn.GetComponent<Button>().onClick.AddListener(() =>
                {
                    friendChatScrollView.transform.SetAsLastSibling();
                    chatType = 1;
                    chatTargetPlayer = friendName;//设置私人聊天和聊天对象
                    //修改按钮颜色
                    lastPressedTag.gameObject.GetComponent<Image>().color = tagNormalColor;
                    friendChatTagBtn.gameObject.GetComponent<Image>().color = tagPressedColor;
                    lastPressedTag = friendChatTagBtn.GetComponent<Button>();
                    //关闭新消息提醒
                    friendChatTagBtn.transform.GetChild(3).gameObject.SetActive(false);
                    go.transform.GetChild(2).gameObject.SetActive(false);
                });
                friendChatTagBtn.transform.GetChild(2).GetComponent<Image>().color =
                    go.transform.GetChild(1).GetComponent<Image>().color;
                var closeChatBtn = friendChatTagBtn.transform.GetChild(1).GetComponent<Button>();
                closeChatBtn.onClick.AddListener(() =>
                {
                    Destroy(friendChatScrollView);
                    Destroy(friendChatTagBtn, 0.1f);
                    chatType = 0;
                    chatTargetPlayer = "";//设置公共聊天
                    lastPressedTag = publicChanelBtn;
                    publicChanelBtn.gameObject.GetComponent<Image>().color = tagPressedColor;
                    closeChatBtn.onClick.RemoveAllListeners();
                });
            }
            chatType = 1;
            chatTargetPlayer = friendName;//设置私人聊天和聊天对象
            //修改按钮颜色
            lastPressedTag.gameObject.GetComponent<Image>().color = tagNormalColor;
            tagListParent.transform.Find(friendName).gameObject.GetComponent<Image>().color = tagPressedColor;
            lastPressedTag = tagListParent.transform.Find(friendName).gameObject.GetComponent<Button>();
            //关闭新消息提醒
            go.transform.GetChild(2).gameObject.SetActive(false);
            tagListParent.transform.Find(friendName).GetChild(3).gameObject.SetActive(false);
        });
        //添加一个好友聊天框
        var friendChatScrollView = GameObject.Instantiate<GameObject>(scroolViewPersonal);
        friendChatScrollView.transform.SetParent(scrollViewPageParent, false);
        friendChatScrollView.transform.SetAsLastSibling();
        friendChatScrollView.gameObject.name = friendName;
        //添加好友聊天标签
        var friendChatTagBtn = GameObject.Instantiate<GameObject>(personelChatPageBtn);
        friendChatTagBtn.transform.SetParent(tagListParent, false);
        friendChatTagBtn.gameObject.name = friendName;
        friendChatTagBtn.transform.GetChild(0).gameObject.GetComponent<Text>().text = friendName;
        friendChatTagBtn.GetComponent<Button>().onClick.AddListener(() =>
        {
            friendChatScrollView.transform.SetAsLastSibling();
            chatType = 1;
            chatTargetPlayer = friendName;//设置私人聊天和聊天对象
            //修改按钮颜色
            lastPressedTag.gameObject.GetComponent<Image>().color = tagNormalColor;
            friendChatTagBtn.gameObject.GetComponent<Image>().color = tagPressedColor;
            lastPressedTag = friendChatTagBtn.GetComponent<Button>();
            //关闭新消息提醒
            friendChatTagBtn.transform.GetChild(3).gameObject.SetActive(false);
            go.transform.GetChild(2).gameObject.SetActive(false);
        });
        friendChatTagBtn.transform.GetChild(2).GetComponent<Image>().color =
            go.transform.GetChild(1).GetComponent<Image>().color;
        var closeChatBtn = friendChatTagBtn.transform.GetChild(1).GetComponent<Button>();
        closeChatBtn.onClick.AddListener(() =>
        {
            Destroy(friendChatScrollView);
            Destroy(friendChatTagBtn, 0.1f);
            chatType = 0;
            chatTargetPlayer = "";//设置公共聊天
            lastPressedTag = publicChanelBtn;
            publicChanelBtn.gameObject.GetComponent<Image>().color = tagPressedColor;
            closeChatBtn.onClick.RemoveAllListeners();
        });
    }
    
    //好友上下线请求
    private void FriendOnOffline(OnOfflineMsgS2C obj)
    {
        if (obj.player == PlayerData.Instance.LoginMsgS2C.account)
        {
            return;
        }
        if (tagListParent.Find(obj.player))
        {
            tagListParent.Find(obj.player).GetChild(2).gameObject.GetComponent<Image>().color = 
                obj.state == 1 ? Color.green : Color.gray;
        }
        friendBtnParent.Find(obj.player).GetChild(1).gameObject.GetComponent<Image>().color =
            obj.state == 1 ? Color.green : Color.gray;
    }

    // Update is called once per frame
    void Update()
    {
        // if (Input.GetKeyDown(KeyCode.A))
        // {
        //     AddMessage("xxx", "aldfja;dfajdkfjakdfjalkjdlkfjamkdjflkmajdlfjadjflajla;f", System.DateTime.Now.ToString());
        // }
    }

    private void OnDisable()
    {
        MessageHelper.Instance.chatHandle -= ChatHandle;
        MessageHelper.Instance.addFriendHandle -= AddFriend;
        MessageHelper.Instance.friendOnOfflineHandle -= FriendOnOffline;
        //退出账号
        //MessageHelper.Instance.SendOnOfflineMsg(PlayerData.Instance.LoginMsgS2C.account, 0);
    }
}
public class MessageHelper
{
    private static MessageHelper instance = new MessageHelper();
    public static MessageHelper Instance => instance;//单例
    
    byte[] data = new byte[4096];//接收消息的缓冲区
    int msgLength = 0;//接收到的消息长度

    //client接收到消息时,把buffer的数据复制到data数据缓冲区,数据长度加上接受的新有效数据流长度,handle处理数据
    public void CopyToData(byte[] buffer, int length)
    {
        Array.Copy(buffer, 0, data, msgLength, length);
        msgLength += length;
        Handle();
    }
    
    private void Handle()
    {
        //包体大小(4) 协议ID(4) 包体(byte[])
        if (msgLength >= 8)
        {
            byte[] _size = new byte[4];
            Array.Copy(data, 0, _size, 0, 4);//把包体大小从第0位缓存4位长度
            int size = BitConverter.ToInt32(_size, 0);//获得包体大小

            //本次要拿的长度
            var _length = 8 + size;//实际完整消息的长度:包体大小(4)+协议ID(4)+包体(byte[])

            while (msgLength>=_length)//判断数据缓冲区的长度是否大于一条完整消息的长度。
            {
                //拿出id
                byte[] _id = new byte[4];
                Array.Copy(data, 4, _id, 0, 4);//把协议ID从第4位缓存4位长度
                int id = BitConverter.ToInt32(_id, 0);//获得协议ID

                //包体
                byte[] body = new byte[size];
                Array.Copy(data, 8, body, 0, size);//把包体从第8位缓存size位长度

                if (msgLength>_length)//如果接收到的数据长度大于这次取出的完整一条数据的长度,说明还有数据
                {
                    for (int i = 0; i < msgLength - _length; i++)
                    {
                        data[i] = data[_length + i];//前面取完一次完整消息了,把后面的消息前挪
                    }
                }
                msgLength -= _length;//减掉已经取完的消息长度
                if (id != (int)MsgID.PingMsg)
                {
                    Debug.Log($"{DateTime.Now} | Message | 发送的消息类型:{id} | 接收的消息内容:{Encoding.UTF8.GetString(body, 0, body.Length)}");
                }
                else
                {
                    Debug.Log($"{DateTime.Now} | Ping | 接收的消息内容:Ping");
                }
                WaitHandle?.Invoke(id, false, Encoding.UTF8.GetString(body, 0, body.Length));
                //根据id进行处理,,实际项目一般使用观察者模式,监听id和Action事件绑定
                switch (id)
                {
                    case (int)MsgID.RegisterMsg://注册请求
                        RigisterMsgHandle(body);
                        break;
                    case (int)MsgID.LoginMsg://登录业务
                        LoginMsgHandle(body);
                        break;
                    case (int)MsgID.ChatMsg://聊天业务
                        ChatMsgHandle(body);
                        break;
                    case (int)MsgID.AddFriend://添加好友
                        AddFriendHandle(body);
                        break;
                    case (int)MsgID.OnOffline://朋友上线下线
                        FriendOnOfflineHandle(body);
                        break;
                    case (int)MsgID.PingMsg://维持连接
                        PingHandle(body);
                        break;
                }
            }
        }
    }

    //一旦开始发送消息,就让客户端等待消息回复,开启定时器,如果定时器结束前没有收到回复,说明断开连接,在GameManager中进行重连
    public event Action<int, bool, string> WaitHandle;
    //按格式封装消息,发送到服务器
    public void SendToServer(int id, string str)
    {
        //Debug.Log("ID:" + id);
        var body = Encoding.UTF8.GetBytes(str);
        byte[] send_buff = new byte[body.Length + 8];

        int size = body.Length;

        var _size = BitConverter.GetBytes(size);
        var _id = BitConverter.GetBytes(id);

        Array.Copy(_size, 0, send_buff, 0, 4);
        Array.Copy(_id, 0, send_buff, 4, 4);
        Array.Copy(body, 0, send_buff, 8, body.Length);
        if (id != (int)MsgID.PingMsg)
        {
            Debug.Log($"{DateTime.Now} | Message | 发送的消息类型:{id} | 发送的消息内容:{Encoding.UTF8.GetString(body, 0, body.Length)}");
        }
        else
        {
            Debug.Log($"{DateTime.Now} | Pong | 发送的消息内容:Pong");
        }
        
        Client.Instance.Send(send_buff);
        //把发送的消息和id传递给订阅WaitHandle的方法,一旦断联,需要重连并重新发送消息。
        WaitHandle?.Invoke(id, true, Encoding.UTF8.GetString(body, 0, body.Length));
    }

    //发送登录的消息给服务器 1002
    public void SendLoginMsg(string account, string pwd)
    {
        LoginMsgC2S msg = new LoginMsgC2S();
        msg.account = account;
        msg.password = pwd;
        var str = JsonHelper.ToJson(msg);//json格式化注册请求
        SendToServer((int)MsgID.LoginMsg, str);//发送
    }

    public Action<LoginMsgS2C> loginHandle;
    //处理登录(结果)请求
    private void LoginMsgHandle(byte[] obj)
    {
        var str = Encoding.UTF8.GetString(obj);//将接受的消息从字节转换为string
        LoginMsgS2C msg = JsonHelper.ToObject<LoginMsgS2C>(str);//在从json格式的string转为对应LoginMsgS2C
        loginHandle?.Invoke(msg);//如果处理注册消息的事件有订阅,就执行。
    }
    
    //发送聊天信息给服务器
    public void SendChatMsg(string account, string chat, int chatType, string chatTarget)
    {
        ChatMsgC2S msg = new ChatMsgC2S();
        msg.player = account;
        msg.msg = chat;
        msg.time = System.DateTime.Now.ToString();
        msg.type = chatType;
        msg.targetPlayer = chatTarget;
        var str = JsonHelper.ToJson(msg);//json格式化注册请求
        SendToServer((int)MsgID.ChatMsg, str);//发送
    }

    public Action<ChatMsgS2C> chatHandle;
    //处理聊天(转发)请求
    private void ChatMsgHandle(byte[] obj)
    {
        var str = Encoding.UTF8.GetString(obj);//将接受的消息从字节转换为string
        ChatMsgS2C msg = JsonHelper.ToObject<ChatMsgS2C>(str);//在从json格式的string转为对应ChatMsgS2C
        chatHandle?.Invoke(msg);//如果处理注册消息的事件有订阅,就执行。
    }
    
    //发送注册的消息给服务器 1001
    public void SendRegisterMsg(string account, string email, string pwd)
    {
        RegisterMsgC2S msg = new RegisterMsgC2S();
        msg.account = account;
        msg.email = email;
        msg.password = pwd;
        var str = JsonHelper.ToJson(msg);//json格式化注册请求
        SendToServer((int)MsgID.RegisterMsg, str);//发送
    }

    public Action<RegisterMsgS2C> registerHandle;
    //处理注册(结果)请求
    private void RigisterMsgHandle(byte[] obj)
    {
        var str = Encoding.UTF8.GetString(obj);//将接受的消息从字节转换为string
        RegisterMsgS2C msg = JsonHelper.ToObject<RegisterMsgS2C>(str);//在从json格式的string转为对应RegisterMsgS2C
        registerHandle?.Invoke(msg);//如果处理注册消息的事件有订阅,就执行。
    }

    //发送添加好友消息
    public void SendAddFriendMsg(string account, string addAccount)
    {
        AddFriendMsgC2S msg = new AddFriendMsgC2S();
        msg.player = account;
        msg.newFriend = addAccount;
        var str = JsonHelper.ToJson(msg);//json格式化注册请求
        SendToServer((int)MsgID.AddFriend, str);//发送
    }

    //处理添加好友请求
    public Action<AddFriendMsgS2C> addFriendHandle;
    private void AddFriendHandle(byte[] obj)
    {
        var str = Encoding.UTF8.GetString(obj);//将接受的消息从字节转换为string
        AddFriendMsgS2C msg = JsonHelper.ToObject<AddFriendMsgS2C>(str);//在从json格式的string转为对应AddFriendMsgS2C
        addFriendHandle?.Invoke(msg);//如果处理注册消息的事件有订阅,就执行。
    }

    //处理朋友上下线请求
    public Action<OnOfflineMsgS2C> friendOnOfflineHandle;
    private void FriendOnOfflineHandle(byte[] obj)
    {
        var str = Encoding.UTF8.GetString(obj);//将接受的消息从字节转换为string
        OnOfflineMsgS2C msg = JsonHelper.ToObject<OnOfflineMsgS2C>(str);//在从json格式的string转为对应OnOfflineMsgS2C
        friendOnOfflineHandle?.Invoke(msg);//如果处理注册消息的事件有订阅,就执行。
    }

    private void PingHandle(byte[] obj)
    {
        var str = Encoding.UTF8.GetString(obj);//将接受的消息从字节转换为string
        SendToServer((int)MsgID.PingMsg, "pong");//发送
    }

    public void SendOnOfflineMsg(string account, int state)
    {
        OnOfflineMsgC2S msg = new OnOfflineMsgC2S();
        msg.player = account;
        msg.state = state;
        var str = JsonHelper.ToJson(msg);//json格式化注册请求
        SendToServer((int)MsgID.OnOffline, str);//发送
    }
    
    //缓存消息至本地
    public void SaveMsgToLocal(ChatMsgS2C msg)
    {
        string path = Application.persistentDataPath + "/" + PlayerData.Instance.LoginMsgS2C.account + "/";
        if (!Directory.Exists(path))
        {
            Directory.CreateDirectory(path);
            if (!File.Exists(path + "Msgs.txt"))
            {
                File.Create(path + "Msgs.txt").Dispose();
            }
        }
        var str = JsonHelper.ToJson(msg) + "\n";
        File.AppendAllText(path + "Msgs.txt", str);
    }

    //从本地加载消息记录
    public bool TryLoadMsgFromLocal(out List<ChatMsgS2C> msgList)
    {
        string path = Application.persistentDataPath + "/" + PlayerData.Instance.LoginMsgS2C.account + "/Msgs.txt";
        if (File.Exists(path))
        {
            List<ChatMsgS2C> msgs = new List<ChatMsgS2C>();
            string[] strs = File.ReadAllLines(path);
            foreach (var str in strs)
            {
                msgs.Add(JsonHelper.ToObject<ChatMsgS2C>(str));
            }
            msgList = msgs;
        }
        else
        {
            msgList = new List<ChatMsgS2C>();
        }
        return File.Exists(path);
    }
}

//1002
public class LoginMsgC2S
{
    public string account;
    public string password;
}

public class LoginMsgS2C
{
    public string account;
    public string password;
    public int result;//0成功 1失败:账号或密码错误
    public Dictionary<string, int> friends;
}

//1001
public class RegisterMsgC2S
{
    public string account;
    public string email;
    public string password;
}

public class RegisterMsgS2C
{
    public string account;
    public string email;
    public string password;
    public int result;//0成功 1失败:已被注册的账号
}

//1003
public class ChatMsgC2S
{
    public string player;
    public string msg;
    public string time;
    public int type;//0世界聊天
    public string targetPlayer;
}

//服务器转发给客户端
public class ChatMsgS2C
{
    public string player;
    public string msg;
    public string time;
    public int type;//0世界聊天
    public string targetPlayer;
}

//1004
public class AddFriendMsgC2S
{
    public string player;
    public string newFriend;
}

public class AddFriendMsgS2C
{
    public string player;
    public string newFriend;
    public int result;
    public Dictionary<string, int> friends;
    public int friendState;
}

//1005
public class OnOfflineMsgC2S
{
    public string player;
    public int state;//0下线,1上线
}

public class OnOfflineMsgS2C
{
    public string player;
    public int state;//0下线,1上线
}

public enum MsgID{
    RegisterMsg = 1001,
    LoginMsg = 1002,
    ChatMsg = 1003,
    AddFriend = 1004,
    OnOffline = 1005,
    PingMsg = 9999,
}
public class GameManager : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Client.Instance.Start();
        
        //打开登录界面
        var loginPrefab = Resources.Load<GameObject>("LoginView");
        var loginView = GameObject.Instantiate<GameObject>(loginPrefab);
        loginView.AddComponent<LoginView>();
    }

    private void OnDestroy()
    {
        //退出账号
        MessageHelper.Instance.SendOnOfflineMsg(PlayerData.Instance.LoginMsgS2C.account, 0);
    }
}
await client.ConnectAsync("6517382f5e.zicp.fun", 39047);
private void OnDestroy()
{
    //退出账号
    MessageHelper.Instance.SendOnOfflineMsg(PlayerData.Instance.LoginMsgS2C.account, 0);
}

注册,登录

添加好友

点击头像,输入好友名称,添加好友,弹出聊天界面,显示好友在线状态。

消息提醒。发消息的一方不提醒。

在公共频道发送消息,会指定显示在公共频道,不会显示在好友聊天处,同时右侧服务器可以明显看出前面的消息的targetPlayer是222即111对222说话,第二条消息没有targetPlayer。

点击消息后,消息提醒红点关闭。

点击头像,退出登录回到登录界面。(仅说明按钮用途)

222下线前,发送一条消息,之后离线,此时可以看到111收到一条消息,同时222的在线状态变为灰色。

222离线后,111发送消息,此时服务器缓存消息,等待222上线进行转发。

222上线,收到消息

上部分是之前缓存至本地的消息,重新加载出来,,后面是离线接收的消息。111没截图,更新了好友在线状态

Server端添加处理聊天业务

namespace Server
{
    internal class PlayerData
    {
        private static PlayerData instance = new PlayerData();
        public static PlayerData Instance => instance;//单例
        //缓存注册信息
        Dictionary<string, RegisterMsgS2C> userMsg = new Dictionary<string, RegisterMsgS2C>();
        //缓存用户的好友即好友在线状态
        Dictionary<string, Dictionary<string, int>> userFriends = new Dictionary<string, Dictionary<string, int>>();
        //缓存发给离线用户的消息
        Dictionary<string, List<ChatMsgS2C>> offlineReceivedMsg = new Dictionary<string, List<ChatMsgS2C>>();

        //注册后,result=0表示注册成功,将这个注册后的信息返回给客户端
        public RegisterMsgS2C Add(RegisterMsgC2S msgC2S)
        {
            var item = new RegisterMsgS2C();
            userMsg[msgC2S.account] = item;
            userFriends[msgC2S.account] = new Dictionary<string, int>();
            item.account = msgC2S.account;
            item.password = msgC2S.password;
            item.result = 0;
            return item;
        }

        //判断是否存在相同的账号
        public bool Contain(string account)
        {
            return userMsg.ContainsKey(account);
        }

        //维护已经登陆的用户
        Dictionary<string, Client> LoginUser = new Dictionary<string, Client>();
        //添加登录用户
        public void AddLoginUser(string account, Client client)
        {
            LoginUser[account] = client;
        }

        //登出已下线的用户
        public void RemoveLogoutUser(string account)
        {
            LoginUser.Remove(account);
        }

        //取出一个客户端用于发送消息
        public Client GetUser(string account)
        {
            if (LoginUser.ContainsKey(account))
            {
                return LoginUser[account];
            }
            else
            {
                return null;
            }
        }

        //移除断线客户端
        public void RemoveDisconnectClient(Client client)
        {
            string temp = "";
            foreach (var userClient in LoginUser)
            {
                if (userClient.Value == client)
                    temp = userClient.Key;
                break;
            }
            LoginUser.Remove(temp);
        }

        //获取全部用户的字典
        public Dictionary<string, Client> GetAllLoginUser()
        {
            return LoginUser;
        }

        //判断某账号是否包含某好友
        public bool ContainsFriend(string account, string newFriend)
        {
            return userFriends[account].ContainsKey(newFriend);
        }

        //添加好友
        public void AddFriend(string account, string newFriend)
        {
            if (!userFriends.ContainsKey(account))
            {
                Dictionary<string, int> friends = new Dictionary<string, int>();
                userFriends.Add(account, friends);
            }
            userFriends[account].Add(newFriend, LoginUser.ContainsKey(newFriend) ? 1 : 0);
        }

        //拿到一个好友和好友在线状态。
        public Dictionary<string, int> GetFriend(string account)
        {
            return userFriends[account];
        }

        //缓存发送给离线用户的消息
        public void AddOfflineMsg(string player, ChatMsgS2C msg)
        {
            if (!offlineReceivedMsg.ContainsKey(player))
            {
                List<ChatMsgS2C> msgs = new List<ChatMsgS2C>();
                offlineReceivedMsg.Add(player, msgs);//新建一个信息缓存列表
            }
            offlineReceivedMsg[player].Add(msg);           
        }

        //清空发送给某一离线用户的消息
        public void ClearOfflineMsg(string player)
        {
            offlineReceivedMsg.Remove(player);
        }

        //检查是否存在发送给离线用户的消息
        public bool CheckOfflineMsg(string player, out List<ChatMsgS2C> msgs)
        {
            bool ret = offlineReceivedMsg.ContainsKey(player);
            msgs = ret ? offlineReceivedMsg[player] : null;
            return ret;
        }
    }
}
class MessageHelper
{
}

//1002
public class LoginMsgC2S
{
    public string account;
    public string password;
}

public class LoginMsgS2C
{
    public string account;
    public string password;
    public int result;//0成功 1失败:账号或密码错误
    public Dictionary<string, int> friends;
}

//1001
public class RegisterMsgC2S
{
    public string account;
    public string email;
    public string password;
}

public class RegisterMsgS2C
{
    public string account;
    public string email;
    public string password;
    public int result;//0成功 1失败:已被注册的账号
}

//1003
public class ChatMsgC2S
{
    public string player;
    public string msg;
    public string time;
    public int type;//0世界聊天
    public string targetPlayer;
}

//服务器转发给所有在线的客户端
public class ChatMsgS2C
{
    public string player;
    public string msg;
    public string time;
    public int type;//0世界聊天
    public string targetPlayer;
}

//1004
public class AddFriendMsgC2S
{
    public string player;
    public string newFriend;
}

public class AddFriendMsgS2C
{
    public string player;
    public string newFriend;
    public Dictionary<string, int> friends;
    public int result;
    public int friendState;
}

//1005
public class OnOfflineMsgC2S
{
    public string player;
    public int state;//0下线,1上线
}

public class OnOfflineMsgS2C
{
    public string player;
    public int state;//0下线,1上线
}

public enum MsgID
{
    RegisterMsg = 1001,
    LoginMsg = 1002,
    ChatMsg = 1003,
    AddFriend = 1004,
    OnOffline = 1005,
    PingMsg = 9999,
}
namespace Server
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.OutputEncoding = Encoding.UTF8;
            TCPServer tcpServer = new TCPServer();
            tcpServer.Start();

            while(true)
            {
                var str = Console.ReadLine();//监听用户输入,不写这句话运行测试会立刻跳出
            }
        }
    }
}
namespace Server.Net
{
    //每一个客户端都是一个独立的Client
    internal class Client
    {
        TcpClient client;
        public Client(TcpClient tcpClient)
        {
            client = tcpClient;
            Receive();
            PingPong();
        }

        byte[] data = new byte[4096];//接收消息的缓冲区
        int msgLength = 0;//接收到的消息长度

        //接收消息
        public async void Receive()
        {
            //处于连接状态就持续接收信息。
            while(client.Connected)
            {
                try
                {
                    byte[] buffer = new byte[4096];//存储接收到的数据,定义初始容量,一般不超过4096或1024
                    //把消息存到buffer中,从第几个字节开始存,存多长,,,,,,返回length表示接收到多少数据。
                    int length = await client.GetStream().ReadAsync(buffer, 0, buffer.Length);

                    if(length > 0)//表示有效信息
                    {
                        //$作用是将{}内容当做表达式
                        //Console.WriteLine($"{DateTime.Now} | 接收到的数据长度:{length}");
                        //Console.WriteLine($"接收到的数据内容:{Encoding.UTF8.GetString(buffer, 0, length)}");//将byte数组转换为字符串
                        //Send(Encoding.UTF8.GetBytes("测试返回..."));                        
                        Array.Copy(buffer, 0, data, msgLength, length);//把接收到length长度的消息复制到数据缓冲区中msgLength索引后的位置
                        msgLength += length;//每次收到网络数据都要加上数据的长度
                        Handle();
                    }
                    else
                    {
                        Console.WriteLine($"{DateTime.Now} | Client  | 客户端已离线:" + client.Client.RemoteEndPoint);
                        PlayerData.Instance.RemoveDisconnectClient(this);//移除关闭的客户端和登录用户
                        client.Close();//出现错误断开客户端连接
                    }
                }
                catch (Exception e)
                {
                    Console.WriteLine($"Receive Error:{e.Message}");
                    PlayerData.Instance.RemoveDisconnectClient(this);//移除关闭的客户端和登录用户
                    client.Close();//出现错误断开客户端连接。                    
                }
            }            
        }

        private void Handle()
        {
            //包体大小(4) 协议ID(4) 包体(byte[])
            if (msgLength >= 8)
            {
                byte[] _size = new byte[4];
                Array.Copy(data, 0, _size, 0, 4);//把包体大小从第0位缓存4位长度
                int size = BitConverter.ToInt32(_size, 0);//获得包体大小

                //本次要拿的长度
                var _length = 8 + size;//实际完整消息的长度:包体大小(4)+协议ID(4)+包体(byte[])
                if (msgLength>=_length)//判断数据缓冲区的长度是否大于一条完整消息的长度。
                {
                    //拿出id
                    byte[] _id = new byte[4];
                    Array.Copy(data, 4, _id, 0, 4);//把协议ID从第4位缓存4位长度
                    int id = BitConverter.ToInt32(_id, 0);//获得协议ID

                    //包体
                    byte[] body = new byte[size];
                    Array.Copy(data, 8, body, 0, size);//把包体从第8位缓存size位长度

                    if (msgLength>_length)//如果接收到的数据长度大于这次取出的完整一条数据的长度,说明还有数据
                    {
                        for (int i = 0; i < msgLength - _length; i++)
                        {
                            data[i] = data[_length + i];//前面取完一次完整消息了,把后面的消息前挪
                        }
                    }
                    msgLength -= _length;//减掉已经取完的消息长度
                    if (id != (int)MsgID.PingMsg)
                    {
                        Console.WriteLine($"{DateTime.Now} | Message | 从{client.Client.RemoteEndPoint} | 接收的消息类型:{id} | 接收的消息内容:{Encoding.UTF8.GetString(body, 0, body.Length)}");
                    }
                    else
                    {
                        Console.WriteLine($"{DateTime.Now} | Pong    | 从{client.Client.RemoteEndPoint} | 接收的消息内容:Pong");
                    }
                    //根据id进行处理,,实际项目一般使用观察者模式,监听id和Action事件绑定
                    switch (id)
                    {
                        case (int)MsgID.RegisterMsg://注册请求
                            RegisterMsgHandle(body);
                            break;
                        case (int)MsgID.LoginMsg://登录业务
                            LoginMsgHandle(body);
                            break;
                        case (int)MsgID.ChatMsg://聊天业务
                            ChatMsgHandle(body);
                            break;
                        case (int)MsgID.AddFriend://添加好友
                            AddFriendHandle(body);
                            break;
                        case (int)MsgID.OnOffline://账号下线
                            OnOfflineHandle(body);
                            break;
                        case (int)MsgID.PingMsg://pingpong维持客户端连接
                            PingPongHandle(body);
                            break;
                    }
                }
            }
        }

        //处理注册请求
        private void RegisterMsgHandle(byte[] obj)
        {
            var msg = JsonHelper.ToObject<RegisterMsgC2S>(obj);
            RegisterMsgS2C response = null;
            if (PlayerData.Instance.Contain(msg.account))
            {
                response = new RegisterMsgS2C();
                response.result = 1;//已经包含该帐号了
            }
            else
            {
                response = PlayerData.Instance.Add(msg);
            }
            SendToClient((int)MsgID.RegisterMsg, JsonHelper.ToJson(response));
        }

        //处理登录请求
        private void LoginMsgHandle(byte[] obj)
        {
            //判断账号和密码是否跟缓存的一直,如果是,返回账号和密码
            var msg = JsonHelper.ToObject<LoginMsgC2S>(obj);
            LoginMsgS2C response = new LoginMsgS2C();
            if (PlayerData.Instance.Contain(msg.account))
            {
                response.result = 0;//已包含该账号
                response.account = msg.account;
                response.password = msg.password;

                response.friends = PlayerData.Instance.GetFriend(msg.account);
                SendToClient((int)MsgID.LoginMsg, JsonHelper.ToJson(response));
                //将用户储存起来
                PlayerData.Instance.AddLoginUser(msg.account, this);

                //告知所有朋友自己上线
                var dct = PlayerData.Instance.GetAllLoginUser();
                var myFriend = PlayerData.Instance.GetFriend(msg.account);
                OnOfflineMsgS2C onofflineMsg = new OnOfflineMsgS2C();
                onofflineMsg.player = msg.account;
                onofflineMsg.state = 1;
                foreach (var friend in myFriend)
                {
                    //给在线客户端发消息,上线
                    if (dct.ContainsKey(friend.Key))
                    {
                        dct[friend.Key].SendToClient((int)MsgID.OnOffline, JsonHelper.ToJson(onofflineMsg));
                    }

                    //更新自己在系统中每个朋友哪里的现在状态。
                    var friendDic = PlayerData.Instance.GetFriend(friend.Key);//我的朋友的朋友状态字典
                    friendDic[msg.account] = 1;
                }
                //检查发送离线缓存消息
                SendOfflineMsgs(msg);
            }
            else
            {
                response.result = 1;//账号或密码错误
                SendToClient((int)MsgID.LoginMsg, JsonHelper.ToJson(response));
            }
            //SendToClient(1002, JsonHelper.ToJson(response));
        }

        //处理聊天请求
        private void ChatMsgHandle(byte[] obj)
        {
            //转发给所有在线的用户
            var msg = JsonHelper.ToObject<ChatMsgC2S>(obj);
            PlayerData.Instance.AddLoginUser(msg.player, this);//更新在线的Client
            ChatMsgS2C sendMsg = new ChatMsgS2C();
            sendMsg.msg = msg.msg;
            sendMsg.player = msg.player;
            sendMsg.type = msg.type;
            sendMsg.time = msg.time;
            sendMsg.targetPlayer = msg.targetPlayer;

            if (sendMsg.type == 1)//私人聊天
            {
                var json = JsonHelper.ToJson(sendMsg);
                SendToClient(1003, json);//自己发的消息自己接收
                if (PlayerData.Instance.GetUser(msg.targetPlayer) != null)//聊天对象在线,就发送信息
                {
                    PlayerData.Instance.GetUser(msg.targetPlayer).SendToClient((int)MsgID.ChatMsg, json);
                }
                else//聊天对象离线,缓存信息等待上线,发送信息
                {
                    PlayerData.Instance.AddOfflineMsg(msg.targetPlayer, sendMsg);
                }
            }
            else if (sendMsg.type == 0)//公共聊天
            {
                var dct = PlayerData.Instance.GetAllLoginUser();
                var json = JsonHelper.ToJson(sendMsg);
                foreach (var item in dct)
                {
                    item.Value.SendToClient((int)MsgID.ChatMsg, json);
                }
            }            
        }

        //处理添加好友请求
        private void AddFriendHandle(byte[] obj)
        {
            var msg = JsonHelper.ToObject<AddFriendMsgC2S>(obj);
            PlayerData.Instance.AddLoginUser(msg.player, this);//更新在线的Client
            AddFriendMsgS2C response2Player = new AddFriendMsgS2C();
            AddFriendMsgS2C response2NewFriend = new AddFriendMsgS2C();
            if (PlayerData.Instance.Contain(msg.newFriend) && msg.player != msg.newFriend)//如果包含这个账户才可以添加
            {
                if (!PlayerData.Instance.ContainsFriend(msg.player, msg.newFriend))//查看是否已经包含好友账户
                {
                    PlayerData.Instance.AddFriend(msg.player, msg.newFriend);
                    PlayerData.Instance.AddFriend(msg.newFriend, msg.player);
                    //更新自己的账号好友
                    response2Player.result = 0;//添加账号成功
                    response2Player.player = msg.player;
                    response2Player.newFriend = msg.newFriend;
                    response2Player.friends = PlayerData.Instance.GetFriend(msg.player);
                    response2Player.friendState = PlayerData.Instance.GetUser(msg.newFriend) == null ? 0 : 1;
                    //更新好友的账号好友
                    response2NewFriend.result = 0;//添加账号成功
                    response2NewFriend.player = msg.newFriend;
                    response2NewFriend.newFriend = msg.player;
                    response2NewFriend.friends = PlayerData.Instance.GetFriend(msg.newFriend);
                    response2NewFriend.friendState = 1;//我在线
                }
                else
                {
                    response2Player.result = 1;//添加失败
                }
                SendToClient((int)MsgID.AddFriend, JsonHelper.ToJson(response2Player));
                PlayerData.Instance.GetUser(msg.newFriend)?.SendToClient((int)MsgID.AddFriend, JsonHelper.ToJson(response2NewFriend));
            }
            else
            {
                response2Player.result = 1;//添加账号失败
                response2Player.player = msg.player;
                response2Player.newFriend = "";
                response2Player.friends = PlayerData.Instance.GetFriend(msg.player);
                response2Player.friendState = 0;
                SendToClient((int)MsgID.AddFriend, JsonHelper.ToJson(response2Player));
            }            
        }

        //账号上下线处理
        private void OnOfflineHandle(byte[] obj)
        {
            var msg = JsonHelper.ToObject<OnOfflineMsgC2S>(obj);                       
            //告知所有朋友自己上线(重连)下线
            var dct = PlayerData.Instance.GetAllLoginUser();
            var myFriend = PlayerData.Instance.GetFriend(msg.player);
            OnOfflineMsgS2C onOfflineMsg = new OnOfflineMsgS2C();
            onOfflineMsg.player = msg.player;
            onOfflineMsg.state = msg.state;
            if (msg.state == 1)
            {
                PlayerData.Instance.AddLoginUser(msg.player, this);//将重连用户储存起来
                PlayerData.Instance.GetUser(msg.player).SendToClient((int)MsgID.OnOffline, JsonHelper.ToJson(onOfflineMsg));//给该用户也发送
            }
            else
            {
                PlayerData.Instance.RemoveLogoutUser(msg.player);
            }
            foreach (var friend in myFriend)
            {
                //给在线客户端发消息,上下线
                if (dct.ContainsKey(friend.Key))
                {
                    dct[friend.Key].SendToClient((int)MsgID.OnOffline, JsonHelper.ToJson(onOfflineMsg));
                }                
                //更新自己在系统中每个朋友哪里的现在状态。
                var friendDic = PlayerData.Instance.GetFriend(friend.Key);//我的朋友的朋友状态字典
                friendDic[msg.player] = msg.state;
            }
            
        }

        //发送消息
        public async void Send(byte[] data)
        {
            try
            {
                //数据写入网络数据流中。就是发送
                await client.GetStream().WriteAsync(data, 0, data.Length);
                //Console.WriteLine("发送成功! " + $"发送的消息内容:{Encoding.UTF8.GetString(data, 0, data.Length)}");
            }
            catch (Exception e)
            {
                client.Close();//关闭客户端
                Console.WriteLine($"send error:{e.Message}");
            }
        }

        //按格式封装后,发送消息
        public void SendToClient(int id, string str)
        {
            //包体转换为byte[]
            var body = Encoding.UTF8.GetBytes(str);

            //包体大小(4) 协议ID(4) 包体内容
            byte[] send_buff = new byte[body.Length + 8];

            int size = body.Length;
            var _size = BitConverter.GetBytes(size);
            var _id = BitConverter.GetBytes(id);

            Array.Copy(_size, 0, send_buff, 0, 4);
            Array.Copy(_id, 0, send_buff, 4, 4);
            Array.Copy(body, 0, send_buff, 8, body.Length);

            Send(send_buff);
            if (id != (int)MsgID.PingMsg)
            {
                Console.WriteLine($"{DateTime.Now} | Message | 向{client.Client.RemoteEndPoint} | 发送的消息类型:{id} | 发送的消息内容:{Encoding.UTF8.GetString(body, 0, body.Length)}");
            }
            else
            {
                Console.WriteLine($"{DateTime.Now} | Ping    | 向{client.Client.RemoteEndPoint} | 发送的消息内容:Ping");
            }
            //Console.WriteLine($"{DateTime.Now} | Message | 发送的消息类型:{id} | 发送的消息内容:{Encoding.UTF8.GetString(body, 0, body.Length)}");
        }

        //检查发送离线缓存的消息
        public async void SendOfflineMsgs(LoginMsgC2S msg)
        {
            await Task.Delay(2000);
            //检查离线缓存消息,若存在,挨个发送,清空缓存消息
            List<ChatMsgS2C> msgs = new List<ChatMsgS2C>();
            if (PlayerData.Instance.CheckOfflineMsg(msg.account, out msgs))
            {
                foreach (var item in msgs)
                {
                    SendToClient((int)MsgID.ChatMsg, JsonHelper.ToJson(item));
                }
                PlayerData.Instance.ClearOfflineMsg(msg.account);//清空
            }
        }

        //接收客户端返回pong的处理,停止等待计时器
        private void PingPongHandle(byte[] obj)
        {
            waitTimer.Change(Timeout.Infinite, Timeout.Infinite);
            offlineCounter = 0;
        }

        //发送一个ping信号,维持链接
        public void SendPing()
        {
            SendToClient((int)MsgID.PingMsg, "ping");
        }

        bool waitPong = true;
        int offlineCounter = 0;
        Timer waitTimer;

        //PingPong心跳维持客户端连接
        private async void PingPong()
        {
            while (client.Connected && waitPong)
            {
                //await Task.Delay(5000);
                SendPing();
                waitTimer = new Timer(CounterCallBack, null, 4000, Timeout.Infinite);
                await Task.Delay(6000);
            }
            PlayerData.Instance.RemoveDisconnectClient(this);
        }

        //每隔5s执行一次,累计offlineCounter到3,表明没有收到客户端返回pong。说明离线
        private void CounterCallBack(object state)
        {
            ++offlineCounter;
            if (client.Client.Connected)
                Console.WriteLine($"{DateTime.Now} | Ping    | 等待客户端{client.Client.RemoteEndPoint}的Pong回复 | 正在离线计数...{offlineCounter}");
            if (offlineCounter == 3)
            {
                offlineCounter = 0;
                waitTimer.Change(Timeout.Infinite, Timeout.Infinite);
                waitTimer.Dispose();
                waitPong = false;
                Console.WriteLine($"{DateTime.Now} | Ping    | 客户端{client.Client.RemoteEndPoint}已断开连接...");
            }
        }
    }
}

附加:消息粘包、心跳机制保活、断线重连

粘包

bug1:下线后,如果发送多条消息,在客户端上线时,一瞬间接收到,效果如同粘包,需要拆包。举例,连续发送三条160长度消息,可能实际显示2条消息,原因,第三条消息和第二条消息粘包,第二条消息长度变为320,但是Receive方法没有考虑这个问题,相当于这段代码只运行了两次,只接收了两次消息

int length = await client.GetStream().ReadAsync(buff, 0, buff.Length);
if (length > 0)
{
    Debug.Log($"接收到的数据长度:{length}");
    MessageHelper.Instance.CopyToData(buff, length);//接收到处理CopyToData给MessageHelper处理信息  
}

需要在CopyToData方法中的Handle处理一下粘包。

private void Handle()
{
    //包体大小(4) 协议ID(4) 包体(byte[])
    if (msgLength >= 8)
    {
        byte[] _size = new byte[4];
        Array.Copy(data, 0, _size, 0, 4);//把包体大小从第0位缓存4位长度
        int size = BitConverter.ToInt32(_size, 0);//获得包体大小

        //本次要拿的长度
        var _length = 8 + size;//实际完整消息的长度:包体大小(4)+协议ID(4)+包体(byte[])

        while (msgLength>=_length)//判断数据缓冲区的长度是否大于一条完整消息的长度。
        {
            //拿出id
            byte[] _id = new byte[4];
            Array.Copy(data, 4, _id, 0, 4);//把协议ID从第4位缓存4位长度
            int id = BitConverter.ToInt32(_id, 0);//获得协议ID

            //包体
            byte[] body = new byte[size];
            Array.Copy(data, 8, body, 0, size);//把包体从第8位缓存size位长度

            if (msgLength>_length)//如果接收到的数据长度大于这次取出的完整一条数据的长度,说明还有数据
            {
                for (int i = 0; i < msgLength - _length; i++)
                {
                    data[i] = data[_length + i];//前面取完一次完整消息了,把后面的消息前挪
                }
            }
            msgLength -= _length;//减掉已经取完的消息长度
            Debug.Log($"收到服务器响应:{id}");
            Debug.Log($"接收到的数据内容:{Encoding.UTF8.GetString(body, 0, body.Length)}");
            //根据id进行处理,,实际项目一般使用观察者模式,监听id和Action事件绑定
            switch (id)
            {
                case 1001://注册请求
                    RigisterMsgHandle(body);
                    break;
                case 1002://登录业务
                    LoginMsgHandle(body);
                    break;
                case 1003://聊天业务
                    ChatMsgHandle(body);
                    break;
                case 1004://添加好友
                    AddFriendHandle(body);
                    break;
                case 1005://朋友上线下线
                    FriendOnOfflineHandle(body);
                    break;
            }
        }
    }
}

心跳机制,通过心跳包维持连接

TCP通信会自动断开。造成这种情况的原因是保持连接的通道如果长时间不通信就会被路由关闭连接 。

  1. 长连接短连接概念

短连接:仅进行一次通信即关闭连接

长连接:每次通信完毕后不关闭连接

  1. 连接的保活

当双方已经建立了连接,但因为网络问题,链路不通,这样长连接就不能使用了。因此,需要使用一些机制对长连接进行保活

  1. 应用层心跳

客户端会开启一个定时任务,定时对已经建立连接的对端应用发送请求(这里的请求是特殊的心跳请求),服务端则需要特殊处理该请求,返回响应。如果心跳持续多次没有收到响应,客户端会认为连接不可用,主动断开连接。

使用服务器向客户端发送心跳包,服务器每一个客户端连接后根据前文都有一个Client保存,在Client构造函数中只有保存客户端的tcpClient和Receive,需要加上PingPong心跳,维持客户端连接。

public Client(TcpClient tcpClient)
{
    client = tcpClient;
    Receive();
    PingPong();
}
//Handle的Switch(id)最后一个PingMsg处理收到的客户端Pong消息
private void Handle()
{
    //包体大小(4) 协议ID(4) 包体(byte[])
    if (msgLength >= 8)
    {
        byte[] _size = new byte[4];
        Array.Copy(data, 0, _size, 0, 4);//把包体大小从第0位缓存4位长度
        int size = BitConverter.ToInt32(_size, 0);//获得包体大小
    
        //本次要拿的长度
        var _length = 8 + size;//实际完整消息的长度:包体大小(4)+协议ID(4)+包体(byte[])
        if (msgLength>=_length)//判断数据缓冲区的长度是否大于一条完整消息的长度。
        {
            //拿出id
            byte[] _id = new byte[4];
            Array.Copy(data, 4, _id, 0, 4);//把协议ID从第4位缓存4位长度
            int id = BitConverter.ToInt32(_id, 0);//获得协议ID
    
            //包体
            byte[] body = new byte[size];
            Array.Copy(data, 8, body, 0, size);//把包体从第8位缓存size位长度
    
            if (msgLength>_length)//如果接收到的数据长度大于这次取出的完整一条数据的长度,说明还有数据
            {
                for (int i = 0; i < msgLength - _length; i++)
                {
                    data[i] = data[_length + i];//前面取完一次完整消息了,把后面的消息前挪
                }
            }
            msgLength -= _length;//减掉已经取完的消息长度
            if (id != (int)MsgID.PingMsg)
            {
                Console.WriteLine($"{DateTime.Now} | Message | 从{client.Client.RemoteEndPoint} | 接收的消息类型:{id} | 接收的消息内容:{Encoding.UTF8.GetString(body, 0, body.Length)}");
            }
            else
            {
                Console.WriteLine($"{DateTime.Now} | Pong    | 从{client.Client.RemoteEndPoint} | 接收的消息内容:Pong");
            }
            //根据id进行处理,,实际项目一般使用观察者模式,监听id和Action事件绑定
            switch (id)
            {
                case (int)MsgID.RegisterMsg://注册请求
                    RegisterMsgHandle(body);
                    break;
                case (int)MsgID.LoginMsg://登录业务
                    LoginMsgHandle(body);
                    break;
                case (int)MsgID.ChatMsg://聊天业务
                    ChatMsgHandle(body);
                    break;
                case (int)MsgID.AddFriend://添加好友
                    AddFriendHandle(body);
                    break;
                case (int)MsgID.OnOffline://账号下线
                    OnOfflineHandle(body);
                    break;
                case (int)MsgID.PingMsg:
                    PingPongHandle(body);
                    break;
            }
        }
    }
}

//接收客户端返回pong的处理,停止等待计时器,重置离线计数器
private void PingPongHandle(byte[] obj)
{
    waitTimer.Change(Timeout.Infinite, Timeout.Infinite);
    offlineCounter = 0;
}

//发送一个ping信号,维持链接
public void SendPing()
{
    SendToClient((int)MsgID.PingMsg, "ping");
}

bool waitPong = true;
int offlineCounter = 0;
Timer waitTimer;

//PingPong心跳维持客户端连接
private async void PingPong()
{
    while (client.Connected && waitPong)
    {
        //await Task.Delay(5000);
        SendPing();
        //开启计时器等待回复,若无回复,开始离线计数
        waitTimer = new Timer(CounterCallBack, null, 4000, Timeout.Infinite);
        await Task.Delay(6000);
    }
    //一旦退出循环说明客户端断开,移除客户端
    PlayerData.Instance.RemoveDisconnectClient(this);
}

//每隔5s执行一次,累计offlineCounter到3,表明没有收到客户端返回pong。说明离线
private void CounterCallBack(object state)
{
    ++offlineCounter;
    if (client.Client.Connected)
        Console.WriteLine($"{DateTime.Now} | Ping    | 等待客户端{client.Client.RemoteEndPoint}的Pong回复 | 正在离线计数...{offlineCounter}");
    if (offlineCounter == 3)
    {
        offlineCounter = 0;
        waitTimer.Change(Timeout.Infinite, Timeout.Infinite);
        waitTimer.Dispose();
        waitPong = false;
        Console.WriteLine($"{DateTime.Now} | Ping    | 客户端{client.Client.RemoteEndPoint}已断开连接...");
    }
}

客户端登录后,服务器开始发送ping维持连接,客户端回复pong。断开网络连接。服务器向客户端发送ping,没有等到pong开始进行离线计数,计数到3没收到pong说明客户端离线。

如果计数到3之前客户端重新连接,服务器将不移除登录客户端。

断线重连

Unity-Client运行时,Start就连接一次服务器(这里不论连接成功与否,不影响后面重连),下面都是客户端的脚本。

断线重连逻辑:当使用到的业务发送消息到服务器,等待回复,等待超时开始重连,等待回复的过程收到无论什么回复,只要收到了消息,说明连接到了服务器;如果没接收到消息,继续尝试重连,并再次发送消息等待回复。

private void Handle()
{
    //包体大小(4) 协议ID(4) 包体(byte[])
    if (msgLength >= 8)
    {
        byte[] _size = new byte[4];
        Array.Copy(data, 0, _size, 0, 4);//把包体大小从第0位缓存4位长度
        int size = BitConverter.ToInt32(_size, 0);//获得包体大小

        //本次要拿的长度
        var _length = 8 + size;//实际完整消息的长度:包体大小(4)+协议ID(4)+包体(byte[])

        while (msgLength>=_length)//判断数据缓冲区的长度是否大于一条完整消息的长度。
        {
            //拿出id
            byte[] _id = new byte[4];
            Array.Copy(data, 4, _id, 0, 4);//把协议ID从第4位缓存4位长度
            int id = BitConverter.ToInt32(_id, 0);//获得协议ID

            //包体
            byte[] body = new byte[size];
            Array.Copy(data, 8, body, 0, size);//把包体从第8位缓存size位长度

            if (msgLength>_length)//如果接收到的数据长度大于这次取出的完整一条数据的长度,说明还有数据
            {
                for (int i = 0; i < msgLength - _length; i++)
                {
                    data[i] = data[_length + i];//前面取完一次完整消息了,把后面的消息前挪
                }
            }
            msgLength -= _length;//减掉已经取完的消息长度
            if (id != (int)MsgID.PingMsg)
            {
                Debug.Log($"{DateTime.Now} | Message | 发送的消息类型:{id} | 接收的消息内容:{Encoding.UTF8.GetString(body, 0, body.Length)}");
            }
            else
            {
                Debug.Log($"{DateTime.Now} | Ping | 接收的消息内容:Ping");
            }
            WaitHandle?.Invoke(id, false, Encoding.UTF8.GetString(body, 0, body.Length));
            //根据id进行处理,,实际项目一般使用观察者模式,监听id和Action事件绑定
            switch (id)
            {
                case (int)MsgID.RegisterMsg://注册请求
                    RigisterMsgHandle(body);
                    break;
                case (int)MsgID.LoginMsg://登录业务
                    LoginMsgHandle(body);
                    break;
                case (int)MsgID.ChatMsg://聊天业务
                    ChatMsgHandle(body);
                    break;
                case (int)MsgID.AddFriend://添加好友
                    AddFriendHandle(body);
                    break;
                case (int)MsgID.OnOffline://朋友上线下线
                    FriendOnOfflineHandle(body);
                    break;
                case (int)MsgID.PingMsg://维持连接
                    PingHandle(body);
                    break;
            }
        }
    }
}

//一旦开始发送消息,就让客户端等待消息回复,开启定时器,如果定时器结束前没有收到回复,说明断开连接,在GameManager中进行重连
public event Action<int, bool, string> WaitHandle;
//按格式封装消息,发送到服务器
public void SendToServer(int id, string str)
{
    //Debug.Log("ID:" + id);
    var body = Encoding.UTF8.GetBytes(str);
    byte[] send_buff = new byte[body.Length + 8];

    int size = body.Length;

    var _size = BitConverter.GetBytes(size);
    var _id = BitConverter.GetBytes(id);

    Array.Copy(_size, 0, send_buff, 0, 4);
    Array.Copy(_id, 0, send_buff, 4, 4);
    Array.Copy(body, 0, send_buff, 8, body.Length);
    if (id != (int)MsgID.PingMsg)
    {
        Debug.Log($"{DateTime.Now} | Message | 发送的消息类型:{id} | 发送的消息内容:{Encoding.UTF8.GetString(body, 0, body.Length)}");
    }
    else
    {
        Debug.Log($"{DateTime.Now} | Pong | 发送的消息内容:Pong");
    }
    
    Client.Instance.Send(send_buff);
    //把发送的消息和id传递给订阅WaitHandle的方法,一旦断联,需要重连并重新发送消息。
    WaitHandle?.Invoke(id, true, Encoding.UTF8.GetString(body, 0, body.Length));
}

GameManager作用是监听是否发送了消息WaitHandle是否执行,发送了消息开始计时器(只要不是退出业务和pong业务,都不需要服务器回复,不需要即使),并且缓存WaitHandle传进来刚刚尝试发送的消息。计时结束前,如果收到了消息WaitHandle执行,就停止等待,说明没有断网,,,如果计时结束前,没收到消息WaitHandle没有执行,说明断网,调用Client.Instance.ReConnect重连,重连时传进去刚刚未发送成功的消息和id,重连-再次发送-等待(计时-失败-重连-再次发送-等待),知道重连成功,发送消息-收到回复。

public class GameManager : MonoBehaviour
{
    public bool beginWait = false;
    private int id = 0;
    private string msg = "";
    // Start is called before the first frame update
    void Start()
    {
        MessageHelper.Instance.WaitHandle += StartTimer;
        Client.Instance.Start();
        //打开登录界面
        var loginPrefab = Resources.Load<GameObject>("LoginView");
        var loginView = GameObject.Instantiate<GameObject>(loginPrefab);
        loginView.AddComponent<LoginView>();
    }

    private void StartTimer(int msgID, bool wait, string body)
    {
        //只有timer没满以及不是发送下线信息时和ping消息,才等待,超时需要重连
        if (currentCount != waitCount && msgID != (int)MsgID.OnOffline && msgID != (int)MsgID.PingMsg)
        {
            beginWait = wait;
            currentCount = 0;
            id = msgID;
            msg = body;
        }
    }

    private void FixedUpdate()
    {
        if (beginWait)
        {
            Timer(id, msg);
        }
    }
    
    public int currentCount;
    public int waitCount = 100;
    public void Timer(int msgID, string body)
    {
        currentCount++;
        if (currentCount == waitCount)
        {
            currentCount = 0;
            beginWait = false;
            Client.Instance.IsConnected = false;
            Client.Instance.ReConnect(msgID, body);
        }
    }

    private void OnDestroy()
    {
        Client.Instance.isRunning = false;
        MessageHelper.Instance.WaitHandle -= StartTimer;
        //退出账号
        if (PlayerData.Instance.LoginMsgS2C != null)
        {
            MessageHelper.Instance.SendOnOfflineMsg(PlayerData.Instance.LoginMsgS2C.account, 0);
        }
        Client.Instance.CloseClient();
    }
}

Client主要负责ReConnect,尝试重连成功会发送刚刚未发送成功的消息,再次等待回复,计时,若失败继续调用重连。

public class Client
{
    private static Client instance = new Client();
    public static Client Instance => instance;//单例模式便于调用

    private TcpClient client;//跟服务器通信需要调用client
    private static bool isConnected = false;
    private Thread checkStateThread;
    public bool isRunning = true;
    public bool IsConnected
    {
        get { return isConnected; }
        set { isConnected = value; }
    }

    public void Start()
    {
        //client = new TcpClient();
        Connect();
    }

    //连接服务器接口,开始时调用
    public async void Connect()
    {
        while (!isConnected)
        {
            try
            {
                if (client != null)
                {
                    client.Close();
                }
                client = new TcpClient();
                await client.ConnectAsync("6517382f5e.zicp.fun", 39047);
                if (client.Connected)
                {
                    Debug.Log("TCP 连接成功");
                    isConnected = true;

                    Receive();
                }
            }
            catch (Exception e)
            {
                Debug.Log(e.Message);
                client.Close();
            }
        }
    }
    
    public async void ReConnect(int msgID, string body)
    {
        while (!isConnected)
        {
            try
            {
                if (client != null)
                {
                    client.Close();
                }
                client = new TcpClient();
                await client.ConnectAsync("6517382f5e.zicp.fun", 39047);
                isConnected = true;
                if (client.Connected)
                {
                    Debug.Log("重连成功");
                    //根据对应界面的功能,实现不同的重连网络需求
                    MessageHelper.Instance.SendToServer(msgID, body);

                    Receive();
                }
            }
            catch (Exception e)
            {
                Debug.Log(e.Message);
                client.Close();
            }
        }
    }

    //接收接口
    public async void Receive()
    {
        while (client.Connected)
        {
            try
            {
                byte[] buff = new byte[4096];
                int length = await client.GetStream().ReadAsync(buff, 0, buff.Length);
                if (length > 0)
                {
                    //Debug.Log($"{DateTime.Now} | 接收到的数据长度:{length}");
                    MessageHelper.Instance.CopyToData(buff, length);//接收到处理CopyToData给MessageHelper处理信息
                }
                else
                {
                    client.Close();
                    isConnected = false;
                }
            }
            catch (Exception e)
            {
                Debug.Log(e.Message);
                isConnected = false;
                client.Close();
            }
        }
    }
    
    //发送接口
    public async void Send(byte[] data)
    {
        try
        {
            await client.GetStream().WriteAsync(data, 0, data.Length);
            //Debug.Log("发送成功! " + $"发送的消息内容:{Encoding.UTF8.GetString(data, 0, data.Length)}");
        }
        catch (Exception e)
        {
            Debug.Log(e.Message);
            isConnected = false;
            client.Close();
        }
    }

    public void CloseClient()
    {
        client?.Close();
    }
}

输出消息格式化

Console.WriteLine($"{DateTime.Now} | Message | 向{client.Client.RemoteEndPoint} | 发送的消息类型:{id} | 发送的消息内容:{Encoding.UTF8.GetString(body, 0, body.Length)}");

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/192336.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

在 Linux 中创建用户 and 给用户设置密码相关操作

目录 一、创建用户 二、给用户设置密码相关操作 ②.①、修改用户密码 ②.②、设置用户密码 ②.③、删除用户密码 一、创建用户 语法&#xff1a; useradd [-g 用户组名 -d home目录] 用户名 在 Linux 中“[]”里面的内容都代表可选&#xff0c;可以不写。 useradd&…

【数据结构】归并排序、快速排序(递归法和非递归法)

文章目录 一、归并排序 递归法 思想 程序代码 时间复杂度 非递归法 思想 程序代码 二、快速排序&#xff08;挖坑法&#xff09; 思想 程序代码 时间复杂度 三、快速排序&#xff08;hoare法&#xff09; 思想 程序代码 hoare法错误集锦 死循环 越界 四、快…

kubernetes pod内容器状态OOMKilled和退出码137全流程解析

kubernetes pod内容器状态OOMKilled和退出码137全流程解析 - 简书 使用event_control监听memory cgroup的oom事件 - 简书 kubernetes/k8s CRI分析-kubelet删除pod分析 - 良凯尔 - 博客园 在kubernetes的实际生产实践中&#xff0c;经常会看到pod内的容器因为内存使用超限被内…

【My Electronic Notes系列——数字电路基础知识】

目录 序言&#xff1a; &#x1f3c6;&#x1f3c6;人生在世&#xff0c;成功并非易事&#xff0c;他需要破茧而出的决心&#xff0c;他需要永不放弃的信念&#xff0c;他需要水滴石穿的坚持&#xff0c;他需要自强不息的勇气&#xff0c;他需要无畏无惧的凛然。要想成功&…

从战略、管理、业务、产品这4个维度,思考从0到1的产品设计

引言对于B端产品经理而言&#xff0c;从0&#xff5e;1的产品设计考验一个人的规划能力、统筹能力与产品设计能力&#xff0c;与日常产品迭代的方法流程具备差异&#xff0c;如何进行从0&#xff5e;1的产品设计&#xff1f;我针对自身经验梳理总结&#xff0c;希望下面的文章能…

教你简单学git(从零基础到进阶)

前言 什么是Git? 当你刚刚接触到程序开发行业&#xff0c;别人通常都会对你说&#xff1a;git就是用来保存代码项目的&#xff0c;防止代码丢失。 是这个道理没错&#xff0c;但是并不准确。 Git是一个分布式版本管理系统&#xff0c;是为了更好地管理Linux内核开发而创立的…

量子投资狂飙?四家量子计算公司两天获得巨额投资

&#xff08;图片来源&#xff1a;网络&#xff09;1月23至24日&#xff0c;加拿大光量子计算公司Xanadu、法国量子计算初创公司Welinq、法国量子计算公司PASQAL、瑞典查尔姆斯大学先后宣布获得新一轮巨额融资。短短两天之内&#xff0c;这四起融资事件无疑将量子计算领域的投资…

【数据库原理与SQL Server应用】Part03——T-SQL语言

【数据库原理与应用】Part03——T-SQL语言一、SQL语言基本概念1.1 T-SQL语言简介T-SQL语言的特点&#xff1a;核心SQL语言的4个部分&#xff1a;1.2 T-SQL语言的语法约定1.3 标识符常规标识符分隔标识符1.4 常量和变量数据类型常量变量1.5 注释1.6 运算符1.7 函数1.7.1 标量函数…

省市高速服务区智能一体机视频解决方案的设计

一、行业背景 1&#xff09;传统服务模式难以满足出行需求 出行人员对高速公路服务质量的要求越来越高&#xff0c;传统服务模式难以满足人们对美好出行的需求&#xff0c;节假日拥堵、服务低效等问题影响着服务区的管理。 2&#xff09;服务区智能化程度有待提高 很多高速…

激活函数有哪些?分别有哪些优缺点?【Sigmoid、tanh、ReLU、Leaky ReLU、ParametricReLU】

一、激活函数的提出 1. Motivation 通用逼近定理(Universal Approximation Theorem):一个包含单一隐含层的多层神经元网络能够以任何期望的精度近似任何连续函数。 构建神经网络的核心目的是拟合函数,通过前向传播得到拟合函数或者判别函数输出最终的结果,最初的神经网络…

Ubuntu Pro专业版笔记

Ubuntu是一个开源的操作系统&#xff0c;源自于Debian&#xff0c;以桌面和服务器端应用为主。它拥有丰富的软件仓库和社区&#xff0c;提供强大的稳定性和易用性。而Ubuntu Pro是Ubuntu的商业版本&#xff0c;提供额外的技术支持和服务。它专为企业环境设计&#xff0c;提供了…

【redis】1-8 Redis命令行客户端基本使用

本节目录1. 内容简介2. 操作3. Another Redis Desktop Manager查看redis中的数据4. 课程目录1. 内容简介 如何进入docker容器&#xff0c;登录redis命令行工具reids-cli的使用使用Another Redis Desktop Manager查看redis中的数据 2. 操作 上一章&#xff0c;redis使用docke…

目前我国外贸企业生存现状如何?

今天&#xff0c;最新的进出口贸易数据发布。海关总署9日公布的数据显示&#xff0c;今年前4个月&#xff0c;我国进出口总值12.58万亿元&#xff0c;同比增长7.9%。虽然整体增速有所回落&#xff0c;但完全在可控范围内。但是增速下的外贸企业就不那么好做了&#xff0c;很多外…

MAC IP地址扫描监控

OpUtils包括 IP 地址监控工具、流氓检测工具和 MAC 地址解析器&#xff0c;用于日常监控和管理 DNS 名称、IP 和 MAC 地址。地址监控工具用于 IP 监控&#xff0c;用于管理 DNS 名称、网络的 IP 和 MAC 地址&#xff0c;并跟踪 IP 地址。 IP地址监控功能 OpUtils提供了以下实…

flutter pc端 使用grpc双向流

官网 grpc-dart&#xff1a;https://github.com/grpc/grpc-dart proto文件 syntax "proto3";option go_package "./";package helloworld;service RouteGuide {rpc GetFeature(Point) returns (Feature) {}rpc ListFeatures(Rectangle) returns (stre…

C#读写T5557芯片卡复制ID门禁卡源码

T5557卡是美国Atmel公司生产的多功能非接触式射频芯片卡&#xff0c;属于125KHz的低频卡&#xff0c;在国内有广大的应用市场&#xff0c;如很多酒店的门禁卡都是使用T5557卡。该芯片共有330bit(比特)的EPROM(分布为10个区块, 每个区块33bit)。0页的块0是被保留用于设置T5557操…

maven导入本地jar包

有些jar包是自己封装的或者来源公司私服等. 引入本地jar包方式 另外一种方式 包所在路径 cmd 这样jar包就在你仓库本地仓库里 然后导入

JVM-性能监控与调优-JVM运行时参数

JVM参数选项 官网地址&#xff1a;https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html 类型一&#xff1a;标准参数选项 > java -help 用法: java [-options] class [args...](执行类)或 java [-options] -jar jarfile [args...](执行 jar 文件) 其…

这都能第六?

文章目录&#x1f31f; 专栏介绍&#x1f31f; Vue默认版本&#x1f31f; 拥抱Vue3的UI&#x1f31f; Vue3显著优势&#x1f31f; 专栏介绍 凉哥作为 Vue 的忠诚粉丝输出过大量的 Vue 文章&#xff0c;应粉丝要求开始更新 Vue3 的相关技术文章&#xff0c;Vue 框架目前的地位大…

compooser remove移除包受版本约束导致失败

由于某个项目想移除某个扩展包&#xff0c;但一直报版本不兼容错导致移除不了。报错如下图。后面只要在移除包compooser语句后面加 --ignore-platform-reqs即可&#xff0c;命令&#xff1a;composer remove xxxxxx --ignore-platform-reqs。 移除扩展包后&#xff0c;执行php …