哎~~ 想想大部分园友应该对 "高性能" 字样更感兴趣,为了吸引眼球所以标题中一定要突出,其实我更喜欢的标题是《猴赛雷,C# 编写 TCP 服务的花样姿势!》

本篇文章的主旨是使用 .NET/C# 实现 TCP 高性能服务的不同方式,包括但不限于如下内容:

在 .NET/C# 中对于 Socket 的支持均是基于 Windows I/O Completion Ports 完成端口技术的封装,通过不同的 Non-Blocking 封装结构来满足不同的编程需求。以上方式均已在 Cowboy.Sockets 中有完整实现,并且 APM 和 TAP 方式已经在实际项目中应用。Cowboy.Sockets 还在不断的进化和完善中,如有任何问题请及时指正。

虽然有这么多种实现方式,但抽象的看,它们是一样一样的,用两个 Loop 即可描述:Accept LoopRead Loop,如下图所示。(这里提及的 "Loop" 指的是一种循环方式,而非特指 while/for 等关键字。)

  • 在任何 TCP Server 的实现中,一定存在一个 Accept Socket Loop,用于接收 Client 端的 Connect 请求以建立 TCP Connection。
  • 在任何 TCP Server 的实现中,一定存在一个 Read Socket Loop,用于接收 Client 端 Write 过来的数据。

如果 Accept 循环阻塞,则会导致无法快速的建立连接,服务端 Pending Backlog 满,进而导致 Client 端收到 Connect Timeout 的异常。如果 Read 循环阻塞,则显然会导致无法及时收到 Client 端发过来的数据,进而导致 Client 端 Send Buffer 满,无法再发送数据。

从实现细节的角度看,能够导致服务阻塞的位置可能在:

  1. Accept 到新的 Socket,构建新的 Connection 需要分配各种资源,分配资源慢;
  2. Accept 到新的 Socket,没有及时触发下一次 Accept;
  3. Read 到新的 Buffer,判定 Payload 消息长度,判定过程长;
  4. Read 到新的 Buffer,发现 Payload 还没有收全,继续 Read,则 "可能" 会导致一次 Buffer Copy;
  5. Payload 接收完毕,进行 De-Serialization 转成可识别的 Protocol Message,反序列化慢;
  6. 由 Business Module 来处理相应的 Protocol Message,处理过程慢;

1-2 涉及到 Accept 过程和 Connection 的建立过程,3-4 涉及到 ReceiveBuffer 的处理过程,5-6 涉及到应用逻辑侧的实现。

Java 中著名的 Netty 网络库从 4.0 版本开始对于 Buffer 部分做了全新的尝试,采用了名叫 ByteBuf 的设计,实现 Buffer Zero Copy 以减少高并发条件下 Buffer 拷贝带来的性能损失和 GC 压力。DotNettyOrleans ,Helios 等项目正在尝试在 C# 中进行类似的 ByteBuf 的实现。

APM 方式:TcpSocketServer

TcpSocketServer 的实现是基于 .NET Framework 自带的 TcpListenerTcpClient 的更进一步的封装,采用基于 APM 的 BeginXXX 和 EndXXX 接口实现。

TcpSocketServer 中的 Accept Loop 指的就是,

  • BeginAccept -> EndAccept-> BeginAccept -> EndAccept -> BeginAccept -> ...

每一个建立成功的 Connection 由 TcpSocketSession 来处理,所以 TcpSocketSession 中会包含 Read Loop,

  • BeginRead -> EndRead -> BeginRead -> EndRead -> BeginRead -> ...

TcpSocketServer 通过暴露 Event 来实现 Connection 的建立与断开和数据接收的通知。

  event EventHandler<TcpClientConnectedEventArgs> ClientConnected;
event EventHandler<TcpClientDisconnectedEventArgs> ClientDisconnected;
event EventHandler<TcpClientDataReceivedEventArgs> ClientDataReceived;

