一.NET Core理论基础
===================.Net Core===================
1)CLI
类似于java的jre
有2种完全不相干的称呼:
Common Language Infrastructure 通用中间架构
Command Line Interface 命令行接口
c#: int一定是4字节。我们不需要关注平台差异。
C语言 int=至少2字节:在不同平台上是不同的字节。我们需要注意差异去编写不同的代码。
基础类型、指令种类、模块、类,方法的结构,二进制的文件格式
C#、VB.NET、F#-->最终编译为统一的IL
二进制的文件格式: 针对不同的操作系统生成不同的格式,还是不同的格式呢? ==》选择了后者统一的格式
dll: PE
.net core需要提供一种容器(工具),确保所有os平台上都可以加载PE格式的dll文件.
.NET Core CLI工具: .net core 2.0才稳定,早期经历很多挫折。因为开始是用不同平台开发的。
不管是vs还是rider,都是通过这个CLI与.net core交互的。
2)CoreCLR // 公共语言运行时,c++编写,可以找成员方法,翻译为原生平台的字节码: 类型、反射、方法、成员、GC
1.中间代码解析
.net程序: 包含中间代码IL,因此可以跨平台。
原生程序(可调用os native接口,只能运行在特定平台)
2.中间代码的编译
JIT(IL语言的编译器)
3.保证类型安全
c语言: int* --> void*, 传递到其它地方的话,是不知道是啥类型的,甚至可以转化为long,发射管不可预料的后果。
c#: string-->object, 我们GetType得到类型。
4.异常处理:
传统是通过返回值,需要开发者费心确定每个函数的返回值。
c#则可以try catch
5.线程管理:
c#对原生线程对象进行包装,我们可以用一致的代码进行多线程开发。 托管线程。
6.GC
只能管理托管代码。
3)CoreFX // 使用c#开发, 减少CoreCLR开发压力
基础类型,c#编写--》我们使用的时间、控制台等库
分开存放: 共用、特定平台
条件编译 // 避免工厂模式的使用。
跨平台兼容的基础类库:partial
.NET Standard 2.0标准: .NET Framework 4.6.1 .NET Core 2.0
4)Roslyn // .net上的高级语言的编译平台(不仅提供了代码编译功能,还提供了代码分析,我们可以调用Roslyn的API进行平台编译)
我们可以把代码粘贴到文本框中,进行编译。
之前是: CSC。 Roslyn则是更加高效,提供了动态编译功能,敲完代码,Roslyn就可以告诉vs代码有哪些错误。
我们可以用扩展方法,自己添加类库的方式,而不需要去修改.net core的源码。
===================ASP .Net Core===================
ASP.NET Core 之前不依赖.NET Core
前身: ASP .NET MVC,早就开源了
MONO: 支持运行到linux
源码:
aspnetcore
runtime
.NET Standard:
它不是程序,而是纯文本的类型和函数声明信息。
规范:相同功能的类型和函数,在不同的.NET开发框架中具有相同的形态。
=============================
https://source.dot.net/#q=Directory
dnSpy
=============================
postman
.NET Standard: 是标准,可以被不同应用程序支持和运行的标准,只是个类库。 版本越高,兼容性越低。 一般是做类库时,可以创建这个。
NotSupportedException:
如: AppDomain.CreateDomain就是在Linux不支持。
.NET Core:
Core CLR 和 .NET Framework CLR区别大吗?
只是某个方法不支持。
==================
ABP=Spring
==========
1. .NET Portability Analyzer
迁移源代码工程的分析工具
https://github.com/microsoft/dotnet-apiport
2. .NET API 目录查询
https://apisof.net/catalog
3. .NET 在线源码
https://source.dot.net/
4. dnSpy
https://github.com/0xd4d/dnSpy
.net core和java运行性能对比
c#
using System.Diagnostics;
class Program
{
static void Main(string[] args)
{
while (true)
{
var list = new List<int>();
var sw = Stopwatch.StartNew();
for (int i = 0; i < 100_000_000; i++)
{
list.Add(i);
list.RemoveAt(0);
}
Console.WriteLine(sw.Elapsed);
}
}
}
/*
Administrator@DESKTOP-JTMBOEI MINGW64 /d/2_test_java/ConsoleApp1/ConsoleApp1/bin/Release/net6.0/win-x64/publish
$ ./ConsoleApp1.exe
00:00:00.3990029
00:00:00.3877960
00:00:00.3774225
00:00:00.3772492
00:00:00.3771980
*/
java
package com.example.demo.testLoop;
import org.springframework.util.StopWatch;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static void main(String[] args) {
while (true) {
List<Integer> list = new ArrayList<>();
long start = System.currentTimeMillis();
for (int i = 0; i < 100_000_000; i++) {
list.add(i);
list.remove(0);
}
System.out.println(System.currentTimeMillis() - start);
}
}
}
/*
739
739
620
626
617
617
617
620
618
617
619
624
629
615
625
*/
结论:
这个例子c#比java快了将近1倍。
同时占用更少的内存,支持跨平台。
所以做游戏用c#可能是更合适的。
三、多线程
1)多线程
(1)CPU核心是有限的
(2)成千上万个线程
(3)不仅每个线程可以执行不同的任务,而且线程间还能协同工作
总结: 多线程就是在有限的核心上执行多个任务
2)分类
(1)原生线程:
操作系统管理的线程叫做:原生线程。
CPU核心都有N个寄存器,从内存读指令,然后执行。
单个核心同一时刻只能运行一个线程,为了实现多个线程运行的效果,就需要逻辑核心进行轮流运行,每个线程只能轮流运行一个很短的时间,也就是线程切换。
2种线程切换:
主动切换:
如:锁被拿走了。
读取文件,线程里的任务会要求暂停运行线程。
被动:
运行超过了时间后,os会强制切换到下一个线程,运行最大时间叫做:时间片。 基于:硬件计时器实现。
线程上下文:CPU各个寄存器的值的数据结构就叫做线程上下文。
线程调度机制:负责安排待运行队列中的线程。
栈空间:原生线程需要,用于保存函数使用的数据。 参数,返回数据。 是线程创建时os分配。
(2)托管线程:
因为原生线程在不同的操作系统上有不同的实现,.net core基于原生线程搭建了一组线程模型,让托管代码在不同平台上用相同的方式运行。
1个托管线程(Thread对象)只能运行一个原生线程: 0:1 1:1,运行之后,就会与原生线程关联。
3)何时创建
我们调用Thread的Start方法,就会关联。
.net运行时内部使用,同时创建,并关联。
非托管代码在原生线程上,首次调用托管代码。
.net程序运行后,主线程调用Main函数时。
4)Task: Thread对象的包装。
5)扫描各个托管线程的变量。
6)标准的托管线程接口。
7)2种模式
抢占模式:不能访问托管堆上的对象,等待GC结束,切换到合作模式。
合作模式: 可以自由访问托管堆上的对象。
8)ThreadStatic: 线程本地存储。
9)学习
先学会用手机,再刷机,再研究os。
也可以是学院派,先研究os,再学习业务。
10)锁
多个线程访问一个资源。原子操作。
11)
线程锁: 让一段代码变成原子操作。
无锁算法: 修改操作的内容以满足原子操作的条件。
四、异步操作
异步:表示执行某项操作后,不等待操作结束,但是可以在操作结束后收到通知。
Linux平台,一个线程栈默认会分配8~10M的空间。
C10K个客户端--》连接服务。
问题:
如果1W个客户端去连接服务器,那消耗的内存就过大!
解决办法:
事件循环机制: 1个或多个线程专门用于捕获对象状态,把执行的阻塞操作换为:非阻塞操作。 再注册时间以处理完成后收到通知。 如:node.js的异步。
但是基于这种机制,编写代码困难。
于是出现:基于回调的异步操作。 如:netty, iocp, aio.
但是c#本身就支持异步编程模型:APM, 带回调委托。
.net core再不同平台使用不同的方案:IOCP, epoll, kqueue。
以回调封装的不好。于是.net封装出了TPL任务并行库。 让任何的异步操作都具有相同的注册,等待结束,和进行处理,处理错误。 并为:await, async打下基础。
Task:表示执行一个没有任何返回类型的异步方法。 ContinueWith:可以在一个异步操作完成后,执行另外一个异步操作。
异步操作具有传染性。一个是异步,其它全部异步了。
一般在任务中使用委托,委托完成任务完成。 委托在.net运行内部的线程池中回调。
第二种是:Promise模式,一种是承诺对象(负责设置操作结果和发送异常),一种是Future将来对象(负责注册回调和接收承诺的结果)。
.net中封装了,其中:承诺接口是不对外公开的,我们只能对指定的类型进行操作。
ValueTask类型=Task的包装类=值类型。
async, await。
================
async, await // 也是基于TPL, 有了这2个后,我们很少再去写Task了,极大的简化的异步的编写。
async用于标记方法会使用await进行一步操作,把整个方法合并为一个异步操作,
非常直观的编写一连串的异步操作。
和我们阻塞操作+ 多线程是差不多的。
只不过多了一个关键字。
只不过阻塞操作需要更多的线程配合。
而异步操作可以在有限的线程中同时执行大量操作。
异步操作:需要多线程的支持,但是不需要很多个多线程,1W个任务,业务2个线程就行了,1个线程处理任务,1个线程检查状态并调用回调。
============实现====
C#编译器把方法内部的逻辑和本地的变量合并在一起作为一个状态机处理。
.net运行时提供了相关类型的支持,原有的方法体会变成创建状态机的代码。
多个异步操作合并为一个:状态机的话,默认state=-1,state=0表示第一个异步执行完成,state=1表示第2个异步执行完成。 state=-2表示整个异步执行完成。
MoveNext会执行到下一步。
最后还是反编译为Task了!
await关键字转化为了awaiter。 TaskAwaiter。
本质就是:每执行一步,记录一个状态。
=============
异步本地存储: AsyncLocal // 通过执行上下文保存, 每一个托管对象都有一个。
因为调用前的线程和返回到的线程不一定是一个线程。
============
执行子任务的时候是要保存上下文的。
==============
线程上下文的切换(唤醒/等待的切换 = 这会消耗资源),但是线程池中线程几乎都是唤醒着的。
除非线程池中线程不够。
===========
大部分情况都涉及IO,因此能用异步就用异步,最大化利用硬件资源, 不用担心乱用。
==========CPU上下文的切换
主动切换 // 自己时间到了
被动切换 // 时间片到了
休眠与唤醒的切换,是毫秒级。
2个线程都醒着,一个线程在重试,一个线程在运行,那切换几乎没有消耗。纳秒.
六、依赖注入
1)基础知识
.NET Core 新增的依赖注入。
IOC的作用:
1.映射依赖,注册类型,注册服务
2.实例解析
为什么要控制反转:
不需要去new类型了,因为每次new可能有风险,变为找第三方创建。
如果是负责的对象,需要依赖很多其它的对象。
抽象:接口=服务。
如:发消息的服务。 我只管发送消息就行, 如: 发送短信,微信,我只管引入:发送消息的服务就行。 我需要短信就注入短信,需要微信就注入微信,我们不需要去修改业务的代码。
为什么需要DI:
帮助管理复杂的依赖关系。
扩展性。
2)DI
内置的DI:
一个构造必须是另外一个的超集。
Sctutor(推荐:程序集注册+筛选): 不是依赖注入框架,.NET内置DI的扩展包,是为了弥补.NET 内置DI的服务注册方式。
底层依然是内置DI。 就算版本更新,Sctutor是针对DI进行抽象扩展。
Autofac: 第三方的依赖注入框架。
很多人为了注册方式。
3)整合第三方的设计: 托管主机系统, ASP .NET
八、Hosting和管道
==============================================Hosting
1)ASP .NET Core本质是一个服务,需要长时间运行。
接收到Http请求后,交给管道进行处理。
这个长时间运行的服务,需要寄宿在托管进程中,提供这个功能的组件叫做Hosting(主机,托管)。
将一个或者多个长时间运行的服务寄宿在Hosting的托管进程中。
长时间运行的托管服务 ==》服务托管主机,用于运行托管服务。
需要创建托管服务,就是集成IHostedService
2)new HostBuilder()进行创建主机对象,进行一下主机配置。
3)为啥要用Hosting?
那是因为它整合了依赖注入框架。
托管服务依赖的服务,都是可以注册在依赖注入框架中。
4)Hosting
实现了依赖注入框架 + 配置服务 + 日志
==============================================管道
1)开发平台
2)请求处理管道 ==》Http处理请求
3)管道的作用:路由处理,缓存...我们甚至可以封装自己的框架
4)GenericWebHostService
5)Host
CreateDefaultBuilder(args) // 封装主机的创建
HostBuilder对象。
注册日志服务,加载环境变量...封装过了
ConfigureWebHostDefaults
kestrel // web服务器处理请求
Microsoft.NET.Sdk.Web(加载了所有的aps .net core的扩展包)
配置管道:中间件
6)开发框架: // 都是通过1个或者多个路由中间件之上构建起来的,实现请求和Action之间的映射。
MVC
WebAPI
7)中间件
请求处理管道: 1个服务器(监听,接受,分发和最终的响应) + 1组中间件
8)流程
Nginx->kestrel-->中间件(代码)-->WebHost主机 -->中间件-->响应
9)HttpContext
10)public delegate Task RequestDelegate(HttpContext context); // 中间件,其实就是由委托组成的处理链
Use: // 接收一个RequestDelegate的委托,返回一个RequestDelegate的委托
next: 代表管道中的下一个中间件。
11)短路的必要性:
如:身份认证中间件,验证没通过,直接响应返回错误码。
12)实现:// 中间件是依赖服务的方式注册的
强类型方式注册中间件:
实现IMiddleware
UseMiddleware
基于约定的中间件(更灵活,不需要实现某个接口,框架会以依赖注入方式注入属性): // 一般是基于接口这种
有效的公共构造函数(RequestDelegate类型,InvokeAsync方法且返回值是Task)
StartUp类型完成中间件的注册(推荐):
ConfigureServices // 自定义服务的注册(服务注册的最后阶段)
Configure // 中间件的注册
九、路由和异常处理
1)3个中间件实现静态文件 、
wwwroot发布为静态资源访问URL就可以读取文件
UseStaticFiles // 可自定义目录
UseDirectoryBrowser // 实现目录浏览的中间件
2)路由 // URL模式与对应中结点之间的映射关系. 路由解析:选择并执行。生成完整的URL。
2个中间件: // 凡是设计到路由的,都由这2个组成
UseRouting() // 目的:分析url,找到一个EndPoint对象
在HttpContext获取到请求参数
DfaMatcher // 确定优先状态自动机
UseEndpoints(xx=>{}) // 能够通过http请求的方式,访问的远程服务,通过请求委托对象进行
在mvc下: 每一个Action就是一个
在WebAPI下:
在别的下面也许不是
MapGet: 路由模板和对应处理器的关联,Get请求。
约束表达式。
终结点:Http请求的方式访问的远程服务的组合。
终节点映射--终节点路由。
RequestDelegate: Http请求处理器委托。
几个类:
EndpointBuilder
EndpointDataSource // 注册的全部转化为这种
获取终结点数据源,然后往里面添加。
3)异常处理 // 注册异常处理中间件
UseDeveloperExceptionPage() // 开发者异常中间件
UseExceptionHandler // 请求处理的委托对象
重定向的路径
客户端错误:400-499
服务器内部错误:500-599
对于Asp .NET Core = kestrel web服务器,因此不需要IIS了,IIS仅仅是一个反向代理角色,是请求的转发,无任何功能。
MVC or WEB API ??? // 创建完mvc或者webapi项目后,这2个是把控制器与url映射做好了
ABP 中 有IDS(认证与授权)
十、会话、认证、授权、跨域
1)会话 // 为什么要有会话Session?
由于http无状态的,就算是同一个应用,多次的请求和回应这个一个事务,每一次都是完全独立的。
所以需要在应用层为两者的消息交换建立一个上下文,来保存多次消息交换的状态。
类比:2个没有记忆能力的人的交流。 每一次单一的http事务都体现为:一问一答的这种对话, 每一次对话对于交互双方来说都是全新的,
他们不知道对于相同的问题他们之前是否已经交流过,也不知道彼此间已经发生过通信,单一的对话实际上是没有多大意义的,有意义的事情一般是针对
同一个主题的多次会话。
SessionMiddleware:
2个guid:
sessionId: 可以作为会话的唯一标识。
sessionKey: 保存在服务端。 以cookie的方式响应给客户端,客户端保存下来,下一次客户端请求时,附加上这个cookie。从而可以准确定位本次会话的数据字典。
2个不同的Session肯定有不同的SessionId,但是可能共享相同的SessionKey。
要想知道2者的区别,需要知道消息中间件如何处理会话。
会话过期后,SessionId被清除,但是SessionKey可能还存在,因此SessionKey不能作为会话标识,只能代表数据标识。
会话本质上就是存储了一个数据容器,来保存数据的状态。
AddDistributedMemoryCache // 基于内存的缓存服务
在首次沟通时,服务器在返回Response Headers中会传输过来cookie值:
set-cookie
httponly: 这个标签,表明防止跨站读取
下一次再请求时,客户端会携带上cookie:
Request Headers:
cookie值就会携带上
2)认证 // 根据对方提供的凭证,确定他的真实身份。
同一个认证模型:确定真实身份的过程。
认证=登录+注销: 基于票据的认证机制。内置是:认证中间件。提取验证用户身份的数据,叫做:安全令牌(认证票据)。
3种认证票据的操作:
SignIn: Ticket Issuer(颁发者) // 其实就是客户端的登录操作,最常见的证明用户信息的类型就是:用户名+密码
Verify: Authenticator(验证者) // 认证方确定了请求者的身份后,就会返回相关的:权限+其它相关的消息。这种往往是:以cookie等进行返回,这个认证票据都有时效性。
SignOut :Ticket Revoker(撤销者) // 往往在过期前就进行注销,以避免其它人冒用自己的身份。
登录,认证,注销
例子:
登录(post请求) // 要求登录后才能进入到首页,不然直接重定向到登录
var identity = new GenericIdentity(userName, ""Password);
var principal = new ClaimsPrincipal(identity);
await context.SignInAsync(principal); // 会跳转到主页?
首页 // 首页显示用户名,并提供注销的链接
注销
await context.SignOutAsync();
context.Response.Redirect("/");
中间件:
AddAuthentication // 注册认证中间件
AddCookie //
认证服务(ConfigureServices) + 认证中间件(Configure)
服务器判断是否认证了: context?.User?.Identity?.IsAuthenticated
没有认证跳转到登录页面:await context.ChallengeAsync(); // 这个里面会采用默认路径
身份和用户的关系:
3个对象:
ClaimsPrincipal // 代表用户,一个用户有多个身份
ClaimsIdentity // 代表身份,一个身份拥有多个信息
Claims // 身份信息
AuthenticationTicket // 认证票据对象,是对ClaimsPrincipal的封装
认证方案名称 // 如:Cookie认证
认证票据过期时间
登录成功和注销后的一些页面地址信息
3)授权 // 通过权限控制,让用户去做他能做的事情。 授权的本质是通过设置一个策略,来决定酒精具有何种特性的用户才能访问某个资源或者执行某个操作。
其实就是检查用户携带的信息,去检查是否满足某个特性。
将用户添加了某个角色,其实就是给这个用户添加了这个权限。
登录归登录,授权是授权,虽然能登录,但是发现无法访问。
到了web api,可以用别的。
4)跨域
浏览器的同源策略:浏览器的,只能操作同源页面的dom,不能操作别的网站的页面。 策略:域名、URI、ip、端口、网络协议等。
CROS规范:通过这个,可以授权给客户端。
JsonP
src、link属性:同源策略对这种不做限制。 标签的加载,都是对页面的一次Get请求。
ajax: 不允许读取返回这种。
浏览器获取到资源提供者提供的资源时,怎样分发给消费者进行进一步的处理。 CROS根据服务提供者的显式授权。 资源提供者究竟该怎样授权,并且把授权的结果返回给浏览器呢?
Origin报头:资源所在的站点。
Access-control-Allow-Origin // 响应报头,也就是授权结果,站点列表。 * : 给所有的消费者进行授权,说明提供的是公共资源。
十一、web api基础
1)OData(老知识,在.NET早就有了) // Open Data Protocol 开放数据协议, 是一种Restful API的标准
这是微软自家的东西,是Restful API的最佳实践,让我们更加专注业务逻辑,无需关心:请求、响应头,状态码,http方法这些东西。
一种设计和使用Restful API的标准。
Restful本身只是一种思想,并没有限制如何设计Restful API。
基本思想:只要符合OData标准,其他人都可以按照OData中定义的方式,然后去使用这个API,来获取或者修改它的资源(可以理解为SQL标准和关系型数据库的关系)。
支持数据模型的描述,我们可以根据这种描述去查询或者编辑这些资源。
有一组元数据,易读,且我们方便创建一组客户端API代理工具且方便创建出一类库,使用OData标注的Restful API使用起来非常简单。
EnableQuery // OData查询协议的方法
体会c#通过扩展方法实现:起步依赖的感觉。
OData可以支持各种查询选项。
2)GraphQL(新知识) // Facebook出品 API查询语言(定义灵活的查询选项获得期望的结果集)
接口的返回值,动态定制的数据格式。
只需要一个查询-->服务端多个解析器。
同一个请求中,可以返回来自不同资源的所有数据。
需要什么格式的数据,根据查询方式来。
三大核心对象:
Queries
Schemas // 由Query组成的一张表
Types // 描述数据模型的抽象类
如:xx/xx 地址下
{
persons{
id
name
address
}
}
复杂查询
十二、实用类库
1)MediatR // 中介者模式的类库 CQRS(命令查询责任隔离,执行查询,执行命令)
简单应用的话,同一个数据模型,适用于增删改查操作。
同一个实体模型,如:有时只需要更新或者查询个别字段。
但是复杂的查询的话,
2种模式:
请求/响应模式 = CQRS
通知消息: 发布订阅模式
2)体会c#使用扩展方法实现类似于java中的起步依赖:
包:
MediatR
MediatR.Extensions.Microsoft.DependencyInjection // 依赖注入的扩展
2个方法:
_mediator.Send()
_mediator.Publish()
读写分离最佳实践。
3)AutoMapper // 轻量级的对象映射库,实现实体之间的转化,避免硬编码.
再ABP中的话,少不了。
解决的问题: 数据库实体对象和显示对象有差异。
同名的不区分大小写的属性进行映射。
4) FluentValidation // 验证用户输入