【Unity】网络游戏编程实践(4)用户登陆管理&UI

2017.6.26 Demo

2017.6.27 更新

之前已经实现了向数据库写入和读取信息的功能,那么很自然地,用户的注册和登录部分就比较容易写逻辑来实现了。下面记录一些关键问题。

首先梳理一下注册过程的逻辑:(服务端操作[S]/客户端操作[C])

[S]开启服务器,连接数据库 -> [C]用户将用户名、密码、确认密码的信息写入输入区 -> [C]点击相关按钮后连接服务器 -> [C]将上述信息按照一定的协议发给服务器 -> [S]服务器按协议解析收到的消息 -> [S]服务器判断消息的正确性,并与数据库交互 -> [S]服务器将结果返回客户端 -> [C]客户端根据收到的结果展示不同的提示页面

1.开启服务器,连接数据库

这个部分前几篇文章讲过,过程大同小异。只要注意相关IP地址和名称就好。

//开启服务器,连接数据库
public void Start(string host, int port)
{
	//数据库
	string connStr = "Database=game;Data Source=127.0.0.1;";
	connStr += "User Id=root;Password=123456;port=3306";
	sqlConn = new MySqlConnection (connStr);
	try
	{
		sqlConn.Open();	
	}
	catch(Exception e) 
	{
		Console.Write ("[数据库]连接失败" + e.Message);
		return;
	}
	//连接池
	conns = new Conn[maxConn];
	for (int i = 0; i < maxConn; i++) 
	{
		conns [i] = new Conn ();
	}
	//Socket
	listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream,ProtocolType.Tcp);
	//Bind
	IPAddress ipAdr = IPAddress.Parse(host);
	IPEndPoint ipEp = new IPEndPoint (ipAdr, port);
	listenfd.Bind (ipEp);
	//Listen
	listenfd.Listen(maxConn);
	//Accept
	listenfd.BeginAccept(AcceptCb, null);
	Console.WriteLine ("[服务器]启动成功");
}
2.用户将用户名、密码、确认密码的信息写入输入区

注意,密码的输入区应该将属性栏的内容类型(Content Type)改为”Password”,如下图所示:

img

3.点击相关按钮后连接服务器

需要连接服务器的按钮有“登录”和“注册”两个。

public void Connection()
{
	//Socket
	socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
	//Connect
	string host = "127.0.0.1";
	int port = 1234;
	socket.Connect (host, port);
	//Recv 倒数第二个参数为回调函数
	socket.BeginReceive(readBuff, 0, BUFFER_SIZE, SocketFlags.None, ReceiveCb, null);
}
4.将上述信息按照一定的协议发给服务器

登录/注册:

public void SendLog()
{
	Connection ();
	string str ="LOGIN " + userId.text.ToString() + " " + passW.text.ToString();
	byte[] bytes = System.Text.Encoding.Default.GetBytes (str);
	socket.Send(bytes);
}

public void SendReg()
{
	Connection ();
	string str ="REG " + userId.text.ToString() + " " + passW.text.ToString() + " " + cfmPassW.text.ToString();
	byte[] bytes = System.Text.Encoding.Default.GetBytes (str);
	socket.Send(bytes);
}
  • 协议

协议是服务器和客户端遵循的一套消息处理的模式,换句话说就是任一方都需要“让对方听懂自己在说什么”和“让自己听懂对方在说什么”。因为我们的系统每次只能向服务器发送一条消息,为了让消息中包含多个信息,则需要协议。这部分是用户自己给出的,为了方便理解和处理,应该尽量简单和准确。

这部分的协议我们定义为:功能关键字(LOGIN/REG)/ID/密码/确认密码(登录功能无此部分),每部分之间用空格符分开。

5.服务器按协议解析收到的消息
private void ReceiveCb(IAsyncResult ar)
{
	Conn conn = (Conn)ar.AsyncState;
	try
	{
		int count = conn.socket.EndReceive(ar);
		//关闭信号
		if(count <= 0)
		{
			Console.WriteLine("收到 [" + conn.GetAddress() + "] 断开连接");
			conn.Close();
			return;
		}
		//数据处理
		string str = System.Text.Encoding.UTF8.GetString(conn.readBuff,0,count);
		Console.WriteLine("收到 [" + conn.GetAddress() + "] 数据:" + str);
		string[] args = str.Split(' ');
		string result = "";
		Console.WriteLine("args[0]=" + args[0]);
		if(args[0] == "LOGIN")
		{
			string id = args[1];
			string pw = args[2];
			Console.WriteLine("用户登录: " + id);
			result = CheckPassWord(id, pw);
		}
		if(args[0] == "REG")
		{
			string id = args[1];
			string pw = args[2];
			string cpw = args[3];
			Console.WriteLine("用户注册: " + id + " " + pw);
			result = Register(id, pw, cpw);
		}
		byte[] bytes = System.Text.Encoding.Default.GetBytes(result);
		conns[index].socket.Send(bytes);
		//继续接收
		conn.socket.BeginReceive(conn.readBuff,conn.buffCount, conn.BuffRemain(),SocketFlags.None,ReceiveCb,conn);

	}
	catch
	{
		Console.WriteLine("收到 [" + conn.GetAddress() +"] 断开连接");
		conn.Close();
	}
}