使用也是简单直接,直接订阅事件通知。

  private static void StartServer()
{
_server = new TcpSocketServer();
_server.ClientConnected += server_ClientConnected;
_server.ClientDisconnected += server_ClientDisconnected;
_server.ClientDataReceived += server_ClientDataReceived;
_server.Listen();
} static void server_ClientConnected(object sender, TcpClientConnectedEventArgs e)
{
Console.WriteLine(string.Format("TCP client {0} has connected {1}.", e.Session.RemoteEndPoint, e.Session));
} static void server_ClientDisconnected(object sender, TcpClientDisconnectedEventArgs e)
{
Console.WriteLine(string.Format("TCP client {0} has disconnected.", e.Session));
} static void server_ClientDataReceived(object sender, TcpClientDataReceivedEventArgs e)
{
var text = Encoding.UTF8.GetString(e.Data, e.DataOffset, e.DataLength);
Console.Write(string.Format("Client : {0} {1} --> ", e.Session.RemoteEndPoint, e.Session));
Console.WriteLine(string.Format("{0}", text));
_server.Broadcast(Encoding.UTF8.GetBytes(text));
}

TAP 方式:AsyncTcpSocketServer

AsyncTcpSocketServer 的实现是基于 .NET Framework 自带的 TcpListener 和 TcpClient 的更进一步的封装,采用基于 TAP 的 async/await 的 XXXAsync 接口实现。

然而,实际上 XXXAsync 并没有创建什么神奇的效果,其内部实现只是将 APM 的方法转换成了 TAP 的调用方式。

  //************* Task-based async public methods *************************
[HostProtection(ExternalThreading = true)]
public Task<Socket> AcceptSocketAsync()
{
return Task<Socket>.Factory.FromAsync(BeginAcceptSocket, EndAcceptSocket, null);
} [HostProtection(ExternalThreading = true)]
public Task<TcpClient> AcceptTcpClientAsync()
{
return Task<TcpClient>.Factory.FromAsync(BeginAcceptTcpClient, EndAcceptTcpClient, null);
}

AsyncTcpSocketServer 中的 Accept Loop 指的就是,

  while (IsListening)
{
var tcpClient = await _listener.AcceptTcpClientAsync();
}

每一个建立成功的 Connection 由 AsyncTcpSocketSession 来处理,所以 AsyncTcpSocketSession 中会包含 Read Loop,

  while (State == TcpSocketConnectionState.Connected)
{
int receiveCount = await _stream.ReadAsync(_receiveBuffer, , _receiveBuffer.Length);
}

为了将 async/await 异步到底,AsyncTcpSocketServer 所暴露的接口也同样是 Awaitable 的。

  public interface IAsyncTcpSocketServerMessageDispatcher
{
Task OnSessionStarted(AsyncTcpSocketSession session);
Task OnSessionDataReceived(AsyncTcpSocketSession session, byte[] data, int offset, int count);
Task OnSessionClosed(AsyncTcpSocketSession session);
}

使用时仅需将一个实现了该接口的对象注入到 AsyncTcpSocketServer 的构造函数中即可。

  public class SimpleMessageDispatcher : IAsyncTcpSocketServerMessageDispatcher
{
public async Task OnSessionStarted(AsyncTcpSocketSession session)
{
Console.WriteLine(string.Format("TCP session {0} has connected {1}.", session.RemoteEndPoint, session));
await Task.CompletedTask;
} public async Task OnSessionDataReceived(AsyncTcpSocketSession session, byte[] data, int offset, int count)
{
var text = Encoding.UTF8.GetString(data, offset, count);
Console.Write(string.Format("Client : {0} --> ", session.RemoteEndPoint));
Console.WriteLine(string.Format("{0}", text)); await session.SendAsync(Encoding.UTF8.GetBytes(text));
} public async Task OnSessionClosed(AsyncTcpSocketSession session)
{
Console.WriteLine(string.Format("TCP session {0} has disconnected.", session));
await Task.CompletedTask;
}
}

当然,对于接口的实现也不是强制了,也可以在构造函数中直接注入方法的实现。

  public AsyncTcpSocketServer(
IPEndPoint listenedEndPoint,
Func<AsyncTcpSocketSession, byte[], int, int, Task> onSessionDataReceived = null,
Func<AsyncTcpSocketSession, Task> onSessionStarted = null,
Func<AsyncTcpSocketSession, Task> onSessionClosed = null,
AsyncTcpSocketServerConfiguration configuration = null)
{}

SAEA 方式:TcpSocketSaeaServer

SAEA 是 SocketAsyncEventArgs 的简写。SocketAsyncEventArgs 是 .NET Framework 3.5 开始支持的一种支持高性能 Socket 通信的实现。SocketAsyncEventArgs 相比于 APM 方式的主要优点可以描述如下:

The main feature of these enhancements is the avoidance of the repeated allocation and synchronization of objects during high-volume asynchronous socket I/O. The Begin/End design pattern currently implemented by the Socket class for asynchronous socket I/O requires a System.IAsyncResult object be allocated for each asynchronous socket operation.

