上一篇文章将网络连接改为异步方式,但仍有不足之处。
异步实际上属于多线程的操作,这可能会造成线程问题,Select多路复用的方式可以同时监听多个客户端Socket列表,如果有可读的socket则返回,否则阻塞,这样不会造成线程问题,且不会造成CPU占用过高。
服务器多路复用Select:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.Reflection;
using UnityEngine;
namespace Server
{
class ClientState
{
public Socket socket;
public Byte[] readBuff = new Byte[1024];
public int buffCount;
}
public static class ServerManager
{
static Socket listenfd;
static Dictionary clients = new Dictionary();
public static void InitServer(string ip,string port)
{
listenfd = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
IPEndPoint ipEp;
if (!string.IsNullOrEmpty(ip))
{
IPAddress ipAdr = IPAddress.Parse(ip);
ipEp = new IPEndPoint(ipAdr, int.Parse(port));
} else
{
ipEp = new IPEndPoint(IPAddress.Any, int.Parse(port));
}
listenfd.Bind(ipEp);
listenfd.Listen(0);
Debug.Log("Server Ready");
List checkRead = new List();
while (true)
{
checkRead.Clear();
checkRead.Add(listenfd);
foreach (ClientState cs in clients.Values)
{
checkRead.Add(cs.socket);
}
Socket.Select(checkRead, null, null, 1000);//多路复用,检测是否有可读或可写
foreach (Socket s in checkRead)
{
if (s == listenfd)
{
ReadListenfd(s);
}
else
{
ReadClientfd(s);
}
}
}
}
static void ReadListenfd(Socket listenfd)
{
Debug.Log("accept client");
Socket clientfd = listenfd.Accept();
ClientState state = new ClientState();
state.socket = clientfd;
clients.Add(clientfd, state);
}
static bool ReadClientfd(Socket client)
{
ClientState state = clients[client];
int count;
try
{
count = client.Receive(state.readBuff, state.buffCount, 1024 - state.buffCount, 0);
state.buffCount += count;
}
catch (SocketException ex)
{
client.Close();
clients.Remove(client);
Debug.Log("client receive fail");
return false;
}
if (count == 0)
{
client.Close();
clients.Remove(client);
Debug.Log("client receive fail");
return false;
}
OnReceiveData(state);
return true;
}
static void OnReceiveData(ClientState state)
{
Socket client = state.socket;
int buffCount = state.buffCount;
if (buffCount < 2)
{
return;
}
int bodyLength = BitConverter.ToInt16(state.readBuff, 0);//从第一位开始,取前两个
if (buffCount < 2 + bodyLength)
{
return;
}
string recStr = Encoding.Default.GetString(state.readBuff, 2, bodyLength);
Debug.Log("收到消息:"+ recStr);
foreach (ClientState s in clients.Values)
{
byte[] sendBytes = Encoding.Default.GetBytes(recStr);
s.socket.Send(sendBytes);
Debug.Log("向客户端发送" + recStr);
}
//继续处理剩余字节
int start = 2 + bodyLength;
int count = buffCount - start;
Array.Copy(state.readBuff, start, state.readBuff, 0, count);
state.buffCount -= start;
OnReceiveData(state);
}
}
}
Socket.Select(checkRead, null, null, 1000);//多路复用,检测是否有可读或可写
Select方式,对socket列表进行监听,没有可读的socket时会阻塞。
ReadListenfd函数负责监听新连接进来的客户端,ReadClientfd负责接收客户端发来的消息。
在上述代码中可以看到对ClientState类新增了buffCount字段,该字段记录了收到客户端传来消息的有效长度,这是为了解决粘包问题的改进。在接收数据时可能产生粘包问题,常规的解决办法有固定长度法,即每次发送协议内容长度固定,还有在发送的信息前标明长度,本文主要介绍在发送的协议头部增加主体消息长度的方式。
在原始的客户端发送协议前拼接该协议长度, 消息长度+消息,以此方式组装成新的协议发送,服务器收到消息后解析消息长度,若收到的消息长度不够,则等待接收到新的消息后在解析。
count = client.Receive(state.readBuff, state.buffCount, 1024 - state.buffCount, 0);
state.buffCount += count;
上述代码记录收到消息的长度。
static void OnReceiveData(ClientState state)
{
Socket client = state.socket;
int buffCount = state.buffCount;
if (buffCount < 2)//消息长度占两个位,若收到消息小于2,则没有收到完整消息
{
return;
}
int bodyLength = BitConverter.ToInt16(state.readBuff, 0);//从[0]位去两位(ToInt16),获得主体消息长度
if (buffCount < 2 + bodyLength)
{
return;
}
string recStr = Encoding.Default.GetString(state.readBuff, 2, bodyLength);
Debug.Log("收到消息:"+ recStr);
foreach (ClientState s in clients.Values)
{
byte[] sendBytes = Encoding.Default.GetBytes(recStr);
s.socket.Send(sendBytes);
Debug.Log("向客户端发送" + recStr);
}
//继续处理剩余字节
int start = 2 + bodyLength;
int count = buffCount - start;
Array.Copy(state.readBuff, start, state.readBuff, 0, count);//将后面的数据前移
state.buffCount -= start;
OnReceiveData(state);
}
上述代码为解析收到的消息,若收到的数据长度小于2,或小于消息长度,则收到消息不完整,等待下次处理。
本文介绍了多路复用以及收到消息粘包问题的解决,但除了收到消息的粘包问题,发送消息也会有不完成的情况发生,下一篇将会介绍。