上一篇实现了Unity客户端的Tcp通讯,这篇把服务端的Tcp也实现一下,并使客户端和服务端进行联调。
对于客户端来说,一个应用(一个设备)对应一个Socket。
但服务端不同,一个服务端需要处理许多个客户端的请求,每有一个客户端和服务端成功建立连接都需要创建一个新的socket的对象,这也体现了Tcp协议中一对一通讯的这一特点。
服务端Socket的创建流程和客户端类似,依然需要三个参数
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
不明白每个参数代表什么意思可以看我的上一篇客户端的博客,里面有详细的介绍。
下面开始服务端Socket的工作流程(这里创建使用.NetFramework的控制台应用即可)
连接部分
1.调用Bind方法绑定一个ip和端口,也就是说客户端只有向这个ip和端口发起请求服务端才能接收到
2.Bind成功之后,调用Listen方法设置最大监听数,当连接的客户端超过这个数值就不再建立新的连接
static void Main(string[] args)
{
m_Socket = new Socket(AddressFamily.InterNetwork,SocketType.Stream,ProtocolType.Tcp);
m_AcceptThread = new Thread(OnAccept);//另起一个线程进入阻塞状态等待客户端连接
m_Clients = new List<RoleClient>();//所有连接的客户端
m_Socket.Bind(new IPEndPoint(IPAddress.Parse(m_IP), m_Port));//绑定ip端口
m_Socket.Listen(50);//最大监听数50
m_AcceptThread.Start();//进入等待连接状态
AppDomain.CurrentDomain.ProcessExit += OnApplicatonQuit;
Console.WriteLine("服务器启动成功!");
Console.WriteLine("监听IP:" + m_IP + ",端口:" + m_Port);
//广播代码
while(true)
{
string str = Console.ReadLine();
if (string.IsNullOrEmpty(str)) continue;
if (str.Equals("close all"))
{
for (int i = m_Clients.Count - 1; i >= 0 ; i--)
{
m_Clients[i].Close(true);
}
}
else
{
for (int i = 0; i < m_Clients.Count; i++)
{
m_Clients[i].Send(1, Encoding.UTF8.GetBytes(str));
}
}
}
}
3.另起一个线程并进入阻塞状态,等待客户端连接
4.有客户端成功连接,创建对应客户端的消息处理实例,并把该客户端加入一个表中以备广播时使用
static void OnAccept()
{
while(true)
{
try
{
Socket client = m_Socket.Accept();//尝试接收一个客户端的连接
IPEndPoint clientPoint = client.RemoteEndPoint as IPEndPoint;
m_Clients.Add(new RoleClient(client, m_Clients));//为客户端建立一个请求处理实例,并加入到表中
Console.WriteLine("客户端:"+ clientPoint.Address.ToString() +"已经连接!");
}
catch
{
continue;
}
}
}
5. 服务端程序关闭同时关闭所有客户端的连接
private static void OnApplicatonQuit(object sender, EventArgs e)
{
for (int i = m_Clients.Count - 1; i > -1 ; i--)
{
m_Clients[i].Close();
}
m_AcceptThread.Abort();
m_Clients.Clear();
}
到这里客户端请求连接,服务端收到连接并创建对应的请求实例的功能已经实现完了,下面开始联调一下试试
先启动服务器,如图,成功监听了本地127.0.0.1的ip和8888端口
然后客户端制作一个测试的界面,把SocketMgr挂到一个空物体上,并编写对应的测试代码
public Button button;
public InputField inputField;
void Start ()
{
button.onClick.AddListener(onClick);
inputField.gameObject.SetActive(false);
SocketMgr.Instance.OnConnectSuccess = delegate ()
{
inputField.gameObject.SetActive(true);
button.transform.Find("Text").GetComponent<Text>().text = "发送";
};
SocketMgr.Instance.OnDisConnect = delegate ()
{
inputField.gameObject.SetActive(false);
button.transform.Find("Text").GetComponent<Text>().text = "连接";
};
SocketMgr.Instance.onReceive = OnReceive;
}
private void OnReceive(ushort arg1, byte[] arg2)
{
inputField.text = arg1 + "," + Encoding.UTF8.GetString(arg2);
}
private void onClick()
{
if(!SocketMgr.Instance.IsConnected)
{
SocketMgr.Instance.Connect("127.0.0.1", 8888);
return;
}
SocketMgr.Instance.Send(1, Encoding.UTF8.GetBytes(inputField.text));
}
SocketMgr就是我上一篇客户端部分封装的Socket通讯框架,具体的代码已经全部都贴到到了博客中,如果想自己试试的可以直接去复制来用。
接下来点击运行,见证奇迹的时刻就要到了
点击连接按钮就会同过connect方法向服务端发送建立连接的请求,这里成功的建立的了连接,此处应有掌声雷动.
拆包、发包部分
服务端拆包和发包与客户端基本类似,不同的地方就在于服务端不存在主线程和非主线程这种区分,拆包后直接在BeginReceive的回调中进行数据派发即可。因为客户端部分已经做了细致的阐述,这里就不多做说明了,直接上代码
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Server
{
public class RoleClient
{
Timer timer;
public RoleClient(Socket socket, List<RoleClient> otherClients)
{
m_OtherClients = otherClients;
m_ReceiveBuffer = new byte[1024 * 512];
m_ReceiveStream = new MemoryStream();
m_ReceiveQueue = new Queue<byte[]>();
m_SendQueue = new Queue<byte[]>();
m_Socket = socket;
m_IsConnected = true;
//m_ReceiveThread = new Thread(CheckReceive);
timer = new Timer(CheckReceiveBuffer, 0, 0,200);
//m_ReceiveThread.Start();
StartReceive();
}
public void Send(ushort msgCode, byte[] buffer)
{
byte[] sendMsgBuffer = null;
using (MemoryStream ms = new MemoryStream())
{
int msgLen = buffer.Length;
byte[] lenBuffer = BitConverter.GetBytes((ushort)msgLen);
byte[] msgCodeBuffer = BitConverter.GetBytes(msgCode);
ms.Write(lenBuffer, 0, lenBuffer.Length);
ms.Write(msgCodeBuffer, 0, msgCodeBuffer.Length);
ms.Write(buffer, 0, msgLen);
sendMsgBuffer = ms.ToArray();
}
lock (m_SendQueue)
{
m_SendQueue.Enqueue(sendMsgBuffer);
CheckSendBuffer();
}
}
public void Close(bool isForce = false)
{
try { m_Socket.Shutdown(SocketShutdown.Both); }
catch { }
if (isForce)
{
IPEndPoint endPoint = m_Socket.RemoteEndPoint as IPEndPoint;
Console.WriteLine("强制关闭与客户端:" + endPoint.Address.ToString() + "的连接");
}
m_IsConnected = false;
m_Socket.Close();
m_ReceiveStream.SetLength(0);
m_ReceiveQueue.Clear();
m_SendQueue.Clear();
timer.Dispose();
if (m_OtherClients != null)
{
m_OtherClients.Remove(this);
}
timer = null;
m_SendQueue = null;
m_ReceiveQueue = null;
m_ReceiveStream = null;
m_ReceiveBuffer = null;
m_OtherClients = null;
}
private void StartReceive()
{
if (!m_IsConnected) return;
m_Socket.BeginReceive(m_ReceiveBuffer, 0, m_ReceiveBuffer.Length, SocketFlags.None, OnReceive, m_Socket);
}
private void OnReceive(IAsyncResult ir)
{
if (!m_IsConnected) return;
try
{
int length = m_Socket.EndReceive(ir);
if(length < 1)
{
IPEndPoint endPoint = m_Socket.RemoteEndPoint as IPEndPoint;
Console.WriteLine("客户端:" + endPoint.Address.ToString() + "已断开连接");
Close();
return;
}
m_ReceiveStream.Position = m_ReceiveStream.Length;
m_ReceiveStream.Write(m_ReceiveBuffer, 0, length);
if (m_ReceiveStream.Length < 3)
{
StartReceive();
return;
}
while (true)
{
m_ReceiveStream.Position = 0;
byte[] msgLenBuffer = new byte[2];
m_ReceiveStream.Read(msgLenBuffer, 0, 2);
int msgLen = BitConverter.ToUInt16(msgLenBuffer, 0) + 2;
int fullLen = 2 + msgLen;
if (m_ReceiveStream.Length < fullLen)
{
break;
}
byte[] msgBuffer = new byte[msgLen];
m_ReceiveStream.Position = 2;
m_ReceiveStream.Read(msgBuffer, 0, msgLen);
lock (m_ReceiveQueue)
{
m_ReceiveQueue.Enqueue(msgBuffer);
}
int remainLen = (int)m_ReceiveStream.Length - fullLen;
if (remainLen < 1)
{
m_ReceiveStream.Position = 0;
m_ReceiveStream.SetLength(0);
break;
}
m_ReceiveStream.Position = fullLen;
byte[] remainBuffer = new byte[remainLen];
m_ReceiveStream.Read(remainBuffer, 0, remainLen);
m_ReceiveStream.Position = 0;
m_ReceiveStream.SetLength(0);
m_ReceiveStream.Write(remainBuffer, 0, remainLen);
remainBuffer = null;
}
}
catch
{
IPEndPoint endPoint = m_Socket.RemoteEndPoint as IPEndPoint;
Console.WriteLine("客户端:" + endPoint.Address.ToString() + "已断开连接");
Close();
return;
}
StartReceive();
}
private void CheckSendBuffer()
{
lock (m_SendQueue)
{
if (m_SendQueue.Count > 0)
{
byte[] buffer = m_SendQueue.Dequeue();
m_Socket.BeginSend(buffer, 0, buffer.Length, SocketFlags.None, SendCallback, m_Socket);
}
}
}
private void CheckReceiveBuffer(object state)
{
lock (m_ReceiveQueue)
{
if (m_ReceiveQueue.Count < 1) return;
byte[] buffer = m_ReceiveQueue.Dequeue();
byte[] msgContent = new byte[buffer.Length - 2];
ushort msgCode = 0;
using (MemoryStream ms = new MemoryStream(buffer))
{
byte[] msgCodeBuffer = new byte[2];
ms.Read(msgCodeBuffer, 0, msgCodeBuffer.Length);
msgCode = BitConverter.ToUInt16(msgCodeBuffer, 0);
ms.Read(msgContent, 0, msgContent.Length);
}
Console.WriteLine("消息编号:" + msgCode + ",内容:" + Encoding.UTF8.GetString(msgContent));
}
}
private void SendCallback(IAsyncResult ir)
{
m_Socket.EndSend(ir);
CheckSendBuffer();
}
private bool m_IsConnected = false;
private Queue<byte[]> m_ReceiveQueue = null;
private Queue<byte[]> m_SendQueue = null;
private MemoryStream m_ReceiveStream = null;
private byte[] m_ReceiveBuffer = null;
private Socket m_Socket = null;
private List<RoleClient> m_OtherClients = null;
}
}
接下来进行最后的测试,即客户端和服务端相互通讯。
测试部分
客户端输入任意字符串,然后点击发送,这里消息编码写死为1,但实际开发中每一个消息都有自己的编号要根据实际情况来决定要发送哪条消息。
服务端成功的接收到数据并解析出消息编码和具体内容
服务端输入任意字符串,看看客户端能否接到消息
客户端也成功的解析出了内容
服务端输入close all看能否跟客户端断开连接
客户端打印日志,断开连接
强制关闭客户端,服务端也能检测到客户端的断开
到这里Tcp通讯的客户端和服务端已经基本实现完了。
但是实际开发中,通讯内容可不仅仅是字符串,而是十分复杂的一些数据结构。检测客户端断开也不能单单凭借endreceive的长度为0就确定客户端断开,各个模块间的数据要分别派发不能造成耦合,那这些是怎么实现的呢?
下面几篇我就依次来进行序列化工具Protobuf、观察者消息派发、以及心跳机制的讲解,详细阐述这些功能是如何实现的。