服务端架构

总体框架

  1. 处理玩家数据
  2. 存储玩家数据

一个简单的服务器架构图如下

image-20220915153617632

模块划分

  1. 网络层:连接客户端,解析协议
  2. 消息层:属于游戏逻辑层,不同的消息肯定会有不同的逻辑处理
  3. 数据层:访问数据库进行数据存储

image-20220915153922286

JSON的编码和解码

服务端有两个地方会涉及编码和解码,一是处理消息时,和客户端一样需要编码和解码,二是在玩家存储数据时,还需要编码和解码。设计一个名为PlayerData

的数据,里面包含需要存储的信息,如金币等级等,不会直接操作数据库,而是访问这个数据类,在必要的时候直接把这个数据类存储到数据库中

添加协议文件

将客户端的部分脚本直接移植到服务端

image-20220915162502006

区别在于服务度不能使用客户端的Json编码器,因为那是Unity提供的工具,服务端肯定是不能直接用的(除非你把DLL拷贝过来)

引用System.web.Extensions

使用.NET提供的JavaScriptJsonSerializer来序列化Json,需要添加DLL

image-20220915164953558

修改MsgBase

将编码和解码都替换为新的DLL提供的方法

namespace Framework
{
    public abstract class MsgBase
    {
        // 用于序列化、反序列JSON对象
        private JavaScriptSerializer serializer = new JavaScriptSerializer();
        
        /// <summary>
        /// 编码
        /// </summary>
        /// <param name="msgBase">协议</param>
        /// <returns></returns>
        public static byte[] Encode(MsgBase msgBase)
        {
            var str = serializer.Serialize(msgBase);
            return System.Text.Encoding.UTF8.GetBytes(str);
        }

        /// <summary>
        /// 解码
        /// </summary>
        /// <param name="protoName">协议名称</param>
        /// <param name="bytes">协议数据</param>
        /// <param name="offset">偏移</param>
        /// <param name="count">长度</param>
        /// <returns></returns>
        public static MsgBase Decode(string protoName, byte[] bytes, int offset, int count)
        {
            var str = System.Text.Encoding.UTF8.GetString(bytes, offset, count);
            return (MsgBase) serializer.Deserialize(str, Type.GetType($"Framework.{protoName},Assembly-CSharp"));
        }
    }
}

网络模块

image-20220916135424887

ClientState

和每一个客户端都需要维护一个Socket连接

namespace Framework
{
    public class ClientState
    {
        public Socket socket; 
        public ByteArray readBuff = new ByteArray(); 
    }
}

开启监听和多路复用

一个用于监听的listenfd

一个管理客户端状态的列表clients

一个用于多路复用的checkRead

namespace Framework
{
    public class NetManager
    {
        //监听Socket
        public static Socket listenfd;

        //客户端Socket及状态信息
        public static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();

        //Select的检查列表
        static List<Socket> checkRead = new List<Socket>();
    }
}

开启服务器的接口如下

namespace Framework
{
    public class NetManager
    {
        /// <summary>
        /// 开启服务器
        /// </summary>
        /// <param name="listenPort"></param>
        public static void StartLoop(string ip,int listenPort)
        {
            // Socket
            listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            // 绑定IP
            IPAddress ipAdr = IPAddress.Parse(ip);
            IPEndPoint ipEp = new IPEndPoint(ipAdr, listenPort);
            listenfd.Bind(ipEp);
            // 开始监听
            listenfd.Listen(0);
            Console.WriteLine("[服务器]启动成功");
            
            //循环
            while (true)
            {
                ResetCheckRead(); //重置checkRead 
                Socket.Select(checkRead, null, null, 1000);
                //检查可读对象
                for (int i = checkRead.Count - 1; i >= 0; i--)
                {
                    Socket s = checkRead[i];
                    if (s == listenfd)
                    {
                        // 处理服务器消息
                    }
                    else
                    {
                        // 不理客户端你消息
                    }
                }

                // 超时处理
            }
        }

        /// <summary>
        /// 重置并填充多路复用列表
        /// </summary>
        private static void ResetCheckRead()
        {
            checkRead.Clear();
            checkRead.Add(listenfd); 
            foreach (var s in clients.Values)
                checkRead.Add(s.socket);
        }
    }
}

