1.客户端实现注册与登录接口

1.在Unity下,创建一个GameManager空对象,用于启动客户端连接以及实例化一个登录页面LoginView的Prefab,并将脚本LoginView挂载在上面。

using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;

public class GameManager : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Client.Instance.Start();

        var loginPrefab = Resources.Load<GameObject>("LoginView");
        //加载Resources/LoginView目录下的一个预制体
        var loginView = GameObject.Instantiate<GameObject>(loginPrefab);
        //实例化这个预制体
        loginView.AddComponent<LoginView>();
        //将LoginView脚本挂载在这个实例化的对象上
    }

    // Update is called once per frame
    void Update()
    {
        if(Input.GetKeyDown(KeyCode.A)) {

            Client.Instance.Send(Encoding.UTF8.GetBytes("login..."));
        }
    }
}

2.创建一个Client类,用于客户端向服务端发起连接请求,并且发送给服务端消息以及接收服务端的响应

using System.Collections;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text;
using UnityEngine;
using LitJson;
using System;

public class Client
{
//将Client类写成单例,方便以后调用
    static Client instance = new Client();
    public static Client Instance => instance;

    TcpClient client;//定义一个TcpClient对象
    // Start is called before the first frame update
    public void Start()
    {
        client = new TcpClient();//创建一个一个TcpClient对象
        Connect();//调用与服务器建立连接的函数
    }

    public async void Connect()
    {
        try
        {
            await client.ConnectAsync("127.0.0.1",7788);//连接IP地址以及端口号
            Debug.Log("TCP 连接成功");
            Receive();//连接成功之后,调用接收服务器响应的函数
        }
        catch (System.Exception e)
        {
            Debug.Log(e.Message);
        }
    }
   
   //接受服务器的消息
    public async void Receive()
    {//处于连接状态
        while(client.Connected)
        {
            try
            {
                
                byte[] buff = new byte[4096];//用于存储从网络接收到的消息。这个数组被称为缓冲区(buffer)
                int length = await client.GetStream().ReadAsync(buff, 0, buff.Length);//读取服务器写入网络流的消息,并返回一个消息的长度
                if (length > 0)//接收的数据长度大于0 ,有数据
                {
                    Debug.Log($"接收到数据了:{length}");
                    MessageHelper.Instance.CopyToData(buff, length);//调用MessageHelper类下的CopyToData函数(将接收到的数据放入另外一个数组)
                    var str = Encoding.UTF8.GetString(buff, 0, length);//将接收的数据转化成Json字符串
                   // Debug.Log(str);
                    //转换成指定的对象
                    //Debug.Log(JsonHelper.ToObject<JsonTest>(Encoding.UTF8.GetString(buff, 0, length)));
                }
                else
                {
                    client.Close();
                }
            }
            catch (System.Exception e)
            {

                Debug.Log(e.Message);
                client.Close();
            }
        }
    }
    //发送消息接收消息不应该阻塞主线程
    public async void Send(byte[] data)
    {
        try
        {
            await client.GetStream().WriteAsync(data, 0, data.Length);
            //将客户端要发送的消息写入网络流
            Debug.Log("发送成功");
        }
        catch (System.Exception e)
        {
        	 Debug.Log(e.Message);
            client.Close();
        }
    }
    
}

3.创建一个脚本LoginView挂载在LoginView对象上,它用于将Unity中的按钮,文本等与相对应的变量绑定,并且当用户点击按钮或者要输入文本时有对应的处理函数,比方说点击注册按钮的时候,会进入注册的面板进行操作;点击登录的时候会将用户输入的账号密码发送给服务端。再而,还要处理登录以及注册的结果,当服务端传过来的结果为0,就调用Tips()函数展现提示。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor.Experimental.RestService;
using UnityEngine;
using UnityEngine.UI;


public class LoginView : MonoBehaviour
{
    GameObject register;
    //注册相关组件
    Button registerBtn;
    InputField usernameInput;
    InputField passwordInput;
    InputField emailInput;
    Button _registerBtn;
    Button _closeBtn;

