《Unity3D网络游戏实战》第7章

  • 服务端架构
    • 总体架构
    • 模块划分
    • 游戏流程
  • Json编码解码
    • 添加协议文件
    • 引用System.web.Extensions
    • 修改MsgBase类
    • 测试
  • 网络模块
    • 整体结构
    • ClientState
    • 开启监听和多路复用
    • 处理监听消息
    • 处理客户端消息
    • 关闭连接
    • 处理协议
    • Timer
    • 发送协议
    • 测试
  • 心跳机制
    • lastPingTime
    • 时间戳
    • 回应MsgPing协议
    • 超时处理
    • 测试程序
  • 玩家的数据结构
    • 完整的ClientState
    • PlayerData
    • Player
    • PlayerManager
  • 配置MySQL数据库
    • 安装和启动MySQL数据库
    • 安装Navicat for MySQL
    • 配置数据库
    • 安装connector
    • MySQL基础知识
  • 数据库模块
    • 连接数据库
    • 防止SQL注入
    • IsAccountExist
    • Register
    • CreatePlayer
    • CheckPassword
    • GetPlayerData
    • UpdatePlayerData
  • 登录注册功能
    • 注册登录协议
    • 记事本协议
    • 注册功能
    • 登录功能
    • 退出功能
    • 获取文本功能
    • 保存文本功能
  • 完整代码

服务端架构

总体架构

单进程服务端结构。

  • 处理客户端的消息
    客户端与服务端通过TCP连接并传递数据。
  • 存储玩家数据
    MySQL数据库保存玩家数据。

模块划分

  • 网络底层
    处理网络连接的底层模块,有粘包半包、协议解析等功能。
  1. 消息处理
    游戏逻辑层,比如收到MsgMove协议,服务端会记录玩家坐标,然后广播。
  2. 事件处理
    玩家上线和下线等。上线,初始化;下线,数据记录。
  • 数据库底层
    提供保存玩家数据、读取玩家数据、注册、检验用户名密码等功能,封装服务端和数据库的交互。
  1. 存储结构
    指定保存数据,比如金币、等级、经验、文本。

游戏流程

  • 连接阶段
    客户端调用Connect连接服务端。连通后,客户端发送登录协议(含用户名、密码等信息),检验通过后,服务端从数据库获取该角色数据,登录成功。
  • 交互阶段
    双端互通协议,MsgMove、MsgAttack等。
  • 登出阶段
    玩家下线,服务端保存数据。定时保存玩家数据(每隔几分钟),相对安全,能够挽回部分突然挂掉在线玩家数据,频繁写数据库,性能较差;玩家下线时保存数据。
状态 说明
连接但未登录 Connect连接,角色未关联,需输入账号密码,服务端验证后从数据库读取角色数据并关联
登录成功 连接和角色关联后,玩家可操作游戏角色,如移动、攻击等

Json编码解码

类的序列化。

  • 服务端与客户端交互需要编码和解码Json协议。
  • PlayerData(存储玩家数据,金币、等级等)类对象需要需要序列化为Json字符串存入数据库。

添加协议文件

net网络模块,proto协议文件。

引用System.web.Extensions

修改MsgBase类

using System;
using System.Linq;
using System.Web.Script.Serialization;public class MsgBase {public string protoName = "null";//编码器static JavaScriptSerializer Js = new JavaScriptSerializer();//编码public static byte[] Encode(MsgBase msgBase){string s = Js.Serialize(msgBase); return System.Text.Encoding.UTF8.GetBytes(s);}//解码public static MsgBase Decode(string protoName, byte[] bytes, int offset, int count){string s = System.Text.Encoding.UTF8.GetString(bytes, offset, count);MsgBase msgBase = (MsgBase)Js.Deserialize(s, Type.GetType(protoName));return msgBase;}//编码协议名(2字节长度+字符串)public static byte[] EncodeName(MsgBase msgBase){//名字bytes和长度byte[] nameBytes = System.Text.Encoding.UTF8.GetBytes(msgBase.protoName);Int16 len = (Int16)nameBytes.Length;//申请bytes数值byte[] bytes = new byte[2+len];//组装2字节的长度信息bytes[0] = (byte)(len%256);bytes[1] = (byte)(len/256);//组装名字bytesArray.Copy(nameBytes, 0, bytes, 2, len);return bytes;}//解码协议名(2字节长度+字符串)public static string DecodeName(byte[] bytes, int offset, out int count){count = 0;//必须大于2字节if(offset + 2 > bytes.Length){return "";}//读取长度Int16 len = (Int16)((bytes[offset+1] << 8 )| bytes[offset] );//长度必须足够if(offset + 2 + len > bytes.Length){return "";}//解析count = 2+len;string name = System.Text.Encoding.UTF8.GetString(bytes, offset+2, len);return name;}
}

