一个简易socket通信结构
服务端
基本的结构
工作需要又需要用到socketTCP通讯,这么多年了,终于稍微能写点了。让我说其实也说不出个啥来,看了很多的异步后稍微对异步socket的导流 endreceive后 再beginreceive 形成一个内循环有了个认识,加上我自己的封包拆包机制,然后再仿那些其它的大多数代码结构弄点onReceive事件进行 收包触发。整个过程就算差不多了 ,基本上是能够可靠运行的 靠谱的 中规中矩的,要说啥创新独到嘛真的谈不上。代码中写了很多low逼注释 也是为了方便自己理解 请无视。下面是server端代码,使用异步机制accept 异步receive ,成员有 clients代表当前在线的客户端 客户端socket包装为EndpointClient ,有onClientAddDel 代表客户端上线掉线事件,有onReceive代表所有客户端的收包事件,clients由于是异步的多线程访问就要涉及多线程管控 所以使用lock ,服务端有sendTo() 毫无疑问这也是通过调用特定的clients来做的。关于close() 网上说socket最好不要直接close ,但是我这里就是简单粗暴的直接close要不然 系统底层socket迟迟不释放 服务端再开的时候端口冲突 ,制造各种问题 ,何况都close了客户端也会断开连接 还会管你数据报文有无接收完整吗,所以不要人云亦云 ,遇到问题了再说 ,结合自己的理解。
然后关于多报文格式的一些补充
这部分其实是后来写了更新上来的:从我们的报文数据定义为 Telegram_Base 就可看的出来,本意是想做成可扩展的,不要管Telegram_Base是什么往后面看你就明白了。并且硬性需求 因为不止一个场景使用我们不可能把报文格式固定住 肯定得做成可扩展的。为此 我们使用模板编程方式 ,好久没有使用模板编程了 ,得复习下 记得上次使用还是《angularjs和ajax的结合使用(四)》里面的 DownloadSomething<T> ,其实我也是半瓢水 ,哈哈哈。
MsgServer 和 MsgClient 初始化的时候 传入不同的泛型 ,则Receive的时候根据不同的泛型解数据包 进而触发 onReceive 事件,你要问为什么就想到这么做可以呢,一句话自然使然 水到渠成,善于观察借鉴 。我上面的处理方式,包括用过的supersocket 也都是这种处理方式。好 改造开始,server和client都变成这样,总之一切都是围绕泛型报文展开的,包括send 和receive都要变成send<T> receive<T> 这样的
整体的类图结构:
左边代表服务端,初始化示例的时候可以通过泛型类方式 绑定Telegram_xxx等不同的 报文处理过程 ,服务端有个全局的缓冲字节receivedbuffer ,收到字节时调用不同报文对应的解析器解析,每当解析到完整包的时候通过action回调执行对应的用户自定义代码,右边是客户端 发送时自然跟服务端报文对应。 大概就这样,整体设计跟上面所描述一致,不太擅长画图 也不知道表述清楚没有。
以下是服务端代码
1 //消息服务器 2 public class MsgServer<T> where T : Telegram_Base 3 { 4 5 6 Socket serverSocket; 7 public Action<List<string>> onClientAddDel; 8 public Action<T> onReceive; 9 bool _isRunning = false; 10 11 int port; 12 13 static List<EndpointClient<T>> clients; 14 15 public bool isRunning { get { return _isRunning; } } 16 public MsgServer(int _port) 17 { 18 this.port = _port; 19 20 clients = new List<EndpointClient<T>>(); 21 22 } 23 24 public void Start() 25 { 26 try 27 { 28 //any 就决定了 ip地址格式是v4 29 //IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, 7654); 30 //socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 31 IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, port); 32 serverSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); 33 serverSocket.Bind(endPoint); 34 serverSocket.Listen(port); 35 36 serverSocket.BeginAccept(new AsyncCallback(AcceptCallback), serverSocket); 37 38 _isRunning = true; 39 40 Logger.GetInstance().AppendMessage(LogLevel.Info, "消息服务启动完成"); 41 } 42 catch (Exception ex) 43 { 44 _isRunning = false; 45 serverSocket = null; 46 47 Logger.GetInstance().AppendMessage(LogLevel.Error, ex.Message); 48 } 49 50 } 51 52 public void Stop() 53 { 54 for (int i = 0; i < clients.Count; i++) 55 { 56 clients[i].Close(); 57 } 58 ClientAddDelGetList(null, EndPointClientsChangeType.ClearAll); 59 serverSocket.Close(); 60 _isRunning = false; 61 } 62 63 64 public void SendTo(T tel) 65 { 66 try 67 { 68 if (string.IsNullOrEmpty(tel.remoteIPPort))//发送到所有 69 { 70 for (int i = 0; i < clients.Count; i++) 71 { 72 clients[i].Send(tel); 73 } 74 } 75 else 76 { 77 for (int i = 0; i < clients.Count; i++)//发送到某个客户端 78 { 79 if (clients[i].remoteIPPort == tel.remoteIPPort) 80 { 81 clients[i].Send(tel); 82 break; 83 } 84 } 85 } 86 87 } 88 catch (Exception ex) 89 { 90 Logger.GetInstance().AppendMessage(LogLevel.Error, ex.Message); 91 } 92 } 93 94 //新增与删除客户端 秉持原子操作 95 List<string> ClientAddDelGetList(EndpointClient<T> cli, EndPointClientsChangeType changeType) 96 { 97 //异步同时有新客户端上线 与下线 不进行资源互斥访问会报错 98 lock (this) 99 { 100 if (changeType == EndPointClientsChangeType.Add) 101 { 102 clients.Add(cli); 103 } 104 else if (changeType == EndPointClientsChangeType.Del) 105 { 106 var beRemoveClient = clients.First(r => r.remoteIPPort == cli.remoteIPPort); 107 if (beRemoveClient != null) 108 clients.Remove(beRemoveClient); 109 } 110 else if (changeType == EndPointClientsChangeType.ClearAll) 111 { 112 clients.Clear(); 113 } 114 else if (changeType == EndPointClientsChangeType.GetAll) 115 { 116 List<string> onLines = new List<string>(clients.Count); 117 for (int i = 0; i < clients.Count; i++) 118 { 119 onLines.Add(clients[i].remoteIPPort); 120 } 121 return onLines; 122 } 123 else 124 { 125 return null; 126 } 127 } 128 return null; 129 } 130 //异步监听客户端 有客户端到来时的回调 131 private void AcceptCallback(IAsyncResult iar) 132 { 133 //server端一直在receive 能够感知到客户端掉线 (连上后 关闭客户端 server立即有错误爆出) 134 //但是同情况 关闭server端 客户端无错误爆出 直到点发送 才有错误爆出 135 //由此得出 处于receive才会有掉线感知 ,send时发现发不出去自然也会有感知 跟人的正常思维理解是一样的 136 //虽然tcp是所谓的长连接 ,通过反复测试 ->但是双方相互都处在一个静止状态 是无法 确定在不在的 137 //连上后平常的情况下 并没有数据流通 的 ,双方只是一个状态的保持而已。 138 //这也是为什么 好多服务 客户端 程序 都有个心跳机制(由此我们可以想到继续完善 弄个客户端列表 心跳超时的剔除列表 正常发消息send 或receive 异常的剔除列表 删除clientSocket 139 //其实非要说吧 一般情况 服务端一直在receive 不用心跳其实也是可以的(客户端可能是真的需要 140 //tcp底层就已经有了一个判断对方在不在的机制 , 对方直接关程序 结束进程 这些 只要tcp在receive就立即能够感知 所以说心跳 用不用看情况吧 141 142 //tcp 不会丢包 哪怕是连接建立了 你还没开始receive 对方却先发了, 143 //对方只要是发了的数据 都由操作系统像个缓存样给你放那的 不会掉 你再隔10秒开始receive都能rec的到 144 145 //tcp甚至在拔掉网线 再重新插上 都可以保证数据一致性 146 //tcp的包顺序能够保证 先发的先到 147 148 //nures代码中那些beginreceivexxx 异步receive的核心机制就是 ,假定数据到的时候把数据保存到xxx数组 149 //真正endreceive的时候 其实数据已经接收 处理完成了 150 try 151 { 152 153 //处理完当前accept 154 Socket currentSocket = serverSocket.EndAccept(iar); 155 156 EndpointClient<T> client = new EndpointClient<T>(currentSocket); 157 158 //新增客户端 159 ClientAddDelGetList(client, EndPointClientsChangeType.Add); 160 161 if (onClientAddDel != null) 162 { 163 List<string> onlines = ClientAddDelGetList(null, EndPointClientsChangeType.GetAll); 164 onClientAddDel(onlines); 165 166 //客户端异常掉线 167 client.onClientDel = new Action<string>((_remoteIPPort) => 168 { 169 ClientAddDelGetList(new EndpointClient<T>() { remoteIPPort = _remoteIPPort }, EndPointClientsChangeType.Del); 170 171 List<string> onlines2 = ClientAddDelGetList(null, EndPointClientsChangeType.GetAll); 172 onClientAddDel(onlines2); 173 }); 174 } 175 176 177 Logger.GetInstance().AppendMessage(LogLevel.Debug, string.Format("new client ->{0}", currentSocket.RemoteEndPoint.ToString())); 178 //Console.WriteLine(string.Format("new client ->{0}", currentSocket.RemoteEndPoint.ToString())); 179 180 //currentSocket.Close(); 181 //Application.Exit(); 182 183 //Thread.Sleep(1000 * 10); 184 client.onReceive += this.onReceive; 185 186 client.BeginReceive(); 187 188 189 //立即开始accept新的客户端 190 if (isRunning == true && serverSocket != null) 191 serverSocket.BeginAccept(AcceptCallback, serverSocket); 192 //beginAccept 最开始的方法可以不一样 ,但最终肯定是一个不断accept的闭环过程 193 //整个过程就像个导流样 ,最开始用异步导流到一个固定的点 然后让其循环源源不断运转 194 195 } 196 catch (Exception ex) 197 { 198 Logger.GetInstance().AppendMessage(LogLevel.Error, ex.Message); 199 Console.WriteLine(ex.Message); 200 } 201 202 } 203 204 205 }
EndpointClient终端代码代表客户端的对口人,他的onReceive 等资源从服务端继承 ,如果服务端想给某个特定客户端发数据则会调用他们中的某一个 毫无疑问这是通过remoteIPport来判断的,这些都是编写基本socket结构轻车熟路的老套路
以下EndpointClient代码
1 public class EndpointClient<T> where T : Telegram_Base 2 { 3 Socket workingSocket; 4 static int receiveBufferLenMax = 5000; 5 byte[] onceReadDatas = new byte[receiveBufferLenMax]; 6 List<byte> receiveBuffer = new List<byte>(receiveBufferLenMax); 7 8 public string remoteIPPort { get; set; } 9 10 //当前残留数据区 长度 11 int receiveBufferLen = 0; 12 13 14 15 public Action<T> onReceive; 16 public Action<string> onClientDel; 17 18 public EndpointClient() 19 { 20 21 } 22 public EndpointClient(Socket _socket) 23 { 24 this.remoteIPPort = _socket.RemoteEndPoint.ToString(); 25 workingSocket = _socket; 26 } 27 28 public void Send(T tel) 29 { 30 //try 31 //{ 32 if (workingSocket == null) 33 { 34 Console.WriteLine("未初始化的EndpointClient"); 35 return; 36 } 37 if (tel is Telegram_Schedule) 38 { 39 Telegram_Schedule telBeSend = tel as Telegram_Schedule; 40 if (telBeSend.dataBytes.Length != telBeSend.dataLen) 41 { 42 Console.WriteLine("尝试发送数据长度格式错误的报文"); 43 return; 44 } 45 46 byte[] sendBytesHeader = telBeSend.dataBytesHeader; 47 byte[] sendbytes = telBeSend.dataBytes; 48 49 //数据超过缓冲区长度 会导致无法拆包 50 if (sendbytes.Length <= receiveBufferLenMax) 51 { 52 workingSocket.BeginSend(sendBytesHeader, 0, sendBytesHeader.Length, 0, null, null); 53 workingSocket.BeginSend(sendbytes, 0, sendbytes.Length, 0, null, null 54 // new AsyncCallback((iar) => 55 //{ 56 // EndpointClient endClient = (EndpointClient)iar.AsyncState; 57 // endClient.workingSocket.EndSend(iar); 58 //}), 59 //this 60 ); 61 } 62 else 63 { 64 Console.WriteLine("发送到调度客户端的数据超过缓冲区长度"); 65 throw new Exception("发送到调度客户端的数据超过缓冲区长度"); 66 } 67 68 69 } 70 else if (tel is Telegram_SDBMsg) 71 { 72 Telegram_SDBMsg telBesendSDB = tel as Telegram_SDBMsg; 73 if (telBesendSDB.dataBytes.Length != telBesendSDB.dataLen) 74 { 75 Console.WriteLine("尝试发送数据长度格式错误的报文"); 76 return; 77 } 78 byte[] sendBytesHeader = telBesendSDB.dataBytesHeader; 79 byte[] sendbytes = telBesendSDB.dataBytes; 80 81 //数据超过缓冲区长度 会导致无法拆包 82 if (sendbytes.Length <= receiveBufferLenMax) 83 { 84 workingSocket.BeginSend(sendBytesHeader, 0, sendBytesHeader.Length, 0, null, null); 85 workingSocket.BeginSend(sendbytes, 0, sendbytes.Length, 0, null, null 86 87 ); 88 } 89 else 90 { 91 Console.WriteLine("发送到调度客户端的数据超过缓冲区长度"); 92 throw new Exception("发送到调度客户端的数据超过缓冲区长度"); 93 } 94 } 95 96 //} 97 //catch (Exception ex) 98 //{ 99 100 // Console.WriteLine(ex.Message); 101 // throw ex; 102 //} 103 } 104 105 public void BeginReceive() 106 { 107 if (workingSocket == null) 108 { 109 Console.WriteLine("未初始化的EndpointClient"); 110 return; 111 } 112 113 receiveBufferLen = 0; 114 workingSocket.BeginReceive(onceReadDatas, 0, receiveBufferLenMax, SocketFlags.None, 115 ReceiveCallback, 116 // new AsyncCallback((IAsyncResult iar) => { 117 // EndpointClient cli = (EndpointClient)iar.AsyncState; 118 // int reds = cli.client.EndReceive(iar); 119 //}), 120 this); 121 } 122 private void ReceiveCallback(IAsyncResult iar) 123 { 124 try 125 { 126 EndpointClient<T> cli = (EndpointClient<T>)iar.AsyncState; 127 Socket socket = cli.workingSocket; 128 int reads = socket.EndReceive(iar); 129 130 if (reads > 0) 131 { 132 133 for (int i = 0; i < reads; i++) 134 { 135 receiveBuffer.Add(onceReadDatas[i]); 136 } 137 138 //具体填充了多少看返回值 此时 数据已经在buffer中了 139 receiveBufferLen += reads; 140 //加完了后解析 阻塞式处理 结束后开启新的接收 141 SloveTelData(); 142 143 if (receiveBufferLenMax - receiveBufferLen > 0) 144 { 145 //接收完了 继续beginreceive 开启异步的下次接收 (如果缓冲区有残留数据 则接收长度变短 ,没接收到的让其留在socket不会丢失 下次接收) 146 socket.BeginReceive(onceReadDatas, 0, receiveBufferLenMax - receiveBufferLen, SocketFlags.None, ReceiveCallback, this); 147 } 148 else//阻塞式处理都完成一遍了 都还没清理出任何缓冲区空间 毫无疑问 整体运转机制已经挂了 不用beginreceive下一次了 149 { 150 Close(); 151 //移除自己 152 if (onClientDel != null) 153 { 154 onClientDel(remoteIPPort); 155 } 156 Logger.GetInstance().AppendMessage(LogLevel.Error, "服务端接口解析数据出现异常"); 157 //Console.WriteLine("服务端接口解析数据出现异常"); 158 //throw new Exception("服务端接口解析数据出现异常"); 159 } 160 } 161 else//reads==0 客户端已关闭 162 { 163 Close(); 164 //移除自己 165 if (onClientDel != null) 166 { 167 onClientDel(remoteIPPort); 168 } 169 } 170 } 171 catch (Exception ex) 172 { 173 Close(); 174 //移除自己 175 if (onClientDel != null) 176 { 177 onClientDel(remoteIPPort); 178 } 179 Logger.GetInstance().AppendMessage(LogLevel.Error, "ReceiveCallback Error" + ex.Message); 180 //Console.WriteLine("ReceiveCallback Error"); 181 //Console.WriteLine(ex.Message); 182 } 183 184 } 185 void SloveTelData() 186 { 187 //进行数据解析 188 189 190 if (typeof(T) == typeof(Telegram_Schedule)) 191 { 192 SloveTelDataUtil slo = new SloveTelDataUtil(); 193 List<Telegram_Schedule> dataEntitys = slo.Slove_Telegram_Schedule(receiveBuffer, receiveBufferLen, this.remoteIPPort); 194 //buffer已经被处理一遍了 使用新的长度 195 receiveBufferLen = receiveBuffer.Count; 196 //解析出的每一个对象都触发 onreceive 197 for (int i = 0; i < dataEntitys.Count; i++) 198 { 199 if (onReceive != null) 200 onReceive(dataEntitys[i] as T); 201 } 202 } 203 else if (typeof(T) == typeof(Telegram_SDBMsg)) 204 { 205 SloveTelSDBMsgUtil sloSDB = new SloveTelSDBMsgUtil(); 206 List<Telegram_SDBMsg> dataEntitys = sloSDB.Slove_Telegram_SDB(receiveBuffer, receiveBufferLen, this.remoteIPPort); 207 receiveBufferLen = receiveBuffer.Count; 208 //解析出的每一个对象都触发 onreceive 209 for (int i = 0; i < dataEntitys.Count; i++) 210 { 211 if (onReceive != null) 212 onReceive(dataEntitys[i] as T); 213 } 214 } 215 216 } 217 218 219 public void Close() 220 { 221 try 222 { 223 receiveBuffer.Clear(); 224 receiveBufferLen = 0; 225 if (workingSocket != null && workingSocket.Connected) 226 workingSocket.Close(); 227 } 228 catch (Exception ex) 229 { 230 Console.WriteLine(ex.Message); 231 } 232 233 } 234 }
数据拆包与封包粘包处理
上面的代码可以看到 数据包处理都在receiveCallback里 SloveTelData,也是通用的套路 ,解析到完整的包后从缓冲区移除 解析多少个包触发多少次事件,残余数据留在缓冲区 然后继续开始新的beginReceive往缓冲区加。在异步机制中 到达endReceive的时候数据已经在缓冲区里了,这个自不用多说噻。数据包和粘包逻辑在公共类库里供客户端服务端共同调用。
以下是Telegram_schedule数据包的粘包处理逻辑 ,当然看代码你知道还有另外一个报文处理逻辑 我不列举了。
1 public class SloveTelDataUtil 2 { 3 List<Telegram_Schedule> solveList; 4 public SloveTelDataUtil() 5 { 6 } 7 8 List<byte> buffer; 9 int bufferLen; 10 int bufferIndex = 0; 11 string remoteIPPort; 12 public List<Telegram_Schedule> Slove_Telegram_Schedule( List< byte> _buffer,int _bufferLen,string _remoteIPPort) 13 { 14 15 solveList = new List<Telegram_Schedule>(); 16 buffer = _buffer; 17 bufferLen = _bufferLen; 18 bufferIndex = 0; 19 remoteIPPort = _remoteIPPort; 20 21 //小于最小长度 直接返回 22 if (bufferLen < 12) 23 return solveList; 24 25 //进行数据解析 26 bool anaysisOK = false; 27 while (anaysisOK=AnaysisData_Schedule()==true)//直到解析的不能解析为止 28 { 29 } 30 return solveList; 31 } 32 33 public bool AnaysisData_Schedule() 34 { 35 if (bufferLen - bufferIndex < GlobalSymbol.Headerlen) 36 return false; 37 38 //解析出一个数据对象 39 Telegram_Schedule telObj = new Telegram_Schedule(); 40 41 //必定是大于最小数据大小的 42 telObj.dataBytesHeader = new byte[GlobalSymbol.Headerlen]; 43 buffer.CopyTo(bufferIndex, telObj.dataBytesHeader, 0, GlobalSymbol.Headerlen); 44 45 byte[] btsHeader = new byte[4]; 46 byte[] btsCommand = new byte[4]; 47 byte[] btsLen = new byte[4]; 48 49 btsHeader[0] = buffer[bufferIndex]; 50 btsHeader[1] = buffer[bufferIndex+1]; 51 btsHeader[2] = buffer[bufferIndex+2]; 52 btsHeader[3] = buffer[bufferIndex+3]; 53 54 bufferIndex += 4; 55 56 btsCommand[0] = buffer[bufferIndex]; 57 btsCommand[1] = buffer[bufferIndex + 1]; 58 btsCommand[2] = buffer[bufferIndex + 2]; 59 btsCommand[3] = buffer[bufferIndex + 3]; 60 61 bufferIndex += 4; 62 63 btsLen[0] = buffer[bufferIndex]; 64 btsLen[1] = buffer[bufferIndex + 1]; 65 btsLen[2] = buffer[bufferIndex + 2]; 66 btsLen[3] = buffer[bufferIndex + 3]; 67 68 bufferIndex += 4; 69 70 71 72 int dataLen = BitConverter.ToInt32(btsLen, 0); 73 telObj.head = BitConverter.ToUInt32(btsHeader, 0); 74 telObj.command = BitConverter.ToInt32(btsCommand, 0); 75 telObj.remoteIPPort = remoteIPPort; 76 77 if(dataLen>0) 78 { 79 //数据区小于得到的数据长度 说明数据部分还没接收到 不删除缓冲区 不做任何处理 80 //下次来了连着头一起解析 81 if (bufferLen - GlobalSymbol.Headerlen < dataLen) 82 { 83 84 bufferIndex -= 12;// 85 86 87 return false; 88 89 } 90 else 91 { 92 93 telObj.dataLen = dataLen; 94 telObj.dataBytes = new byte[dataLen]; 95 buffer.CopyTo(bufferIndex, telObj.dataBytes, 0, dataLen); 96 97 solveList.Add(telObj); 98 //bufferIndex += dataLen; 99 100 //解析成功一次 移除已解析的 101 for (int i = 0; i < GlobalSymbol.Headerlen+dataLen; i++) 102 { 103 buffer.RemoveAt(0); 104 } 105 bufferIndex = 0; 106 bufferLen = buffer.Count; 107 return true; 108 } 109 } 110 else 111 { 112 113 telObj.dataLen = 0; 114 solveList.Add(telObj); 115 //bufferIndex += 0; 116 //解析成功一次 移除已解析的 117 for (int i = 0; i < GlobalSymbol.Headerlen; i++) 118 { 119 buffer.RemoveAt(0); 120 } 121 //解析成功一次因为移除了缓冲区 bufferIndex置0 122 bufferIndex = 0; 123 bufferLen = buffer.Count; 124 return true; 125 } 126 127 } 128 129 }
我们看到用到的数据包对象是Telegram_Schedule ,中间保存有报文数据,数据发送的目标等信息。
以下是数据包结构代码
1 public class Telegram_Base 2 { 3 public string remoteIPPort { get; set; } 4 5 //头部内容的序列化 6 public byte[] dataBytesHeader { get; set; } 7 8 //数据内容 9 public byte[] dataBytes { get; set; } 10 11 public int headerLen { get; set; } 12 //数据长度 4字节 13 public int dataLen { get; set; } 14 15 public string jsonStr { get; set; } 16 virtual public void SerialToBytes() 17 { 18 19 } 20 21 virtual public void SloveToTel() 22 { 23 24 } 25 26 } 27 28 29 public class Telegram_Schedule:Telegram_Base 30 { 31 32 //头部标识 4字节 33 public UInt32 head { get; set; } 34 //命令对应枚举的 int 4字节 35 public int command { get; set; } 36 37 38 override public void SerialToBytes() 39 { 40 //有字符串数据 但是待发送字节是空 41 if ((string.IsNullOrEmpty(jsonStr) == false ))//&& (dataBytes==null || dataBytes.Length==0) 42 { 43 dataBytes = Encoding.UTF8.GetBytes(jsonStr); 44 dataLen = dataBytes.Length; 45 dataBytesHeader = new byte[GlobalSymbol.Headerlen]; 46 47 head = GlobalSymbol.HeaderSymbol; 48 49 byte[] btsHeader = BitConverter.GetBytes(head); 50 byte[] btsCommand = BitConverter.GetBytes(command); 51 byte[] btsLen = BitConverter.GetBytes(dataLen); 52 53 Array.Copy(btsHeader, 0, dataBytesHeader, 0, 4); 54 Array.Copy(btsCommand, 0, dataBytesHeader, 4, 4); 55 Array.Copy(btsLen, 0, dataBytesHeader, 8, 4); 56 57 } 58 else if((string.IsNullOrEmpty(jsonStr) == true )&& (dataBytes==null || dataBytes.Length==0)){ 59 dataLen = 0; 60 dataBytes = new byte[0]; 61 62 dataBytesHeader = new byte[GlobalSymbol.Headerlen]; 63 64 head = GlobalSymbol.HeaderSymbol; 65 66 byte[] btsHeader = BitConverter.GetBytes(head); 67 byte[] btsCommand = BitConverter.GetBytes(command); 68 byte[] btsLen = BitConverter.GetBytes(dataLen); 69 70 Array.Copy(btsHeader, 0, dataBytesHeader, 0, 4); 71 Array.Copy(btsCommand, 0, dataBytesHeader, 4, 4); 72 Array.Copy(btsLen, 0, dataBytesHeader, 8, 4); 73 } 74 } 75 76 override public void SloveToTel() 77 { 78 //只解析字符串数据部分 ,header 和len 在接收之初就已解析 79 try 80 { 81 if (this.dataBytes != null && this.dataBytes.Length > 0) 82 this.jsonStr = Encoding.UTF8.GetString(this.dataBytes); 83 } 84 catch (Exception ex) 85 { 86 Logger.GetInstance().AppendMessage(LogLevel.Error, "data部分字符串解析出错 " + ex.Message); 87 } 88 } 89 90 }
客户端代码
最后是客户端,有了上面的结构,客户端就不足为谈了,稍微了解socket的人都熟知套路的 基本跟EndpointClient一致
1 public class MsgClient<T> where T : Telegram_Base 2 { 3 Socket workingSocket; 4 //缓冲区最大数据长度 5 static int receiveBufferLenMax = 5000; 6 //单次receive数据(取决于tcp底层封包 但是不会超过缓冲区最大长度 7 byte[] onceReadDatas = new byte[receiveBufferLenMax]; 8 //未解析到完整数据包时的残余数据保存区 9 List<byte> receiveBuffer = new List<byte>(receiveBufferLenMax); 10 11 string serverIP { get; set; } 12 int serverPort { get; set; } 13 public string localIPPort { get; set; } 14 15 //残余缓冲区数据长度 16 int receiveBufferLen = 0; 17 18 bool _isConnected { get; set; } 19 20 21 //收一个包时触发 22 public Action<T> onReceive; 23 //与服务端断链时触发 24 public Action<string> onClientDel; 25 26 27 public bool isConnected { get { return _isConnected; } } 28 public MsgClient(string _serverIP, int _port) 29 { 30 serverIP = _serverIP; 31 serverPort = _port; 32 _isConnected = false; 33 } 34 35 public void Connect() 36 { 37 try 38 { 39 workingSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.IP); 40 IPEndPoint ipport = new IPEndPoint(IPAddress.Parse(serverIP), serverPort); 41 workingSocket.Connect(ipport); 42 43 localIPPort = workingSocket.LocalEndPoint.ToString(); 44 _isConnected = true; 45 BeginReceive(); 46 } 47 catch (Exception ex) 48 { 49 workingSocket = null; 50 _isConnected = false; 51 52 Logger.GetInstance().AppendMessage(LogLevel.Error,"连接到服务端出错:"+ ex.Message); 53 } 54 55 } 56 57 58 59 60 public void Send(T tel) 61 { 62 try 63 { 64 if (_isConnected == false) 65 { 66 Console.WriteLine("未连接到服务器"); 67 return; 68 } 69 if (tel is Telegram_Schedule) 70 { 71 Telegram_Schedule telBeSend = tel as Telegram_Schedule; 72 if (telBeSend.dataBytes.Length != telBeSend.dataLen) 73 { 74 Console.WriteLine("尝试发送数据长度格式错误的报文"); 75 return; 76 } 77 byte[] sendBytesHeader = telBeSend.dataBytesHeader; 78 byte[] sendbytes = telBeSend.dataBytes; 79 80 //数据超过缓冲区长度 会导致无法拆包 81 if (sendbytes.Length <= receiveBufferLenMax) 82 { 83 workingSocket.BeginSend(sendBytesHeader, 0, sendBytesHeader.Length, 0, null, null); 84 workingSocket.BeginSend(sendbytes, 0, sendbytes.Length, 0, null, null 85 86 ); 87 } 88 else 89 { 90 Logger.GetInstance().AppendMessage(LogLevel.Error, "发送到调度客户端的数据超过缓冲区长度"); 91 throw new Exception("发送到调度客户端的数据超过缓冲区长度"); 92 } 93 94 95 } 96 else if (tel is Telegram_SDBMsg) 97 { 98 Telegram_SDBMsg telBesendSDB = tel as Telegram_SDBMsg; 99 if (telBesendSDB.dataBytes.Length != telBesendSDB.dataLen) 100 { 101 Console.WriteLine("尝试发送数据长度格式错误的报文"); 102 return; 103 } 104 byte[] sendBytesHeader = telBesendSDB.dataBytesHeader; 105 byte[] sendbytes = telBesendSDB.dataBytes; 106 107 //数据超过缓冲区长度 会导致无法拆包 108 if (sendbytes.Length <= receiveBufferLenMax) 109 { 110 workingSocket.BeginSend(sendBytesHeader, 0, sendBytesHeader.Length, 0, null, null); 111 workingSocket.BeginSend(sendbytes, 0, sendbytes.Length, 0, null, null); 112 } 113 else 114 { 115 Logger.GetInstance().AppendMessage(LogLevel.Error, "发送到调度客户端的数据超过缓冲区长度"); 116 throw new Exception("发送到调度客户端的数据超过缓冲区长度"); 117 } 118 } 119 120 } 121 catch (Exception ex) 122 { 123 Logger.GetInstance().AppendMessage(LogLevel.Error, ex.Message); 124 Close(); 125 //Console.WriteLine(ex.Message); 126 //throw ex; 127 } 128 } 129 130 public void BeginReceive() 131 { 132 receiveBufferLen = 0; 133 workingSocket.BeginReceive(onceReadDatas, 0, receiveBufferLenMax, SocketFlags.None, 134 ReceiveCallback, 135 136 this); 137 } 138 private void ReceiveCallback(IAsyncResult iar) 139 { 140 try 141 { 142 MsgClient<T> cli = (MsgClient<T>)iar.AsyncState; 143 Socket socket = cli.workingSocket; 144 int reads = socket.EndReceive(iar); 145 146 if (reads > 0) 147 { 148 149 for (int i = 0; i < reads; i++) 150 { 151 receiveBuffer.Add(onceReadDatas[i]); 152 } 153 154 //具体填充了多少看返回值 此时 数据已经在buffer中了 155 156 receiveBufferLen += reads; 157 158 //加完了后解析 阻塞式处理 结束后开启新的接收 159 SloveTelData(); 160 161 162 163 if (receiveBufferLenMax - receiveBufferLen > 0) 164 { 165 //接收完了 继续beginreceive 开启异步的下次接收 (如果缓冲区有残留数据 则接收长度变短 ,没接收到的让其留在socket不会丢失 下次接收) 166 socket.BeginReceive(onceReadDatas, 0, receiveBufferLenMax - receiveBufferLen, SocketFlags.None, ReceiveCallback, this); 167 } 168 else//阻塞式处理都完成一遍了 都还没清理出任何缓冲区空间 毫无疑问 整体运转机制已经挂了 不用beginreceive下一次了 169 { 170 Close(); 171 172 Console.WriteLine("服务端接口解析数据出现异常"); 173 throw new Exception("服务端接口解析数据出现异常"); 174 } 175 } 176 else//reads==0客户端已关闭 177 { 178 Close(); 179 } 180 } 181 catch (Exception ex) 182 { 183 Close(); 184 185 Console.WriteLine("ReceiveCallback Error"); 186 Console.WriteLine(ex.Message); 187 } 188 189 } 190 private void SloveTelData() 191 { 192 193 //进行数据解析 194 195 196 if (typeof(T) == typeof(Telegram_Schedule)) 197 { 198 SloveTelDataUtil slo = new SloveTelDataUtil(); 199 List<Telegram_Schedule> dataEntitys = slo.Slove_Telegram_Schedule(receiveBuffer, receiveBufferLen, serverIP + ":" + serverPort.ToString()); 200 //buffer已经被处理一遍了 使用新的长度 201 receiveBufferLen = receiveBuffer.Count; 202 //解析出的每一个对象都触发 onreceive 203 for (int i = 0; i < dataEntitys.Count; i++) 204 { 205 if (onReceive != null) 206 onReceive(dataEntitys[i] as T); 207 } 208 } 209 else if (typeof(T) == typeof(Telegram_SDBMsg)) 210 { 211 SloveTelSDBMsgUtil sloSDB = new SloveTelSDBMsgUtil(); 212 List<Telegram_SDBMsg> dataEntitys = sloSDB.Slove_Telegram_SDB(receiveBuffer, receiveBufferLen, serverIP + ":" + serverPort.ToString()); 213 receiveBufferLen = receiveBuffer.Count; 214 //解析出的每一个对象都触发 onreceive 215 for (int i = 0; i < dataEntitys.Count; i++) 216 { 217 if (onReceive != null) 218 onReceive(dataEntitys[i] as T); 219 } 220 } 221 222 } 223 224 225 public void Close() 226 { 227 try 228 { 229 _isConnected = false; 230 231 receiveBuffer.Clear(); 232 receiveBufferLen = 0; 233 if (workingSocket != null && workingSocket.Connected) 234 workingSocket.Close(); 235 } 236 catch (Exception ex) 237 { 238 Console.WriteLine(ex.Message); 239 } 240 241 } 242 243 }
服务端调用
构建一个winform基本项目
1 List<string> clients; 2 TCPListener server2; 3 private void button1_Click(object sender, EventArgs e) 4 { 5 server = new MsgServer<Telegram_Schedule>(int.Parse(tbx_port.Text)); 6 7 server.Start(); 8 if (server.isRunning == true) 9 { 10 button1.Enabled = false; 11 12 server.onReceive += new Action<Telegram_Base>( 13 (tel) => 14 { 15 this.BeginInvoke(new Action(() => 16 { 17 if (tel is Telegram_Schedule) 18 { 19 Telegram_Schedule ts = tel as Telegram_Schedule; 20 ts.SloveToTel(); 21 //Console.WriteLine(string.Format("commandType:{0}", ((ScheduleTelCommandType)ts.command).ToString())); 22 23 textBox1.Text += ts.remoteIPPort + ">" + ts.jsonStr + "\r\n"; 24 25 //数据回发测试 26 string fromip = ts.remoteIPPort; 27 string srcMsg = ts.jsonStr; 28 string fromServerMsg = ts.jsonStr + " -from server"; 29 ts.jsonStr = fromServerMsg; 30 31 32 //如果消息里有指向信息 则转送到对应的客户端 33 if (clients != null) 34 { 35 string to = null; 36 for (int i = 0; i < clients.Count; i++) 37 { 38 if (srcMsg.Contains(clients[i])) 39 { 40 to = clients[i]; 41 break; 42 } 43 } 44 45 if (to != null) 46 { 47 ts.remoteIPPort = to; 48 string toMsg; 49 //toMsg= srcMsg.Replace(to, ""); 50 toMsg = srcMsg.Replace(to, fromip); 51 ts.jsonStr = toMsg; 52 ts.SerialToBytes(); 53 54 server.SendTo(ts); 55 } 56 else 57 { 58 ts.SerialToBytes(); 59 server.SendTo(ts); 60 } 61 } 62 } 63 })); 64 65 } 66 ); 67 68 server.onClientAddDel += new Action<List<string>>((onlines) => 69 { 70 this.BeginInvoke( 71 new Action(() => 72 { 73 clients = onlines; 74 listBox1.Items.Clear(); 75 76 for (int i = 0; i < onlines.Count; i++) 77 { 78 listBox1.Items.Add(onlines[i]); 79 } 80 })); 81 }); 82 } 83 }
客户端调用
1 MsgClient<Telegram_Schedule> client; 2 TCPClient client2; 3 private void btn_start_Click(object sender, EventArgs e) 4 { 5 client = new MsgClient<Telegram_Schedule>(tbx_ip.Text, int.Parse(tbx_port.Text)); 6 7 client.Connect(); 8 9 if (client.isConnected == true) 10 { 11 btn_start.Enabled = false; 12 13 label1.Text = client.localIPPort; 14 15 client.onReceive = new Action<Telegram_Base>((tel) => 16 { 17 this.BeginInvoke( 18 new Action(() => 19 { 20 tel.SloveToTel(); 21 tbx_rec.Text += tel.jsonStr + "\r\n"; 22 23 })); 24 }); 25 } 26 27 } 28 29 30 private void btn_send_Click(object sender, EventArgs e) 31 { 32 33 34 if (client == null || client.isConnected == false) 35 return; 36 37 //for (int i = 0; i < 2; i++) 38 //{ 39 Telegram_Schedule tel = new Telegram_Schedule(); 40 //tel.command = (int)ScheduleTelCommandType.MsgC2S; 41 42 tel.jsonStr = tbx_remoteip.Text+">"+ tbx_msgSend.Text; 43 tel.SerialToBytes();//发出前要先序列化 44 45 client.Send(tel); 46 //} 47 48 }
实现效果
可以多客户端连接互相自由发送消息,服务端可以编写转发规则代码,那些什么棋牌啊 互动白板 以及其他类似的应用就可以基于此之上发挥想象了。标题叫一个简易socket通信结构 ,一路走下来你看整个结构其实也并不是那么简易 ,断断续续调了几周 解决了很多问题。完美的支持报文格式切换,这里只是拿聊天功能做个示例,但是其实这已经是一个由我编写出来 然后测试 并成功商业应用了的部分哈 ,可靠 稳定 你值得拥有。都知道流行GitHub 但是本人并不习惯把代码放GitHub上见谅。