    //登入相关组件
    Button loginBtn;
    InputField usernameInput_Login;
    InputField passwordInput_Login;
    // Start is called before the first frame update
    void Start()
    {
        register = transform.Find("Register").gameObject;
        //打开注册界面
        registerBtn = transform.Find("RegisterBtn").GetComponent<Button>();
        registerBtn.onClick.AddListener(OpenRegisterView);

        //关闭注册界面
        _closeBtn = transform.Find("Register/View/CloseBtn").GetComponent<Button>();
        _closeBtn.onClick.AddListener(CloseRegisterView);
        //注册界面输入框
        usernameInput = transform.Find("Register/View/UsernameInput").GetComponent<InputField>();
        emailInput = transform.Find("Register/View/EmailInput").GetComponent<InputField>();
        passwordInput = transform.Find("Register/View/PasswordInput").GetComponent<InputField>();

        //注册界面-注册按钮
        _registerBtn transform.Find("Register/View/RegisterBtn").GetComponent<Button>();
        _registerBtn.onClick.AddListener(RegisterOnClick);

        //登录界面-登录按钮
        loginBtn = transform.Find("LoginBtn").GetComponent<Button>();
        loginBtn.onClick.AddListener(LoginOnClick);

        usernameInput_Login = transform.Find("UsernameInput").GetComponent<InputField>();
        passwordInput_Login = transform.Find("PasswordInput").GetComponent<InputField>();

        //处理登录结果
        MessageHelper.Instance.loginHandle += LoginHandle;
        MessageHelper.Instance.rigisterHandle += RegisterHandle;
    }

     private void RegisterHandle(RegisterMsgS2C obj)
    {
        if(obj.result == 0)
        {
            Debug.Log("注册成功");
            Tips("注册成功");
            //注册成功后将注册面板关闭
            register.gameObject.SetActive(false);
            //将服务器传过来的账号和密码赋值给客户端的账号和密码
            usernameInput_Login.text = obj.account;
            passwordInput_Login.text = obj.password;
        }
        else
        {
            Tips($"注册失败,错误码:{obj.result}");
        }
    }

    private void LoginHandle(LoginMsgS2C obj)
    {
        if (obj.result == 0)
        {
            PlayerData.Instance.loginMsgS2C = obj;
            Debug.Log("登录成功");

            //登录成功后将登录面板关闭
            this.gameObject.SetActive(false);
            //打开聊天窗口
            //加载一个预制体
            var chatView = Resources.Load<GameObject>("ChatView");
            //将这个预制体实例化
            var chatObj = GameObject.Instantiate<GameObject>(chatView);
            //将这个组件加到chatObj对象上
            chatObj.AddComponent<ChatView>();   
        }
        else
        {
            Tips($"登录失败,错误码:{obj.result}");
        }
    }

    private void LoginOnClick()
    {
        if (string.IsNullOrEmpty(usernameInput_Login.text))
        {
            Tips("请输入账号!!!!");
            return;
        }
        
        if (string.IsNullOrEmpty(passwordInput_Login.text))
        {
            Tips("请输入密码!!!");
            return;
        }

        //发送数据给服务端
        MessageHelper.Instance.SendLoginMsg(usernameInput_Login.text, passwordInput_Login.text);
    }

    private void RegisterOnClick()
    {
        if(string.IsNullOrEmpty(usernameInput.text))
        {
            Tips("请输入账号!!!");
            return;
        }
        if (string.IsNullOrEmpty(emailInput.text))
        {
            Tips("请输入邮箱!!!");
            return;
        }
        if (string.IsNullOrEmpty(passwordInput.text))
        {
            Tips("请输入密码!!!");
            return;
        }
        //发送数据给服务端
        MessageHelper.Instance.SendRegisterMsg(usernameInput.text,emailInput.text,passwordInput.text);
    }
    private void CloseRegisterView()
    {
        register.gameObject.SetActive(false);
    }
    private void OpenRegisterView()
    {
        register.gameObject.SetActive(true);
    }
    
