Kestrel.Transport.Sockets分析与使用

相信大家都清楚asp core有着非常出色的性能,它出色的性能也源于网络服务模块Kestrel;在techempower测试中Kestrel基础核心达到了700万级别的RPS吞吐能力,具备这样的能力那对应的Kestrel.Transport.Sockets也应有着不错的性能。接下来简单地分析一下Kestrel.Transport.Sockets的设计和使用,并进行简单的并发处理能力测试。

async/await

async/await的使用这几年时间里大放异彩,现有新功能的IO操作方式无一不支持它,毕竟可以同步的代码方式来实现异步处理功能,不管是开发,调试还是维护都带来的极大的便利性;既然这样Kestrel.Transport.Sockets也在基础的socket异步基础功能上引入了async/await设计,大大简化了上层应用编写的复杂度;下面看一下针对SocketAsyncEventArgs封装的Awaitable。

    public class SocketAwaitableEventArgs : SocketAsyncEventArgs, ICriticalNotifyCompletion
    {
        private static readonly Action _callbackCompleted = () => { };

        private readonly PipeScheduler _ioScheduler;

        private Action _callback;

        public SocketAwaitableEventArgs(PipeScheduler ioScheduler)
        {
            _ioScheduler = ioScheduler;
        }

        public SocketAwaitableEventArgs GetAwaiter() => this;
        public bool IsCompleted => ReferenceEquals(_callback, _callbackCompleted);

        public int GetResult()
        {
            Debug.Assert(ReferenceEquals(_callback, _callbackCompleted));

            _callback = null;

            if (SocketError != SocketError.Success)
            {
                ThrowSocketException(SocketError);
            }

            return BytesTransferred;

            void ThrowSocketException(SocketError e)
            {
                throw new SocketException((int)e);
            }
        }

        public void OnCompleted(Action continuation)
        {
            if (ReferenceEquals(_callback, _callbackCompleted) ||
                ReferenceEquals(Interlocked.CompareExchange(ref _callback, continuation, null), _callbackCompleted))
            {
                Task.Run(continuation);
            }
        }

        public void UnsafeOnCompleted(Action continuation)
        {
            OnCompleted(continuation);
        }

        public void Complete()
        {
            OnCompleted(this);
        }

        protected override void OnCompleted(SocketAsyncEventArgs _)
        {
            var continuation = Interlocked.Exchange(ref _callback, _callbackCompleted);

            if (continuation != null)
            {
                _ioScheduler.Schedule(state => ((Action)state)(), continuation);
            }
        }
    }

这个Awaitable的设计得非常好,它没没有引用新的对象,而是直接在SocketAsyncEventArgs的基础派生下来实现,这样在高并吞吐的情况可以更好地降低新对象的开销;这即能使用await的同时也无需增加对象的开销,不过PipeScheduler的调用设计竟然使用了匿名函数的方式带入,这样会增加了对象的开销;还有就是SocketAsyncEventArgs完成后还投递给一个线程调度去完成后面的工作,如果协议分析的工作量不大的情况个人感觉这个处理有些重了,不过使用都可以实现自己的PipeScheduler或直接改成执行continuation,最好是根据情况来配置最佳。

引入System.IO.Pipelines

在之前的文章已经说过Pipe,它是一个Buffer读写对象,其重要作用是可以把不连续的数据内存块连接起来处理起来,这样可以使普通开发人员避开Buffer的创建和回收的繁琐工作(毕竟这一块工作要做好还是有点难度的)。Pipe不紧紧提供了不连续数据Buffer的读写,它还具备一套await状态机制可以让使用人员从socket的receive和send工作分离出来。每个连接会分配两个Pipe对象,主要负责Socket的receive和send工作;其工作原理如下:

基于Pipe使用者只需要关心应用协议处理处理即可,而这个处理会变得非常简单;只需要关注Pipe的Writer和Reader即可。虽然这样做带来了便利性,但经过Pipe多了两层状态通讯多多少少会有性能上的影响,但这些影响相对Buffer开销,GC和处理来说则还是有比较好的回报的。这里还是要重吐嘲一下MS,为什么Writer和Reader不按BinaryReader和BinaryWriter的基准作为设计,其实Pipe对普通使用者来说还是不怎友好的!

使用

Kestrel.Transport.Sockets的使用还真有点让人头痛,首先它没有完善的文档,还有设计集成度也比较高。要搞清楚怎么用对于新手来说还真不怎容易,出于研究它的设计和对比查看了一段时间源码才总结出来如何用;最终发现要用得好还需真要再做一层封装才能更好的用于实限应用中;下面讲解一下如何简单地使用它吧,首先你要在Nuget中引用它。