处理监听消息

也就是连接请求

namespace Framework
{
    public class NetManager
    {
        /// <summary>
        /// 处理监听消息(客户端请求连接)
        /// </summary>
        /// <param name="listenfd"></param>
        public static void ReadListenfd(Socket listenfd)
        {
            try
            {
                Socket clientfd = listenfd.Accept();
                Console.WriteLine($"[客户端连接]{clientfd.RemoteEndPoint}");
                ClientState state = new ClientState {socket = clientfd};
                clients.Add(clientfd, state);
            }
            catch (SocketException ex)
            {
                Console.WriteLine("Accept fail" + ex.ToString());
            }
        }
    }
}

处理客户端消息

如果缓冲区剩余空间不够那么会先尝试释放

然后开始正式接受数据

namespace Framework
{
    public class NetManager
    {
        /// <summary>
        /// 处理客户端消息
        /// </summary>
        /// <param name="clientfd"></param>
        public static void ReadClientfd(Socket clientfd)
        {
            var state = clients[clientfd];
            var readBuff = state.readBuff;
            
            // 接收
            // 缓冲区不够,清除,若依旧不够,只能返回
            // 缓冲区长度只有1024,单条协议超过缓冲区长度时会发生错误,根据需要调整长度
            int count = 0;
            if (readBuff.Remain <= 0)
            {
                readBuff.MoveBytes();
                if (readBuff.Remain <= 0)
                {
                    Console.WriteLine("接受数据错误,消息长度超过最大上限");
                    Close(state);
                    return;
                }
            }

            try
            {
                count = clientfd.Receive(readBuff.Bytes, readBuff.WriteIndex, readBuff.Remain, 0);
            }
            catch (SocketException ex)
            {
                Console.WriteLine("Receive SocketException " + ex.ToString());
                Close(state);
                return;
            }

            // 客户端关闭
            if (count <= 0)
            {
                Console.WriteLine("Socket Close " + clientfd.RemoteEndPoint.ToString());
                Close(state);
                return;
            }

            // 消息处理
            readBuff.WriteIndex += count;
            //处理二进制消息
            OnReceiveData(state);
            //移动缓冲区
            readBuff.CheckAndMoveBytes();
        }
    }
}

关闭连接

利用反射触发消息,然后关闭客户端连接

/// <summary>
/// 关闭连接
/// </summary>
/// <param name="state"></param>
public static void Close(ClientState state)
{
    // 事件分发
    MethodInfo method = typeof(EventHandler).GetMethod("OnDisconnect");
    object[] ob = {state};
    method.Invoke(null, ob);
    // 关闭
    state.socket.Close();
    clients.Remove(state.socket);
}

处理协议

总体和客户端类似

namespace Framework
{
    public class NetManager
    {
        /// <summary>
        /// 数据处理
        /// </summary>
        /// <param name="state"></param>
        public static void OnReceiveData(ClientState state)
        {
            ByteArray readBuffer = state.readBuff;
            // 消息长度
            if (readBuffer.Length <= 2)
                return;

            // 获取消息体长度
            int readIdx = readBuffer.ReadIndex;
            byte[] bytes = readBuffer.Bytes;
            Int16 bodyLength = (Int16) ((bytes[readIdx + 1] << 8) | bytes[readIdx]);
            if (readBuffer.Length < bodyLength)
                return;
            readBuffer.ReadIndex += 2;
            
            // 解析协议名
            int nameCount = 0;
            string protoName = MsgBase.DecodeName(readBuffer.Bytes, readBuffer.ReadIndex, out nameCount);
            if (protoName == "")
            {
                Console.WriteLine("OnReceiveData MsgBase.DecodeNamefail");
                Close(state);
            }
            readBuffer.ReadIndex += nameCount;
            
            // 解析协议体
            int bodyCount = bodyLength - nameCount;
            MsgBase msgBase = MsgBase.Decode(protoName, readBuffer.Bytes, readBuffer.ReadIndex, bodyCount);
            readBuffer.ReadIndex += bodyCount;
            readBuffer.CheckAndMoveBytes();
            
            // 分发消息
            MethodInfo method = typeof(SysMsgHandler).GetMethod(protoName);
            object[] o = {state, msgBase};
            Console.WriteLine("[Receive]" + protoName);
            if (method != null)
                method.Invoke(null, o);
            else
                Console.WriteLine("OnReceiveData Invoke fail " + protoName);

            // 继续读取消息
            if (readBuffer.Length > 2)
                OnReceiveData(state);
        }
    }
}