    // Update is called once per frame
    void Update()
    {
        
    }
    GameObject _tipsObj;
    //提示面板
    public void Tips(string str)
    {
        //如果提示信息存在,先将他销毁
        if(_tipsObj != null)
        {
            GameObject.Destroy( _tipsObj );
        }
        //找到TipsItem组件
        var tipsItem = transform.Find("TipsItem").gameObject;
        //将找到的这个组件实例化
        _tipsObj = GameObject.Instantiate<GameObject>(tipsItem);
        //设置为当前对象(this)的子对象,并保持相对位置不变。
        //当第二个参数为 false 时,会保持_tipsObj对象在其新父对象中的本地位置、旋转和缩放,而不会受到新父对象的影响。
        _tipsObj.transform.SetParent(this.transform, false);
        //找到_tipsObj下的一个text组件
        var content = _tipsObj.transform.Find("Text").GetComponent<Text>();
        //将这个text组件的内容填充数据
        content.text = str;
        //将这个TipsItem组件显示出来
        _tipsObj.gameObject.SetActive(true);
        //过了1.5秒之后将这个_tipsObj给销毁掉
        GameObject.Destroy( _tipsObj ,1.5f);
    }
}

4.创建一个JsonHelper类,用于解析数据以及生成Json字符串,由于在TCP(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层协议,要将字节数组解析成Json字符串,然后再将它反序列化成一个指定的对象,
比如:接受一个对象作为参数,并将其序列化为 JSON 格式的字符串,接受一个 JSON 格式的字符串作为参数,并将其反序列化为指定类型的对象,接受一个字节数组作为参数,假定这个字节数组表示一个 UTF-8 编码的字符串,并将其反序列化为指定类型的对象。

using LitJson;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using UnityEngine;


internal class JsonHelper
{
    //接受一个对象作为参数,并将其序列化为 JSON 格式的字符串。
    public static string ToJson(object x)
    {
        string str = JsonMapper.ToJson(x);
        return str;
    }
    //接受一个 JSON 格式的字符串作为参数,并将其反序列化为指定类型的对象。
    public static T ToObject<T>(string x)
    {
        return JsonMapper.ToObject<T>(x);
    }
    //接受一个字节数组作为参数,假定这个字节数组表示一个 UTF-8 编码的字符串,并将其反序列化为指定类型的对象。
    public static T ToObject<T>(byte[] b)
    {
        string x = Encoding.UTF8.GetString(b, 0, b.Length);
        return ToObject<T>(x);
    }
    public static string GetTestToString()
    {
        JsonTest jsonTest = new JsonTest();
        jsonTest.id = 1;
        jsonTest.name = "jsonTest";
        //先用ToJson将jsonTest序列化为JSON格式的字符串,再用ToObject将jsonTest反序列化为指定类型的对象
        var jsonTestObj = ToObject<JsonTest>(ToJson(jsonTest));
       // Console.WriteLine(jsonTestObj.id);
        Debug.Log($"{jsonTestObj.id} // {jsonTestObj.name}");
        return ToJson(jsonTest);
    }
}
public class JsonTest
{
    public int id;
    public string name;
}

5.创建一个MessageHelper类,用于处理消息,解决粘包问题,将一条消息分为包体大小,消息Id以及包体数据,然后将数据按规定的格式发送给服务端,如果客户端接收到服务端的响应,会触发委托,从而调用相关方法,如注册成功后输出注册成功提示并且关闭注册面板,并且处理服务器返回来的数据比如密码账号等信息。

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor.Experimental.RestService;
using UnityEngine;
using UnityEngine.UI;


public class LoginView : MonoBehaviour
{
    GameObject register;
    //注册相关

    Button registerBtn;
    InputField usernameInput;
    InputField passwordInput;
    InputField emailInput;
    Button _registerBtn;
    Button _closeBtn; 

    //登入相关
    Button loginBtn;
    InputField usernameInput_Login;
    InputField passwordInput_Login;
    // Start is called before the first frame update
    void Start()
    {
        register = transform.Find("Register").gameObject;
        //打开注册界面
        registerBtn = transform.Find("RegisterBtn").GetComponent<Button>();
        registerBtn.onClick.AddListener(OpenRegisterView);

        //关闭注册界面
        _closeBtn = transform.Find("Register/View/CloseBtn").GetComponent<Button>();
        _closeBtn.onClick.AddListener(CloseRegisterView);
        //注册界面输入框
        usernameInput = transform.Find("Register/View/UsernameInput").GetComponent<InputField>();
        emailInput = transform.Find("Register/View/EmailInput").GetComponent<InputField>();
        passwordInput = transform.Find("Register/View/PasswordInput").GetComponent<InputField>();

        //注册界面-注册按钮

        _registerBtn = transform.Find("Register/View/RegisterBtn").GetComponent<Button>();
        _registerBtn.onClick.AddListener(RegisterOnClick);

        //登录界面-登录按钮
        loginBtn = transform.Find("LoginBtn").GetComponent<Button>();
        loginBtn.onClick.AddListener(LoginOnClick);

        usernameInput_Login = transform.Find("UsernameInput").GetComponent<InputField>();
        passwordInput_Login = transform.Find("PasswordInput").GetComponent<InputField>();

        //处理登录结果

        MessageHelper.Instance.loginHandle += LoginHandle;
        MessageHelper.Instance.rigisterHandle += RegisterHandle;
    }

    
	//处理服务端返回来的注册信息
    private void RegisterHandle(RegisterMsgS2C obj)
    {
        if(obj.result == 0)
        {
            Debug.Log("注册成功");
            Tips("注册成功");
            //注册成功后将注册面板关闭
            register.gameObject.SetActive(false);
            //将服务器传过来的账号和密码赋值给客户端的账号和密码
            usernameInput_Login.text = obj.account;
            passwordInput_Login.text = obj.password;
        }
        else
        {
            Tips($"注册失败,错误码:{obj.result}");
        }
    }
	//处理服务器返回来的登录信息
    private void LoginHandle(LoginMsgS2C obj)
    {
        if (obj.result == 0)
        {
            PlayerData.Instance.loginMsgS2C = obj;
            Debug.Log("登录成功");

            //登录成功后将登录面板关闭
            this.gameObject.SetActive(false);
            //打开聊天窗口
            //加载一个预制体
            var chatView = Resources.Load<GameObject>("ChatView");
            //将这个预制体实例化
            var chatObj = GameObject.Instantiate<GameObject>(chatView);
            //将这个组件加到chatObj对象上
            chatObj.AddComponent<ChatView>();   
        }
        else
        {
            Tips($"登录失败,错误码:{obj.result}");
        }
    }
	//处理点击登录事件
    private void LoginOnClick()
    {
        if (string.IsNullOrEmpty(usernameInput_Login.text))
        {
            Tips("请输入账号!!!!");
            return;
        }
        
        if (string.IsNullOrEmpty(passwordInput_Login.text))
        {
            Tips("请输入密码!!!");
            return;
        }

        //发送数据给服务端
        MessageHelper.Instance.SendLoginMsg(usernameInput_Login.text, passwordInput.text);
    }
	//处理注册点击事件
    private void RegisterOnClick()
    {
        if(string.IsNullOrEmpty(usernameInput.text))
        {
            Tips("请输入账号!!!");
            return;
        }
        if (string.IsNullOrEmpty(emailInput.text))
        {
            Tips("请输入邮箱!!!");
            return;
        }
        if (string.IsNullOrEmpty(passwordInput.text))
        {
            Tips("请输入密码!!!");
            return;
        }
        //发送数据给服务端
        MessageHelper.Instance.SendRegisterMsg(usernameInput.text,emailInput.text,passwordInput.text);
    }
    private void CloseRegisterView()
    {
        register.gameObject.SetActive(false);
    }
    private void OpenRegisterView()
    {
        register.gameObject.SetActive(true);
    }
    
    // Update is called once per frame
    void Update()
    {
        
    }
    GameObject _tipsObj;
    //提示面板
    public void Tips(string str)
    {
        //如果提示信息存在,先将他销毁
        if(_tipsObj != null)
        {
            GameObject.Destroy( _tipsObj );
        }
        //找到TipsItem组件
        var tipsItem = transform.Find("TipsItem").gameObject;
        //将找到的这个组件实例化
        _tipsObj = GameObject.Instantiate<GameObject>(tipsItem);
        //设置为当前对象(this)的子对象,并保持相对位置不变。
        //当第二个参数为 false 时,会保持_tipsObj对象在其新父对象中的本地位置、旋转和缩放,而不会受到新父对象的影响。
        _tipsObj.transform.SetParent(this.transform, false);
        //找到_tipsObj下的一个text组件
        var content = _tipsObj.transform.Find("Text").GetComponent<Text>();
        //将这个text组件的内容填充数据
        content.text = str;
        //将这个TipsItem组件显示出来
        _tipsObj.gameObject.SetActive(true);
        //过了1.5秒之后将这个_tipsObj给销毁掉
        GameObject.Destroy( _tipsObj ,1.5f);
    }
}

2.服务端实现注册与登录接口

1.创建一个Client类,用于接收客户端发送来的消息,判断它是什么类型的消息,并且根据消息的Id将响应消息发送给客户端

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace Server.Net
{
    internal class Client
    {
        TcpClient client;//创建TCP连接的客户端的类
        public Client(TcpClient tcpClient) {

            client = tcpClient;

            Receive();//接受客户端发来的消息函数
        }
        byte[] data = new byte[4096];
        int msgLenth = 0;//代表数据的长度
        //接受客户端的消息
        public async void Receive()
        {
            //处于连接状态
            while (client.Connected)
            {
                try
                {
                    byte[] buffer = new byte[4096];
                    //读取客户端发送来的数据
                    int length = await client.GetStream().ReadAsync(buffer, 0, buffer.Length);
                    //它使用一个循环和 ReadAsync 方法从客户端流中读取数据,并将其存储在内存流中。最后,将内存流中的数据转换为字符串进行处理。
                    if (length > 0)//数据有长度的时候
                    {
                        Console.WriteLine($"接受到数据长度:{length}");
                        Console.WriteLine($"接受到数据内容:{Encoding.UTF8.GetString(buffer, 0, length)}");
                        
                        //将收到的数据Copy到data缓存区里面
                        Array.Copy(buffer, 0, data, msgLenth, length);
                        msgLenth += length;//缓冲区的长度
                        // Send(Encoding.UTF8.GetBytes("测试返回..."));
                        Handle();
                    }
                    else
                    {
                        //客户端关闭了
                        client.Close();
                    }

                }
                catch (Exception e)
                {

                    Console.WriteLine($"Receive Error:{e.Message}");
                    //出现错误将客户端给关闭掉
                    client.Close();
                }
            }
        }

        private void Handle()
        {
            //粘包指的是在网络通信中,多个小的数据包被合并成一个大的数据包发送到接收方的现象。
            //包体大小(4),协议ID(4)包体(byte[])
            if (msgLenth >= 8)//接受到的数据的长度
            {
                byte[] _size = new byte[4];//存放包体里里面的数据
                Array.Copy(data, 0, _size, 0, 4);
                //"包体大小为 4 字节"是在描述消息体大小所占用的存储空间的长度。
                int size = BitConverter.ToInt32(_size, 0);//用于将字节数组表示的整数值转换为一个32位有符号整数。
                //_size 数组中存储的字节数据
                //本次要拿的长度
                var _length = 8 + size;//消息的总长度
                if (msgLenth >= _length)
                {
                    //拿出Id
                    byte[] _id = new byte[4];//存放包体里里面的数据
                    Array.Copy(data, 4, _id, 0, 4);
                    //"包体大小为 4 字节"是在描述消息体大小所占用的存储空间的长度。
                    int id = BitConverter.ToInt32(_id, 0);
                    //包体
                    byte[] body = new byte[size];
                    Array.Copy(data, 8, body, 0, size);

                    if (msgLenth>_length)
                    {
                        for (int i = 0; i < msgLenth - _length; i++)
                        {
                            data[i] = data[_length + i];
                        }

                    }
                    msgLenth -= _length;
                    Console.WriteLine($"收到客户端请求:{id}");
                    switch(id)
                    {
                        case 1001://注册请求
                            RigisterMsgHandle(body);
                            break;
                        case 1002://登录请求
                            LoginMsgHandle(body);
                            break;
                        case 1003://聊天业务
                            ChatMsgHandle(body);
                            break;
                    }
                }
            }
        }
        //处理注册请求

        public void RigisterMsgHandle(byte[] obj)
        {
           var msg = JsonHelper.ToObject<RegisterMsgC2S>(obj);//将字节数组转化成RegisterMsgC2S对象

            RegisterMsgS2C response = new RegisterMsgS2C();
            if (PlayerData.Instance.Contain(msg.account)) {
                
                response.result = 1;//已经包含该账号了
            }
            else
            {
                response = PlayerData.Instance.Add(msg);
            }
            SendToClient(1001,JsonHelper.ToJson(response));

        }
        //处理登录请求

        public 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;
                PlayerData.Instance.AddLoginUser(msg.account,this);
            }
            else
            {
                response.result= 1;//账号或者密码错误
            }
            SendToClient(1002,JsonHelper.ToJson(response));
        }
        //处理聊天业务
        public void ChatMsgHandle(byte[] obj)
        {
            //转发给所有在线的用户
            var msg = JsonHelper.ToObject<ChatMsgC2S>(obj);//将消息转化成指定的对象
            ChatMsgS2C sendMsg = new ChatMsgS2C();//
            sendMsg.msg = msg.msg;
            sendMsg.player = msg.player;
            sendMsg.type = msg.type;
            
            var dct = PlayerData.Instance.GetAllLoginUser();//得到所有的登录的用户
            var json = JsonHelper.ToJson(sendMsg);//将消息转化成字符串
            foreach (var item in dct)
            {
                item.Value.SendToClient(1003, json);//将这个消息一一发送给登录的用户
            }
        }

        //发送消息
        public async void Send(byte[] data)
        {
            try
            {
                await client.GetStream().WriteAsync(data, 0, data.Length);
                Console.WriteLine("发送成功");
            }
            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);
            //从 _size 数组中复制了 4 个元素,从索引 0 开始,然后将它们粘贴到 send_buff 数组中,从索引 0 开始。
            Array.Copy(_id, 0, send_buff, 4, 4);
            Array.Copy(body, 0, send_buff, 8, body.Length);
            Send(send_buff);//发送给客户端
        }
    } 

}