测试

using System;class Test {public static void Main(string[] args) {MsgMove msgMove = new MsgMove();msgMove.x = 100;msgMove.y = -20;byte[] bytes = MsgBase.Encode(msgMove);string s = System.Text.Encoding.UTF8.GetString(bytes);Console.WriteLine(s);s = "{\"protoName\":\"MsgMove\",\"x\":100,\"y\":-20,\"z\":0}";bytes = System.Text.Encoding.UTF8.GetBytes(s);msgMove = (MsgMove) MsgBase.Decode("MsgMove", bytes, 0, bytes.length);Console.WriteLine(msgMove.x);Console.WriteLine(msgMove.y);Console.WriteLine(msgMove.z);}
}

网络模块

整体结构

  1. 网络管理器NetManager,处理select多路复用。
  2. ClientState类,定义客户端信息。
  3. MsgHandler类,处理网络消息,根据消息类型,分拆到多个文件中(BattleMsgHandler.cs处理战斗相关协议,SysMsgHandler处理MsgPing,MsgPong等系统协议)。
  4. 事件处理类EventHandler。

程序引入玩家列表,玩家登录后clientState与player对象关联。通过clientState是否持有player对象判断客户端状态。

logic,代表游戏逻辑部分。

ClientState

客户端信息,一个客户端连接对应一个ClientState对象。

using System.Net.Sockets;public class ClientState
{public Socket socket; public ByteArray readBuff = new ByteArray(); //Pingpublic long lastPingTime = 0;//玩家public Player player;
}

开启监听和多路复用

using System;
using System.Net;
using System.Net.Sockets;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;class NetManager {//监听Socketpublic static Socket listenfd;//客户端Socket及状态信息public static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();//Select的检查列表static List<Socket> checkRead = new List<Socket>();//ping间隔public static long pingInterval = 30;
}
public static void StartLoop(int listenPort) {//Socketlistenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);//BindIPAddress ipAdr = IPAddress.Parse("0.0.0.0");IPEndPoint ipEp = new IPEndPoint(ipAdr, listenPort);listenfd.Bind(ipEp);//Listenlistenfd.Listen(0);Console.WriteLine("[服务器]启动成功");//循环while(true){ResetCheckRead();  //重置checkReadSocket.Select(checkRead, null, null, 1000);//阻塞1秒,直到可读//检查可读对象for(int i = checkRead.Count-1; i>=0; i--){Socket s = checkRead[i];if(s == listenfd){ReadListenfd(s);}else{ReadClientfd(s);}}//超时Timer();}
}
//填充checkRead列表
public static void ResetCheckRead(){checkRead.Clear();checkRead.Add(listenfd); foreach(ClientState s in clients.Values){checkRead.Add(s.socket);}
}

处理监听消息