Timer

定时器方法

/// <summary>
/// 定时器
/// </summary>
public static void Timer()
{
    // 消息分发
    MethodInfo mei = typeof(EventHandler).GetMethod("OnTimer");
    object[] ob = {};
    mei.Invoke(null, ob);
}

发送协议

namespace Framework
{
    public class NetManager
    {
        /// <summary>
        /// 发送数据
        /// </summary>
        /// <param name="cs"></param>
        /// <param name="msg"></param>
        public static void Send(ClientState cs, MsgBase msg)
        {
            // 状态判断
            if (cs == null)
                return;
            if (!cs.socket.Connected)
                return;

            // 数据编码
            byte[] nameBytes = MsgBase.EncodeName(msg);
            byte[] bodyBytes = MsgBase.Encode(msg);
            int len = nameBytes.Length + bodyBytes.Length;
            byte[] sendBytes = new byte[2 + len];
            
            // 组装长度
            sendBytes[0] = (byte) (len % 256);
            sendBytes[1] = (byte) (len / 256);
            
            // 组装名字
            Array.Copy(nameBytes, 0, sendBytes, 2, nameBytes.Length);
            
            // 组装消息体
            Array.Copy(bodyBytes, 0, sendBytes, 2 + nameBytes.Length, bodyBytes.Length);
            
            // 为简化代码,不设置回调
            try
            {
                cs.socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, null, null);
            }
            catch (SocketException ex)
            {
                Console.WriteLine("Socket Close on BeginSend" + ex.ToString());
            }
        }
    }
}

测试

添加功能协议

public partial class  MsgHandler
{
    public static void MsgMove(ClientState c, MsgBase msgBase)
    {
        var msg = msgBase as MsgMove;
        msg.x++;
        NetManager.Send(c, msg);
    }
        
    public static void MsgPing(ClientState c, MsgBase msgBase)
    {
        Console.WriteLine("MsgPing");
    }
}

添加事件协议

public static void OnDisconnect(ClientState c)
{
    Console.WriteLine("Event Close");
}

客户端监听

void Start()
{
    NetManager.AddMsgListener("MsgMove",(msg =>
    {
        var move = msg as MsgMove;
        Debug.Log($"Move {move.x} {move.y} {move.z}");
    }));
        
    connect.onClick.AddListener((() =>
    {
        NetManager.Connect("127.0.0.1", 8765);
    }));
        
    move.onClick.AddListener((() =>
    {
        var msg = new MsgMove();
        msg.x = 1;
        msg.y = 2;
        msg.z = 3;

        NetManager.Send(msg);
    }));
}

客户端结果:

image-20220918123012944

服务端结果:

image-20220918123024863

心跳机制

在ClientState中定义一个字段用于记录心跳时间

namespace Framework
{
    public class ClientState
    {
        public long lastPingTime = 0;
    }
}

在NetManager中设置好服务器接受PING的间隔,最好和客户端一致

namespace Framework
{
    public static class NetManager
    {
		private static long pingInterval = 30;
    }
}

时间戳

1970年1月1日至今的秒数

namespace Framework
{
    public static class NetManager
    {
		/// <summary>
        /// 获取时间戳
        /// </summary>
        /// <returns></returns>
        public static long GetTimeStamp() 
        {
            TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);
            return Convert.ToInt64(ts.TotalSeconds);
        }
    }
}

然后在客户端建立连接时,刷新客户端的时间戳

namespace Framework
{
    public static class NetManager
    {
		/// <summary>
        /// 处理监听消息(客户端请求连接)
        /// </summary>
        /// <param name="listenfd"></param>
        public static void ReadListenfd(Socket listenfd)
        {
            ...
            ClientState state = new ClientState {socket = clientfd,lastPingTime = GetTimeStamp()};
        	...
        }
    }
}