2.创建一个TCPServer类,用于监听指定的IP和端口,并且与客户端建立连接,接受连接的请求

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;

namespace Server.Net
{
    internal class TCPServer
    {
        TcpListener tcpListener;
        //启动服务端
        public void Start()
        {
            try
            {
                //创建监听器,这里不指定端口默认主机的IP端口
                tcpListener = TcpListener.Create(7788);
                //启动监听器,参数是最大连接队列长度
                tcpListener.Start(500);
                Console.WriteLine("TCP Server Start");
                //启动服务器后调用客户端连接
                Accpet();//接受客户端传来的消息
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
            
        }
        public Client tempClient;
        public Client client;
        //async改成异步,监听客户端的连接
        public async void Accpet()
        {
            try
            {
                //接受客户端连接
                TcpClient tcpClient = await tcpListener.AcceptTcpClientAsync();
                //将客户端的IP地址打印
                Console.WriteLine("客户端已连接:" + tcpClient.Client.RemoteEndPoint);
                //构建一个Client
                client = new Client(tcpClient);
                tempClient = client;//将连接过来的客户端赋值给tempClient
                //继续接受来自客户端的连接
                Accpet();
            }
            catch (Exception e)
            {
                Console.WriteLine($"Accpet:{e.Message}");

                tcpListener.Stop();//停止监听客户段的连接
            }
           
        }
    }
}

3.创建一个JsonHelper类,作用和客户端的一样,将接受的数据以及发送的消息解析或生成Json字符串,指定的对象等等

using LitJson;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Server
{
    internal class JsonHelper
    {
        //接受一个对象作为参数,并将其序列化为 JSON 格式的字符串。
        public static string ToJson(object x)
        {
            string str = JsonMapper.ToJson(x);
            return str;
        }
        //接受一个 JSON 格式的字符串作为参数,并将其反序列化为指定类型的对象。
        public static T ToObject<T>(string x)
        {
            return JsonMapper.ToObject<T>(x);
        }
        //接受一个字节数组作为参数,假定这个字节数组表示一个 UTF-8 编码的字符串,并将其反序列化为指定类型的对象。
        public static T ToObject<T>(byte[] b)
        {
            string x = Encoding.UTF8.GetString(b,0,b.Length);
            return ToObject<T>(x);
        }
        public static string GetTestToString()
        {
            JsonTest jsonTest = new JsonTest();
            jsonTest.id = 1;
            jsonTest.name = "jsonTest";
            //先用ToJson将jsonTest序列化为JSON格式的字符串,再用ToObject将jsonTest反序列化为指定类型的对象
            var jsonTestObj = ToObject<JsonTest>(ToJson(jsonTest));
            Console.WriteLine(jsonTestObj.id);
            Console.WriteLine($"{ jsonTestObj.id} // { jsonTestObj.name}");
            return ToJson(jsonTest);
        }
    }
    public class JsonTest
    {
        public int id;
        public string name;
    }
}

4.创建一个MessageHelper类,里面存放着各个模块的信息,如注册输入的密码,账号和邮箱,如果是响应给客户端的信息,会定义一个结果变量,判断消息是否正确响应

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
internal class MessageHelper
{

}
public class LoginMsgC2S
{
    public string account;
    public string password;
}
//1002
public class LoginMsgS2C
{
    public string account;
    public string password;
    public int result;//0成功1失败,账号或者密码错误
}
//1001
public class RegisterMsgC2S
{
    public string account;
    public string email;
    public string password;

}
public class RegisterMsgS2C
{
    public string account;
    public string password;
    public string email;
    public int result;//0成功1已被注册账号
}
//1003
public class ChatMsgC2S
{
    public string player;
    public string msg;
    public int type;//0世界聊天
}

public class ChatMsgS2C
{
    public string player;
    public string msg;
    public int type;//0世界聊天
}

6.创建一个PlayerData类,模拟数据库存放注册信息,并且定义字典维护已经登录的用户

using Server.Net;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Server
{
    internal class PlayerData
    {
        public static PlayerData instance = new PlayerData();
        public static PlayerData Instance => instance;

       Dictionary<string,RegisterMsgS2C> userMsg = new Dictionary<string,RegisterMsgS2C>();
        public RegisterMsgS2C Add(RegisterMsgC2S msg)
        {
            var item = new RegisterMsgS2C();
            userMsg[msg.account] = item;//将这个注册的信息存在字典里面
            item.account = msg.account;
            item.password = msg.password;
            item.result = 0;
            return item;
        }
        //判断这个账户是否已经存在
        public bool Contain(string account)
        {
            return userMsg.ContainsKey(account);
        }
        //维护已经登录的用户
        Dictionary<string,Client> LoginUser = new Dictionary<string,Client>();//键是账号account,Client是客户端即用户
        public void AddLoginUser(string account, Client client)
        {
            LoginUser[account] = client;
        }
        //得到所有的用户
        public Dictionary<string,Client> GetAllLoginUser() 
        {
            return LoginUser;
        }
    }
}

7.创建一个主函数,用于启动服务端,读取输入流的一行字符并将该行作为字符串返回

using Server.Net;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Server
{
    internal class Program
    {
        static void Main(string[] args)
        {
            TCPServer tCPServer = new TCPServer();
            //启动服务端
            tCPServer.Start();
            while (true)
            {
                //标准输入流(默认为键盘)中读取下一行字符,并将该行作为字符串返回
                 var str = Console.ReadLine();
                //tCPServer.tempClient.Send(Encoding.UTF8.GetBytes($"测试主动发送数据:{str}"));
                var jsonStr = JsonHelper.GetTestToString();
                // tCPServer.tempClient.Send(Encoding.UTF8.GetBytes(str));
                //tCPServer.client.Send(Encoding.UTF8.GetBytes(str));
                //tCPServer.client.Send(Encoding.UTF8.GetBytes(jsonStr));

            }
        }
    }
}

3.实现实时多人聊天功能

客户端方面思路:

创建一个脚本ChatView.cs,首先在Resources文件夹上加载一个聊天页面的预制体,并且将其实例化,将ChatView脚本挂载在这个对象上,当点击发送按钮之后,触发onClick事件调用SendChatMsg函数,在这个函数当中会检验这个输入框里面是否有文本,有的话,将这个消息以及发送消息的账号发送给服务端,在这个函数中,实例化ChatMsgC2S对象,将账号,聊天文本以及类型赋值给这个对象并将这个对象利用Json转化成字符串,调用SendToServer函数,字符串以及消息ID为参数,通过这个函数,将消息的包体数据解析出来,将这个消息包体发送给服务端。当服务端接收到消息调用Handle函数对消息的数据进行处理,处理完之后根据Id判断它是聊天业务类型的消息,继续处理然后将响应发送给客户端,客户端收到服务端的响应之后,将消息拷贝到新数组,对数据进行处理再用Id判断是处理聊天业务类型的消息,如果返回的消息不为null,就会触发委托绑定的函数ChatHandle,这个函数中调用AddMessage函数,这个函数中,实例化一个文本框,将用户的Id以及接收来的消息赋值到Unity面板中。

1.创建一个ChatView类,绑定Unity要用到的组件,调用AddMessage函数将哪个用户输入的什么信息显示在Unity面板中,运用一个协程,一直将scrollbar.value置为0,保证页面是最新的聊天数据

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

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

