开篇先说明,这个游戏制作也是我跟随别人的教程制作的游戏,因此想要了解更多的内容可以去看siki老师的视频,我这里做笔记的目的有两个,一个是帮助喜欢看文字版教程的朋友进一步的学习,一个是保存自己在学习中的一个逻辑和思路,如果看到这个视频会对你有帮助,那就再好不过了,当然这里的话是其中百分之30的内容,后面的制作我会放在我的其他博客下继续
1. 关于IP和端口号的知识
我们经常申请的端口号是49152-65535,1024以前的基本是知名端口
2. 绑定我们的IP和端口号
namespace tcp服务器端
{
classProgram
{
staticvoid Main(string[] args)
{
//首先,创建我们的Socket,是ipv4的地址,以流的形式传输,使用的是tcp协议
Socketmysocket = newSocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//第二部,创建我们的ip地址
IPAddress ipaddress = IPAddress.Parse("127.0.0.1");
//将ip地址和我们指定的端口号进行绑定
IPEndPoint ipendpoint = new IPEndPoint(ipaddress, 88);
//将socket和端口号进行绑定
mysocket.Bind(ipendpoint);
}
}
}
3. 创建我们的服务器端
这里的话,我们让socket开始监听以后,就创建一个接受客户端连接的socket,我们一般是用receive和send来收发数据,这里要注意传递的数据都是字节数组,我们要将它转变成
classProgram
{
staticvoid Main(string[] args)
{
//首先,创建我们的Socket,是ipv4的地址,以流的形式传输,使用的是tcp协议
Socket mysocket = new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);
//第二部,创建我们的ip地址
IPAddress ipaddress =IPAddress.Parse("127.0.0.1");
//将ip地址和我们指定的端口号进行绑定
IPEndPoint ipendpoint = new IPEndPoint(ipaddress,88);
//将socket和端口号进行绑定
mysocket.Bind(ipendpoint);
mysocket.Listen(0);
Socket clientSocket =mysocket.Accept();//接受客户端连接
//向客户端发送一条信息
string msg = "hello客户端";
byte[] data = System.Text.Encoding.UTF8.GetBytes(msg);
clientSocket.Send(data);
//从客户端接受数据
byte[] getdata = newbyte[1024];
int count = clientSocket.Receive(getdata);
//这里表示用getdata这个数组来接受客户端传递的数据,再将传递数据长度的字节数组转换成字符串再输出
string msg2 = System.Text.Encoding.UTF8.GetString(getdata, 0,count);
Console.WriteLine(msg2);
clientSocket.Close();
mysocket.Close();
}
4. 创建我们的客户端
客户端也是一样创建自己的socket,和服务器提供的端口相连接,然后再收发信息即可
5. using System;
6. using System.Collections.Generic;
7. using System.Linq;
8. using System.Text;
9. using System.Threading.Tasks;
10.using System.Net.Sockets;
11.using System.Net;
12.
13.namespace tcp客户端
14.{
15. classProgram
16. {
17. staticvoid Main(string[] args)
18. {
19. Socket clientSocket = newSocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
20. IPEndPoint iPEndPoint = newIPEndPoint(IPAddress.Parse("10.128.230.79"), 88);
21. clientSocket.Connect(iPEndPoint);
22. //客户端接受输出
23. byte[] receivedata = newbyte[1024];
24. int count = clientSocket.Receive(receivedata);
25. string message = Encoding.UTF8.GetString(receivedata, 0,count);
26. Console.WriteLine(message);
27.
28. //客户端向服务器发送数据
29. string msg2 = Console.ReadLine();
30. clientSocket.Send(Encoding.UTF8.GetBytes(msg2));
31.
32. Console.ReadKey();
33. clientSocket.Close();
34.
35.
36. }
37. }
38. }
5.如何异步获取到数据
这里我们使用一个BeginReceive开始异步接受数据,传递过来我们的客户端这个参数,然后重复调用一个AsyncCallBack的方法,通过begin和end来实现多次对于信息的接受
staticbyte[] databuffer = newbyte[1024];
clientSocket.BeginReceive(databuffer, 0, 1024, SocketFlags.None,AsyncCallback, clientSocket);
}
staticvoidAsyncCallback(IAsyncResult ar){
Socket clientSocket = ar.AsyncStateas Socket;
int count = clientSocket.EndReceive(ar);
string msg = Encoding.UTF8.GetString(databuffer, 0, count);
Console.WriteLine("从客户端接受信息" + msg);
clientSocket.BeginReceive(databuffer, 0, 1024, SocketFlags.None,AsyncCallback, clientSocket);
}
当然客户端的话,只要while True发送信息即可
//客户端向服务器发送数据
while (true)
{
string msg2 = Console.ReadLine();
clientSocket.Send(Encoding.UTF8.GetBytes(msg2));
}
6.异步与客户端建立连接
想要同时和多个客户端进行连接,也是使用一个mySocket.BeginAccept和endAccept来进行开始接受连接和结束连接,多次执行即可实现和多个客户端建立连接
staticvoid StartSocket()
{
//首先,创建我们的Socket,是ipv4的地址,以流的形式传输,使用的是tcp协议
Socket mysocket = newSocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//第二部,创建我们的ip地址
IPAddress ipaddress =IPAddress.Parse("10.128.230.79");
//将ip地址和我们指定的端口号进行绑定
IPEndPoint ipendpoint = newIPEndPoint(ipaddress, 88);
//将socket和端口号进行绑定
mysocket.Bind(ipendpoint);
mysocket.Listen(0);
mysocket.BeginAccept(AcceptCallback, mysocket);//异步接受多个客户端的连接
}
staticvoidAcceptCallback(IAsyncResult ar)
{
Socket mysocket = ar.AsyncState as Socket;
//这里表示完成一次客户端的连接
Socket clientSocket=mysocket.EndAccept(ar);
//向客户端发送一条信息
string msg = "hello客户端";
byte[] data = System.Text.Encoding.UTF8.GetBytes(msg);
clientSocket.Send(data);
//我们异步从客户端那里接受多条数据
clientSocket.BeginReceive(databuffer, 0, 1024, SocketFlags.None,AsyncCallback, clientSocket);
//这里就是继续等待下一个客户端的连接
mysocket.BeginAccept(AcceptCallback, mysocket);
}
7.处理客户端关闭的异常
这里我们使用try catch捕捉异常就可以处理客户端远程强制退出的问题,当我们正常关闭的时候,客户端会向服务器发送长度为0的数据,所以我们这里判断一下如果为0,就关闭与客户端的连接
try
{
clientSocket = ar.AsyncState as Socket;
int count = clientSocket.EndReceive(ar);
if (count == 0)
{
clientSocket.Close();
}
string msg = Encoding.UTF8.GetString(databuffer, 0, count);
Console.WriteLine("从客户端接受信息" + msg);
clientSocket.BeginReceive(databuffer, 0, 1024, SocketFlags.None,AsyncCallback, clientSocket);
}
catch(Exception e)
{
Console.WriteLine(e);
if (clientSocket != null)
{
clientSocket.Close();
}
}
这里我们也可以让客户端正常关闭
当输出c,就断开连接
while (true)
{
string msg2 = Console.ReadLine();
clientSocket.Send(Encoding.UTF8.GetBytes(msg2));
if (msg2 == "c")
{
clientSocket.Close();
return;
}
}
8.tcp中的粘包和分包
这个粘包和分包是tcp的优化性能
粘包是如果信息过短就会将多次信息打一个包发送,如果信息过长则会分成多个包进行发送
在游戏开发中,我们常会产生多次小信息的传输,这里来讲解解决方案
我们可以在数值传输前加一个数据长度作为判断,假设你定义为40个字节,如果长度小于40个字节,就继续接受,如果到达40个字节,就进行传输
这里当我们要传递一个值类型的数据,就可以用BitConverter.GetBytes这种方式,将它转变一个四个字节的字节数组,这样就可以避免粘包的问题
下面就是一个解决的例子,我们用这个方法对字符串进行转换即可,我们把它做成一个工具类可以随时进行调用
//在这里专门创建一个方法,用来计算传递的字符串的长度,并且将它组拼成一个新的数组用来避免粘包的产生
publicstaticbyte[] GetBytes(string data)
{
//将字符串转变为数组
byte[] databuf = Encoding.UTF8.GetBytes(data);
//计算字节数组长度
int datalength = databuf.Length;
//将它转变为4个字节长度的字节数组用来计算长度
byte[]countLength= BitConverter.GetBytes(datalength);
//组拼成新的字节数组,即长度加原本的数组
byte[] newByte = countLength.Concat(databuf).ToArray();
return newByte;
}
9.关于服务器如何去接受客户端分包的信息
首先,我们让客户端向服务器循环发送100条输出,这种数据如果不进行解析就会出现粘包的情况
for (int i = 0; i < 100;i++)
{
//循环向客户端输出100条数据
clientSocket.Send(Message.GetBytes(i.ToString()));
}
然后,我们创建一个Message类来处理各种客户端传递过来的信息,首先,定义一个数组来接受客户端的信息,然后一个startIndex来记录当前传递过来的数据在数组中的位置,RemainLength记录剩余还能使用的长度,addCount用来更新当前数组存放到什么位置
这里讲解一下我们的解析方法,当更新完StartIndex后,我们就要判断它的长度,首先用BitConvert.toInt32来获取数组的长度,如果超过了开始我们定义的数据长度的话,那就是我们现在解析完全的数据,我们用Encoding.Utf8来将字节数组进行转换,然后用Array.copy()来更新我们的数组,也就是将解析完全的数据移除数组,然后再更新我们的StartIndex
classMessage
{
//每条信息存储的数组
byte[] data= newbyte[1024];
//这个是记录当前数组存储到第几个字节的标志位
intstartIndex = 0;
publicbyte[] Data
{
get { return data; }
}
publicint StartIndex
{
get { return startIndex; }
}
publicint RemainLength
{
get { return data.Length - startIndex; }
}
publicvoid AddCount(int count)
{
startIndex += count;
}
publicvoid ReadMessage()
{
while (true)
{
if (startIndex <= 4)
{
return;
}
//读取数组的长度,自动省略前四个字节长度
int count = BitConverter.ToInt32(data, 0);
if (startIndex - 4 >= count)
{
String s =Encoding.UTF8.GetString(data, 4, count);
Console.WriteLine("解析了一条数据:" + s);
Array.Copy(data, count + 4,data, 0, startIndex - count - 4);
startIndex -= (count + 4);
}
else { break; }
}
}
}
然后,我们在服务器中调用这个Message来的方法,首先我们接收到客户端的信息,存放到定义的数组里面,从开始索引开始存储,然后可存储的长度和数组剩余长度有关,如果超出的话就不会再进行接受
每次当我们接受到数据的时候,我们就获取数据长度,然后更新StartIndex,然后调用Message的解析方法,这样就可以循环解析数据并且避免了粘包的问题
lientSocket.BeginReceive(msg3.Data,msg3.StartIndex, msg3.RemainLength, SocketFlags.None, AsyncCallback,clientSocket);
//这里就是继续等待下一个客户端的连接
mysocket.BeginAccept(AcceptCallback, mysocket);
}
staticvoidAsyncCallback(IAsyncResult ar) {
Socket clientSocket = null;
try
{
clientSocket = ar.AsyncState as Socket;
int count = clientSocket.EndReceive(ar);
//解析接收到的数据
msg3.AddCount(count);
msg3.ReadMessage();
if (count == 0)
{
clientSocket.Close();
}
clientSocket.BeginReceive(msg3.Data, msg3.StartIndex, msg3.RemainLength,SocketFlags.None, AsyncCallback, clientSocket);
10.使用mysql做查询操作
因为我们的丛林战争要使用mysql做数据库,这里来讲解一下mysql用法,首先我们要用msql创建一个数据库,定义好它的名字,连接ip地址,端口号,连接的用户名和密码,接着就是打开连接,创建命令语句,执行命令,然后按照列名读取出数据,具体操作可以参照代码和注释啦
classProgram
{
staticvoid Main(string[] args)
{
string con = "Database=test_007;DataSource=localhost;port=3306;User Id=root;Password=1234;";
//创建数据库的连接
MySqlConnection coon = newMySqlConnection(con);
//开启和数据库的连接
coon.Open();
//书写数据库操作的命令和指定的连接
MySqlCommand command = new MySqlCommand("select * from user",coon);
//执行查询操作并且将信息传递给reader
MySqlDataReader reader =command.ExecuteReader();
//这个方法会查询一行数据然后判断下一行是否还有数据
while (reader.Read())
{
//根据列名获得数据
string username = reader.GetString("username");
string password = reader.GetString("password");
Console.WriteLine(username + ": " + password);
}
//执行结束关闭连接
reader.Close();
coon.Close();
Console.ReadKey();
}
}
11.用Mysql做插入操作
这里的话我们使用的是字符串组拼进行的数据库操作,但是这样的操作一个问题,可能会因为用户的恶意操作对数据库进行污染,类似于密码为deletefromuser这种
stringusername = "haha";string password = "111";
MySqlCommand cmd = new MySqlCommand("insert into user set username='" + username + "'" + ",password='" + password + "'",coon);
//进行不查询的sql语句
cmd.ExecuteNonQuery();
解决这个问题的方法就是用@进行注册变量,然后用AddWithValue来进行组拼
stringusername = "haha";string password = "111111";
MySqlCommand cmd = new MySqlCommand("insert into user setusername=@un,password=@pwd",coon);
cmd.Parameters.AddWithValue("un", username);
cmd.Parameters.AddWithValue("pwd", password);
删除和更新的话就是update和delete,操作和上面类似,就介绍到这里了
12.服务器的分层架构
13.初步创建我们的服务器端
这里我们创建出服务器的Socket,提供接受端口号和ip地址创建IpendPoint的方法,并且使用异步的方法接受客户端传递的值
namespace游戏服务器.Sever
{
classSever
{
//这里的话,我们创建我们游戏的服务器端
privateIPEndPoint ipEndpoint;
private Socket severSocket;
public Sever()
{
}
//根据我们提供的Ip地址和端口号创立连接
public Sever(string ipStr,int port)
{
SetIpAndPort(ipStr, port);
}
publicvoid SetIpAndPort(string ipStr,int port)
{
ipEndpoint = newIPEndPoint(IPAddress.Parse(ipStr), port);
}
publicvoid Start()
{
severSocket = newSocket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
severSocket.Bind(ipEndpoint);
//表示最大连接长度为无限
severSocket.Listen(0);
//接受客户端的连接
severSocket.BeginAccept(AcceptCallBack, null);
}
//处理连接回调
publicvoidAcceptCallBack(IAsyncResult ar)
{
Socket clientSocket =severSocket.EndAccept(ar);
}
}
14.创建我们的客户端
这里的话是处理我们和服务器端的一些交互和客户端要处理的事物,首先,我们要在客户端里创建自身的socket,并且持有到Sever端的Socket,然后我们在Start调用BeginReceive开始异步接受服务器端传递的信息,这里对于粘包分包的问题我们放在其他类里进行处理,然后我们写回调函数来处理接受的,当接收到的信息为0的时候,我们就断开和服务器之间的连接,这里我们用tryCatch来捕捉异常。然后我们书写一个Close方法,来关闭连接,这里,在Sever端我们提供一个方法来移除客户端,当然要先lock再移除,以防止出错
using System;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Text;
usingSystem.Threading.Tasks;
usingSystem.Net.Sockets;
usingSystem.Net;
namespace游戏服务器.Sever
{
classClient
{
private SocketclientSocket;
private Seversever;
publicClient()
{
}
publicClient(Socket clientSocket, Sever sever)
{
//持有一个对socket的引用
clientSocket = this.clientSocket;
this.sever = sever;
}
//用来处理和服务器端之间的通信
publicvoid Start()
{
clientSocket.BeginReceive(null, 0, 0,SocketFlags.None, ReceiveCallBack, null);
}
publicvoidReceiveCallBack(IAsyncResult ar)
{
try
{
int count = clientSocket.EndReceive(ar);
if (count == 0)
{
Close();
}
}
catch (Exception e)
{
Close();
Console.WriteLine(e);
}
}
publicvoid Close()
{
if (clientSocket != null)
{
clientSocket.Close();
sever.RemoverClient(this);
}
}
}
}
15.引入Messgae类来对信息进行处理
using System;
usingSystem.Collections.Generic;
usingSystem.Linq;
usingSystem.Text;
usingSystem.Threading.Tasks;
namespace游戏服务器.Sever
{
classMessage
{
//在这里专门创建一个方法,用来计算传递的字符串的长度,并且将它组拼成一个新的数组用来避免粘包的产生
publicstaticbyte[] GetBytes(string data)
{
//将字符串转变为数组
byte[] databuf = Encoding.UTF8.GetBytes(data);
//计算字节数组长度
int datalength = databuf.Length;
//将它转变为4个字节长度的字节数组用来计算长度
byte[] countLength = BitConverter.GetBytes(datalength);
//组拼成新的字节数组,即长度加原本的数组
byte[] newByte = countLength.Concat(databuf).ToArray();
return newByte;
}
}
}
16.完成Controller层的搭建
首先,我们客户端向服务器发出的请求都是通过Controller层来进行处理的,这里来讲解一下如何初步搭建Controller层
首先,我们要创建一个抽象基类BaseController,我们的子类都是继承自这个Controller,不同的需求对应不同的Controller
然后,我们创建一个Common的类库,这个是服务器端和客户端共享的代码,因为客户端向服务器端发出请求的时候,是通过RequestCode发送消息,而服务器端也需要这个RequestCode知道你调用的是哪一个Controller
这里我们创建一个枚举类型的RequestCode和ActionCode类
然后我们在BaseController里创建一个空的RequestCode和一个默认的实现方法
abstractclassBaseController
{
RequestCode requestCode = RequestCode.None;
//子类可以选择性实现这个方法,这个是默认的处理方法
publicvirtualvoid DefaultHandle()
{
}
}
17.
这里来解释一下客户端和服务器端交互的原理
首先,当我们的客户端向服务器端发起了一个请求,比如注册,这时候我们会向服务器的Controller层发送一个数据长度+RequestCode+ActionCode+数据的这样一个信息,数据长度用来解决粘包分包的问题,RequestCode用来判断用哪一个Controller来执行,ActionCode用来判断用哪一个方法来解决这个请求,这两个Code都是int类型的四个字节的数据
然后我们的Controller层也会根据你的请求进行判断,如果是要做出响应的请求,服务器端就会向客户端回发一个数据长度+RequestCode+数据的信息,我们的客户端会根据对应的RequestCode进行相应的反应,比如注册请求的响应,就会回复一个注册成功
这里的话我们一个服务器端可以同时响应多个客户端的请求,而一个客户端只能回应一个服务器的RequestCode
18.创建我们的ControllerMannager
这里的话,我们需要一个类来管理我们的Controller,并且这个管理类为了避免冗余度过高,我们选择用Sever服务器端作为中介者,来进行交互
首先我们要一个字典,用来存储各种Controller,然后我们就要对它进行初始化了
namespace游戏服务器.Controller
{
classControllerManager
{
//这个是用来管理Controller,来决定什么时候调用什么Controller
//首先创建一个字典,用来存储各种Controller
privateDictionary<RequestCode,BaseController> requestDictionary=newDictionary<RequestCode,BaseController>();
publicControllerManager()
{
Init();
}
void Init()
{
//在这里进行初始化
}
}
}
这里我们在Sever构建一个接口,以后其他类想要使用ControllerMannager,都要通过Sever类来执行
private ControllerManagercontrollerManager = newControllerManager();
19.通过ControllerMannager来处理分发的请求
首先,我们要提供一个DefaultController来判断当客户端传递的是没有ActionCode的时候来怎么处理,当然这里首先我们要在BaseController提供一个方法来获得它的RequestCode
publicRequestCode RequestCode
{
get
{
return RequestCode;
}
}
这样我们就可以在ControllerMannager里实例化DefaultController来进行错误处理了
void Init()
{
//给一个处理错误的Controller
DefaultController defaultController= newDefaultController();
requestDictionary.Add(defaultController.RequestCode, defaultController);
}
然后的话,我们就提供一个方法来处理客户端向服务器端请求Controller的方法,首先,我们要传递过来客户端发送的数据,请求的RequestCode和ActionCode,然后我们获得基类的Controller,根据tryGetValue来判断是否存在客户端请求的requestCode是否有对应的Controller,如果有的话,我们就要用Enum里的GetName,根据类型,来获得ActionCode里的值,再将其转变成为字符串类型,这里我们ActionCode里面定义的是None这个值类型,我们将其转换成字符串类型
接下来的话,我们运用的就是C#里的反射机制,通过controller.getType.getMethod,来根据方法名获取到方法信息,这里的话,我们需要引用一下System.Reflection
如果存在这个方法的话,我们就可以用MethodInfo里的invoke方法,来根据参数调用这个方法了,这里参数需要是object[]类型,我们转换一下就可以使用了前面的参数是指在什么下运行,我们指定为controller,这样的话,我们就可以调用方法了,当然这里有个返回值,判断方法调用是否需要给客户端做出回应,这个我们后面再讲
publicvoidHandleRequest(RequestCode requestCode,ActionCode actionCode,string data)
{
BaseController controller;
//判断是否根据对应的RequestCode获取到对应的Controller
bool isGet=requestDictionary.TryGetValue(requestCode, out controller);
if (isGet == false)
{
Console.WriteLine("无法获得到" + requestCode + "对应的Controller");return;
}
//如果有对应的RequestCode的话,我们就要获取到对应的方法名,这里是获取到对应ActionCode里的值
string methodName = Enum.GetName(typeof(ActionCode), actionCode);
//这里是用到C#中的反射机制,将对应类型的对应的方法名的方法信息获得到
MethodInfo mi= controller.GetType().GetMethod(methodName);
if (mi == null)
{
Console.WriteLine("该方法" + methodName + "不存在");
return;
}
//在controller中调用
object[] paramaters = newobject[] { data };
//根据o来判断是否响应
object o= mi.Invoke(controller, paramaters);
}
}
19.处理客户端请求的响应
首先,我们BaseController默认的方法,传递的除了数据以外,还有对应的客户端和服务器端
publicvirtualstring DefaultHandle(string data,Clientclient,Sever sever)
{
//默认不给客户端返回数据
returnnull;
}
然后的话,我们在ControllerMannager中传递对应的服务器
private Sever sever;
publicControllerManager(Sever sever)
{
this.sever = sever;
InitController();
}
这样的话,在Sever端的构造函数里,我们就可以直接构造出ControllerManager并且将自身传递过去
public Sever(string ipStr,int port)
{
controllerManager = new ControllerManager(this);
SetIpAndPort(ipStr, port);
}
然后我们怎么处理对客户端发送消息的响应呢,我们在Sever端里提供一个处理方法,SendMessage,用来作为客户端和ControllerManager的中介,通过这个方法来处理课都俺的信息
publicvoid SendMessage(Clientclient,RequestCode requestCode,string data)
{
//TODO
}
这样我们就可以在ControllerManager里调用sever的这个方法来处理客户端的请求了
object o=mi.Invoke(controller, paramaters);
if (o == null || string.IsNullOrEmpty(o.ToString()))
{
//如果空就不进行处理
return;
}
//通过sever端进行中介来发送数据
sever.SendMessage(client, requestCode, o asstring);