回应PING协议

namespace Framework
{
    public partial class MsgHandler
    {
        public static void MsgPing(ClientState c, MsgBase msgBase)
        {
            Console.WriteLine("MsgPing");
            c.lastPingTime = NetManager.GetTimeStamp();
            MsgPong msgPong = new MsgPong();
            NetManager.Send(c, msgPong);
        }
    }
}

超时处理

当服务器很久没有收到消息时,可以认为已经断开连接了,在服务器的定时事件OnTimer中编写心跳处理的机制

不能在遍历的同时删除,所以每次只断开一个连接就return了

namespace Framework
{
    public partial class MsgHandler
    {
        public static void OnTimer()
        {
            CheckPing();
        }
        
        /// <summary>
        /// Ping检查
        /// </summary>
        public static void CheckPing()
        {
            // 现在的时间戳
            long timeNow = NetManager.GetTimeStamp();
            // 遍历,删除
            foreach (ClientState s in NetManager.clients.Values)
            {
                if (timeNow - s.lastPingTime > NetManager.pingInterval * 4)
                {
                    Console.WriteLine("Ping Close " + s.socket.RemoteEndPoint.ToString());
                    NetManager.Close(s);
                    return;
                }
            }
        }

    }
}

测试

让服务器的Interval设置为2,客户端的Interval设置为比较大的一个值比如30

image-20220918122906697

玩家数据结构

ClientState

目前只包含了服务器所需要的一些数据,还需要添加玩家数据

image-20220918192246280

PlayerManager

管理所有Player数据的一个管理器

namespace Framework
{
    public class PlayerManager
    {
        // 玩家列表
        public static Dictionary<string, Player> players = new Dictionary<string, Player>();

        /// <summary>
        /// 玩家是否在线
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public static bool IsOnline(string id)
        {
            return players.ContainsKey(id);
        }

        /// <summary>
        /// 玩家列表
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public static Player GetPlayer(string id)
        {
            if (players.ContainsKey(id))
                return players[id];
            return null;
        }

        /// <summary>
        /// 添加玩家
        /// </summary>
        /// <param name="id"></param>
        /// <param name="player"></param>
        public static void AddPlayer(string id, Player player)
        {
            players.Add(id, player);
        }

        /// <summary>
        /// 移除玩家
        /// </summary>
        /// <param name="id"></param>
        public static void RemovePlayer(string id)
        {
            players.Remove(id);
        }
    }
}

NetManager存储所有客户端状态

PlayerManager存储所有玩家数据

image-20220918194220994

MySql

略(记得装个Navicat可视化)

数据库模块

连接数据库

需要添加引用MySql.Data.dll

namespace Framework
{
    public class DbManager
    {
        public static MySqlConnection mysql;

        //连接mysql数据库
        public static bool Connect(string db, string ip, int port, string user, string pw)
        {
            // 创建MySqlConnection对象
            mysql = new MySqlConnection();
            // 连接参数
            mysql.ConnectionString = $"Database={db};Data Source = {ip};port = {port};User Id = {user};Password = {pw}";
            // 连接
            try
            {
                mysql.Open();
                Console.WriteLine("[数据库]connect success");
                return true;
            }
            catch (Exception e)
            {
                Console.WriteLine("[数据库]connect fail, " + e.Message);
                return false;
            }
        }
    }
}

用Navicat新建一个连接

image-20220918205610562

再新建一个数据库

然后可以测试一下了

namespace Server
{
    class Program
    {   
        private const string DB = "playerdata";
        private const string DB_IP = "127.0.0.1";
        private const int DB_PORT = 3306;
        private const string DB_USER = "root";
        private const string DB_PW = "415753";

        static void Main(string[] args)
        {
            DbManager.Connect(DB, DB_IP, DB_PORT, DB_USER, DB_PW);
        }
    }
}

image-20220918210500710

防止依赖注入

简单来说就是防止玩家名称和SQL语句一致

加一个函数进行合法性检查