构建

Kestrel.Transport.Sockets的使用入口是SocketTransportFactory,只要能构建这个对象那接下工作就简单很多,首先看一下这个对象的构造函数

public SocketTransportFactory(IOptions<SocketTransportOptions> options, IApplicationLifetime applicationLifetime, ILoggerFactory loggerFactory);

三个参数都是接口……没有文档的情况还真有点头痛。ILoggerFactory引用Microsoft.Extensions.Logging可以得到,剩下两个简单地实现一下即可。

IOptions<SocketTransportOptions>

    public class SocketOpetion : IOptions<SocketTransportOptions>
    {
        public SocketTransportOptions Value => new SocketTransportOptions();
    }

IApplicationLifetime

    public class ApplicationLifetime : IApplicationLifetime
    {
        public ApplicationLifetime() : this(new CancellationToken(), new CancellationToken(), new CancellationToken())
        {

        }
        public ApplicationLifetime(CancellationToken started, CancellationToken stopping, CancellationToken stoped)
        {

            ApplicationStarted = started;
            ApplicationStopping = stopping;
            ApplicationStopped = stoped;
        }
        public CancellationToken ApplicationStarted { get; set; }

        public CancellationToken ApplicationStopping { get; set; }

        public CancellationToken ApplicationStopped { get; set; }

        public virtual void StopApplication()
        {

        }
    }

创建服务

以上接口的实现都有了,接下来就可以创建SocketTransportFactory对象了

        private static async void ListenSocket(int prot)
        {
            var loggerFactory = new LoggerFactory();
            ApplicationLifetime applicationLifetime = new ApplicationLifetime();
            var server = new SocketTransportFactory(new SocketOpetion(), applicationLifetime, loggerFactory);
            await server.Create(new AnyEndPointInformation(prot), new Program()).BindAsync();
        }

同样SocketTransportFactory的Create方法也需要两个接口参数,一个是监听类型和地址描述,一个连接调度器。这里只需要IP端口监听所以实现起来比较简单:

    public class AnyEndPointInformation : IEndPointInformation
    {
        public AnyEndPointInformation(int port)
        {
            IPEndPoint = new IPEndPoint(IPAddress.Any, port);
        }

        public ListenType Type => ListenType.IPEndPoint;

        public IPEndPoint IPEndPoint { get; set; }

        public string SocketPath => throw new NotImplementedException();

        public ulong FileHandle => throw new NotImplementedException();

        public FileHandleType HandleType { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }

        public bool NoDelay => true;
    }

接下来的工作就在IConnectionDispatcher接口的OnConnection方法下处理连接

        public void OnConnection(TransportConnection connection)
        {
            Session session = new Session(connection);
            Task.Run(session.StartRecive);
        }

刚开始以为有了TransportConnection就可以进行数据接收和发送,但事情是我想得太简单了!其实TransportConnection并不具备数据处理能力,因为里面两路的Pipe是空的……使用者需要自己定义对应的Pipe并设置给它,以上代码的Session是需要自己实现的,名称随自己喜欢定义;实现接口IDuplexPipe,设置两路的Pipe对象,然后设置到TransportConnection.Application属性上。实现IDuplexPipe后就可以进行数据接收和发送功能了,以下是实现了一个简单的StartRecive后回发数据,有收有发才便于下面测试的工作。

        public async Task StartRecive()
        {
            while (true)
            {
                var data = await Receiver.ReadAsync();
                if (data.IsCompleted)
                {
                    this.Dispose();
                    break;
                }
                var buffers = data.Buffer;
                var end = buffers.End;
                if (buffers.IsSingleSegment)
                {
                    ReadOnlyMemory<byte> b = buffers.First;
                    var sbuf = Sender.GetMemory(b.Length);
                    b.CopyTo(sbuf);
                    Sender.Advance(b.Length);
                }
                else
                {
                    foreach (var b in buffers)
                    {
                        var sbuf = Sender.GetMemory(b.Length);
                        b.CopyTo(sbuf);
                        Sender.Advance(b.Length);
                    }
                }
                var flush = await Sender.FlushAsync();
                Receiver.AdvanceTo(end);
            }
        }

测试

既然研究它自然就会关心它的性能情况,针对以上最简单接收后返回的功能进行了一个压力测试。测试结果总体上来说还算不错,但算不上非常出色;最终测结果在一台E3 1230V2的PC机上测试结果是:10000连接,接近20万rps。