unity-学习笔记
搭建服务器与客户端连接-方法一
我是看b站一个大佬的视频写的,链接如下
https://www.bilibili.com/video/BV1t7411E7fR?p=11&spm_id_from=pageDriver
搭建服务器
在vs2017里创建新项目
新项目选择控制台应用程序
创建之后在解决方案里添加一些文件夹和类
大致如图
首先需要创建一个启动类
作用是启动服务器
添加while的目的是为了能让程序一直运行
class Program
{
static void Main(string[] args)
{
ServerSocket server = new ServerSocket();
server.StartConnect();
while (true)
{
}
}
}
然后创建一个服务器类
核心是通过socket进行通信
使用socket通信需要为socket设置一些参数才能成功通信
如ip和端口号
使用bind方法连接其他客户端
服务器的话还需要设置listen去监听客户端发送的消息
然后还需要使用accept接收客户端发送的socket实例,然后根据这个实例进行消息的获取,accept是非异步方法,所以当服务器进行到这个方法的时候会被阻塞,直到获得有客户端的socket才进行后面的内容
//创建构造函数
private string ip = "127.0.0.1";
private int port = 30000;
//服务器socket
private Socket socket;
private IPEndPoint iep;
private byte[] readBuffer = new byte[1024];
public void StartConnect()
{
try
{
//创建socket
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//打开服务器socket接收其他socket的权限
iep = new IPEndPoint(IPAddress.Parse(ip), port);
//设置socket,和客户端的socket进行连接
socket.Bind(iep);
//设置socket,一次性能同时访问服务器的客户端数量
socket.Listen(1000);
//获得客户端的socket
Console.WriteLine("服务器开启成功!");
while (true)
{
Console.WriteLine("出现客户端!");
Socket client = socket.Accept();
Player player = new Player(client);//把客户端的socket放在player类里进行管理
}
}
catch (Exception e)
{
Console.WriteLine("服务器开启失败");
Console.WriteLine("原因为" + e.Message);
}
}
创建一个单例类,当其他需要单例的类可以直接继承这个方法,然后就可以自动成为单例
public class SingleTon<T> where T :class,new ()
{
private static T instance;
public static T Instance
{
get
{
if (instance==null)
{
instance = new T();
}
return instance;
}
}
}
创建player类
public class Player:Client
{
public Player(Socket _socket):base(_socket)
{
}
}
继承client,这样创建这个类就可以连带创建client类
创建client类
private Socket client;
Thread thread;
ThreadStart ts;
private byte[] readBuffer;//获取到的字节流
int maxBuffer = 10240;
private bool isRun = false;
private int alread = 0;//已经读取的字节索引
private int userByte = 0;//已经使用的字节数
public Client(Socket _client)
{
client = _client;
ts = new ThreadStart(run);
thread = new Thread(ts);
thread.IsBackground = true;
thread.Start();
}
public void run()
{
isRun = true;
readBuffer = new byte[maxBuffer];
try
{
client.BeginReceive(readBuffer,0,maxBuffer,0,new AsyncCallback(Receive), client);
Console.WriteLine(Encoding.UTF8.GetString(readBuffer));
}
catch(Exception e)
{
Console.WriteLine("client的run方法"+e.Message);
}
/*while (isRun)
{
}*/
}
/// <summary>
/// 获得回调函数
/// </summary>
/// <param name="ar"></param>
private void Receive(IAsyncResult ar)
{
Socket socket = (Socket)ar.AsyncState;
//在请求结束的时候获得请求的数据的字节数
int length = 0;
try
{
length = socket.EndReceive(ar);
}
catch(Exception e)
{
Console.WriteLine("client的receive方法" + e.Message);
}
if (length>0)
{
alread += length;
if (alread>4)//说明信息里包括头信息是有数据的
{
Dispose();
}
}
try
{
client.BeginReceive(readBuffer, alread, maxBuffer, 0, new AsyncCallback(Receive), client);
}
catch(Exception e)
{
Console.WriteLine(e.Message);
}
}
private void Dispose()
{
while (true)
{
try
{
//需要先获得请求头,根据头进行数据解析
byte[] head = new byte[4];
Buffer.BlockCopy(readBuffer, userByte, head, 0, 4);//获得从头信息之后开始的需要的数据
int length = BitConverter.ToInt32(head, 0);
//Console.WriteLine("获得的头信息的长度为:"+length);
if (alread >= userByte + length + 4)
{
byte[] temp = new byte[length];
Buffer.BlockCopy(readBuffer, userByte + 4, temp, 0, length);
Console.WriteLine("有数据");
string content = Encoding.UTF8.GetString(temp);
JsonData jd = JsonMapper.ToObject<JsonData>(content);
if (jd != null)
{
//解析数据
DisposePacket(jd);
}
userByte = userByte + length + 4;
if ((alread - userByte) >= 4)
{
continue;
}
else
{
if ((alread - userByte) <= 0)
{
readBuffer = new byte[maxBuffer];
}
else
{
byte[] t = new byte[alread - userByte];
Buffer.BlockCopy(readBuffer, userByte, t, 0, t.Length);
readBuffer = new byte[maxBuffer];
Buffer.BlockCopy(t, 0, readBuffer, 0, t.Length);
}
alread = alread - userByte;
userByte = 0;
break;
}
}
}
catch(Exception e)
{
Console.WriteLine("dispose方法出现了问题"+e.Message);
}
}
}
private void DisposePacket(JsonData jd)
{
Console.WriteLine("接收消息,内容为:" + jd.ToJson());
JsonData jd1 = new JsonData();
jd1["name"] = 2;
jd1["user"] = "yes";
SendMessage(jd1);
}
private void SendMessage(JsonData jd)
{
byte[] content = Encoding.UTF8.GetBytes(jd.ToJson());
byte[] head = new byte[4];
head = BitConverter.GetBytes(content.Length);
byte[] ss = new byte[head.Length+content.Length];
//把头信息写入ss字节里
Buffer.BlockCopy(head,0,ss,0,4);
//把实际数据写入ss字节里,头信息的后面
Buffer.BlockCopy(content,0,ss,4,content.Length);
client.Send(ss);
}
client类里需要的方法有:
在构造函数里需要把方法写在里面,并且是通过开启多线程的方式,这样就可以让其他的客户端访问服务器了
对socket进行解析的方法
发送消息给客户端socket的方法
逻辑大概是:
获得客户端的socket,然后使用beginreceive方法获得socket里的字节流信息和回调函数
当有字节流信息的时候才对其进行解析
回调函数的作用就是当一次请求完毕之后才会执行的方法
在这个方法里写对数据的处理即可
这里在看教程的时候,教程说一般的服务器和客户端之间的通信都是需要有消息头的,这个的目的是为了在数据发送的时候对其进行判断,才能知道消息是什么类型,然后对应的消息类型去进行对应的方法处理,如登录、注册、装备信息等的处理方法都不同(猜想调用的mysql方法啥的是不一样的,所以才需要对其进行判断把~)
所以需要先对发送的字节流的头消息进行判断,当有头消息并且头消息后面还有数据的时候才接着走后面解析消息的方法
这里大佬对字节流的处理方式是使用Buffer.BlockCopy()
这个方法的好处是能对两个byte数组进行数据的替换
里面有五个参数
第一个是被复制的数组,第二个是从被复制的数组的哪个地方开始读取,第三个是要复制的数组,第四个是从要复制的数组的哪个地方开始,第五个是复制的长度
大佬还解决了当传送的消息不是连续的时候的问题
通过userbyte记录已经使用的字节,然后需要的时候从这个userbyte才开始读取数据
这里面的逻辑有点复杂,我理解了几天才大概明白这个流程
直接复制上面的代码是可以保证运行的,我也基本没改过代码,理解的话最好是自己敲一遍,我是这几天理解的时候敲了5、6次才对socket的通信有了比较清楚的认识
上面就只是讲重要的部分,后面再更新客户端的实现方法~
2021.6.15更新思路
如何在通过服务器的情况下添加用户角色到服务器里
个人推测是通过客户端返回的数据(用户角色的信息,如血量,体力,速度等属性)然后在服务器的场景中实例化角色,将属性赋值到对应的角色中,从而实现场景中添加用户角色,至于控制移动角色,这个还有待考证~
客户端的实现方法和服务器里的client方法如出一撤,区别就是
1需要连接的是服务器的ip
2然后需要对json数据进行解析的话,需要导入litjson.dll或者源码,通过jsonmapper对数据进行实例化即可~
3需要创建一个关闭socket的方法,否则当客户端的unity关闭的时候服务器会报错~
private static Socket client;
private static Thread thread;
private static ThreadStart ts;
private static byte[] readBuffer;//获取到的字节流
private static int maxBuffer = 30000;
private static bool isRun = false;
private static int alread = 0;//已经读取的字节索引
private static int userByte = 0;//已经使用的字节数
public ClientDemo()
{
//client = _client;
//ts = new ThreadStart(run);
//thread = new Thread(ts);
//thread.IsBackground = true;
//thread.Start();
}
public static void StartThread()
{
client = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
IPEndPoint iep = new IPEndPoint(IPAddress.Parse("127.0.0.1"),30000);
try
{
Debug.Log("链接服务器成功");
client.Connect(iep);
}
catch(Exception e)
{
Debug.Log("链接服务器失败"+e.Message);
}
ClientDemo.isRun = true;
ClientDemo.readBuffer = new byte[ClientDemo.maxBuffer];
ClientDemo.ts = new ThreadStart(run);
thread = new Thread(ts);
thread.IsBackground = true;
thread.Start();
}
public static void run()
{
isRun = true;
readBuffer = new byte[maxBuffer];
try
{
client.BeginReceive(readBuffer, 0, maxBuffer, 0, new AsyncCallback(Receive), client);
Console.WriteLine(Encoding.UTF8.GetString(readBuffer));
}
catch (Exception e)
{
Debug.Log("client的run方法"+e.Message);
}
/*while (isRun)
{
}*/
}
/// <summary>
/// 获得回调函数
/// </summary>
/// <param name="ar"></param>
private static void Receive(IAsyncResult ar)
{
Socket socket = (Socket)ar.AsyncState;
//在请求结束的时候获得请求的数据的字节数
int length = 0;
try
{
length = socket.EndReceive(ar);
}
catch (Exception e)
{
Console.WriteLine("client的receive方法" + e.Message);
}
if (length > 0)
{
alread += length;
if (alread > 4)//说明信息里包括头信息是有数据的
{
Dispose();
}
}
try
{
client.BeginReceive(readBuffer, alread, maxBuffer, 0, new AsyncCallback(Receive), client);
}
catch (Exception e)
{
Console.WriteLine(e.Message);
}
}
private static void Dispose()
{
while (true)
{
try
{
//需要先获得请求头,根据头进行数据解析
byte[] head = new byte[4];
Buffer.BlockCopy(readBuffer, userByte, head, 0, 4);//获得从头信息之后开始的需要的数据
int length = BitConverter.ToInt32(head, 0);
Console.WriteLine("获得的头信息的长度为:" + length);
if (alread >= userByte + length + 4)
{
byte[] temp = new byte[length];
Buffer.BlockCopy(readBuffer, userByte + 4, temp, 0, length);
string content = Encoding.UTF8.GetString(temp);
JsonData jd = JsonMapper.ToObject<JsonData>(content);
if (jd != null)
{
//解析数据
DisposePacket(jd);
}
userByte = userByte + length + 4;
if ((alread - userByte) >= 4)
{
continue;
}
else
{
if ((alread - userByte) <= 0)
{
readBuffer = new byte[maxBuffer];
}
else
{
byte[] t = new byte[alread - userByte];
Buffer.BlockCopy(readBuffer, userByte, t, 0, t.Length);
readBuffer = new byte[maxBuffer];
Buffer.BlockCopy(t, 0, readBuffer, 0, t.Length);
}
alread = alread - userByte;
userByte = 0;
break;
}
}
}
catch (Exception e)
{
Console.WriteLine("dispose方法出现了问题" + e.Message);
}
}
}
private static void DisposePacket(JsonData jd)
{
//Console.WriteLine("接收消息,内容为:" + jd.ToJson());
Debug.Log(jd.ToJson());
}
public static void SendMessage(JsonData jd)
{
byte[] content = Encoding.UTF8.GetBytes(jd.ToJson());
byte[] head = new byte[4];
head = BitConverter.GetBytes(content.Length);
byte[] ss = new byte[head.Length + content.Length];
//把头信息写入ss字节里
Buffer.BlockCopy(head, 0, ss, 0, 4);
//把实际数据写入ss字节里,头信息的后面
Buffer.BlockCopy(content, 0, ss, 4, content.Length);
client.Send(ss);
}
public static void Close()
{
isRun = false;
ClientDemo.thread.Abort();
ClientDemo.client.Close();
Debug.Log("关闭线程");
}
方法二
https://www.bilibili.com/video/BV1rJ411D7VJ
使用photon框架,最多支持20人在同一个房间里