//读取Listenfd
public static void ReadListenfd(Socket listenfd){try{Socket clientfd = listenfd.Accept();//访问套接字时出错、Socket已经关闭等情形下会抛出异常Console.WriteLine("[Accept]" + clientfd.RemoteEndPoint.ToString());ClientState state = new ClientState();state.socket = clientfd;state.lastPingTime = GetTimeStamp();clients.Add(clientfd, state);}catch(SocketException ex){Console.WriteLine("[Accept fail]" + ex.ToString());}
}

处理客户端消息

//读取Clientfd
public static void ReadClientfd(Socket clientfd){ClientState state = clients[clientfd];ByteArray readBuff = state.readBuff;//接收int count = 0;//缓冲区不够,清除,若依旧不够,只能返回//缓冲区长度只有1024,单条协议超过缓冲区长度时会发生错误,根据需要调整长度if(readBuff.remain <=0){OnReceiveData(state);readBuff.MoveBytes();};if(readBuff.remain <=0){Console.WriteLine("Receive fail , maybe msg length > buff capacity");Close(state);return;}try{count = clientfd.Receive(readBuff.bytes, readBuff.writeIdx, readBuff.remain, 0);}catch(SocketException ex){//发生异常,连接失效,Close。Console.WriteLine("Receive SocketException " + ex.ToString());Close(state);return;}//客户端关闭if(count <= 0){//客户端主动断开连接,服务端收到长度为0数据,Close。Console.WriteLine("Socket Close " + clientfd.RemoteEndPoint.ToString());Close(state);return;}//消息处理readBuff.writeIdx += count;//处理二进制消息OnReceiveData(state);//处理粘包分包问题//移动缓冲区readBuff.CheckAndMoveBytes();
}

关闭连接

  1. 分发OnDisconnect事件,让程序可以在玩家掉线时做些处理;
  2. 调用socket.Close关闭连接;
  3. 将客户端状态state移除clients列表。
//关闭连接
public static void Close(ClientState state){//消息分发MethodInfo mei =  typeof(EventHandler).GetMethod("OnDisconnect");object[] ob = {state};mei.Invoke(null, ob);//关闭state.socket.Close();clients.Remove(state.socket);
}

处理协议

//数据处理
public static void OnReceiveData(ClientState state){ByteArray readBuff = state.readBuff;//消息长度if(readBuff.length <= 2) {return;}Int16 bodyLength = readBuff.ReadInt16();//消息体if(readBuff.length < bodyLength){return;}//解析协议名int nameCount = 0;string protoName = MsgBase.DecodeName(readBuff.bytes, readBuff.readIdx, out nameCount);if(protoName == ""){Console.WriteLine("OnReceiveData MsgBase.DecodeName fail");Close(state);return;}readBuff.readIdx += nameCount;//解析协议体int bodyCount = bodyLength - nameCount;MsgBase msgBase = MsgBase.Decode(protoName, readBuff.bytes, readBuff.readIdx, bodyCount);readBuff.readIdx += bodyCount;readBuff.CheckAndMoveBytes();//分发消息MethodInfo mi =  typeof(MsgHandler).GetMethod(protoName);object[] o = {state, msgBase};Console.WriteLine("[Receive]" + protoName);if(mi != null){mi.Invoke(null, o);}else{Console.WriteLine("OnReceiveData Invoke fail " + protoName);}//继续读取消息if(readBuff.length > 2){OnReceiveData(state);}
}

Timer

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

发送协议

//发送
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()); }
}

测试

  1. 协议处理

BattleMsgHandler.cs

using System;public partial class MsgHandler {public static void MsgMove(ClientState c, MsgBase msgBase){MsgMove msgMove = (MsgMove)msgBase;Console.WriteLine(msgMove.x);msgMove.x++;NetManager.Send(c, msgMove);}
}

SysMsgHandler.cs

using System;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);}
}

partial表明类是局部类型,允许将一个类、结构或接口分成几个部分,分别实现在几个不同的cs文件中。

  1. 事件处理
using System;public partial class EventHandler {public static void OnDisconnect(ClientState c){Console.WriteLine("Close");}public static void OnTimer(){}
}
  1. 启动网络监听
using System;class Server {public static void Main(strin[] args) {NetManager.StartLoop(8888);}
}
  1. 开始测试

客户端发送MsgMove协议。

心跳机制

lastPingTime

using System.Net.Sockets;public class ClientState
{public Socket socket; public ByteArray readBuff = new ByteArray(); //Pingpublic long lastPingTime = 0;//玩家public Player player;
}
class NetManager {public static long pingInterval = 30;
}

时间戳

1970年1月1日零点到现在的秒数。

//获取时间戳
public static long GetTimeStamp() {TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0);return Convert.ToInt64(ts.TotalSeconds);
}

回应MsgPing协议

using System;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);}
}

超时处理

服务端长时间未收到MsgPing时,认为连接已经断开。

public static void OnTimer(){CheckPing();
}//Ping检查
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;}}
}

测试程序

设置服务端pingInterval值为2。

玩家的数据结构

完整的ClientState

using System.Net.Sockets;public class ClientState {public Socket socket; public ByteArray readBuff = new ByteArray(); //Pingpublic long lastPingTime = 0;//玩家public Player player;
}

PlayerData

public class PlayerData{//金币public int coin = 0;//记事本public string text = "new text";
}

Player

using System;public class Player {//idpublic string id = "";//指向ClientStatepublic ClientState state;//构造函数public Player(ClientState state){this.state = state;}//临时数据,如:坐标public int x; public int y; public int z;//数据库数据public PlayerData data;//发送信息public void Send(MsgBase msgBase){NetManager.Send(state, msgBase);}
}