上述代码中的“数据处理”部分即为服务器对协议的处理,因为这部分容易出错,所以代码中加入了不少调试的语句。依据空格拆分消息的语句如下:

string[] args = str.Split(' ');
6.服务器判断消息的正确性,并与数据库交互

这部分很关键,一些判定条件包括但不限于:

(1)防SQL注入

所谓SQL注入,就是通过输入请求,把SQL命令插入到SQL语句中,以达到欺骗服务器执行恶意SQL命令的目的。假设服务端要获取玩家的数据,可能使用如下的SQL语句:

string sql = "SELECT * FROM player WHERE id=" + id";

正常情况下,该语句能够完成读取数据的工作。但如果一名玩家注册了类似“wanjia;DELETE * FROM player;”的名字,则上面的SQL语句会变为:

SELECT * FROM player WHERE id= wanjia; DELETE * FROM player;

(这样的玩家如同毒瘤,后果不堪设想)

当然,防范这个问题也比较简单,即加入对非法字符(如 ; * 等)的判断:

//判定安全字符串
public bool IsSafeStr(string str)
{
	return !Regex.IsMatch(str, @"[-|;|,|\/|\(|\)|\[|\]|\{|\}|%|@|\*|!|\']");
}

(2)判定两次密码输入是否一致

(3)通过查询数据库判定用户名是否已被注册

private bool CanRegister(string id)
{
	//查询id是否存在
	string cmdStr = string.Format("select * from user where id='{0}';", id);
	MySqlCommand cmd = new MySqlCommand (cmdStr, sqlConn);
	try
	{
		MySqlDataReader dataReader = cmd.ExecuteReader();
		bool hasRows = dataReader.HasRows;
		dataReader.Close();
		return !hasRows;
	}
	catch(Exception e)
	{
		Console.WriteLine ("[DataMgr]CanRegister fail " + e.Message);
		return false;
	}
}

(4)登陆密码校验

public string CheckPassWord(string id, string pw)
{
	//防止sql注入
	if (!IsSafeStr (id) || !IsSafeStr (pw))
		return "SYMBOL_INV";
	//查询
	string cmdStr = string.Format("select * from user where id = '{0}' and pw = '{1}';", id, pw);
	MySqlCommand cmd = new MySqlCommand (cmdStr, sqlConn);
	try
	{
		MySqlDataReader dataReader = cmd.ExecuteReader();
		bool hasRows = dataReader.HasRows;
		dataReader.Close();
		if(hasRows)
			return "LOG_SUCCESS";
		else
			return "INF_WRONG";
	}
	catch(Exception e) 
	{
		Console.WriteLine ("[DataMgr]CheckPassWord " + e.Message);
		return "UNKNOWN_ERROR";
	}
}
7.服务器将结果返回客户端

第5步中已解决此问题:

conns[index].socket.Send(bytes);
8.客户端根据收到的结果展示不同的提示页面
//因为只有主线程能够修改UI组件的属性
//因此在Update里更换文本
void Update()
{
//		//保证每次变化只更新一次窗口
	if (resOld != resNew) {
		UpdateWindow (resNew);
		Debug.Log("Window updated!");
	}
	resOld = resNew;
//		//recvText.text = recvStr;
}

void UpdateWindow (string res)
{

	if (res == "INF_WRONG") {
		Debug.Log("用户名/密码错误!");
		infImg[0].gameObject.SetActive (true);
	} else if (res == "SYMBOL_INV") {
		Debug.Log("使用非法字符!");
		infImg[1].gameObject.SetActive (true);
	} else if (res == "LOG_SUCCESS") {
		Debug.Log("登录成功!");
		infImg[2].gameObject.SetActive (true);
	} else if (res == "ID_HASREG") {
		Debug.Log("该账户已被注册");
		infImg[3].gameObject.SetActive (true);
	} else if (res == "PASS_INC") {
		Debug.Log("两次密码不一致!");
		infImg[4].gameObject.SetActive (true);
	} else if (res == "REG_SUCCESS") {
		Debug.Log("注册成功!");
		infImg[5].gameObject.SetActive (true);
	}
}

将窗口附在Image对象上,通过改变对应对象的显示状态来控制反馈。

跳转的逻辑比较简单,不再赘述,部分页面效果如下:

  • 登录主界面

img

  • 登录异常界面

img

  • 注册异常界面

img

  • 注册成功界面

img


目前存在的问题:

  • 未引入心跳机制,发生异常时不会主动断开连接,造成连接池资源浪费。
  • 消息未改变时,再次点击同一按钮时不会再次弹窗。
  • 其他未发现问题…

参考资料:

《Unity3D 网络游戏实战》罗培羽·著

书中源代码已获作者授权使用。


用户登录部分算是告一段落了,下一步的目标是实现玩家数据的管理和更新。