服务端

基本的结构

工作需要又需要用到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上见谅。

一个简易socket通信结构的更多相关文章

  1. 【Java TCP/IP Socket】TCP Socket通信中由read返回值造成的的死锁问题(含代码)(转)

    书上示例 在第一章<基本套接字>中,作者给出了一个TCP Socket通信的例子——反馈服务器,即服务器端直接把从客户端接收到的数据原原本本地反馈回去. 书上客户端代码如下: 1 2 3 ...

  2. C语言 linux环境基于socket的简易即时通信程序

    转载请注明出处:http://www.cnblogs.com/kevince/p/3891033.html      ——By Kevince 最近在看linux网络编程相关,现学现卖,就写了一个简易 ...

  3. 调试一个socket通信bug的心理过程和反思

    背景交代.最近在玩lua的服务端编码, 有项目A,B,AB都是同一个模子的.我手上有A的winsocket客户端和服务端的代码,B项目早期的一份linux下的lua client.服务端.客户端都是L ...

  4. Python简易聊天工具-基于异步Socket通信

    继续学习Python中,最近看书<Python基础教程>中的虚拟茶话会项目,觉得很有意思,自己敲了一遍,受益匪浅,同时记录一下. 主要用到异步socket服务客户端和服务器模块asynco ...

  5. 分析一个socket通信: server/client

    分析一个socket通信: server/client1 server 1. 创建一个server_socket文件,并绑定端口,然后监听端口 (socket, bind, listen) 2. 查询 ...

  6. vue + socket.io实现一个简易聊天室

    vue + vuex + elementUi + socket.io实现一个简易的在线聊天室,提高自己在对vue系列在项目中应用的深度.因为学会一个库或者框架容易,但要结合项目使用一个库或框架就不是那 ...

  7. Socket 由浅入深,开发一个真正的通信应用

    在说socket之前.我们先了解下相关的网络知识: 端口  在Internet上有很多这样的主机,这些主机一般运行了多个服务软件,同时提供几种服务.每种服务都打开一个Socket,并绑定到一个端口上, ...

  8. TTMS 一个基于Java Swing的Socket通信的剧院票务管理系统

    TTMS (Theater Ticket Management System) 点我进入github TTMS全称剧院票务管理系统,分为客户端和服务器端.服务器端可以接收客户端连接请求,客户端相当于我 ...

  9. 清晰易懂TCP通信原理解析(附demo、简易TCP通信库源码、解决沾包问题等)C#版

    目录 说明 TCP与UDP通信的特点 TCP中的沾包现象 自定义应用层协议 TCPLibrary通信库介绍 Demo演示 未完成功能 源码下载 说明 我前面博客中有多篇文章讲到了.NET中的网络编程, ...

  10. C 实现一个简易的Http服务器

    引言 做一个老实人挺好的,至少还觉得自己挺老实的. 再分享一首 自己喜欢的诗人的一首 情景诗. 每个人总会有问题,至少喜欢就好, 本文 参照 http 协议   http://www.cnblogs. ...

随机推荐

  1. Redis性能优化的18招

    前言 Redis在我们的日常开发工作中,使用频率非常高,已经变成了必不可少的技术之一. Redis的使用场景也很多. 比如:保存用户登录态,做限流,做分布式锁,做缓存提升数据访问速度等等. 那么问题来 ...

  2. 分析什么情况下回有大量的垃圾回收(GC)

    在前端性能监控中,大量的垃圾回收(GC)通常是由以下原因导致的: 内存泄漏:当页面中的对象没有被正确地释放或引用计数错误时,会导致内存泄漏.当内存中的对象达到一定数量时,JavaScript 引擎会执 ...

  3. mongo迁移工具之mongo-shake

    最近需要进行MongoDB中数据迁移,之前使用过阿里系的redisShake感觉不错, 这次打算使用mongoShake来进行同步 github: https://github.com/alibaba ...

  4. python之模拟数据Faker

    Faker,它解决的问题是python模拟(随机)数据!不知道大家在工作中没有用到过假数据,特别前后端开发的人员,应该经常用到,前端人员页面展示,效果展示.后端人员数据库数据模拟.今天给大家介绍的这个 ...

  5. brew之加速

    有没有出现这种场景:使用brew install 安装程序,一直卡在brew updating,这可能是使用着默认的github镜像源导致,那么我们就需要将其切换到国内 1.镜像切换(推荐中科大) 1 ...

  6. js 实现可缓存方法

    1.概述 有些场景下,如果一些函数需要大量的运算,但是他们的传入的参数是一样的,这个时候,我们可以将这些运算缓存下来,之后的运算就可以不用重复计算了. 2.实现方法 <script> // ...

  7. Navicat连接Oracle数据库报错:oracle library is not loaded解决方法

    连接Oracle时提示"oracle library is not loaded". 去Oracle官网下载Oracle Instant Client Downloads. htt ...

  8. 类型判断运算符(as、is、is!)

    类型判断运算符 as.is.is! 运算符是在运行时判断对象类型的运算符. as 类型转换(也用作指定 类前缀)) is 如果对象是指定类型则返回 true is! 如果对象是指定类型则返回 fals ...

  9. ASP.NET Core IHostBuilder

    HostBuilder 很显然,HostBuildr 就是用来构建 Host 的构建器. IHostBuilder 定义 通过 Build() 方法,构建器返回构建的 IHost 对象实例. 具体怎么 ...

  10. 【YashanDB知识库】Oracle pipelined函数在YashanDB中的改写

    本文内容来自YashanDB官网,原文内容请见 https://www.yashandb.com/newsinfo/7802940.html?templateId=1718516 [问题分类]功能使用 ...