namespace Framework
{
    public class DbManager
    {
        /// <summary>
        /// 合法性检查
        /// </summary>
        /// <param name="str"></param>
        /// <returns></returns>
        public static bool IsSafeString(string str)
        {
            return !Regex.IsMatch(str, @"[-|;|,|\/|\(|\)|\[|\]|\}|\{|%|@|\*|!|\']");
        }
    }
}

IsAccountExist

账号是否存在的判断

namespace Framework
{
    public class DbManager
    {
        /// <summary>
        /// 账号是否存在
        /// </summary>
        /// <param name="id">张海ID</param>
        /// <returns></returns>
        public static bool IsAccountExist(string id)
        {
            // 防SQL注入
            if (!IsSafeString(id))
                return false;

            // SQL语句
            string s = $"select * from account where account = '{id}';";
            
            // 查询
            try
            {
                var cmd = new MySqlCommand(s, mysql);
                var dataReader = cmd.ExecuteReader();
                var hasRows = dataReader.HasRows;
                dataReader.Close();
                return !hasRows;
            }
            catch (Exception e)
            {
                Console.WriteLine("[数据库]IsSafeString err, " + e.Message);
                return false;
            }
        }
    }
}

账号注册

namespace Framework
{
    public class DbManager
    {
       	/// <summary>
        /// 注册账号
        /// </summary>
        /// <param name="account"></param>
        /// <param name="pw"></param>
        /// <returns></returns>
        public static bool Register(string account, string pw)
        {
            // 防SQL注入
            if (!IsSafeString(account))
            {
                Console.WriteLine("[数据库]Register fail, id not safe");
                return false;
            }

            if (!IsSafeString(pw))
            {
                Console.WriteLine("[数据库]Register fail, pw not safe");
                return false;
            }

            // 能否注册
            if (!IsAccountExist(account))
            {
                Console.WriteLine("[数据库]Register fail, id exist");
                return false;
            }

            // 写入数据库User表
            string sql = $"insert into account set account= '{account}',password= '{pw}'";
            try
            {
                MySqlCommand cmd = new MySqlCommand(sql, mysql);
                cmd.ExecuteNonQuery();
                return true;
            }
            catch (Exception e)
            {
                Console.WriteLine("[数据库]Register fail " + e.Message);
                return false;
            }
        }
    }
}

用Navicat新建一张表

image-20220918212007899

测试一下

namespace Server
{
    class Program
    {
        private const string DB = "playerdata";
        private const string DB_IP = "127.0.0.1";
        private const int DB_PORT = 3306;
        private const string DB_USER = "root";
        private const string DB_PW = "415753";

        static void Main(string[] args)
        {
            if (DbManager.Connect(DB, DB_IP, DB_PORT, DB_USER, DB_PW))
            {
                DbManager.Register("415753928", "415753");
            }
        }
    }
}

image-20220918212137509

CheckPassword

登录时服务端程序需要检测玩家输入的用户名和密码是否正确

namespace Framework
{
    public partial class DbManager
    {
        /// <summary>
        /// 账号验证
        /// </summary>
        /// <param name="account"></param>
        /// <param name="pw"></param>
        /// <returns></returns>
        public static bool CheckPassword(string account, string pw)
        {
            if (!IsSafeString(account))
            {
                Console.WriteLine("[数据库]CheckPassword Err,非法账号");
                return false;
            }

            if (!IsSafeString(pw))
            {
                Console.WriteLine("[数据库]CheckPassword Err,非法密码");
                return false;
            }

            // 查询
            var sql = $"select * from account where account = '{account}' and pw = '{pw}';"; 
            try
            {
                var cmd = new MySqlCommand(sql, mysql);
                var dataReader = cmd.ExecuteReader();
                bool hasRows = dataReader.HasRows;
                dataReader.Close();
                return hasRows;
            }
            catch (Exception e)
            {
                Console.WriteLine("[数据库]CheckPassword Err:" + e.Message);
                return false;
            }
        }
    }
}

记事本

一个简单的记事本程序,拥有注册、登录、在线记事本,三个功能

  • MsgRegister 注册协议
  • MsgLogin 登录协议
  • MsgGetText 获取文本协议
  • MsgSaveText 修改文本协议

