前言 上一篇 实现了Unity客户端的TCP通信,这篇把服务端的TCP也实现一下,并使客户端和服务端进行联调。
对于客户端来说,一个应用(一个设备)对应一个Socket。
但服务端不同,一个服务端需要处理许多个客户端的请求,每有一个客户端和服务端成功建立连接都需要创建一个新的socket的对象,这也体现了Tcp协议中一对一通讯的这一特点。
服务端Socket的创建流程和客户端类似,依然需要三个参数
1 Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
不明白每个参数代表什么意思可以看我的上一篇 客户端的部分,里面有详细的介绍。
下面开始服务端Socket的工作流程,这里服务端使用.NetFramework创建控制台应用来进行开发。
建立连接 服务的连接分为以下几个步骤:
调用Bind方法绑定一个IP和端口,成功之后调用Listen方法设置最大监听数,当连接的客户端超过这个数值就不再建立新的连接
另起一个线程并进入阻塞状态,调用Accept方法等待客户端连接
有客户端成功连接,创建对应客户端的消息处理实例,并把该客户端加入一个表中以备广播时使用
服务端程序关闭同时关闭所有客户端的连接
Bind 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 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)); m_Socket.Listen(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)); } } } }
Accept 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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 ; } } }
Abort 1 2 3 4 5 6 7 8 9 10 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挂到一个空物体上,并编写对应的测试代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 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的回调中进行数据派发即可。因为客户端部分已经做了细致的阐述这里就不多赘述,直接上代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 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 ; timer = new Timer(CheckReceiveBuffer, 0 , 0 ,200 ); 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、观察者消息派发、以及心跳机制,详细阐述这些功能是如何实现的。