单进程服务端结构。
状态 | 说明 |
---|---|
连接但未登录 | Connect连接,角色未关联,需输入账号密码,服务端验证后从数据库读取角色数据并关联 |
登录成功 | 连接和角色关联后,玩家可操作游戏角色,如移动、攻击等 |
类的序列化。
net网络模块,proto协议文件。
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);
}
}
程序引入玩家列表,玩家登录后clientState与player对象关联。通过clientState是否持有player对象判断客户端状态。
logic,代表游戏逻辑部分。
客户端信息,一个客户端连接对应一个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();
}
//关闭连接
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);
}
}
//定时器
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协议。
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);
}
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。
using System.Net.Sockets;
public class ClientState {
public Socket socket;
public ByteArray readBuff = new ByteArray();
//Ping
public long lastPingTime = 0;
//玩家
public Player player;
}
public class PlayerData{
//金币
public int coin = 0;
//记事本
public string text = "new text";
}
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);
}
}
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);
}
}
xampp安装包安装,xmapp-control.exe启动,Start按钮开启MySQL服务。默认数据库端口3306,用户root,密码空。
专为MySQL数据库服务的管理工具。
新建连接,连接名:127.0.0.1;
主机名或IP地址:127.0.0.1;
端口:3306;
用户名:root;
密码:
新建game库,包含account和player表。
account含id(账号,text)和pw(密码,text)。
player含id和data(数据,text)。
引用MySql.Data.dll
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流程:
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命令的目的。
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, @"[-|;|,|\/|\(|\)|\[|\]|\}|\{|%|@|\*|!|\']");
}
//是否存在该用户
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;
}
}
//注册
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("注册成功");
}
}
}
//创建角色
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("创建成功");
}
}
}
//检测用户名密码
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;
}
}
//获取玩家数据
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;
}
}
//保存角色
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);
在线记事本
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);
}