    Button sendBtn;
    InputField sendInput;
    // Start is called before the first frame update
    void Start()
    {
        chatItem = Resources.Load<GameObject>("ChatItem");
        chatItemParent = this.transform.Find("ScrollView/Viewport/Content");
        scrollbar = this.transform.Find("ScrollView/Vertical").GetComponent<Scrollbar>();//滚动条,让它一直显示最新的消息

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

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

        MessageHelper.Instance.chatHandle += ChatHandle;//收到服务端那边的聊天数据,进行处理
    }

    private void ChatHandle(ChatMsgS2C obj)
    {
        AddMessage(obj.player, obj.msg);
    }

    //显示消息
    public void AddMessage(string title, string content)
    {
        var go = GameObject.Instantiate<GameObject>(chatItem);
        go.transform.SetParent(chatItemParent, false);
        var titleText = go.transform.Find("Background/Title").GetComponent<Text>();
        titleText.text = title;//将用户的账号赋值到Unity的Text组件

        var chat = go.transform.Find("Background/Text").GetComponent<Text>();
        chat.text = content;

        StartCoroutine(ReSetScrollbar());

    }

    public IEnumerator ReSetScrollbar()
    {
        yield return new WaitForEndOfFrame();
        scrollbar.value = 0;//保证页面是最新的聊天数据
    }