PlayerManager

using System;
using System.Collections.Generic;public class PlayerManager {//玩家列表static Dictionary<string, Player> players = new Dictionary<string, Player>();//玩家是否在线public static bool IsOnline(string id){return players.ContainsKey(id);}//获取玩家public static Player GetPlayer(string id){return players[id];}//添加玩家public static void AddPlayer(string id, Player player){players.Add(id, player);}//删除玩家public static void RemovePlayer(string id){players.Remove(id);}
}

配置MySQL数据库

  • 安装和启动MySQL服务器,让它监听某个端口;
  • 使用库编码和解码MySQL特定形式的协议。

安装和启动MySQL数据库

xampp安装包安装,xmapp-control.exe启动,Start按钮开启MySQL服务。默认数据库端口3306,用户root,密码空。

安装Navicat for MySQL

专为MySQL数据库服务的管理工具。

新建连接,连接名:127.0.0.1;
主机名或IP地址:127.0.0.1;
端口:3306;
用户名:root;
密码:

配置数据库

新建game库,包含account和player表。
account含id(账号,text)和pw(密码,text)。
player含id和data(数据,text)。

安装connector

引用MySql.Data.dll

MySQL基础知识

MySQL数据类型 子类
数字类型 整数:tinyint、smallint、mediumint、int、bigint
浮点数:float、double、real、decimal
日期和时间 date、time、datetime、timestamp、year
字符串 char、varchar
文本 tinytext、text、mediumtext、longtext
二进制 tinyblob、blob、mediumblob、longblob
MySQL语句 说明
select 查询数据
select 列名称 from 表名称 [查询条件];
select * from msg where name = “小明”;
insert 插入数据
insert [into] 表名 [(列名1,列名2,列名3,…)] values(值1,值2,值3,…);
insert into msg values(1, “小明”, “你好”);
insert into students(“name”, “msg”) values(“小红”, “Love”);
update 更新数据
update 表名称 set 列名称 = 新值 where 更新条件;
update msg set msg = “ha” where id = 123;
delete 删除数据
delete from 表名称 where 删除条件;
delete from msg where id = 123;

操作MySQL流程:

  • 连接MySQL(数据库地址、端口、用户名、密码)
  • 选择数据库
  • 执行SQL语句
  • 关闭数据库

数据库模块

连接数据库

DbManager.cs

using System;
using MySql.Data.MySqlClient;
using System.Text.RegularExpressions;
using System.Web.Script.Serialization;public class DbManager {public static MySqlConnection mysql;static JavaScriptSerializer Js = new JavaScriptSerializer();//连接mysql数据库public static bool Connect(string db, string ip, int port, string user, string pw) {//创建MySqlConnection对象mysql = new MySqlConnection();//连接参数string s = string.Format("Database={0};Data Source={1}; port={2};User Id={3}; Password={4}", db, ip, port, user, pw);mysql.ConnectionString = s;//连接try{mysql.Open();Console.WriteLine("[数据库]connect succ ");return true;}catch (Exception e){Console.WriteLine("[数据库]connect fail, " + e.Message);return false;}}
}
using System;class Test {public static void Main(string[] args) {DbManager.Connect("game", "127.0.0.1", 3306, "root", "");}
}
csc DbManager.cs Test.cs -reference:MySql.Data.dll

防止SQL注入

SQL注入,指通过输入请求,把SQL命令插入到SQL语句中,以达到欺骗服务器执行恶意SQL命令的目的。

string sql = "select * from player where id = " + id;

id = "xiaoming; delete * from player;";

sql = "select * from player where id = xiaoming; delete * from player;";

在拼装SQL语句前,对用户输入的字符串进行安全性检测(含有逗号、分号等特殊字符的字符串判定为不安全字符串),能够有效地防止SQL注入。

using System.Text.RegularExpressions;//判定安全字符串
private static bool IsSafeString(string str) {return !Regex.IsMatch(str, @"[-|;|,|\/|\(|\)|\[|\]|\}|\{|%|@|\*|!|\']");
}

IsAccountExist

//是否存在该用户
public static bool IsAccountExist(string id) {//防sql注入if (!DbManager.IsSafeString(id)){return false;}//sql语句string s = string.Format("select * from account where id='{0}';", id);  //查询try {MySqlCommand cmd = new MySqlCommand(s, mysql); MySqlDataReader dataReader = cmd.ExecuteReader(); bool hasRows = dataReader.HasRows;dataReader.Close();return !hasRows;}catch(Exception e){Console.WriteLine("[数据库] IsSafeString err, " + e.Message);return false;}
}

