KestrelServer详解[2]: 网络链接的创建
《注册监听终结点(Endpoint)》已经详细讲述了如何使用KestrelServer,现在我们来简单聊聊这种处理器的总体设计和实现原理。当KestrelServer启动的时候,注册的每个终结点将转换成对应的“连接监听器”,后者在监听到初始请求时会创建“连接”,请求的接收和响应的回复都在这个连接中完成。本文提供的示例演示已经同步到《ASP.NET Core 6框架揭秘-实例演示版》)
一、连接上下文(ConnectionContext )
监听器创建的连接时一个抽象的概念,我们可以将其视为客户端和服务端完成消息交换而构建的“上下文”,该上下文通过如下这个ConnectionContext类型表示。ConnectionContext派生于抽象基类BaseConnectionContext,后者实现了IAsyncDisposable接口。每个连接具有一个通过ConnectionId属性表示的ID,它的LocalEndPoint和RemoteEndPoint属性返回本地(服务端)和远程(客户端)终结点。服务器提供的特性集合体现在它的Features属性上,另一个Items提供了一个存放任意属性的字典。ConnectionClosed属性提供的CancellationToken可以用来接收连接关闭的通知。Abort方法可以中断当前连接,这两个方法在ConnectionContext被重写。ConnectionContext类型的Transport属性提供的IDuplexPipe对象是用来对请求和响应进行读写的双向管道。
public abstract class ConnectionContext : BaseConnectionContext
{
public abstract IDuplexPipe Transport { get; set; }
public override void Abort(ConnectionAbortedException abortReason);
public override void Abort();
} public abstract class BaseConnectionContext : IAsyncDisposable
{
public virtual EndPoint? LocalEndPoint { get; set; }
public virtual EndPoint? RemoteEndPoint { get; set; }
public abstract string ConnectionId { get; set; }
public abstract IFeatureCollection Features { get; }
public abstract IDictionary<object, object?> Items { get; set; }
public virtual CancellationToken ConnectionClosed { get; set; } public abstract void Abort();
public abstract void Abort(ConnectionAbortedException abortReason);
public virtual ValueTask DisposeAsync();
}
如果采用HTTP 1.X和HTTP 2协议,KestrelServer会采用TCP套接字(Socket)进行通信,对应的连接体现为一个SocketConnection对象。如果采用的是HTTP 3,会采用基于UDP的QUIC协议进行通信,对应的连接体现为一个QuicStreamContext对象。如下面的代码片段所示,这两个类型都派生于TransportConnection,后者派生于ConnectionContext。
internal abstract class TransportConnection : ConnectionContext
internal sealed class SocketConnection : TransportConnection
internal sealed class QuicStreamContext : TransportConnection
二、连接监听器(IConnectionListener )
KestrelServer同时支持三个版本的HTTP协议,HTTP 1.X和HTTP 2建立在TCP协议之上,针对这样的终结点会转换成通过如下这个IConnectionListener接口表示的监听器。它的EndPoint属性表示监听器绑定的终结点,当AcceptAsync方法被调用时,监听器便开始了网络监听工作。当来自某个客户端端的初始请求抵达后,它会将创建代表连接的ConnectionContext上下文创建出来。另一个UnbindAsync方法用来解除终结点绑定,并停止监听。
public interface IConnectionListener : IAsyncDisposable
{
EndPoint EndPoint { get; }
ValueTask<ConnectionContext?> AcceptAsync(CancellationToken cancellationToken = default(CancellationToken));
ValueTask UnbindAsync(CancellationToken cancellationToken = default(CancellationToken));
}
QUIC利用传输层的UDP协议实现了真正意义上的“多路复用”,所以它将对应的连接监听器接口命名为IMultiplexedConnectionListener。它的AcceptAsync方法创建的是代表多路复用连接的MultiplexedConnectionContext对象,后者的AcceptAsync会将ConnectionContext上下文创建出来。QuicConnectionContext 类型是对MultiplexedConnectionContext的具体实现,它的AcceptAsync方法创建的就是上述的QuicStreamContext对象,该类型派生于抽象类TransportMultiplexedConnection。
public interface IMultiplexedConnectionListener : IAsyncDisposable
{
EndPoint EndPoint { get; }
ValueTask<MultiplexedConnectionContext?> AcceptAsync(IFeatureCollection? features = null,CancellationToken cancellationToken = default(CancellationToken));
ValueTask UnbindAsync(CancellationToken cancellationToken = default(CancellationToken));
} public abstract class MultiplexedConnectionContext : BaseConnectionContext
{
public abstract ValueTask<ConnectionContext?> AcceptAsync(CancellationToken cancellationToken = default(CancellationToken));
public abstract ValueTask<ConnectionContext> ConnectAsync(IFeatureCollection? features = null,CancellationToken cancellationToken = default(CancellationToken));
} internal abstract class TransportMultiplexedConnection : MultiplexedConnectionContext
internal sealed class QuicConnectionContext : TransportMultiplexedConnection
KestrelServer使用的连接监听器均由对应的工厂来构建。如下所示的IConnectionListenerFactory接口代表用来构建IConnectionListener监听器的工厂,IMultiplexedConnectionListenerFactory工厂则用来构建IMultiplexedConnectionListener监听器。
public interface IConnectionListenerFactory
{
ValueTask<IConnectionListener> BindAsync(EndPoint endpoint,CancellationToken cancellationToken = default(CancellationToken));
} public interface IMultiplexedConnectionListenerFactory
{
ValueTask<IMultiplexedConnectionListener> BindAsync(EndPoint endpoint, IFeatureCollection? features = null,CancellationToken cancellationToken = default(CancellationToken));
}
三、总体设计
上面围绕着“连接”介绍了一系列接口和类型,它们之间的关系体现在如图1所示的UML中。KestrelServer启动时会根据每个终结点支持的HTTP协议利用IConnectionListenerFactory或者IMultiplexedConnectionListenerFactory工厂来创建代表连接监听器的IConnectionListener或者IMultiplexedConnectionListener对象。IConnectionListener监听器会直接将代表连接的ConnectionContext上下文创建出来,IMultiplexedConnectionListener监听器创建的则是一个MultiplexedConnectionContext上下文,代表具体连接的ConnectionContext上下文会进一步由该对象进行创建。