登录注册协议

namespace Framework
{
    /// <summary>
    /// 注册协议
    /// </summary>
    public class MsgRegister : MsgBase
    {
        public override string protoName => "MsgRegister";

        // 结果
        public int result;
        // 账号
        public string account;
        // 密码
        public string password;
    }

    /// <summary>
    /// 登录协议
    /// </summary>
    public class MsgLogin : MsgBase
    {
        public override string protoName => "MsgLogin";

        // 结果
        public int result = 0;
        // 账号
        public string account = "";
        // 密码
        public string password = "";
    }

    /// <summary>
    /// 强制下线
    /// </summary>
    public class MsgKick : MsgBase
    {
        public override string protoName => "MsgKick";
        // 结果
        public int reason = 0;
    }
}

记事本协议

namespace Framework
{
    /// <summary>
    /// 获取记事本内容
    /// </summary>
    public class MsgGetText : MsgBase
    {
        public override string protoName => "MsgGetText";
        public int result;
        // 服务端回
        public string text = "";
        // 账号
        public string account;
    }

    /// <summary>
    /// 保存记事本内容
    /// </summary>
    public class MsgSaveText : MsgBase
    {
        public override string protoName => "MsgSaveText";

        // 结果
        public int result = 0;
        // 账号
        public string account;
        // 客户端发
        public string text = "";
    }
}

注册功能

namespace Framework
{
    public partial class MsgHandler
    {
        public static void MsgRegister(ClientState c, MsgBase msgBase)
        {
            var msg = msgBase as MsgRegister;
            if (msg == null) 
                return;
            // 注册成功
            msg.result = DbManager.Register(msg.account, msg.password) ? 1 : 0;
            NetManager.Send(c, msg);
        }
    }
}

登录功能

namespace Framework
{
    public partial class MsgHandler
    {
        public static void MsgLogin(ClientState c, MsgBase msgBase)
        {
            var msg = msgBase as MsgLogin;
            if (msg == null)
                return;
            
            // 密码校验
            if (!DbManager.CheckPassword(msg.account, msg.password))
            {
                msg.result = 1;
                NetManager.Send(c, msg);
                return;
            }

            // 不允许再次登录
            if (c.isLogin)
            {
                msg.result = 2;
                NetManager.Send(c, msg);
                return;
            }
            
            //返回协议
            msg.result = 0;
            NetManager.Send(c, msg);
        }
    }
}

获取文本功能

新建一张表

image-20220919184113689

namespace Framework
{
    public partial class MsgHandler
    {
        public static void MsgGetText(ClientState c, MsgBase msgBase)
        {
            var msg = (MsgGetText) msgBase;
            if (msg == null) 
                return;
            if (!c.isLogin) 
                return;
            // 获取text
            DbManager.GetNotePad(msg);
            NetManager.Send(c, msg);
        }
    }
}

数据库的对应方法

using System;
using MySql.Data.MySqlClient;