Register

//注册
public static bool Register(string id, string pw) {//防sql注入if(!DbManager.IsSafeString(id)){Console.WriteLine("[数据库] Register fail, id not safe");return false;}if(!DbManager.IsSafeString(pw)){Console.WriteLine("[数据库] Register fail, pw not safe");return false;}//能否注册if(!IsAccountExist(id)) {Console.WriteLine("[数据库] Register fail, id exist");return false;}//写入数据库User表string sql = string.Format("insert into account set id ='{0}' ,pw ='{1}';", id, pw);try{MySqlCommand cmd = new MySqlCommand(sql, mysql);cmd.ExecuteNonQuery();return true;}catch(Exception e) {Console.WriteLine("[数据库] Register fail " + e.Message);return false;}
}

明文密码应该加密(md5加密等)存入数据库。

测试

using System;class Test {public static void Main(string[] args) {if(!DbManager.Connect("game", "127.0.0.1", 3306, "root", "")) {return;}//测试if(DbManager.Register("lpy", "123456")){Console.WriteLine("注册成功");}}
}

CreatePlayer

//创建角色
public static bool CreatePlayer(string id){//防sql注入if(!DbManager.IsSafeString(id)){Console.WriteLine("[数据库] CreatePlayer fail, id not safe");return false;}//序列化PlayerData playerData = new PlayerData();string data = Js.Serialize(playerData); //写入数据库string sql = string.Format("insert into player set id ='{0}' ,data ='{1}';", id, data);try {MySqlCommand cmd = new MySqlCommand(sql, mysql);cmd.ExecuteNonQuery();return true;} catch(Exception e){Console.WriteLine("[数据库] CreatePlayer err, " + e.Message);return false;}
}

测试

using System;class Test {public static void Main(string[] args) {if(!DbManager.Connect("game", "127.0.0.1", 3306, "root", "")) {return;}//注册if(DbManager.Register("lpy", "123456")){Console.WriteLine("注册成功");}//测试if(DbManager.CreatePlayer("aglab")){Console.WriteLine("创建成功");}}
}

CheckPassword

//检测用户名密码
public static bool CheckPassword(string id, string pw) {//防sql注入if(!DbManager.IsSafeString(id)){Console.WriteLine("[数据库] CheckPassword fail, id not safe");return false;}if(!DbManager.IsSafeString(pw)){Console.WriteLine("[数据库] CheckPassword fail, pw not safe");return false;}//查询string sql = string.Format("select * from account where id='{0}' and pw='{1}';", id, pw);  try {MySqlCommand cmd = new MySqlCommand(sql, mysql);  MySqlDataReader dataReader = cmd.ExecuteReader();bool hasRows = dataReader.HasRows;dataReader.Close();return hasRows;}catch(Exception e){Console.WriteLine("[数据库] CheckPassword err, " + e.Message);return false;}
}

GetPlayerData

//获取玩家数据
public static PlayerData GetPlayerData(string id){//防sql注入if(!DbManager.IsSafeString(id)){Console.WriteLine("[数据库] GetPlayerData fail, id not safe");return null;}//sqlstring sql = string.Format("select * from player where id ='{0}';", id);try{//查询MySqlCommand cmd = new MySqlCommand(sql, mysql); MySqlDataReader dataReader = cmd.ExecuteReader(); if(!dataReader.HasRows){dataReader.Close();return null;}//读取dataReader.Read();string data = dataReader.GetString("data");//反序列化PlayerData playerData = Js.Deserialize<PlayerData>(data);dataReader.Close();return playerData;}catch(Exception e){Console.WriteLine("[数据库] GetPlayerData fail, " + e.Message);return null;}
}

UpdatePlayerData

//保存角色
public static bool UpdatePlayerData(string id, PlayerData playerData){//序列化string data = Js.Serialize(playerData); //sqlstring sql = string.Format("update player set data='{0}' where id ='{1}';", data, id);//更新try {MySqlCommand cmd = new MySqlCommand(sql, mysql);cmd.ExecuteNonQuery();return true;} catch(Exception e){Console.WriteLine("[数据库] UpdatePlayerData err, " + e.Message);return false;}
}

测试

DbManager.CreatePlayer("aglab");
PlayerData pd = DbManager.GetPlayerData("aglab");
pd.coin = 256;
DbManager.UpdatePlayerData("aglab", pd);

登录注册功能

在线记事本

  • 连接
  • 注册(MsgRegister协议)
  • 登录(MsgLogin协议)
  • 获取文本(MsgGetText协议获取已保存的文本信息)
  • 操作(编辑文本)
  • 保存文本(MsgSaveText协议更新文本信息)
  • 退出

注册登录协议

LoginMsg.cs

//注册
public class MsgRegister:MsgBase {public MsgRegister() {protoName = "MsgRegister";}//客户端发public string id = "";public string pw = "";//服务端回(0-成功,1-失败)public int result = 0;
}//登陆
public class MsgLogin:MsgBase {public MsgLogin() {protoName = "MsgLogin";}//客户端发public string id = "";public string pw = "";//服务端回(0-成功,1-失败)public int result = 0;
}//踢下线(服务端推送)
public class MsgKick:MsgBase {public MsgKick() {protoName = "MsgKick";}//原因(0-其他人登陆同一账号)public int reason = 0;
}

记事本协议

NotepadMsg.cs

//获取记事本内容
public class MsgGetText:MsgBase {public MsgGetText() {protoName = "MsgGetText";}//服务端回public string text = "";
}//保存记事本内容
public class MsgSaveText:MsgBase {public MsgSaveText() {protoName = "MsgSaveText";}//客户端发public string text = "";//服务端回(0-成功 1-文字太长)public int result = 0;
}

注册功能

//注册协议处理
public static void MsgRegister(ClientState c, MsgBase msgBase){MsgRegister msg = (MsgRegister)msgBase;//注册if(DbManager.Register(msg.id, msg.pw)){DbManager.CreatePlayer(msg.id);msg.result = 0;}else{msg.result = 1;}NetManager.Send(c, msg);
}

登录功能

//登陆协议处理
public static void MsgLogin(ClientState c, MsgBase msgBase){MsgLogin msg = (MsgLogin)msgBase;//密码校验if(!DbManager.CheckPassword(msg.id, msg.pw)){msg.result = 1;NetManager.Send(c, msg);return;}//不允许再次登陆if(c.player != null){msg.result = 1;NetManager.Send(c, msg);return;}//如果已经登陆,踢下线if(PlayerManager.IsOnline(msg.id)){//发送踢下线协议Player other = PlayerManager.GetPlayer(msg.id);MsgKick msgKick = new MsgKick();msgKick.reason = 0;other.Send(msgKick);//断开连接NetManager.Close(other.state);}//获取玩家数据PlayerData playerData = DbManager.GetPlayerData(msg.id);if(playerData == null){msg.result = 1;NetManager.Send(c, msg);return;}//构建PlayerPlayer player = new Player(c);player.id = msg.id;player.data = playerData;PlayerManager.AddPlayer(msg.id, player);c.player = player;//返回协议msg.result = 0;player.Send(msg);
}

退出功能

public static void OnDisconnect(ClientState c){Console.WriteLine("Close");//Player下线if(c.player != null){//保存数据DbManager.UpdatePlayerData(c.player.id, c.player.data);//移除PlayerManager.RemovePlayer(c.player.id);}
}

获取文本功能

//获取记事本内容
public static void MsgGetText(ClientState c, MsgBase msgBase){MsgGetText msg = (MsgGetText)msgBase;Player player = c.player;if(player == null) return;//获取textmsg.text = player.data.text;player.Send(msg);
}

保存文本功能

//保存记事本内容
public static void MsgSaveText(ClientState c, MsgBase msgBase){MsgSaveText msg = (MsgSaveText)msgBase;Player player = c.player;if(player == null) return;//获取textplayer.data.text = msg.text;player.Send(msg);
}

完整代码

《Unity3D网络游戏实战》第7章相关推荐

  1. 【实战】Unity3d实战之Unity3d网络游戏实战篇(11):消息分发

    Unity3d实战之Unity3d网络游戏实战篇(11):消息分发 学习书籍<Unity3d网络游戏实战> 罗培羽著 机械工业出版社 本文是作者在学习过程中遇到的认为值得记录的点,因此引用 ...

  2. 【实战】Unity3d实战之Unity3d网络游戏实战篇(9):协议

    Unity3d实战之Unity3d网络游戏实战篇(9):协议 学习书籍<Unity3d网络游戏实战> 罗培羽著 机械工业出版社 本文是作者在学习过程中遇到的认为值得记录的点,因此引用的代码 ...

  3. Unity3D网络游戏实战——实践出真知:大乱斗游戏

    前言 这一章是教我们做一个大乱斗游戏.但是书中的代码有些前后不一致导致运行错误,如果你也碰到了这样的情况,可以参考我的代码 我们要完成的主要有以下这些事 左键操控角色行走 右键操控角色攻击 受到攻击掉 ...

  4. Unity3D网络游戏实战——通用服务器框架

    前言 网络游戏涉及客户端和服务端.服务端程序记录玩家数据,处理客户端发来的协议.本文就介绍一套通用客户端的实现. 该框架基于Select多路复用处理网络消息,具有粘包半包处理.心跳机制等功能,还是用M ...

  5. Unity3D网络游戏实战——网络游戏的开端:Echo

    前言 虽然最爱单机游戏,但是和朋友一起玩联网游戏可以获得双倍快乐!所以开始学习网络游戏相关的知识啦 1.1藏在幕后的服务端 客户端和客户端之间通过服务端的消息转发进行通信. 为了支撑很多玩家,游戏服务 ...

  6. Unity3D网络游戏实战——通用客户端模块

    前言 书中说的是搭建一套商业级的客户端网络模块,一次搭建长期使用. 本章主要是完善大乱斗游戏中的网络模块,解决粘包分包.完整发送数据.心跳机制.事件分发等功能 6.1网络模块设计 核心是静态类NetM ...

  7. Unity3D网络游戏实战——正确收发数据流

    前言 本章主要介绍和实现怎样正确和高效地处理TCP数据(数据流).也解决了上一章我们遇到的一些问题 4.1TCP数据流 4.1.1系统缓冲区 收到对端数据时,操作系统会将数据存入到Socket的接收缓 ...

  8. Unity3D 网络游戏框架(一、网络基础)

    1.套接字(Socket)         网络上两个程序通过一个双向的通信连接实现数据交换,这个连接的一端称为一个Socket.一个Socket包含了进行网络通信必须的五种信息:连接使用的协议.本地 ...

  9. Unity3D网络游戏0.1

    一.网络游戏的架构 (1)网络游戏:   分为客户端和服务端两个部分,客户端程序运行在用户的电脑或手机上,服务端程序运行在游戏运营商的服务器上.   以下是一些典型的游戏客户端.            ...

最新文章

  1. SQL Server 文件和文件组
  2. 组合数学题 Codeforces Round #108 (Div. 2) C. Pocket Book
  3. java设计模式-建造者模式
  4. 笔记:Java虚拟机运行时数据区
  5. Java进阶 | 泛型机制与反射原理
  6. 浏览器推送 comet技术
  7. Android调用系统照相机
  8. 【Flink】 Flink 源码之 SQL 执行流程
  9. eclipse中快捷搜索文件快捷键
  10. PyTorch:模型训练和预测
  11. 什么软件测试情侣头像,即刻情侣头像配对器
  12. 通信原理---FPGA---HDB3码编码
  13. 大麦网抢票程序(一)之大麦网网站分析
  14. win10 此电脑 网络位置 怎么删除
  15. 致远互联发布A6+在打什么牌?
  16. 闲谈IPv6-我们在技术思维上需要作出改变(1)
  17. Android实现NFC芯片制造商识别
  18. 机器学习(Machine Learning)深度学习(Deep Learning)资料(Chapter 2)
  19. win2008r2用户账户控制什么意思_养老保险统筹账户是什么意思?有什么用?
  20. mybatis-plus使用this.saveBatch报java.lang.NullPointer空指针异常

热门文章

  1. java程序设计基础笔试题库,智慧职教Java程序设计基础题库及答案
  2. php框架开发的优势,浅谈关于php开发框架的九个优势
  3. mac图形设计软件TurboLayout安装教程
  4. Linux 杀毒软件发现的漏洞可使得黑客获得 root 权限
  5. html5 隐藏摄像头,js控制摄像头拍照 请问html5怎么关闭摄像头?
  6. 木马病毒隐身穿墙术解密之文件注入和反弹连接
  7. javascript eq()用法
  8. GCC源码分析(十) — 函数节点的gimple高端化
  9. dell服务器启动顺序如何设置_Dell PowerEdge 服务器启动指南
  10. 国富论(英语:The Wealth of Nations)