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

《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);
		//组装名字bytes
		Array.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(); 
	//Ping
	public 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 {
	//监听Socket
	public 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) {
	//Socket
	listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
	
	//Bind
	IPAddress ipAdr = IPAddress.Parse("0.0.0.0");
	IPEndPoint ipEp = new IPEndPoint(ipAdr, listenPort);
	listenfd.Bind(ipEp);
	
	//Listen
	listenfd.Listen(0);
	
	Console.WriteLine("[服务器]启动成功");
	//循环
	while(true){
		ResetCheckRead();  //重置checkRead
		Socket.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(); 
	//Ping
	public 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(); 
	//Ping
	public 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 {
	//id
	public string id = "";
	//指向ClientState
	public 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;
	}

	//sql
	string 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); 
	//sql
	string 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;
	}
	//构建Player
	Player 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;
	//获取text
	msg.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;
	//获取text
	player.data.text = msg.text;
	player.Send(msg);
}

完整代码

你可能感兴趣的:(C#,tcp/ip,整理,unity,3d)