    private void SendChatMsg()
    {
        if(string.IsNullOrEmpty(sendInput.text))
        {
            return;
        }
        MessageHelper.Instance.SendChatMsg(PlayerData.Instance.loginMsgS2C.account, sendInput.text);//发送给服务器,账号也发送给服务器
    }

    // Update is called once per frame
    void Update()
    {
        //if(Input.GetKeyDown(KeyCode.Escape))
        //{
        //    AddMessage("xxx", "yyydydy dyydydy,dydy,.dyd;yddy;");
        //}
    }
}

2.在MessageHelper类当中加上发送给服务器的SendChatMsg函数以及处理服务器响应回来数据的ChatMsgHandle函数

public void SendChatMsg(string account, string chat)
 {
     ChatMsgC2S msg = new ChatMsgC2S();
     msg.player = account;
     msg.msg = chat;
     msg.type = 0;

     var str = JsonHelper.ToJson(msg);
     SendToServer(1003,str);
 }
 public Action<ChatMsgS2C> chatHandle;
 //处理聊天(转发)业务
 public void ChatMsgHandle(byte[] obj)
 {
     var str = Encoding.UTF8.GetString(obj);//将字节转化成字符串
     ChatMsgS2C msg = JsonHelper.ToObject<ChatMsgS2C>(str);//将str反序列化为RegisterMsgS2C对象
     chatHandle?.Invoke(msg);//rigisterHandle 不为 null,就会调用带有参数 msg 的委托
 }

4.当收到服务端的响应数据之后,会触发委托chatHandle调用ChatHandle函数,将这个消息显示在Unty面板上

MessageHelper.Instance.chatHandle += ChatHandle;//收到服务端那边的聊天数据,进行处理
 private void ChatHandle(ChatMsgS2C obj)
 {
     AddMessage(obj.player, obj.msg);
 }
 public void AddMessage(string title, string content)
{
    var go = GameObject.Instantiate<GameObject>(chatItem);
    go.transform.SetParent(chatItemParent, false);
    var titleText = go.transform.Find("Background/Title").GetComponent<Text>();
    titleText.text = title;//将用户的账号赋值到Unity的Text组件

    var chat = go.transform.Find("Background/Text").GetComponent<Text>();
    chat.text = content;

    StartCoroutine(ReSetScrollbar());

}