C# 对 TCP 客户端的状态封装
本文为 Dennis Gao 原创技术文章,发表于博客园博客,未经作者本人允许禁止任何形式的转载。
TCP客户端连接TCP服务器端有几种应用状态:
- 与服务器的连接已建立
- 与服务器的连接已断开
- 与服务器的连接发生异常
应用程序可按需求合理处理这些逻辑,比如:
- 连接断开后自动重连
- 连接断开后选择备用地址重连
- 所有状态变化上报告警
本文描述的TcpClient实现了状态变化的事件通知机制。
1 /// <summary> 2 /// 异步TCP客户端 3 /// </summary> 4 public class AsyncTcpClient : IDisposable 5 { 6 #region Fields 7 8 private TcpClient tcpClient; 9 private bool disposed = false; 10 private int retries = 0; 11 12 #endregion 13 14 #region Ctors 15 16 /// <summary> 17 /// 异步TCP客户端 18 /// </summary> 19 /// <param name="remoteEP">远端服务器终结点</param> 20 public AsyncTcpClient(IPEndPoint remoteEP) 21 : this(new[] { remoteEP.Address }, remoteEP.Port) 22 { 23 } 24 25 /// <summary> 26 /// 异步TCP客户端 27 /// </summary> 28 /// <param name="remoteEP">远端服务器终结点</param> 29 /// <param name="localEP">本地客户端终结点</param> 30 public AsyncTcpClient(IPEndPoint remoteEP, IPEndPoint localEP) 31 : this(new[] { remoteEP.Address }, remoteEP.Port, localEP) 32 { 33 } 34 35 /// <summary> 36 /// 异步TCP客户端 37 /// </summary> 38 /// <param name="remoteIPAddress">远端服务器IP地址</param> 39 /// <param name="remotePort">远端服务器端口</param> 40 public AsyncTcpClient(IPAddress remoteIPAddress, int remotePort) 41 : this(new[] { remoteIPAddress }, remotePort) 42 { 43 } 44 45 /// <summary> 46 /// 异步TCP客户端 47 /// </summary> 48 /// <param name="remoteIPAddress">远端服务器IP地址</param> 49 /// <param name="remotePort">远端服务器端口</param> 50 /// <param name="localEP">本地客户端终结点</param> 51 public AsyncTcpClient( 52 IPAddress remoteIPAddress, int remotePort, IPEndPoint localEP) 53 : this(new[] { remoteIPAddress }, remotePort, localEP) 54 { 55 } 56 57 /// <summary> 58 /// 异步TCP客户端 59 /// </summary> 60 /// <param name="remoteHostName">远端服务器主机名</param> 61 /// <param name="remotePort">远端服务器端口</param> 62 public AsyncTcpClient(string remoteHostName, int remotePort) 63 : this(Dns.GetHostAddresses(remoteHostName), remotePort) 64 { 65 } 66 67 /// <summary> 68 /// 异步TCP客户端 69 /// </summary> 70 /// <param name="remoteHostName">远端服务器主机名</param> 71 /// <param name="remotePort">远端服务器端口</param> 72 /// <param name="localEP">本地客户端终结点</param> 73 public AsyncTcpClient( 74 string remoteHostName, int remotePort, IPEndPoint localEP) 75 : this(Dns.GetHostAddresses(remoteHostName), remotePort, localEP) 76 { 77 } 78 79 /// <summary> 80 /// 异步TCP客户端 81 /// </summary> 82 /// <param name="remoteIPAddresses">远端服务器IP地址列表</param> 83 /// <param name="remotePort">远端服务器端口</param> 84 public AsyncTcpClient(IPAddress[] remoteIPAddresses, int remotePort) 85 : this(remoteIPAddresses, remotePort, null) 86 { 87 } 88 89 /// <summary> 90 /// 异步TCP客户端 91 /// </summary> 92 /// <param name="remoteIPAddresses">远端服务器IP地址列表</param> 93 /// <param name="remotePort">远端服务器端口</param> 94 /// <param name="localEP">本地客户端终结点</param> 95 public AsyncTcpClient( 96 IPAddress[] remoteIPAddresses, int remotePort, IPEndPoint localEP) 97 { 98 this.Addresses = remoteIPAddresses; 99 this.Port = remotePort; 100 this.LocalIPEndPoint = localEP; 101 this.Encoding = Encoding.Default; 102 103 if (this.LocalIPEndPoint != null) 104 { 105 this.tcpClient = new TcpClient(this.LocalIPEndPoint); 106 } 107 else 108 { 109 this.tcpClient = new TcpClient(); 110 } 111 112 Retries = 3; 113 RetryInterval = 5; 114 } 115 116 #endregion 117 118 #region Properties 119 120 /// <summary> 121 /// 是否已与服务器建立连接 122 /// </summary> 123 public bool Connected { get { return tcpClient.Client.Connected; } } 124 /// <summary> 125 /// 远端服务器的IP地址列表 126 /// </summary> 127 public IPAddress[] Addresses { get; private set; } 128 /// <summary> 129 /// 远端服务器的端口 130 /// </summary> 131 public int Port { get; private set; } 132 /// <summary> 133 /// 连接重试次数 134 /// </summary> 135 public int Retries { get; set; } 136 /// <summary> 137 /// 连接重试间隔 138 /// </summary> 139 public int RetryInterval { get; set; } 140 /// <summary> 141 /// 远端服务器终结点 142 /// </summary> 143 public IPEndPoint RemoteIPEndPoint 144 { 145 get { return new IPEndPoint(Addresses[0], Port); } 146 } 147 /// <summary> 148 /// 本地客户端终结点 149 /// </summary> 150 protected IPEndPoint LocalIPEndPoint { get; private set; } 151 /// <summary> 152 /// 通信所使用的编码 153 /// </summary> 154 public Encoding Encoding { get; set; } 155 156 #endregion 157 158 #region Connect 159 160 /// <summary> 161 /// 连接到服务器 162 /// </summary> 163 /// <returns>异步TCP客户端</returns> 164 public AsyncTcpClient Connect() 165 { 166 if (!Connected) 167 { 168 // start the async connect operation 169 tcpClient.BeginConnect( 170 Addresses, Port, HandleTcpServerConnected, tcpClient); 171 } 172 173 return this; 174 } 175 176 /// <summary> 177 /// 关闭与服务器的连接 178 /// </summary> 179 /// <returns>异步TCP客户端</returns> 180 public AsyncTcpClient Close() 181 { 182 if (Connected) 183 { 184 retries = 0; 185 tcpClient.Close(); 186 RaiseServerDisconnected(Addresses, Port); 187 } 188 189 return this; 190 } 191 192 #endregion 193 194 #region Receive 195 196 private void HandleTcpServerConnected(IAsyncResult ar) 197 { 198 try 199 { 200 tcpClient.EndConnect(ar); 201 RaiseServerConnected(Addresses, Port); 202 retries = 0; 203 } 204 catch (Exception ex) 205 { 206 ExceptionHandler.Handle(ex); 207 if (retries > 0) 208 { 209 Logger.Debug(string.Format(CultureInfo.InvariantCulture, 210 "Connect to server with retry {0} failed.", retries)); 211 } 212 213 retries++; 214 if (retries > Retries) 215 { 216 // we have failed to connect to all the IP Addresses, 217 // connection has failed overall. 218 RaiseServerExceptionOccurred(Addresses, Port, ex); 219 return; 220 } 221 else 222 { 223 Logger.Debug(string.Format(CultureInfo.InvariantCulture, 224 "Waiting {0} seconds before retrying to connect to server.", 225 RetryInterval)); 226 Thread.Sleep(TimeSpan.FromSeconds(RetryInterval)); 227 Connect(); 228 return; 229 } 230 } 231 232 // we are connected successfully and start asyn read operation. 233 byte[] buffer = new byte[tcpClient.ReceiveBufferSize]; 234 tcpClient.GetStream().BeginRead( 235 buffer, 0, buffer.Length, HandleDatagramReceived, buffer); 236 } 237 238 private void HandleDatagramReceived(IAsyncResult ar) 239 { 240 NetworkStream stream = tcpClient.GetStream(); 241 242 int numberOfReadBytes = 0; 243 try 244 { 245 numberOfReadBytes = stream.EndRead(ar); 246 } 247 catch 248 { 249 numberOfReadBytes = 0; 250 } 251 252 if (numberOfReadBytes == 0) 253 { 254 // connection has been closed 255 Close(); 256 return; 257 } 258 259 // received byte and trigger event notification 260 byte[] buffer = (byte[])ar.AsyncState; 261 byte[] receivedBytes = new byte[numberOfReadBytes]; 262 Buffer.BlockCopy(buffer, 0, receivedBytes, 0, numberOfReadBytes); 263 RaiseDatagramReceived(tcpClient, receivedBytes); 264 RaisePlaintextReceived(tcpClient, receivedBytes); 265 266 // then start reading from the network again 267 stream.BeginRead( 268 buffer, 0, buffer.Length, HandleDatagramReceived, buffer); 269 } 270 271 #endregion 272 273 #region Events 274 275 /// <summary> 276 /// 接收到数据报文事件 277 /// </summary> 278 public event EventHandler<TcpDatagramReceivedEventArgs<byte[]>> DatagramReceived; 279 /// <summary> 280 /// 接收到数据报文明文事件 281 /// </summary> 282 public event EventHandler<TcpDatagramReceivedEventArgs<string>> PlaintextReceived; 283 284 private void RaiseDatagramReceived(TcpClient sender, byte[] datagram) 285 { 286 if (DatagramReceived != null) 287 { 288 DatagramReceived(this, 289 new TcpDatagramReceivedEventArgs<byte[]>(sender, datagram)); 290 } 291 } 292 293 private void RaisePlaintextReceived(TcpClient sender, byte[] datagram) 294 { 295 if (PlaintextReceived != null) 296 { 297 PlaintextReceived(this, 298 new TcpDatagramReceivedEventArgs<string>( 299 sender, this.Encoding.GetString(datagram, 0, datagram.Length))); 300 } 301 } 302 303 /// <summary> 304 /// 与服务器的连接已建立事件 305 /// </summary> 306 public event EventHandler<TcpServerConnectedEventArgs> ServerConnected; 307 /// <summary> 308 /// 与服务器的连接已断开事件 309 /// </summary> 310 public event EventHandler<TcpServerDisconnectedEventArgs> ServerDisconnected; 311 /// <summary> 312 /// 与服务器的连接发生异常事件 313 /// </summary> 314 public event EventHandler<TcpServerExceptionOccurredEventArgs> ServerExceptionOccurred; 315 316 private void RaiseServerConnected(IPAddress[] ipAddresses, int port) 317 { 318 if (ServerConnected != null) 319 { 320 ServerConnected(this, 321 new TcpServerConnectedEventArgs(ipAddresses, port)); 322 } 323 } 324 325 private void RaiseServerDisconnected(IPAddress[] ipAddresses, int port) 326 { 327 if (ServerDisconnected != null) 328 { 329 ServerDisconnected(this, 330 new TcpServerDisconnectedEventArgs(ipAddresses, port)); 331 } 332 } 333 334 private void RaiseServerExceptionOccurred( 335 IPAddress[] ipAddresses, int port, Exception innerException) 336 { 337 if (ServerExceptionOccurred != null) 338 { 339 ServerExceptionOccurred(this, 340 new TcpServerExceptionOccurredEventArgs( 341 ipAddresses, port, innerException)); 342 } 343 } 344 345 #endregion 346 347 #region Send 348 349 /// <summary> 350 /// 发送报文 351 /// </summary> 352 /// <param name="datagram">报文</param> 353 public void Send(byte[] datagram) 354 { 355 if (datagram == null) 356 throw new ArgumentNullException("datagram"); 357 358 if (!Connected) 359 { 360 RaiseServerDisconnected(Addresses, Port); 361 throw new InvalidProgramException( 362 "This client has not connected to server."); 363 } 364 365 tcpClient.GetStream().BeginWrite( 366 datagram, 0, datagram.Length, HandleDatagramWritten, tcpClient); 367 } 368 369 private void HandleDatagramWritten(IAsyncResult ar) 370 { 371 ((TcpClient)ar.AsyncState).GetStream().EndWrite(ar); 372 } 373 374 /// <summary> 375 /// 发送报文 376 /// </summary> 377 /// <param name="datagram">报文</param> 378 public void Send(string datagram) 379 { 380 Send(this.Encoding.GetBytes(datagram)); 381 } 382 383 #endregion 384 385 #region IDisposable Members 386 387 /// <summary> 388 /// Performs application-defined tasks associated with freeing, 389 /// releasing, or resetting unmanaged resources. 390 /// </summary> 391 public void Dispose() 392 { 393 Dispose(true); 394 GC.SuppressFinalize(this); 395 } 396 397 /// <summary> 398 /// Releases unmanaged and - optionally - managed resources 399 /// </summary> 400 /// <param name="disposing"><c>true</c> to release both managed 401 /// and unmanaged resources; <c>false</c> 402 /// to release only unmanaged resources. 403 /// </param> 404 protected virtual void Dispose(bool disposing) 405 { 406 if (!this.disposed) 407 { 408 if (disposing) 409 { 410 try 411 { 412 Close(); 413 414 if (tcpClient != null) 415 { 416 tcpClient = null; 417 } 418 } 419 catch (SocketException ex) 420 { 421 ExceptionHandler.Handle(ex); 422 } 423 } 424 425 disposed = true; 426 } 427 } 428 429 #endregion 430 }
使用举例
1 class Program 2 { 3 static AsyncTcpClient client; 4 5 static void Main(string[] args) 6 { 7 LogFactory.Assign(new ConsoleLogFactory()); 8 9 // 测试用,可以不指定由系统选择端口 10 IPEndPoint remoteEP = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9999); 11 IPEndPoint localEP = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 9998); 12 client = new AsyncTcpClient(remoteEP, localEP); 13 client.Encoding = Encoding.UTF8; 14 client.ServerExceptionOccurred += 15 new EventHandler<TcpServerExceptionOccurredEventArgs>(client_ServerExceptionOccurred); 16 client.ServerConnected += 17 new EventHandler<TcpServerConnectedEventArgs>(client_ServerConnected); 18 client.ServerDisconnected += 19 new EventHandler<TcpServerDisconnectedEventArgs>(client_ServerDisconnected); 20 client.PlaintextReceived += 21 new EventHandler<TcpDatagramReceivedEventArgs<string>>(client_PlaintextReceived); 22 client.Connect(); 23 24 Console.WriteLine("TCP client has connected to server."); 25 Console.WriteLine("Type something to send to server..."); 26 while (true) 27 { 28 try 29 { 30 string text = Console.ReadLine(); 31 client.Send(text); 32 } 33 catch (Exception ex) 34 { 35 Console.WriteLine(ex.Message); 36 } 37 } 38 } 39 40 static void client_ServerExceptionOccurred( 41 object sender, TcpServerExceptionOccurredEventArgs e) 42 { 43 Logger.Debug(string.Format(CultureInfo.InvariantCulture, 44 "TCP server {0} exception occurred, {1}.", 45 e.ToString(), e.Exception.Message)); 46 } 47 48 static void client_ServerConnected( 49 object sender, TcpServerConnectedEventArgs e) 50 { 51 Logger.Debug(string.Format(CultureInfo.InvariantCulture, 52 "TCP server {0} has connected.", e.ToString())); 53 } 54 55 static void client_ServerDisconnected( 56 object sender, TcpServerDisconnectedEventArgs e) 57 { 58 Logger.Debug(string.Format(CultureInfo.InvariantCulture, 59 "TCP server {0} has disconnected.", e.ToString())); 60 } 61 62 static void client_PlaintextReceived( 63 object sender, TcpDatagramReceivedEventArgs<string> e) 64 { 65 Console.Write(string.Format("Server : {0} --> ", 66 e.TcpClient.Client.RemoteEndPoint.ToString())); 67 Console.WriteLine(string.Format("{0}", e.Datagram)); 68 } 69 }
TCP客户端State
1 /// <summary> 2 /// Internal class to join the TCP client and buffer together 3 /// for easy management in the server 4 /// </summary> 5 internal class TcpClientState 6 { 7 /// <summary> 8 /// Constructor for a new Client 9 /// </summary> 10 /// <param name="tcpClient">The TCP client</param> 11 /// <param name="buffer">The byte array buffer</param> 12 public TcpClientState(TcpClient tcpClient, byte[] buffer) 13 { 14 if (tcpClient == null) 15 throw new ArgumentNullException("tcpClient"); 16 if (buffer == null) 17 throw new ArgumentNullException("buffer"); 18 19 this.TcpClient = tcpClient; 20 this.Buffer = buffer; 21 } 22 23 /// <summary> 24 /// Gets the TCP Client 25 /// </summary> 26 public TcpClient TcpClient { get; private set; } 27 28 /// <summary> 29 /// Gets the Buffer. 30 /// </summary> 31 public byte[] Buffer { get; private set; } 32 33 /// <summary> 34 /// Gets the network stream 35 /// </summary> 36 public NetworkStream NetworkStream 37 { 38 get { return TcpClient.GetStream(); } 39 } 40 }
与客户端的连接已建立事件参数
1 /// <summary> 2 /// 与客户端的连接已建立事件参数 3 /// </summary> 4 public class TcpClientConnectedEventArgs : EventArgs 5 { 6 /// <summary> 7 /// 与客户端的连接已建立事件参数 8 /// </summary> 9 /// <param name="tcpClient">客户端</param> 10 public TcpClientConnectedEventArgs(TcpClient tcpClient) 11 { 12 if (tcpClient == null) 13 throw new ArgumentNullException("tcpClient"); 14 15 this.TcpClient = tcpClient; 16 } 17 18 /// <summary> 19 /// 客户端 20 /// </summary> 21 public TcpClient TcpClient { get; private set; } 22 }
与客户端的连接已断开事件参数
/// <summary> /// 与客户端的连接已断开事件参数 /// </summary> public class TcpClientDisconnectedEventArgs : EventArgs { /// <summary> /// 与客户端的连接已断开事件参数 /// </summary> /// <param name="tcpClient">客户端</param> public TcpClientDisconnectedEventArgs(TcpClient tcpClient) { if (tcpClient == null) throw new ArgumentNullException("tcpClient"); this.TcpClient = tcpClient; } /// <summary> /// 客户端 /// </summary> public TcpClient TcpClient { get; private set; } }
与服务器的连接发生异常事件参数
1 /// <summary> 2 /// 与服务器的连接发生异常事件参数 3 /// </summary> 4 public class TcpServerExceptionOccurredEventArgs : EventArgs 5 { 6 /// <summary> 7 /// 与服务器的连接发生异常事件参数 8 /// </summary> 9 /// <param name="ipAddresses">服务器IP地址列表</param> 10 /// <param name="port">服务器端口</param> 11 /// <param name="innerException">内部异常</param> 12 public TcpServerExceptionOccurredEventArgs( 13 IPAddress[] ipAddresses, int port, Exception innerException) 14 { 15 if (ipAddresses == null) 16 throw new ArgumentNullException("ipAddresses"); 17 18 this.Addresses = ipAddresses; 19 this.Port = port; 20 this.Exception = innerException; 21 } 22 23 /// <summary> 24 /// 服务器IP地址列表 25 /// </summary> 26 public IPAddress[] Addresses { get; private set; } 27 /// <summary> 28 /// 服务器端口 29 /// </summary> 30 public int Port { get; private set; } 31 /// <summary> 32 /// 内部异常 33 /// </summary> 34 public Exception Exception { get; private set; } 35 36 /// <summary> 37 /// Returns a <see cref="System.String"/> that represents this instance. 38 /// </summary> 39 /// <returns> 40 /// A <see cref="System.String"/> that represents this instance. 41 /// </returns> 42 public override string ToString() 43 { 44 string s = string.Empty; 45 foreach (var item in Addresses) 46 { 47 s = s + item.ToString() + ','; 48 } 49 s = s.TrimEnd(','); 50 s = s + ":" + Port.ToString(CultureInfo.InvariantCulture); 51 52 return s; 53 } 54 }
接收到数据报文事件参数
1 /// <summary> 2 /// 接收到数据报文事件参数 3 /// </summary> 4 /// <typeparam name="T">报文类型</typeparam> 5 public class TcpDatagramReceivedEventArgs<T> : EventArgs 6 { 7 /// <summary> 8 /// 接收到数据报文事件参数 9 /// </summary> 10 /// <param name="tcpClient">客户端</param> 11 /// <param name="datagram">报文</param> 12 public TcpDatagramReceivedEventArgs(TcpClient tcpClient, T datagram) 13 { 14 TcpClient = tcpClient; 15 Datagram = datagram; 16 } 17 18 /// <summary> 19 /// 客户端 20 /// </summary> 21 public TcpClient TcpClient { get; private set; } 22 /// <summary> 23 /// 报文 24 /// </summary> 25 public T Datagram { get; private set; } 26 }
与服务器的连接已建立事件参数
1 /// <summary> 2 /// 与服务器的连接已建立事件参数 3 /// </summary> 4 public class TcpServerConnectedEventArgs : EventArgs 5 { 6 /// <summary> 7 /// 与服务器的连接已建立事件参数 8 /// </summary> 9 /// <param name="ipAddresses">服务器IP地址列表</param> 10 /// <param name="port">服务器端口</param> 11 public TcpServerConnectedEventArgs(IPAddress[] ipAddresses, int port) 12 { 13 if (ipAddresses == null) 14 throw new ArgumentNullException("ipAddresses"); 15 16 this.Addresses = ipAddresses; 17 this.Port = port; 18 } 19 20 /// <summary> 21 /// 服务器IP地址列表 22 /// </summary> 23 public IPAddress[] Addresses { get; private set; } 24 /// <summary> 25 /// 服务器端口 26 /// </summary> 27 public int Port { get; private set; } 28 29 /// <summary> 30 /// Returns a <see cref="System.String"/> that represents this instance. 31 /// </summary> 32 /// <returns> 33 /// A <see cref="System.String"/> that represents this instance. 34 /// </returns> 35 public override string ToString() 36 { 37 string s = string.Empty; 38 foreach (var item in Addresses) 39 { 40 s = s + item.ToString() + ','; 41 } 42 s = s.TrimEnd(','); 43 s = s + ":" + Port.ToString(CultureInfo.InvariantCulture); 44 45 return s; 46 } 47 }
与服务器的连接已断开事件参数
1 /// <summary> 2 /// 与服务器的连接已断开事件参数 3 /// </summary> 4 public class TcpServerDisconnectedEventArgs : EventArgs 5 { 6 /// <summary> 7 /// 与服务器的连接已断开事件参数 8 /// </summary> 9 /// <param name="ipAddresses">服务器IP地址列表</param> 10 /// <param name="port">服务器端口</param> 11 public TcpServerDisconnectedEventArgs(IPAddress[] ipAddresses, int port) 12 { 13 if (ipAddresses == null) 14 throw new ArgumentNullException("ipAddresses"); 15 16 this.Addresses = ipAddresses; 17 this.Port = port; 18 } 19 20 /// <summary> 21 /// 服务器IP地址列表 22 /// </summary> 23 public IPAddress[] Addresses { get; private set; } 24 /// <summary> 25 /// 服务器端口 26 /// </summary> 27 public int Port { get; private set; } 28 29 /// <summary> 30 /// Returns a <see cref="System.String"/> that represents this instance. 31 /// </summary> 32 /// <returns> 33 /// A <see cref="System.String"/> that represents this instance. 34 /// </returns> 35 public override string ToString() 36 { 37 string s = string.Empty; 38 foreach (var item in Addresses) 39 { 40 s = s + item.ToString() + ','; 41 } 42 s = s.TrimEnd(','); 43 s = s + ":" + Port.ToString(CultureInfo.InvariantCulture); 44 45 return s; 46 } 47 }
本文为 Dennis Gao 原创技术文章,发表于博客园博客,未经作者本人允许禁止任何形式的转载。