也就是说,优点就是无需为每次调用都生成 IAsyncResult 等对象,向原生 Socket 更靠近一些。

使用 SocketAsyncEventArgs 的推荐步骤如下:

  1. Allocate a new SocketAsyncEventArgs context object, or get a free one from an application pool.
  2. Set properties on the context object to the operation about to be performed (the callback delegate method and data buffer, for example).
  3. Call the appropriate socket method (xxxAsync) to initiate the asynchronous operation.
  4. If the asynchronous socket method (xxxAsync) returns true in the callback, query the context properties for completion status.
  5. If the asynchronous socket method (xxxAsync) returns false in the callback, the operation completed synchronously. The context properties may be queried for the operation result.
  6. Reuse the context for another operation, put it back in the pool, or discard it.

重点在于池化(Pooling),池化的目的就是为了重用和减少运行时分配和垃圾回收的压力。

TcpSocketSaeaServer 即是对 SocketAsyncEventArgs 的应用和封装,并实现了 Pooling 技术。TcpSocketSaeaServer 中的重点是 SaeaAwaitable 类,SaeaAwaitable 中内置了一个 SocketAsyncEventArgs,并通过 GetAwaiter 返回 SaeaAwaiter 来支持 async/await 操作。同时,通过 SaeaExtensions 扩展方法对来扩展 SocketAsyncEventArgs 的 Awaitable 实现。

  public static SaeaAwaitable AcceptAsync(this Socket socket, SaeaAwaitable awaitable)
public static SaeaAwaitable ConnectAsync(this Socket socket, SaeaAwaitable awaitable)
public static SaeaAwaitable DisonnectAsync(this Socket socket, SaeaAwaitable awaitable)
public static SaeaAwaitable ReceiveAsync(this Socket socket, SaeaAwaitable awaitable)
public static SaeaAwaitable SendAsync(this Socket socket, SaeaAwaitable awaitable)

SaeaPool 则是一个 QueuedObjectPool<SaeaAwaitable> 的衍生实现,用于池化 SaeaAwaitable 实例。同时,为了减少 TcpSocketSaeaSession 的构建过程,也实现了 SessionPool 即 QueuedObjectPool<TcpSocketSaeaSession>。

TcpSocketSaeaServer 中的 Accept Loop 指的就是,

  while (IsListening)
{
var saea = _acceptSaeaPool.Take(); var socketError = await _listener.AcceptAsync(saea);
if (socketError == SocketError.Success)
{
var acceptedSocket = saea.Saea.AcceptSocket;
} _acceptSaeaPool.Return(saea);
}

每一个建立成功的 Connection 由 TcpSocketSaeaSession 来处理,所以 TcpSocketSaeaSession 中会包含 Read Loop,

  var saea = _saeaPool.Take();
saea.Saea.SetBuffer(_receiveBuffer, , _receiveBuffer.Length); while (State == TcpSocketConnectionState.Connected)
{
saea.Saea.SetBuffer(, _receiveBuffer.Length); var socketError = await _socket.ReceiveAsync(saea);
if (socketError != SocketError.Success)
break; var receiveCount = saea.Saea.BytesTransferred;
if (receiveCount == )
break;
}

同样,TcpSocketSaeaServer 对外所暴露的接口也同样是 Awaitable 的。

  public interface ITcpSocketSaeaServerMessageDispatcher
{
Task OnSessionStarted(TcpSocketSaeaSession session);
Task OnSessionDataReceived(TcpSocketSaeaSession session, byte[] data, int offset, int count);
Task OnSessionClosed(TcpSocketSaeaSession session);
}

使用起来也是简单直接:

  public class SimpleMessageDispatcher : ITcpSocketSaeaServerMessageDispatcher
{
public async Task OnSessionStarted(TcpSocketSaeaSession session)
{
Console.WriteLine(string.Format("TCP session {0} has connected {1}.", session.RemoteEndPoint, session));
await Task.CompletedTask;
} public async Task OnSessionDataReceived(TcpSocketSaeaSession session, byte[] data, int offset, int count)
{
var text = Encoding.UTF8.GetString(data, offset, count);
Console.Write(string.Format("Client : {0} --> ", session.RemoteEndPoint));
Console.WriteLine(string.Format("{0}", text)); await session.SendAsync(Encoding.UTF8.GetBytes(text));
} public async Task OnSessionClosed(TcpSocketSaeaSession session)
{
Console.WriteLine(string.Format("TCP session {0} has disconnected.", session));
await Task.CompletedTask;
}
}