namespace Framework
{
    public partial class  DbManager
    {
        public static void GetNotePad(MsgGetText msg)
        {
            msg.text = "";
            // 防SQL注入
            if (!IsSafeString(msg.account))
            {
                msg.result = 1;
                return;
            }
            // 查询
            try
            {
                var cmd = mysql.CreateCommand();
                cmd.CommandText = $"select * from notepad where account = '{msg.account}';";
                using (var read = cmd.ExecuteReader())
                {
                    if (!read.HasRows)
                    {
                        msg.result = 2;
                    }
                    else
                    {
                        read.Read();
                        msg.result = 0;
                        msg.text = read.GetString("text");
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("[数据库]语法错误:" + e.Message);
            }
        }
    }
}

保存文本功能

namespace Framework
{
    public partial class MsgHandler
    {
        public static void MsgSaveText(ClientState c, MsgBase msgBase)
        {
            MsgSaveText msg = (MsgSaveText)msgBase;
            if(msg == null) 
                return;
            // 获取text
            DbManager.SaveNotePad(msg);
            NetManager.Send(c, msg);
        }
    }
}

数据库对应操作

using System;
using MySql.Data.MySqlClient;

namespace Framework
{
    public partial class  DbManager
    {
	    public static void SaveNotePad(MsgSaveText msg)
        {
            // 防SQL注入
            if (!IsSafeString(msg.account))
            {
                msg.result = 1;
                return;
            }
            
            // 查询
            try
            {
                var cmd = new MySqlCommand($"select * from notepad where account = '{msg.account}';", mysql);
                using (var read = cmd.ExecuteReader())
                {
                    // 不存在那么新增
                    if (!read.HasRows)
                    {
                        var add = 
                            new MySqlCommand($"insert into notepad set account = '{msg.account}',text ='{msg.text}';", mysql);
                        add.ExecuteNonQuery();
                    }
                    // 存在那么修改
                    else
                    {
                        var update = 
                            new MySqlCommand($"update notepad set text = '{msg.text}' where account = '{msg.account}';", mysql);
                        update.ExecuteNonQuery();
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine("[数据库]语法错误:" + e.Message);
            }
        }
    }
}

测试一下

基本框架如下

public class Test : MonoBehaviour
{
    public InputField account;
    public InputField password;
    public Button login;
    public Button register;
    public InputField text;
    public Button get;
    public Button save;
    
    void Start()
    {
        NetManager.Connect("127.0.0.1", 8765);
    }

    private void Update()
    {
        NetManager.Update();
    }
}

image-20220919193645013

登录

public class Test : MonoBehaviour
{
    void Start()
    {
        ...

        login.onClick.AddListener((() =>
        {
            if (string.IsNullOrEmpty(account.text) || string.IsNullOrEmpty(password.text)) 
                return;
            var msg = new MsgLogin() {account = account.text, password = password.text};
            NetManager.Send(msg);
        }));
        
        NetManager.AddMsgListener("MsgLogin",(msgBase =>
        {
            var msg = msgBase as MsgLogin;
            if (msg == null) 
                return;
            switch (msg.result)
            {
                case 0:
                    accountText = msg.account;
                    break;
                case 1:
                    Debug.Log("账号或密码错误");
                    break;
                case 2:
                    Debug.Log("不允许重复登录");
                    break;
            }
        }));
    }
}

注册

public class Test : MonoBehaviour
{
    void Start()
    {
        ...

        register.onClick.AddListener((() =>
        {
            if (string.IsNullOrEmpty(account.text) || string.IsNullOrEmpty(password.text)) 
                return;
            var msg = new MsgRegister() {account = account.text, password = password.text};
            NetManager.Send(msg);
        }));
        
        NetManager.AddMsgListener("MsgRegister",(msgBase =>
        {
            var msg = msgBase as MsgRegister;
            switch (msg.result)
            {
                case 1:
                    Debug.Log("注册失败");
                    break;
            }
        }));
    }
}

获取

public class Test : MonoBehaviour
{
    void Start()
    {
        ...

        get.onClick.AddListener((() =>
        {
            var msg = new MsgGetText() {account = accountText};
            NetManager.Send(msg);
        }));

        NetManager.AddMsgListener("MsgGetText", (msgBase) =>
        {
            var msg = msgBase as MsgGetText;
            switch (msg.result)
            {
                case 0:
                    text.text = msg.text;
                    break;
                case 1:
                    Debug.Log("账号非法");
                    break;
                case 2:
                    Debug.Log("没有数据");
                    break;
                case 3:
                    Debug.Log("没有登录");
                    break;
            }
        });
    }
}

保存

public class Test : MonoBehaviour
{
    void Start()
    {
        ...

         save.onClick.AddListener((() =>
        {
            if (string.IsNullOrEmpty(text.text))
                return;
            var msg = new MsgSaveText() {account = accountText,text = text.text};
            NetManager.Send(msg);
        }));

        NetManager.AddMsgListener("MsgSaveText", (msgBase) =>
        {
            var msg = msgBase as MsgSaveText;
            switch (msg.result)
            {
                case 1:
                    Debug.Log("非法账号");
                    break;
                case 3:
                    Debug.Log("没有登录");
                    break;
            }
        });
    }
}

image-20220919200910242