服务端架构
总体架构
单进程服务端结构。
- 处理客户端的消息
客户端与服务端通过TCP连接并传递数据。 - 存储玩家数据
MySQL数据库保存玩家数据。
模块划分
- 网络底层
处理网络连接的底层模块,有粘包半包、协议解析等功能。
- 消息处理
游戏逻辑层,比如收到MsgMove协议,服务端会记录玩家坐标,然后广播。 - 事件处理
玩家上线和下线等。上线,初始化;下线,数据记录。
- 数据库底层
提供保存玩家数据、读取玩家数据、注册、检验用户名密码等功能,封装服务端和数据库的交互。
- 存储结构
指定保存数据,比如金币、等级、经验、文本。
游戏流程
- 连接阶段
客户端调用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;
}
public static byte[] EncodeName(MsgBase msgBase){
byte[] nameBytes = System.Text.Encoding.UTF8.GetBytes(msgBase.protoName);
Int16 len = (Int16)nameBytes.Length;
byte[] bytes = new byte[2+len];
bytes[0] = (byte)(len%256);
bytes[1] = (byte)(len/256);
Array.Copy(nameBytes, 0, bytes, 2, len);
return bytes;
}
public static string DecodeName(byte[] bytes, int offset, out int count){
count = 0;
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);
}
}
网络模块
整体结构
- 网络管理器NetManager,处理select多路复用。
- ClientState类,定义客户端信息。
- MsgHandler类,处理网络消息,根据消息类型,分拆到多个文件中(BattleMsgHandler.cs处理战斗相关协议,SysMsgHandler处理MsgPing,MsgPong等系统协议)。
- 事件处理类EventHandler。
程序引入玩家列表,玩家登录后clientState与player对象关联。通过clientState是否持有player对象判断客户端状态。
logic,代表游戏逻辑部分。
ClientState
客户端信息,一个客户端连接对应一个ClientState对象。
using System.Net.Sockets;
public class ClientState
{
public Socket socket;
public ByteArray readBuff = new ByteArray();
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 {
public static Socket listenfd;
public static Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();
static List<Socket> checkRead = new List<Socket>();
public static long pingInterval = 30;
}
public static void StartLoop(int listenPort) {
listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPAddress ipAdr = IPAddress.Parse("0.0.0.0");
IPEndPoint ipEp = new IPEndPoint(ipAdr, listenPort);
listenfd.Bind(ipEp);
listenfd.Listen(0);
Console.WriteLine("[服务器]启动成功");
while(true){
ResetCheckRead();
Socket.Select(checkRead, null, null, 1000);
for(int i = checkRead.Count-1; i>=0; i--){
Socket s = checkRead[i];
if(s == listenfd){
ReadListenfd(s);
}else{
ReadClientfd(s);
}
}
Timer();
}
}
public static void ResetCheckRead(){
checkRead.Clear();
checkRead.Add(listenfd);
foreach(ClientState s in clients.Values){
checkRead.Add(s.socket);
}
}
处理监听消息
public static void ReadListenfd(Socket listenfd){
try{
Socket clientfd = listenfd.Accept();
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());
}
}
处理客户端消息
public static void ReadClientfd(Socket clientfd){
ClientState state = clients[clientfd];
ByteArray readBuff = state.readBuff;
int count = 0;
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){
Console.WriteLine("Receive SocketException " + ex.ToString());
Close(state);
return;
}
if(count <= 0){
Console.WriteLine("Socket Close " + clientfd.RemoteEndPoint.ToString());
Close(state);
return;
}
readBuff.writeIdx += count;
OnReceiveData(state);
readBuff.CheckAndMoveBytes();
}
关闭连接
- 分发OnDisconnect事件,让程序可以在玩家掉线时做些处理;
- 调用socket.Close关闭连接;
- 将客户端状态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());
}
}
测试
- 协议处理
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文件中。
- 事件处理
using System;
public partial class EventHandler {
public static void OnDisconnect(ClientState c){
Console.WriteLine("Close");
}
public static void OnTimer(){
}
}
- 启动网络监听
using System;
class Server {
public static void Main(strin[] args) {
NetManager.StartLoop(8888);
}
}
- 开始测试
客户端发送MsgMove协议。
心跳机制
lastPingTime
using System.Net.Sockets;
public class ClientState
{
public Socket socket;
public ByteArray readBuff = new ByteArray();
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();
}
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();
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 {
public string id = "";
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();
public static bool Connect(string db, string ip, int port, string user, string pw) {
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) {
if (!DbManager.IsSafeString(id)){
return false;
}
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) {
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;
}
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){
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) {
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){
if(!DbManager.IsSafeString(id)){
Console.WriteLine("[数据库] GetPlayerData fail, id not safe");
return null;
}
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);
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 = "";
public int result = 0;
}
public class MsgLogin:MsgBase {
public MsgLogin() {protoName = "MsgLogin";}
public string id = "";
public string pw = "";
public int result = 0;
}
public class MsgKick:MsgBase {
public MsgKick() {protoName = "MsgKick";}
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 = "";
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 = 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");
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;
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;
player.data.text = msg.text;
player.Send(msg);
}
完整代码
|