RIO 方式:TcpSocketRioServer

从 Windows 8.1 / Windows Server 2012 R2 开始,微软推出了 Registered I/O Networking Extensions 来支持高性能 Socket 服务的实现,简称 RIO。

The following functions are supported for Windows Store apps on Windows 8.1, Windows Server 2012 R2, and later. Microsoft Visual Studio 2013 Update 3 or later is required for Windows Store apps.

  • RIOCloseCompletionQueue
  • RIOCreateCompletionQueue
  • RIOCreateRequestQueue
  • RIODequeueCompletion
  • RIODeregisterBuffer
  • RIONotify
  • RIOReceive
  • RIOReceiveEx
  • RIORegisterBuffer
  • RIOResizeCompletionQueue
  • RIOResizeRequestQueue
  • RIOSend
  • RIOSendEx

到目前为止,.NET Framework 还没有推出对 RIO 的支持,所以若想在 C# 中实现 RIO 则只能通过 P/Invoke 方式,RioSharp 是开源项目中的一个比较完整的实现。

Cowboy.Sockets 直接引用了 RioSharp 的源代码,放置在 Cowboy.Sockets.Experimental 名空间下,以供实验和测试使用。

同样,通过 TcpSocketRioServer 来实现 Accept Loop,

_listener.OnAccepted = (acceptedSocket) =>
{
Task.Run(async () =>
{
await Process(acceptedSocket);
})
.Forget();
};

通过 TcpSocketRioSession 来处理 Read Loop,

  while (State == TcpSocketConnectionState.Connected)
{
int receiveCount = await _stream.ReadAsync(_receiveBuffer, , _receiveBuffer.Length);
if (receiveCount == )
break;
}

测试代码一如既往的类似:

  public class SimpleMessageDispatcher : ITcpSocketRioServerMessageDispatcher
{
public async Task OnSessionStarted(TcpSocketRioSession session)
{
//Console.WriteLine(string.Format("TCP session {0} has connected {1}.", session.RemoteEndPoint, session));
Console.WriteLine(string.Format("TCP session has connected {0}.", session));
await Task.CompletedTask;
} public async Task OnSessionDataReceived(TcpSocketRioSession session, byte[] data, int offset, int count)
{
var text = Encoding.UTF8.GetString(data, offset, count);
//Console.Write(string.Format("Client : {0} --> ", session.RemoteEndPoint));
Console.Write(string.Format("Client : --> "));
Console.WriteLine(string.Format("{0}", text)); await session.SendAsync(Encoding.UTF8.GetBytes(text));
} public async Task OnSessionClosed(TcpSocketRioSession session)
{
Console.WriteLine(string.Format("TCP session {0} has disconnected.", session));
await Task.CompletedTask;
}
}

参考资料

本篇文章《C#高性能TCP服务的多种实现方式》由 Dennis Gao 发表自博客园个人博客,未经作者本人同意禁止以任何的形式转载,任何自动的或人为的爬虫转载行为均为耍流氓。