图1 “连接”相关的接口和类型
四、利用连接接收请求和回复响应
下面演示的实例直接利用IConnectionListenerFactory工厂创建的IConnectionListener监听器来监听连接请求,并利用建立的连接来接收请求和回复响应。由于表示连接的ConnectionContext上下文直接面向传输层,接受的请求和回复的响应都体现为二进制流,解析二进制数据得到请求信息是一件繁琐的事情。这里我们借用了“HttpMachine”NuGet包提供的HttpParser组件来完成这个任务,为此我们为它定义了如下这个HttpParserHandler类型。如果将这个HttpParserHandler对象传递给HttpParser对象,后者在请求解析过程中会调用前者相应的方法,我们利用这些方法利用读取的内容将描述请求的HttpRequestFeature特性构建出来。源代码可以从这里查看。
public class HttpParserHandler : IHttpParserHandler
{
private string? headerName = null;
public HttpRequestFeature Request { get; } = new HttpRequestFeature(); public void OnBody(HttpParser parser, ArraySegment<byte> data) => Request.Body = new MemoryStream(data.Array!, data.Offset, data.Count);
public void OnFragment(HttpParser parser, string fragment) { }
public void OnHeaderName(HttpParser parser, string name) => headerName = name;
public void OnHeadersEnd(HttpParser parser) { }
public void OnHeaderValue(HttpParser parser, string value) => Request.Headers[headerName!] = value;
public void OnMessageBegin(HttpParser parser) { }
public void OnMessageEnd(HttpParser parser) { }
public void OnMethod(HttpParser parser, string method) => Request.Method = method;
public void OnQueryString(HttpParser parser, string queryString) => Request.QueryString = queryString;
public void OnRequestUri(HttpParser parser, string requestUri) => Request.Path = requestUri;
}
如下所示的演示程序利用WebApplication对象的Services属性提供的IServicePovider对象来提供IConnectionListenerFactory工厂。我们调用该工厂的BindAsync方法创建了一个连接监听器并将其绑定到采用5000端口本地终结点。在一个无限循环中,我们调用监听器的AcceptAsync方法开始监听连接请求,并最终将代表连接的ConnectionContext上下文创建出来。
using App;
using HttpMachine;
using Microsoft.AspNetCore.Connections;
using Microsoft.AspNetCore.Http.Features;
using System.Buffers;
using System.IO.Pipelines;
using System.Net;
using System.Text; var factory = WebApplication.Create().Services.GetRequiredService<IConnectionListenerFactory>();
var listener = await factory.BindAsync(new IPEndPoint(IPAddress.Any, 5000));
while (true)
{
var context = await listener.AcceptAsync();
_ = HandleAsync(context!); static async Task HandleAsync(ConnectionContext connection)
{
var reader = connection!.Transport.Input;
while (true)
{
var result = await reader.ReadAsync();
var request = ParseRequest(result);
reader.AdvanceTo(result.Buffer.End);
Console.WriteLine("[{0}]Receive request: {1} {2} Connection:{3}",connection.ConnectionId, request.Method, request.Path, request.Headers?["Connection"] ?? "N/A"); var response = @"HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 12 Hello World!";
await connection.Transport.Output.WriteAsync(Encoding.UTF8.GetBytes(response));
if (request.Headers.TryGetValue("Connection", out var value) && string.Compare(value, "close", true) == 0)
{
await connection.DisposeAsync();
return;
}
if (result.IsCompleted)
{
break;
}
}
} static HttpRequestFeature ParseRequest(ReadResult result)
{
var handler = new HttpParserHandler();
var parserHandler = new HttpParser(handler);
parserHandler.Execute(new ArraySegment<byte>(result.Buffer.ToArray()));
return handler.Request;
}
}
针对连接的处理实现在HandleAsync方法中。HTTP 1.1默认会采用长连接,多个请求会使用同一个连接发送过来,所以针对单个请求的接收和处理会放在一个循环中,直到连接被关闭。请求的接收利用ConnectionContext对象的Transport属性返回的IDuplexPipe对象来完成。简单起见,我们假设每个请求的读取刚好能够一次完成,所以每次读取的二进制刚好是一个完整的请求。读取的二进制内容利用ParseRequest方法借助于HttpParser对象转换成HttpRequestFeature对象后,我们直接生成一个表示响应报文的字符串并采用UTF-8对其编码,编码后的响应利用上述的IDuplexPipe对象发送出去。这份手工生成的“Hello World!”响应将以图18-5的形式呈现在浏览器上。

