Unity3D网络游戏实战|02 基本消息收发
异步
客户端
public class AsyncClient : MonoBehaviour
{
//UGUI
public InputField InputFeld;
public Button connect;
public Button send;
//定义套接字
private Socket socket;
//接收缓冲区
private byte[] readBuff = new byte[1024];
private string recvStr = "";
private string sendStr;
private void Start()
{
connect.onClick.AddListener(Connetion);
send.onClick.AddListener(Send);
}
private void Connetion()
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.BeginConnect("127.0.0.1", 8888, ConnectCallback, socket);
}
private void ConnectCallback(IAsyncResult ar)
{
var client = ar.AsyncState as Socket;
client.EndConnect(ar);
Debug.Log("Socket Connect Succ ");
client.BeginReceive( readBuff, 0, 1024, 0, ReceiveCallback, client);
}
private void ReceiveCallback(IAsyncResult ar)
{
var client = ar.AsyncState as Socket;
int count = client.EndReceive(ar);
recvStr = System.Text.Encoding.UTF8.GetString(readBuff, 0, count);
Debug.Log(recvStr);
client.BeginReceive( readBuff, 0, 1024, 0, ReceiveCallback, client);
}
private void Send()
{
//Send
sendStr = InputFeld.text;
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(sendStr);
socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, socket);
}
private void SendCallback(IAsyncResult ar)
{
var client = ar.AsyncState as Socket;
client.EndSend(ar);
}
}
Socket的异步方法中,BeginXXX和EndXXX总是成对的出现
Socket.BeginConnect ( IP , Port , CallBack , State) 等待连接
- void Func(IAsyncResult): CallBack 回调函数
- Object: State 一个想要传入的对象数据,会被放入IAsyncResult.State中
Socket.BeginReceive( Buffer , Offset , Size , SocketFlags , CallBack , State) 等待接受数据
- byte[]: Buffer 用于存储数据
- int: Offset 偏移量,正常就是0
- int: Size 最大可接受字节数
- SocketFLags: SocketFlags 一些标志,这里设置为0
- SocketFlags.None 无
Socket发送缓冲区
每一个Socket内部都有一个发送数据缓冲区,默认为8KB,还没有被服务器确认的数据会留在这里,当缓冲区满时,就会阻塞线程,直到部分数据被确认完之后腾出空间
同样的也有一个接受缓冲区,道理一致
服务端
- 等待请求
- 处理消息
ClientState
一个简单的数据结构,记录所有通讯的socket
public class Server : MonoBehaviour
{
public class ClientState
{
public Socket socket;
public byte[] buff = new byte[1024];
}
private Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();
}
等待请求
public class Server : MonoBehaviour
{
private Socket socket;
private IPAddress ipAdr;
private IPEndPoint ipEp;
private void Start()
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
ipAdr = IPAddress.Parse("127.0.0.1");
ipEp = new IPEndPoint(ipAdr, 8888);
socket.Bind(ipEp);
socket.Listen(0);
// 开始等待Socket请求
socket.BeginAccept(AcceptCallback, socket);
}
private void AcceptCallback(IAsyncResult ar)
{
try
{
var server = (Socket) ar.AsyncState;
var handle = server.EndAccept(ar);
if (clients.TryGetValue(handle, out var state))
{
state.socket = handle;
}
else
{
state = new ClientState();
state.socket = handle;
clients.Add(handle, state);
}
// 开始处理消息
handle.BeginReceive(state.buff, 0, 1024, 0, ReceiveCallback, state);
// 等待下一次请求
server.BeginAccept (AcceptCallback, server);
}
catch (SocketException ex)
{
Console.WriteLine("Socket Accept fail" + ex.ToString());
}
}
}
每一次连接请求,都会触发Accept(哪怕是同一台电脑),而handle就是客户端的Socket连接
处理消息
public class Server : MonoBehaviour
{
private void ReceiveCallback(IAsyncResult ar)
{
try
{
var state = (ClientState) ar.AsyncState;
var handle = state.socket;
int count = handle.EndReceive(ar);
//客户端关闭
if(count == 0)
{
handle.Close();
clients.Remove(handle);
Console.WriteLine("Socket Close");
return;
}
string recvStr = System.Text.Encoding.Default.GetString(state.buff, 0, count);
Debug.Log("客户端发送数据:" + recvStr);
byte[] sendBytes = System.Text.Encoding.Default.GetBytes("服务器发送数据:" + recvStr);
handle.Send(sendBytes);
handle.BeginReceive( state.buff, 0, 1024, 0, ReceiveCallback, state);
}
catch (SocketException ex)
{
Console.WriteLine("Socket Receive fail" + ex.ToString());
}
}
}
测试一下,左边是服务端,右边是客户端
聊天室
客户端
public class ChatClient : MonoBehaviour
{
//定义套接字
private Socket socket;
//接收缓冲区
private bool isChange = false;
private byte[] buffer = new byte[1024];
private StringBuilder sb = new StringBuilder();
private void Start()
{
connect.onClick.AddListener(Connetion);
send.onClick.AddListener(Send);
}
private void Connetion()
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
socket.BeginConnect("127.0.0.1", 8888, ConnectCallback, socket);
}
private void ConnectCallback(IAsyncResult ar)
{
var client = ar.AsyncState as Socket;
client.EndConnect(ar);
SendMsg("客户端1 加入聊天");
client.BeginReceive( buffer, 0, 1024, 0, ReceiveCallback, client);
}
// ============ 接受数据 ============
private void ReceiveCallback(IAsyncResult ar)
{
var client = ar.AsyncState as Socket;
int count = client.EndReceive(ar);
AddMsg(Encoding.UTF8.GetString(buffer, 0, count));
client.BeginReceive( buffer, 0, 1024, 0, ReceiveCallback, client);
}
private void AddMsg(string msg)
{
sb.Append($"{msg}\n");
isChange = true;
}
private void Update()
{
if (isChange)
{
text.text = sb.ToString();
isChange = false;
}
}
// ============ 发送数据 ============
private void Send()
{
SendMsg(inputFeld.text);
}
private void SendMsg(string msg)
{
byte[] sendBytes = System.Text.Encoding.Default.GetBytes(msg);
socket.BeginSend(sendBytes, 0, sendBytes.Length, 0, SendCallback, socket);
}
private void SendCallback(IAsyncResult ar)
{
var client = ar.AsyncState as Socket;
client.EndSend(ar);
}
}
服务端
服务端的程序同上
测试
客户端1:
客户端2:
服务端:
Poll状态监测
异步程序很好,但可能引发很多问题,比如UI不能再子线程中操作UI
相比之下同步代码就简单安全,问题是同步代码会阻塞线程,我们不可能接受他卡在那里
对于接受数据而言,Receive就是一个阻塞方法,但实际上我们不需要一直等待Reveice,只要socket有数据时再去Receive,就不会一直卡主了
if(socket.有数据)
{
socket.receive
}
那么如何检查socket是否有数据呢,微软提供了一个Poll方法
public bool Pool (int microSeconds,SelectMode mode)
- int : 等待响应时间,以微秒为单位,-1为永远等待,0为不等待
- SelectMode
- SelectRead : 判断socket是否可读
- SelectWrite : 判断socket是否可写
- SelectError : 判断是否有错误
修改客户端
其他不用修改,只需要更改一下Update
private void Update()
{
if (socket == null)
{
return;
}
if (socket.Poll(0, SelectMode.SelectRead))
{
byte[] readBuff = new byte[1024];
int count = socket.Receive(readBuff);
string recvStr = Encoding.Default.GetString(readBuff, 0, count);
sb.Append($"{recvStr}\n");
text.text = sb.ToString();
}
}
很简单,就在Update里不断判断socket是否有数据,有数据就开始读取
修改服务端
类似客户端,之前我们服务端是写在Unity生命周期里的,但实际上服务器肯定不可能运行一个Unity,我们直接改成C#代码
首先还是一个数据类,用于存储客户端的Socket以及一个存储发送数据的字节数组
namespace Server
{
public class ClientState
{
public Socket socket;
public byte[] buff = new byte[1024];
}
}
然后是正式的服务类,首先是Init的方法,我们先创建服务端Socket,只有用一个while循环不断检测是否有连接请求
namespace Server
{
internal class Server : IDisposable
{
// IP地址
private const string IP = "127.0.0.1";
// 端口号
private const int HOST = 8888;
// 服务器Socket
private Socket server;
// 客户端Socket及状态信息
private Dictionary<Socket, ClientState> clients = new Dictionary<Socket, ClientState>();
public void Init()
{
//Socket
server = new Socket(AddressFamily.InterNetwork,SocketType.Stream, ProtocolType.Tcp);
//Bind
var ipAdr = IPAddress.Parse(IP);
var ipEp = new IPEndPoint(ipAdr, HOST);
server.Bind(ipEp);
//Listen
server.Listen(0);
Console.WriteLine("[服务器]启动成功");
//主循环
while (true)
{
//检查listenfd
if (server.Poll(0, SelectMode.SelectRead))
{
AcceptClient(server);
}
//检查clientfd
foreach (var state in clients.Values)
{
var client = state.socket;
if (client.Poll(0, SelectMode.SelectRead))
{
if (!ReadClientfd(client))
{
break;
}
}
}
//防止CPU占用过高
System.Threading.Thread.Sleep(1);
}
}
public void Dispose()
{
if (server != null)
{
Console.WriteLine("[服务器]关闭");
server.Dispose();
}
}
}
}
如果有连接请求,那么将连接到的客户端程序放入字典中
namespace Server
{
internal class Server : IDisposable
{
//读取Listenfd
public void AcceptClient(Socket server)
{
Console.WriteLine("Accept");
var clientfd = server.Accept();
var state = new ClientState();
state.socket = clientfd;
clients.Add(clientfd, state);
}
}
}
然后依次判断每个客户端是否有数据发送,最后广播
namespace Server
{
internal class Server : IDisposable
{
//读取Clientfd
public bool ReadClientfd(Socket clientfd)
{
ClientState state = clients[clientfd];
//接收
int count = 0;
try
{
count = clientfd.Receive(state.buff);
}
catch (SocketException ex)
{
clientfd.Close();
clients.Remove(clientfd);
Console.WriteLine("Receive SocketException " + ex.ToString());
return false;
}
//客户端关闭
if (count == 0)
{
clientfd.Close();
clients.Remove(clientfd);
Console.WriteLine("Socket Close");
return false;
}
//广播
string recvStr = Encoding.UTF8.GetString(state.buff, 0, count);
Console.WriteLine("Receive" + recvStr);
string sendStr = clientfd.RemoteEndPoint.ToString() + ":" + recvStr;
byte[] sendBytes = Encoding.UTF8.GetBytes(sendStr);
foreach (ClientState cs in clients.Values)
{
cs.socket.Send(sendBytes);
}
return true;
}
}
}
Select 多路复用
Poll很好的解决了异步编程的一些问题,但把逻辑放在while里,会造成CPU的很大消耗,于是又出现了Select,可以一次性检测多个Socket的状态
public static void Select(
IList checkRead,
IList check Write,
IList checkError,
int microSeconds
)
很好理解,传入一个socket数组,一次性检测所有符合要求的socket
服务端
服务端需要做出一些修改,一次性检测所有客户端,主要体现在while里
namespace Server
{
internal class Server : IDisposable
{
// 多路复用
List<Socket> checkRead = new List<Socket>();
public void Init()
//主循环
while (true)
{
//填充checkRead列表
checkRead.Clear();
checkRead.Add(server);
foreach (var state in clients.Values)
{
checkRead.Add(state.socket);
}
//select
Socket.Select(checkRead, null, null, 1000);
//检查可读对象
foreach (var socket in checkRead)
{
if (socket == server)
{
AcceptClient(socket);
}
else
{
ReadClientfd(socket);
}
}
}
}
}
}
客户端
客户端因为只连接了一个服务器,所以无须修改了