C#高性能TCP服务的多种实现方式的更多相关文章

  1. C# 高性能 TCP 服务的多种实现方式

    哎~~ 想想大部分园友应该对 "高性能" 字样更感兴趣,为了吸引眼球所以标题中一定要突出,其实我更喜欢的标题是<猴赛雷,C# 编写 TCP 服务的花样姿势!>. 本篇文 ...

  2. C# 高性能 TCP 服务的多种实现方式Cowboy.Sockets

      本篇文章的主旨是使用 .NET/C# 实现 TCP 高性能服务的不同方式,包括但不限于如下内容: APM 方式,即 Asynchronous Programming Model TAP 方式,即 ...

  3. 【转】C# 高性能 TCP 服务的多种实现方式

    原文链接: http://www.cnblogs.com/gaochundong/p/csharp_tcp_service_models.html 开源库: https://github.com/ga ...

  4. C#高性能TCP服务

    C#高性能TCP服务 哎~~ 想想大部分园友应该对 "高性能" 字样更感兴趣,为了吸引眼球所以标题中一定要突出,其实我更喜欢的标题是<猴赛雷,C#编写TCP服务的花样姿势!& ...

  5. php中连接tcp服务的三种方式

    首先需要现有一个 tcp 服务,我们使用 php中的 socket 系列函数实现 <?php //创建socket套接字 $socket = socket_create(AF_INET, SOC ...

  6. SVN服务的模式和多种访问方式 多种访问原理图解与优缺点

    SVN企业应用场景 SVN任是当前企业的主流.git正在发展,未来会成为主流.如果大家精力足够,建议同时掌握. 1.4运维人员掌握版本管理 对于版本管理系统,运维人员需要掌握的技术点: 1.安装.部署 ...

  7. 子进程回收资源两种方式,僵尸进程与孤儿进程,守护进程,进程间数据隔离,进程互斥锁,队列,IPC机制,线程,守护线程,线程池,回调函数add_done_callback,TCP服务端实现并发

    子进程回收资源两种方式 - 1) join让主进程等待子进程结束,并回收子进程资源,主进程再结束并回收资源. - 2) 主进程 “正常结束” ,子进程与主进程一并被回收资源. from multipr ...

  8. 高性能 TCP/UDP/HTTP 通信框架 HP-Socket v4.1.1

    HP-Socket 是一套通用的高性能 TCP/UDP/HTTP 通信框架,包含服务端组件.客户端组件和 Agent 组件,广泛适用于各种不同应用场景的 TCP/UDP/HTTP 通信系统,提供 C/ ...

  9. 高性能 TCP/UDP/HTTP 通信框架 HP-Socket v4.1.2

    HP-Socket 是一套通用的高性能 TCP/UDP/HTTP 通信框架,包含服务端组件.客户端组件和 Agent 组件,广泛适用于各种不同应用场景的 TCP/UDP/HTTP 通信系统,提供 C/ ...

随机推荐

  1. DDD CQRS架构和传统架构的优缺点比较

    明天就是大年三十了,今天在家有空,想集中整理一下CQRS架构的特点以及相比传统架构的优缺点分析.先提前祝大家猴年新春快乐.万事如意.身体健康! 最近几年,在DDD的领域,我们经常会看到CQRS架构的概 ...

  2. Gradle配置APK自动签名完整流程

    转载请注明出处:http://www.cnblogs.com/LT5505/p/6256683.html 一.生成签名 1.命令行生成签名,输入命令keytool -genkey -v -keysto ...

  3. BIOS中未启用虚拟化支持系列~~例如:因此无法安装Hyper-V

    异常处理汇总-服务器系列:http://www.cnblogs.com/dunitian/p/4522983.html 一般都是启动一下CUP虚拟化就可以了 比如华硕的:

  4. PHP赋值运算

    1. 赋值运算:= ,意思是右边表达式的值赋给左边的运算数. $int1=10; $int1=$int1-6; //$int1=4 echo $int1,"<br>"; ...

  5. jQuery学习之路(4)- 动画

    ▓▓▓▓▓▓ 大致介绍 通过jQuery中基本的动画方法,能够轻松地为网页添加非常精彩的视觉效果,给用户一种全新的体验 ▓▓▓▓▓▓ jQuery中的动画 ▓▓▓▓▓▓ show()和hide()方法 ...

  6. iOS 键盘添加完成按钮,delegate和block回调

    这个是一个比较初级一点的文章,新人可以看看.当然实现这个需求的时候自己也有一点收获,记下来吧. 前两天产品要求在工程的所有数字键盘弹出时,上面带一个小帽子,上面安装一个“完成”按钮,这个完成按钮也没有 ...

  7. trigger事件模拟

    事件模拟trigger 在操作DOM元素中,大多数事件都是用户必须操作才会触发事件,但有时,需要模拟用户的操作,来达到效果. 需求:页面初始化时触发搜索事件并获取input控件值,并打印输出(效果图如 ...

  8. maven 快照

    大型应用软件一般由多个模块组成,一般它是多个团队开发同一个应用程序的不同模块,这是比较常见的场景.例如,一个团队正在对应用程序的应用程序,用户界面项目(app-ui.jar:1.0) 的前端进行开发, ...

  9. python-time 模块

    1.时间戳是以秒为单位的浮点小数,时间戳以自1970年1月1日午夜到现在经过了的时间来表示 2.时间模块引入方式:import time 3.返回时间戳 time.time() 4.返回时间元组:ti ...

  10. Jquery EasyUI 开发实录

    有好几年没有用过EasyUI了,最近在外包做的一个项目中新增功能时,又用到了,本以为和按照以前那样用就可以了,可当我真正用的时候,发现许多地方不一样了,就连官网的文档都更新了,最突出的就是不知道什么时 ...