图2 面向“连接”编程
按照HTTP 1.1规范的约定,如果客户端希望关闭默认开启的长连接,可以在请求中添加“Connection:Close”报头。HandleAsync方法在处理每个请求时会确定是否携带了此报头,并在需要的时候调用ConnectionContext上下文的 DisposeAsync方法关闭并释放当前连接。该方法在对请求进行处理时会将此报头和连接的ID输出到控制台上。图2所示的控制台输出是先后接收到三次请求的结果,后面两次显式添加了“Connection:Close”,可以看出前两次复用同一个连接。
KestrelServer详解[2]: 网络链接的创建的更多相关文章
- Spring框架系列(10) - Spring AOP实现原理详解之AOP代理的创建
上文我们介绍了Spring AOP原理解析的切面实现过程(将切面类的所有切面方法根据使用的注解生成对应Advice,并将Advice连同切入点匹配器和切面类等信息一并封装到Advisor).本文在此基 ...
- Oracle10g数据泵impdp参数详解--摘自网络
Oracle10g数据泵impdp参数详解 2011-6-30 12:29:05 导入命令Impdp • ATTACH 连接到现有作业, 例如 ATTACH [=作业名]. • C ...
- 一文详解 WebSocket 网络协议
WebSocket 协议运行在TCP协议之上,与Http协议同属于应用层网络数据传输协议.WebSocket相比于Http协议最大的特点是:允许服务端主动向客户端推送数据(从而解决Http 1.1协议 ...
- KestrelServer详解[3]: 自定义一个迷你版的KestrelServer
和所有的服务器一样,KestrelServer最终需要解决的是网络传输的问题.在<网络连接的创建>,我们介绍了KestrelServer如何利用连接接听器的建立网络连接,并再次基础上演示了 ...
- 详解SQLServer如何链接远程MySQL数据库
最近遇到“SQL如何链接远程MySQL”这个问题,现在问题终于解决,特把方法贴出来:(我所用的操作系统是Win7,数据库是SQL2005.) 1.在SQL SERVER服务器上安装MYSQL ODBC ...
- 002——Angular 目录结构分析、app.module.ts 详解、以及 Angular 中创建组件、组件 详解、 绑定数据
一.目录结构分析 二. app.module.ts.组件分析 1.app.module.ts 定义 AppModule,这个根模块会告诉 Angular 如何组装该应用. 目前,它只声明了 AppCo ...
- 详解C# 网络编程系列:实现类似QQ的即时通信程序
https://www.jb51.net/article/101289.htm 引言: 前面专题中介绍了UDP.TCP和P2P编程,并且通过一些小的示例来让大家更好的理解它们的工作原理以及怎样.Net ...
- NetBios 的结构体详解(网络控制块NCB)
对之前网络基础编程用到控制块NCB进行介绍(补充): 在Win32环境下,使用VC++6.0进行NetBIOS程序开发时, 需要用到nb30.h文件和netapi32.lib静态链接库.前者定义了Ne ...
- 转: Android开发中的MVP架构详解(附加链接比较不错)
转: http://www.codeceo.com/article/android-mvp-artch.html 最近越来越多的人开始谈论架构.我周围的同事和工程师也是如此.尽管我还不是特别深入理解M ...
随机推荐
- [LeetCode]1450. 在既定时间做作业的学生人数
给你两个整数数组 startTime(开始时间)和 endTime(结束时间),并指定一个整数 queryTime 作为查询时间. 已知,第 i 名学生在 startTime[i] 时开始写作业并于 ...
- Objective-C 基础教程第三章,面向对象编程基础知
目录 Objective-C 基础教程第三章,面向对象编程基础知 0x00 前言 0x01 间接(indirection) 0x02 面向对象编程中使用间接 面向过程编程 面向对象编程 0x03 OC ...
- 数据库项目部署(nginx)
1.在虚拟机2008server下载nginx http://nginx.org/download/nginx-1.17.10.zip 注1:此版本为window版本 linux版本 2. 解压软件至 ...
- python中特殊参数self的作用
特殊参数self的作用:self会接收实例化过程中传入的数据,当实例对象创建后,实例便会代替 self,在代码中运行. self代表的是类的实例本身,方便数据的流转.对此,我们需要记住两点: 第一点: ...
- ajax读本地文件
前置条件: 1. 编辑器,我用的是HbuilderX,解压即食,非常美味,点击传送门. 2. jQuery.min.js,点击传送门. 简单的项目目录: a. test.html内容 <!DOC ...
- 如何利用Smartbi做数据分析:车企销量排名TOP10的车型
下面我们来对比一下去年和今年的销量.同比增长情况: 图1. 轿车销量月趋势 图2. SUV销量月趋势 不难看出2月.7月为两个销量低点,而7月销量受天气影响较大,烈日.高温,毫无看车热情,复杂多变的外 ...
- petite-vue源码剖析-从静态视图开始
代码库结构介绍 examples 各种使用示例 scripts 打包发布脚本 tests 测试用例 src directives v-if等内置指令的实现 app.ts createApp函数 blo ...
- Scala学习笔记(详细)
第2章 变量 val,var,声明变量必须初始化:变量类型确定后不可更改 数据类型:与java有相同的数据类型,在scala中数据类型都是对象 特殊类型:Unit:表示无值,只有一个实例值写出(),相 ...
- Java课程设计---安装解压版mysyql
1.解压 将mysql-5.7.13-winx64.rar解压到C盘根目录.(将原有data文件夹清空,注意是清空,不是删除data文件夹) 2. 配置环境变量MYSQL_HOME (参照java打开 ...
- 详解pandas的read_csv方法
楔子 使用pandas做数据处理的第一步就是读取数据,数据源可以来自于各种地方,csv文件便是其中之一.而读取csv文件,pandas也提供了非常强力的支持,参数有四五十个.这些参数中,有的很容易被忽 ...