-
2018-10-03 10:25:17
超链接标签及属性
对于超链接标签 href:跳转目标网址或者目标页面,注意url位置要加上协议,例如http://www.baidu.com
常用有 http:超文本传输协议 ftp:文件传输协议
a标签属性traget=" " 即链接所需要跳转的页面
_self跳转当前页面。 _parent跳转父页面 _blank跳转新的空白页面。
若代码中有标签属性包含name 可跳转其名为name的页面。
简单的列表
无序列表:
有序列表:
列表标签中放置
- 为一个元素。
frameset 框架
对于框架标签可以替代body标签。定义一个框架集可以包含多个框架,每个框架都有独立的页面。
属性rows表示子框架是被分成上下多部分,按行布局,以上下两部分为例rows="200px,* " 第一个框架占200像素,*表示第二个框架占其剩下的
cols表示子框架按左右,以列的方式布局。col="200px,* " 同上
在里面可以继续嵌套子框架
其属性src:要指向一个(网页页面,图片)的url。若在创建框架时可以给其添加一个name属性。这时便可以设置在一个框架里点击链接(target=“name”)后在另一个框架中显示。
初识css和css的创建。
层叠样式表(Cascading Style Sheets)用以样式定义显示HTML元素
每一个标签都有一个属性style用来编写css代码控制组件。
css代码模板的格式:属性名称:属性值。属性与属性之间采用分号分隔。
css可用作标签选择器;根据标签名定义所有的标签都采用的样式。
- 定义css可以在头部里面称为内部样式表,例:
<!DOCTYPE html> <html> <head> <style type="text/css"> div{ width:800px; height: 500px ; background-color: aqua; } </style> ............. </head> <body> ............. </body> </html>
这段代码定义了所有的div模块的宽度高度和颜色。之后使用的div模块如果不再设置属性都以定义的格式表示。
定义css可以在项目中创建一个css文件。将上述代码里面的部分放入文件中。称为外部样式表。
在里面添加标签便可以将css文件里的格式模板引入到这段代码中。
一些关于C/S与B/S框架的知识
传统的结构方式有两种 架构方式C/S(Client/Server构,B/S(Browser/Srver)浏览器服务器架构。
从名称上就可以显然的看出,C/S架构需要客户下载特定的客户端然后与服务区进行交互。
而B/S架构只需要用户使用浏览器便可访问服务器。
C/S进行维护和管理的成本较大,但是其安全性较高。B/S对服务器要求较高,但其个性化程度低,不利于实现复杂的应用。
(可参照端游 和页游进行思考)
更多相关内容 - 为一个元素。
-
02-列表,表格与框架
2018-09-18 16:59:48colspan(跨列) ,rowspan(跨行) cellpadding(单元格内边距) ,cellspacing(单元格外边距) border (边框) 对齐方式 框架 frameset frame iframe (列表) &amp;amp;amp;lt;ul&amp;...html02
- 列表
- 无序列表
- type
- 有序列表
- type
- 定义列表
- 无序列表
- 表格
- tr ,td
- colspan(跨列) ,rowspan(跨行)
- cellpadding(单元格内边距) ,cellspacing(单元格外边距)
- border (边框)
- 对齐方式
- 框架
- frameset
- frame
- iframe
(列表)
<ul> <li>这是一个无序列表</li> <li>这是一个无序列表</li> <li>这是一个无序列表</li> </ul> <ol> <li>这是一个有序列表</li> <li>这是一个有序列表</li> <li>这是一个有序列表</li> </ol> <dl> <dt>列表项一</dt> <dd>列表内容一</dd> <dt>列表项二</dt> <dd>列表内容二</dd> </dl>
名称 说明 项目符号 无序 ul声明无序列表,li声明列表项 通过type属性设置项目符号disc(默认)、square和circle 有序 ol声明有序列表,li声明列表项 通过type属性设置项目顺序1(数字,默认)、A(大写字母)、a(小写字母)、I(大写罗马数字)和i(小写罗马数字) 定义 dl声明定义列表,dt声明列表项,dd声明定义列表内容 无项目符号和显示顺序 (表格)
<table border='2' height='400' width='600'> <tr> <td>第一行的第一个单元格</td> <td>第一行的第二个单元格</td> <td rowspan='2'>第一行的第三个单元格(但是我跨了俩列)</td> </tr> <tr> <td colspan='2'>第二行的第一个单元格(但是我跨了俩列)</td> </tr> </table>
表格的属性
属性名称 值类型 备注 cellpadding number 单元格内边距的像素 cellspacing number 单元格外边距的像素 height number 表格高度的像素 width number 表格宽度的像素 border number 表格边框的像素 单元格的属性
属性名称 值类型 备注 colspan number 跨列的数目 rowspan number 跨行的数目 align string 水平对齐 left(default),center,right valign string 垂直对齐 top,middle,bottom,baseline PS:valign的baseline 只需要把其中一个单元格的字体变大就能看出效果
(框架)
一个网页被我分割成了三列二行<frameset cols="25%,50%,*" rows ="50%,*" border="5"> //cols:分割的列, rows:分割的行 ,border每个页面的边框 <frame src="1.html "> //src :第一个页面的地址 <frame src="2.html "> <frame src="3.html "> <frame src="4.html "> <frame src="5.html "> <frame src="6.html "> </frameset> <iframe src="path" name="mainFrame" frameborder=“1" scrolling="yes/no" noresize="noresize" width="x" height="y"></iframe> scr:连接位置 name:窗口名称 frameborder:窗口边框 scrolling:滚动条yes/no noresize:是否可调整窗口大小 ,默认可以,val:noresize不可以 width:窗口宽度 height:窗口高度
PS-1:个人感觉iframe用得比较多
PS-2:frameset与body是同级标签(俩个只能有一个) - 列表
-
84PHP框架 1.3.0.zip
2019-05-23 15:23:19新增了对IP(IPV4)黑白名单的操作模块;新增了对客户端信息记录的功能;新增了错误页中,能够取消自动跳转的功能,并将倒计时调整为7秒;新增了示例页面、默认报错页面在IE6-9下的样式兼容性;新增了代码压缩的功能... -
Next 常用的框架
2021-03-02 10:30:51常用的框架 分布式缓存框架 Microsoft Velocity:微软自家分布式缓存服务框架。 Memcahed:一套分布式的高速缓存系统,目前被许多网站使用以提升网站的访问速度。 Redis:是一个高性能的KV数据库。它的出现很大...常用的框架
分布式缓存框架
Microsoft Velocity:微软自家分布式缓存服务框架。
Memcahed:一套分布式的高速缓存系统,目前被许多网站使用以提升网站的访问速度。
Redis:是一个高性能的KV数据库。 它的出现很大程度补偿了Memcached在某些方面的不足。
EnyimMemcached:访问Memcached最优秀的.NET客户端,集成不错的分布式均衡算法。
CacheManager 用于缓存的管理,支持Redis.Memcached,couchbase等 https://github.com/MichaCo/CacheManager
Redis和数据连接驱动框架
ServiceStack.Redis:https://github.com/ctstone/csredis
StackExchange.Redis:https://stackexchange.github.io/StackExchange.Redis/ReleaseNotes
csredis:https://github.com/ctstone/csredis
MySQL Connector:完全托管的 MySQL ADO.NET 数据库提供程序、连接器
开源的.NET系统推荐
OXITE:微软ASP.NET MVC案例演示框架。
PetShop:微软ASP.Net宠物商店。分层思想的代表。
eShopOnContainers:微服务架构和Docker容器的一个简单的.Net Core的应用。微服务思想代表。
ABP(aspnetboilerplate),提供一系列工具用于web应用创建,支持 ASP.NET Core, ASP.NET MVC & Web API,也提供了web应用的模板。领域驱动思想代表。 https://github.com/aspnetboilerplate/aspnetboilerplate(.net core 版本,tag分支有支持.net版本的)。
Orchard:国外一个MVC开源的博客系统。
SSCLI:微软在NET Framework 2.0时代的开源代码。
DasBlog:国外一个基于ASP.NET的博客系统。
BlogEngine.NET:国外一款免费开源的博客系统。
Dotnetnuke.NET:一套非常优秀的基于ASP.NET的开源门户网站程序。
Discuz.NET:国内开源的论坛社区系统。
nopCommerce和Aspxcommerce:国外一套高质量的开源B2C网站系统。
JumboTCMS和DTCMS:国内两款开源的网站管理系统。
nopCommerce 开源电商网站,开发环境asp.net mvc(支持.net core),使用技术(autofac,ef,页面插件等) https://github.com/nopSolutions/nopCommerce
LightGBM 用于机器学习 https://github.com/Microsoft/LightGBM
IdentityServer,用户授权网站(支持openid和OAuth 2.0),可用于单点登录和第三方授权等
https://github.com/IdentityServer/IdentityServer3(.net版本)
https://github.com/IdentityServer/IdentityServer4(.net core版本)
日志记录异常处理
Log4Net.dll:轻量级的免费开源.NET日志记录框架。
Enterprise Library Log Application Black:微软企业库日志记录。
Elmah:实现最流行的ASP.NET应用异常日志记录框架。
NLog:是一个简单灵活的日志记录类库,性能比Log4Net高,使用和维护难度低。
关于NoSQL数据库
MongoDB:分布式文件存储数据库。
Membase:家族的一个新的重量级的成员。
自动任务调度框架
Quartz.NET:开源的作业调度和自动任务框架。
Topshelf:另一种创建Windows服务的开源框架。
Hangfire 任务调度开发利器 https://github.com/HangfireIO/Hangfire
依赖注入IOC容器框架
Unity:微软patterns&practicest团队开发的IOC依赖注入框架,支持AOP横切关注点。
MEF(Managed Extensibility Framework):是一个用来扩展.NET应用程序的框架,可开发插件系统。
Spring.NET:依赖注入、面向方面编程(AOP)、数据访问抽象,、以及ASP.NET集成。
Autofac:最流行的依赖注入和IOC框架,轻量且高性能,对项目代码几乎无任何侵入性。
PostSharp:实现静态AOP横切关注点,使用简单,功能强大,对目标拦截的方法无需任何改动。
Ninject:基于.NET轻量级开源的依赖注入IOC框架。
常用的几个ORM框架
EF(ADO.NET Entity Framework):微软基于ADO.NET开发的ORM框架。
NHibernate:面向.NET环境的轻量级的ORM框架。
SqlMapper.cs:用于小项目的通用的C#数据库访问类。
AutoMapper:流行的对象映射框架,可减少大量硬编码,很小巧灵活,性能表现也可接受。
SubSonic:优秀的开源的ORM映射框架,同时提供符合自身需要的代码生成器。
FluentData:开源的基于Fluent API的链式查询ORM轻量级框架。
Dapper:轻量级高性能基于EMIT生成的ORM框架。
EmitMapper:性能较高的ORM框架,运行时通过EMIT动态生成IL代码,并非采用反射机制。
办公系列框架(文档操作)
NPIO:该项目是 .NET 版本的 POI 项目,原始的 Java 版本位于http://poi.apache.org/ 。
DocX:不需要安装word软件,通过C#操作word文件。
Open XML SDK:Open XML SDK 是一个开源库,用于处理 Open XML 文档。
ITextSharp:iText 是一个PDF库,用于创建、修改、检查和维护 (PDF)格式的文档。格式和数据类型转换
Newtonsoft.Json:目前.NET开发中最流行的JSON序列化库,为新版的WebApi库提供基础。
System.JSON.dll:微软自己开发的JSON序列化组件(需要单独下载)。
DataContractJsonSerializer 和 DataContractXmlSerializer:微软在WCF中使用的序列化器。
JavaScriptSerializer:微软默认针对WEB开发者提供的JSON格式化器。
iTextSharp、PDFsharp 和 PDF.NET:通过.NET处理和生成PDF文档的组件。
SharpZipLib.dll:免费开源的ZIP和GZIP文件解压缩组件。
DocX:不需要安装word软件,通过C#操作word文件。
SharpSerializer:开源XML和、二进制、JSON、压缩和优化框架。
人工智能与数学框架
Math.NET:强大的数学运算、微积分、解方程和科学运算。
Accord.NET:机器学习框架,包含了音频和图像处理的库(计算机视觉、计算机听觉、信号处理和统计)
语法解析和模板引擎Template Engine
RazorEngine:基于微软 Razor 解析器引擎的开源模板引擎。官网。
Nustache:无逻辑模板的开源库。官网。
DotLiquid:Ruby Liquid 模板语言的 C# 移植版本。
MarkdownSharp:C# 实现的开源 Markdown 处理器,在 Stack Overflow 中使用。
JavaScript.NET和ClearScript(微软出品):基于.NET开发的JavaScript引擎。
ExCSS:C# 的 CSS3 解析器开发库。
HtmlAgilityPack:一个灵活的 HTML 解析器,可以对 DOM 进行读写,支持 XPATH 和 XSLT。
反射和动态语言
Clay dynamic:开源的动态语言dynamic框架让您形如JavaScript的方式创建对象。
ExposedObject:在类的外部通过动态语言dynamic的方式访问私有成员。
PrivateObject:微软单元测试框架中便捷在外部调用类内部私有成员的一个类。
跨平台和运行时解决方案
MONO.NET:跨平台的.NET运行环境,让.NET跨平台运行成为可能。
DotGnu Portable.NET:类似于MONO.NET的跨平台运行时。
Phalanger:将PHP编译成.NET,可实现PHP与.NET互操作。
VMDotNet:中国移动飞信所使用过的.NET运行时。
Unity3D:微软大力支持的机遇C#和JavaScript的跨平台游戏开发框架。
Cassini、IIS Express和Cassinidev:开源的ASP.NET执行环境。
Katana:微软基于OWIN规范实现的非IIS寄宿ASP.NET和MVC等。
IKVM.NET:基于.NET的Java虚拟机,让JAVA运行在.NET之上。
WEB开发和设计
Jumony Core:基于.NET开发的HTML引擎。
Microsoft.mshtml.dll、Winista.HtmlParser.dll 和 HtmlAgilityPack.dll:解析处理HTML文档的框架。
JavaScript.NET和ClearScript(微软出品):基于.NET开发的JavaScript引擎。
NCrawler:其HTML处理引擎htmlagilitypack的的开源网络爬虫软件。
AntiXSS:微软官方预防跨站XSS脚本入侵攻击的开源类库,它通过白名单机制进行内容编码。
YUICompressor.NET、Microsoft Ajax Minifier 和 Google Closure Compiler:JavaScrip和CSS压缩器。
NancyFx:是一个不错的轻量级开源.NET WEB框架。如果想快速做个简单的WEB应用。
AspNetPager:国内知名的ASP.NET分页控件,支持多种分页方式。
NOPI.dll:导出Excel报表的插件(基于微软OpenXml实现)(nopi.css.dl通过css设置样式)
Enterprise Library:微软针对企业级应用开发的最佳实践组件。
PowerCollections:国外一个牛人写的高级开源集合。
包管理
NuGet:.NET 包管理器。官网
MyGet:为 NuGet、NPM、Bower 和 VSIX 提供程序包仓库宿主,同样提供 CI 服务。
Paket:.NET 的一个包依赖管理器,支持 NuGet 包和 GitHub 仓库。
移动互联网和云计算
PushSharp:通过.NET向各种移动平台推送消息。
mono for Android:用.NET语言开发安卓应用:
MonoTouch:用.NET语言开发iOS应用。
PhoneGap和AppCan:跨平台基于HTML5的移动开发平台。
Cordova:PhoneGap贡献给Apache后的开源项目,是驱动PhoneGap的核心引擎。
网络通信和网络协议
SuperSocket:基于.NET轻量级的可扩展的Socket开发框架。
SuperWebSocket:通过.NET实现TML5 WebSocket框架。
XProxy:支持插件的基础代理程序集,内置NAT、加解密、反向、直接和间接代理。
websocket-sharp:websocket-sharp是一个C#实现websocket协议客户端和服务端,websocket-sharp支持RFC 6455;WebSocket客户端和服务器;消息压缩扩展;安全连接;HTTP身份验证;查询字符串,起始标题和Cookie;通过HTTP代理服务器连接;.NET Framework 3.5或更高版本(包括兼容环境,如Mono)。
网络通讯框架
SignalR:ASP.NET 库,开发者可以通过它在 web 应用程序中非常简单地实现实时功能。
Helios:一套高性能的Socket通信中间件。
图形和图像处理框架
Paint.NET:基于.NET小巧灵活强大的图形处理开源项目。
Imagemagick.NET:用C#对开源图像处理组件Imagemagick的封装。
Skimpt:基于.NET开源的屏幕截图软件。
ImageGlue.NET:商业的图像处理组件,支持的格式列了一大堆。
Sprite and Image Optimization Framework:微软CSS精灵,多图合成一张大图和CSS样式。
NGraphics:NGraphics 是一个 .NET 跨平台矢量图形渲染。
DynamicImage:高性能开源图片处理库,用于 ASP.NET。
桌面应用程序框架
DevExpress:一个全球知名的桌面应用程序UI控件库。
Prism:微软开发的针对WPF和Silverlight的MVVM框架,通过功能模块化的思想,来讲复杂的业务功能和UI耦合性进行分离。
WPFToolkit 和 Fluent Ribbon Control Suite:开发类似于Office风格的Ribbon菜单。
Netron Diagramming Library:开源图形绘制组件
http://www.csharpopensource.com/netrondiagramminglibrary/
https://sourceforge.net/projects/netron-reloaded/
测试和性能评估方面
Faker.Net:方便生成大批量测试数据的框架。
Nunit:一个轻量级的单元测试框架。
Moq:非常流行的Mock框架,支持LINQ,灵活且高性能。
xUnit:比NUnit更好的单元测试框架,升级改进版的Nunit框架。
MiniProfiler和Glimpse:基于MVC的两款性能事件监控框架。
事务和分布式事务支持
KtmIntegration:一个支持NTFS文件系统的事务开源类。
NET Transactional File Manager:对文件系统操作(复制、移动和删除)加入事务支持。
分词、全文检索和搜索引擎
Lucene.net:流行高性能的全文索引库,可用于为各类信息提供强大的搜索功能。
Lucene.Net.Analysis.PanGu:支持Lucene.Net最新版的盘古中文分词扩展库。
ElasticSearch:http://nest.azurewebsites.net/
数据验证组件整理
FluentValidation for .NET:基于LINQ表达式方法链Fluent接口验证组件。
Microsoft.Practices.EnterpriseLibrary.Validation.dll:微软企业库验证程序块。
CuttingEdge.Conditions:基于Fluent接口方法练接口的契约编程组件。
DotNetOpenAuth:让网站具备支持OpenID、OAuth、InfoCard等身份验证的能力。
开源图表统计控件
Visifire:一套效果非常好的WPF图表控件,支持3D绘制、曲线、折线、扇形、环形和梯形。
SparrowToolkit:一套WPF图表控件集,支持绘制动态曲线,可绘制示波器、CPU使用率和波形。
DynamicDataDisplay:微软开源的WPF动态曲线图,线图、气泡图和热力图。
安全性框架
OAuth:超轻量级 OAuth 1.0a 签名生成库,C# 编写
ASP.NET Identity:用于 ASP.NET 应用程序的新身份系统。
IdentityServer:可扩展的 OAuth2 和 OpenID 连接提供程序框架
HashLib:HashLib 包含了几乎所有你见过的哈希算法,它几乎支持所有东西并且非常容易使用
编译器和IDE框架
ILSpy:ILSpy 是一个开源的 .NET 程序集查看器和反编译器
Visual Studio Code:非常棒的编辑器,来自微软,基于 GitHub Atom
MSBuild也是一个MIT License的开源软件,可以在Github上看到它的 仓库。
Visual Studio 插件
Resharper:Visual Studio 开发人员生产力工具
打包发布和自动构建持续集成和升级框架
Wix Toolset:强大的工具集,用于创建你自己的 Windows 安装程序
Squirrel:Squirrel 即是一套工具也是一个类库,可以无安全管理 Windows 桌面程序的安装和更新
MSBuild:微软构建引擎(MSBuild)是 .NET 和 Visual Studio 的构建平台
Psake:基于 .NET 的自动化构建工具,使用 PowerShell 编写
中间件与消息队列
RabbitMQ
RabbitMQ是使用Erlang编写的一个开源的消息队列,本身支持很多的协议:AMQP,XMPP, SMTP, STOMP,也正因如此,它非常重量级,更适合于企业级的开发。同时实现了Broker构架,这意味着消息在发送给客户端时先在中心队列排队。对路由,负载均衡或者数据持久化都有很好的支持。
MassTransit 可用于创建基于消息的服务和应用,依赖于RabbitMQ https://github.com/MassTransit/MassTransit
Redis
Redis是一个基于Key-Value对的NoSQL数据库,开发维护很活跃。虽然它是一个Key-Value数据库存储系统,但它本身支持MQ功能,所以完全可以当做一个轻量级的队列服务来使用。对于RabbitMQ和Redis的入队和出队操作,各执行100万次,每10万次记录一次执行时间。测试数据分为128Bytes、512Bytes、1K和10K四个不同大小的数据。实验表明:入队时,当数据比较小时Redis的性能要高于RabbitMQ,而如果数据大小超过了10K,Redis则慢的无法忍受;出队时,无论数据大小,Redis都表现出非常好的性能,而RabbitMQ的出队性能则远低于Redis。
ZeroMQ
ZeroMQ号称最快的消息队列系统,尤其针对大吞吐量的需求场景。ZeroMQ能够实现RabbitMQ不擅长的高级/复杂的队列,但是开发人员需要自己组合多种技术框架,技术上的复杂度是对这MQ能够应用成功的挑战。ZeroMQ具有一个独特的非中间件的模式,你不需要安装和运行一个消息服务器或中间件,因为你的应用程序将扮演这个服务器角色。你只需要简单的引用ZeroMQ程序库,可以使用NuGet安装,然后你就可以愉快的在应用程序之间发送消息了。但是ZeroMQ仅提供非持久性的队列,也就是说如果宕机,数据将会丢失。其中,Twitter的Storm 0.9.0以前的版本中默认使用ZeroMQ作为数据流的传输(Storm从0.9版本开始同时支持ZeroMQ和Netty作为传输模块)。
ActiveMQ
ActiveMQ是Apache下的一个子项目。 类似于ZeroMQ,它能够以代理人和点对点的技术实现队列。同时类似于RabbitMQ,它少量代码就可以高效地实现高级应用场景。
Kafka/Jafka
Kafka是Apache下的一个子项目,是一个高性能跨语言分布式发布/订阅消息队列系统,而Jafka是在Kafka之上孵化而来的,即Kafka的一个升级版。具有以下特性:快速持久化,可以在O(1)的系统开销下进行消息持久化;高吞吐,在一台普通的服务器上既可以达到10W/s的吞吐速率;完全的分布式系统,Broker、Producer、Consumer都原生自动支持分布式,自动实现负载均衡;支持Hadoop数据并行加载,对于像Hadoop的一样的日志数据和离线分析系统,但又要求实时处理的限制,这是一个可行的解决方案。Kafka通过Hadoop的并行加载机制统一了在线和离线的消息处理。Apache Kafka相对于ActiveMQ是一个非常轻量级的消息系统,除了性能非常好之外,还是一个工作良好的分布式系统。
-
Laravel5.1 框架关联模型之后操作实例分析
2020-12-20 04:30:05本文实例讲述了Laravel5.1 框架关联模型之后操作。分享给大家供大家参考,具体如下: 之前写过关于模型关联的笔记,但是模型关联好后的一些使用没有介绍,今天补上 1 写入关联模型 1.1 使用Save方法(一对多) 我们... -
“信创”:研究框架.pdf
2020-08-26 08:06:17华泰投研团队最全报告,适用于安可信创的行业分析。产业概况、产业全景、核心环节、集团布局、投资标的。 具有实际的行业研究价值! -
详细讲解go web框架之gin框架源码解析记录及思路流程和理解
2021-11-19 15:07:38首先gin 框架是在 官方提供的net/http标准包进行的相应封装。 那么要想理解gin框架, 就要先懂一些 net/http标准包 的相关知识。 可以参考中文的 文档: https://studygolang.com/pkgdoc 可以参考的很多文章...开篇
-
首先gin 框架是在 官方提供的net/http标准包进行的相应封装。
-
那么要想理解gin框架, 就要先懂一些 net/http标准包 的相关知识。
-
可以参考中文的 文档: https://studygolang.com/pkgdoc
-
可以参考的很多文章视频等:
- gin源码分析流程思维导图:https://www.processon.com/view/link/5f4b70c2079129356ec5cb70#map
- gin源码解读
- 哔哩哔哩视频讲 gin源码: https://www.bilibili.com/video/BV1Br4y1N7DG?spm_id_from=333.999.0.0
参考大佬文章
: 两篇gin技术内幕:https://zhuanlan.zhihu.com/p/133208366(重点)参考大佬两篇
:深入gin框架内幕 https://zhuanlan.zhihu.com/p/102303084(重点)- https://www.cnblogs.com/yjf512/p/9670990.html
- https://blog.csdn.net/u012988972/article/details/118991542
- 等等资料和视频(资料文章能找到很多,我也没咋都看,但是有机会可以多看看,融入融入)
另外
: 文章源码的上述注释解析,来自机器翻译,可能有不准确的情况。
-
有话讲:
- 有的地方可能是我自己的推敲与判断,整体分析的思路,也是一步步来的。
- 锻炼阅读源码的能力, 如何搞清楚一个框架的定位,以及如何,从那一步开始研究框架和源码,这都是一个学习的过程。
- 开场,会从最基本的入门小例子讲起。
- 阅读优秀的代码,还会发现很多比较优质的代码书写的相关技巧!都会总结一下。比如 for 遍历先初始化 len(t), 保证结构体是否实现相关接口等等。
-
同时还可以参考对应的 官方文档, 有相关项目经验后再来分析源码,这样你就会思考,
我在框架中使用的某些功能,或者逻辑处理流程是如何实现的?
这些都很关键。- github上当然有官方的文档介绍: https://github.com/gin-gonic/gin#gin-web-framework, 并且还有相关的 例子,就在examples 目录下。
- 在打开 github 查看源码的时候,可以将链接变为 https://github1s.com, 加个 “1s” , 项目会被打开成 vscode 风格! 方便查看。 很棒~
- 还有相关的中文文档, 访问比较快:https://www.kancloud.cn/shuangdeyu/gin_book/949413
-
引用文章中的一段描述介绍开篇:
开始
1. 从入门教程推出 Engine 对象
-
从最开始的语句来入门教程:
func main() { // 创建一个默认的路由引擎 r := gin.Default() // GET:请求方式;/hello:请求的路径 // 当客户端以GET方法请求/hello路径时,会执行后面的匿名函数 r.GET("/hello", func(c *gin.Context) { // c.JSON:返回JSON格式的数据 c.JSON(200, gin.H{ "message": "Hello world!", }) }) // 启动HTTP服务,默认在0.0.0.0:8080启动服务 r.Run() }
-
我们点击进去 Run 函数的源码:
//执行命令将路由器连接到http。服务器并开始侦听和服务HTTP请求。 // http的快捷方式。ListenAndServe (addr,路由器) //注意:这个方法会无限期地阻塞调用goroutine,除非发生错误。 func (engine *Engine) Run(addr ...string) (err error) { defer func() { debugPrintError(err) }() trustedCIDRs, err := engine.prepareTrustedCIDRs() if err != nil { return err } engine.trustedCIDRs = trustedCIDRs address := resolveAddress(addr) debugPrint("Listening and serving HTTP on %s\n", address) err = http.ListenAndServe(address, engine) return }
- 其中最核心的语句就是 :
err = http.ListenAndServe(address, engine)
-
这就是调用的官方标准包, net/http。 可以去查看对应的用法:
- 它负责监听 tcp请求,并且交给 handler 参数去调用一个 Server函数处理接收到的链接。
- 一般handler 为 nil, 那么默认有一个 DefaultServeMux 对象, 去调用 server函数处理请求。
-
打开 http.ListenAndServe 源码,会发现,它获取的handler参数就是传递给 Server对象的。
// ListenAndServe监听TCP网络地址addr,然后调用 //使用handler处理传入连接的请求。 //已接受的连接配置为启用TCP keep-alive。 // //处理程序通常为nil,在这种情况下使用DefaultServeMux。 // // ListenAndServe总是返回一个非nil错误。 func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} //源码中创建 server服务器对象,将handler传递进去 return server.ListenAndServe() }
-
Server函数: server对象的方法,接收每一个链接,并开启 go程,读取请求,调用handler回复请求。
-
默认的 DefaultServeMux
// ServeMux是一个HTTP请求复用器。 //它将每个传入请求的URL与一个已注册的列表进行匹配 // 的模式,并调用处理程序的模式 //最接近URL。 // //模式名称固定,根路径,如"/favicon.ico", //或根子树,如"/images/"(注意后面的斜杠)。 //较长的模式优先于较短的模式,因此 //如果有两个处理程序注册"/images/" //和"/images/thumbnails/",后一个处理器将是 //调用以"/images/thumbnails/"开头的路径 //前将接收任何其他路径的请求 / / /图片/子树。 // //注意,因为以斜杠结尾的模式命名了根子树, //模式"/"匹配所有未被其他注册的路径 //模式,而不仅仅是URL与Path == "/"。 // //如果一个子树已经注册,并且接收到一个命名为 //不带末尾斜杠的子树根,ServeMux重定向它 //请求到子树根(添加末尾的斜杠)。这种行为可以 //被一个单独的注册路径覆盖 //末尾的斜杠。例如,注册“/images/”会导致ServeMux //重定向请求"/images"到"/images",除非"/images"有 //已单独注册。 // //模式可以选择以主机名开头,限制匹配为 //该主机上的url。特定于主机的模式优先 //通用模式,以便处理程序可以注册这两个模式 // "/codesearch"和"codesearch.google.com/"不需要接管 //请求“http://www.google.com/”。 // // ServeMux还负责清理URL请求路径和主机 //头,剥离端口号和重定向任何请求包含。或 / / . .元素或重复的斜杠到等效的、更干净的URL。 type ServeMux struct { mu sync.RWMutex m map[string]muxEntry es []muxEntry // slice of entries sorted from longest to shortest. hosts bool // whether any patterns contain hostnames } // NewServeMux allocates and returns a new ServeMux. func NewServeMux() *ServeMux { return new(ServeMux) } // DefaultServeMux is the default ServeMux used by Serve. var DefaultServeMux = &defaultServeMux var defaultServeMux ServeMux
-
而对应的server对象在这
type Server struct { Addr string // 监听的TCP地址,如果为空字符串会使用":http" Handler Handler // 调用的处理器,如为nil会调用http.DefaultServeMux ReadTimeout time.Duration // 请求的读取操作在超时前的最大持续时间 WriteTimeout time.Duration // 回复的写入操作在超时前的最大持续时间 MaxHeaderBytes int // 请求的头域最大长度,如为0则用DefaultMaxHeaderBytes TLSConfig *tls.Config // 可选的TLS配置,用于ListenAndServeTLS方法 // TLSNextProto(可选地)指定一个函数来在一个NPN型协议升级出现时接管TLS连接的所有权。 // 映射的键为商谈的协议名;映射的值为函数,该函数的Handler参数应处理HTTP请求, // 并且初始化Handler.ServeHTTP的*Request参数的TLS和RemoteAddr字段(如果未设置)。 // 连接在函数返回时会自动关闭。 TLSNextProto map[string]func(*Server, *tls.Conn, Handler) // ConnState字段指定一个可选的回调函数,该函数会在一个与客户端的连接改变状态时被调用。 // 参见ConnState类型和相关常数获取细节。 ConnState func(net.Conn, ConnState) // ErrorLog指定一个可选的日志记录器,用于记录接收连接时的错误和处理器不正常的行为。 // 如果本字段为nil,日志会通过log包的标准日志记录器写入os.Stderr。 ErrorLog *log.Logger // 内含隐藏或非导出字段 }
-
- 其中最核心的语句就是 :
-
可以看到, 我们的代码中:
err = http.ListenAndServe(address, engine)
-
也就是将 engine 传递进去, 作为 handler 参数
,也就是不使用默认的 DefaultServeMux -
engine 是 *Engine 类型。 也就是 我们 gin.Default() 出来的“引擎”(Default 出来的默认带有两个中间件)。 engin 既然可以作为 Handler 类型参数, 那么肯定实现了对应的接口:
//处理程序响应HTTP请求。 // // serverhttp应该向ResponseWriter写入应答头和数据 //返回。返回请求完成的信号;它 // 的ResponseWriter或从 //请求。正文后或与之同时完成 // ServeHTTP电话。 // //根据HTTP客户端软件、HTTP协议版本和 //客户端和Go服务器之间的任何中介,它可能不会 //可以读取请求。身体写完后给 // ResponseWriter。谨慎的处理程序应该读取请求。身体 //首先,然后回复。 // //除了读取主体外,处理程序不应该修改 //请求提供。 // //如果ServeHTTP崩溃,服务器(调用ServeHTTP的人)会假设 //将panic的影响与主动请求隔离。 //它恢复panic,记录堆栈跟踪到服务器错误日志, //关闭网络连接或者发送HTTP/2 // RST_STREAM,取决于HTTP协议。中止处理程序,所以 //客户端看到一个中断的响应,但服务器没有记录 //一个错误,慌乱与值ErrAbortHandler。 type Handler interface { ServeHTTP(ResponseWriter, *Request) }
-
-
总结:
- 调用Run方法, 实际底层是使用 http包的方法, 将 Engine 类型对象作为 handler 参数传递进去(实现了Handler接口),构造 Server对象, 代替默认的 DefaultServeMux 处理请求链接,DefaultServeMux 实现了 相关的 match 等方法(去server.go 搜索ServeMux, 找到它实现的相关方法) 。
2. Engine 对象介绍及handleHTTPRequest接口实现与路由方法树树相关操作
- 认清
Engine 对象
及giin框架。- 源码上方的介绍:
Engine 是框架的实例,它包含muxer,中间件和配置设置
。- 可以看到是非常重要的! 同时我们也知道了 gin 框架的主要作用, 就是 充当了 muxer 代替 net/http 包里 启动服务器对象的默认 muxer, 同时还提供了 中间件,配置设置等功能
- 源码上方的介绍:
- 我们查看源码,找到Egine 对象: 比较长,注释很清晰,每个功能的作用。
-
参考文章: https://zhuanlan.zhihu.com/p/102303084, Engine 相关常用的方法,在这里都有介绍。一定进去查看下!
- 理解了其实还可以使用 goland编辑器, 鼠标停在gin,Default()。Defalut上。 然后出现返回类型, *Engine, 点击一下,就会出现相关具有的方法 和 内容。(缺少一个maxParams参数,老版本这个参数是在路由树的里定义的)
//Engine是框架的实例,它包含muxer,中间件和配置设置。 //创建一个Engine实例,使用New()或Default() type Engine struct { // 路由组,在实际开发过程中我们通常会使用路由组来组织和管理一些列的路由. 比如: /apis/,/v1/等分组路由 RouterGroup // 开启自动重定向。如果当前路由没有匹配到,但是存在不带/开头的handler就会重定向. 比如: 用户输入/foo/但是存在一个/foo 就会自动重定向到该handler,并且会向客户端返回301或者307状态码(区别在于GET方法和其他方法) RedirectTrailingSlash bool // 如果开启该参数,没有handler注册时,路由会尝试自己去修复当前的请求地址. // 修复流程: // 1.首位多余元素会被删除(../ or //); 2.然后路由会对新的路径进行不区分大小写的查找;3.如果能正常找到对应的handler,路由就会重定向到正确的handler上并返回301或者307.(比如: 用户访问/FOO 和 /..//Foo可能会被重定向到/foo这个路由上) RedirectFixedPath bool // 如果开启该参数,当当前请求不能被路由时,路由会自己去检查其他方法是否被允许.在这种情况下会响应"Method Not Allowed",并返回状态码405; 如果没有其他方法被允许,将会委托给NotFound的handler HandleMethodNotAllowed bool // 是否转发客户端ip ForwardedByClientIP bool // 如果开启将会在请求中增加一个以"X-AppEngine..."开头的header AppEngine bool // 如果开启将会使用url.RawPath去查找参数(默认:false) UseRawPath bool // 如果开启,请求路径将不会被转义. 如果UseRawPath为false,该参数实际上就为true(因为使用的是url.Path) UnescapePathValues bool // maxMemory参数的值(http.Request的ParseMultipartForm调用时的参数) MaxMultipartMemory int64 // 是否删除额外的反斜线(开始时可解析有额外斜线的请求) RemoveExtraSlash bool // 分隔符(render.Delims表示使用HTML渲染的一组左右分隔符,具体可见html/template库) delims render.Delims // 设置在Context.SecureJSON中国的json前缀 secureJsonPrefix string // 返回一个HTMLRender接口(用于渲染HTMLProduction和HTMLDebug两个结构体类型的模板) HTMLRender render.HTMLRender // html/template包中的FuncMap map[string]interface{} ,用来定义从名称到函数的映射 FuncMap template.FuncMap // 以下是gin框架内部定义的一些属性 // HandlersChain 是一个HandlerFunc 的数组(HandlerFunc其实就是一个Context的指针,Context会在下一节讲解) allNoRoute HandlersChain allNoMethod HandlersChain noRoute HandlersChain // 上述连接中有介绍 noMethod HandlersChain // 这里定义了一个可以临时存取对象的集合(sync.Pool是线程安全的,主要用来缓存为使用的item以减少GC压力,使得创建高效且线程安全的空闲队列) pool sync.Pool // methodTrees是methodTree的切片(methodTree是一个包含请求方法和node指针的结构体,node是一个管理path的节点树) trees methodTrees }
-
下方还有一句:
var _ IRouter = &Engine{}
通过匿名变量定义 某个接口类型的值,这样的写法,作用是,确保 Engine{} 实现了 IRouter 接口。
一个技巧。 -
找到对应实现接口的方法: ServeHTTP:
// ServeHTTP conforms to the http.Handler interface. func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { // SYNC Pool对象池的概念: 减少内存申请,并 类型断言,转换为 Context类型指针。 c := engine.pool.Get().(*Context) // 拿到对象后,下面三步才进行对象的初始化。 c.writermem.reset(w) c.Request = req //因为是结构体并且sync.pool机制不会主动重置`Context`,所以手动重置`Context` c.reset() // 关键一步,调用handleHTTPRequest函数,处理请求。 engine.handleHTTPRequest(c) engine.pool.Put(c) }
-
关键的一步就是处理http请求的函数: handleHTTPRequest ,传递进去初始化好的上下文。
-
上源码:
func (engine *Engine) handleHTTPRequest(c *Context) { // 1. 获取相关数据 和 设置 httpMethod := c.Request.Method rPath := c.Request.URL.Path unescape := false if engine.UseRawPath && len(c.Request.URL.RawPath) > 0 { rPath = c.Request.URL.RawPath unescape = engine.UnescapePathValues } if engine.RemoveExtraSlash { rPath = cleanPath(rPath) } // Find root of the tree for the given HTTP method // 找到路由树 // 执行请求对应的函数,写入Response t := engine.trees for i, tl := 0, len(t); i < tl; i++ { if t[i].method != httpMethod { continue } root := t[i].root // Find route in tree value := root.getValue(rPath, c.params, unescape) if value.params != nil { c.Params = *value.params } if value.handlers != nil { c.handlers = value.handlers c.fullPath = value.fullPath c.Next() c.writermem.WriteHeaderNow() return } if httpMethod != "CONNECT" && rPath != "/" { if value.tsr && engine.RedirectTrailingSlash { redirectTrailingSlash(c) return } if engine.RedirectFixedPath && redirectFixedPath(c, root, engine.RedirectFixedPath) { return } } break } if engine.HandleMethodNotAllowed { for _, tree := range engine.trees { if tree.method == httpMethod { continue } if value := tree.root.getValue(rPath, nil, unescape); value.handlers != nil { c.handlers = engine.allNoMethod serveError(c, http.StatusMethodNotAllowed, default405Body) return } } } c.handlers = engine.allNoRoute serveError(c, http.StatusNotFound, default404Body) }
-
源码解析:
- (1) 首先获得 请求方法等数据,信息,进行相关操作。
- (2)获取 gin.trees, 路由方法树。
-
这里参考之前写的路由树的,前缀树Trie, Radix树相关的内容和实现。:go语言简单实现 前缀树(Trie)与了解基数树(Radix-Tree)
-
对应的是 Engine里面的
trees methodTrees 类型
。 而methodTrees 类型是这样定义的: type methodTrees []methodTree -
methodTree 是这样定义的:
// 记录当前的路由字符 和 对应的 radix或前缀树的根节点! type methodTree struct { method string root *node }
-
对应的节点类型 node 为: 这和我们自己实现的前缀树基本一致,可以看到多了一些属性, 并且还具有相关 addRoute等方法。
type node struct { path string indices string wildChild bool nType nodeType priority uint32 children []*node // child nodes, at most 1 :param style node at the end of the array handlers HandlersChain fullPath string }
-
我们顺藤摸瓜,前面从 methodTree 找到了 请求方法对应的树结构node节点类型。那么各个字段的含义呢?
path
: 节点路经字符串“当前节点保存的 路径字符”indices
(目录,索引):和children字段是对应的。保存的是 children 节点的 path字段的 首字母。在进行查找的时候,就可以通过当前字段来判断,该树的子节点是否满足后序的匹配规则。children
:保存儿子节点handlers
HandlersChain: 处理函数的链条。 这里寻找对应的处理函数,以及执行流程!还可以用来判断当前节点是否是正确的路由,当前节点 handlers字段为 nil 说明没有注册处理函数, 就是一个正确的 路由!!(我的想法)
priority
: uint32 : 优先级,表示子节点,子子节点注册的handler数量。nType
nodeType: 表示节点类型。其中的类型:type nodeType uint8 const ( static nodeType = iota // default 默认值,静态的节点。 root 表示是根节点 param 参数节点,就例如 router.GET("/user/:name", func(c *gin.Context), name := c.Param("name") 获取捕获的姓名 catchAll 表示当前节点有 匹配符 * ,匹配 )
wildChild
bool:节点是否是参数节点fullPath
string:完整路径
-
下面实战讲解,进行测试,以我的一个抽奖项目为例,路由组的代码是这样的:
- 当然了,每个路由组里面还有更细小的路由。
// 指定路由,加载路由 // 1. 网站首页 v1 := app.Group("/") addIndexRouters(v1, &Index) // 2. admin 后台管理路由 v2 := app.Group("/admin") // 启用中间件: BasicAuth()(验证)中间件 v2.Use(gin.BasicAuth(gin.Accounts{ "admin": "123456", // 添加一个管理员用户 })) addAdminRouters(v2, &Admin) // 3. 商品管理页 v3 := v2.Group("/gift") addAdminGiftRouters(v3, &AdminGift) // 4. 优惠券管理页 v4 := v2.Group("/code") addAdminCodeRouters(v4, &AdminCode) // 5. 中奖结果管理 v5 := v2.Group("/result") addAdminResultRouters(v5, &AdminResult) // 6. 用户管理 v6 := v2.Group("/user") addAdminUserRouters(v6, &AdminUser) // 7. ip 黑名单管理 v7 := v2.Group("/blackip") addAdminBlackipRouters(v7, &AdminBlackip)
-
可以看到,
engin.tress
的结构是这样的:我目前有 GET,GEAD,POST三种方法。
-
那么对应的内部 node节点如何?打开POST方法对应的路由树来看里面路由比较少,大致就是这种:
-
再来分析一波小的GET方法下: 可以看到对应的 “l” 的Priority 是3,说明下面有三个处理函数。三个注册的handler。
-
以 /login路由为例子,我怎么区分是否 是正确路由呢? 当节点的 handlers 为nil,说明不存在该路由!
-
总结:路由方法树
- 1 前缀树,或者说是Radix 基数树数据及结构
- 2 通过 handlers 判断是否是 尾值(是否是正确路由)
- 3 注意node节点的结构, indexes 表示子节点的 path 首字母,用于搜索。还有节点类型等一些其他的特性。 实现具体路由功能,要比 基本的数据结构radix复杂一些。
-
- (3)
遍历找到对应方法,gin中是采用slice实现的,并没有使用map结构!
- 首先,这里 t 是个 slice结构,那么for遍历它寻找对应方法的操作就是 线性的!为啥要用线性的浪费时间呢? 而不使用 map 结构?
- 注意代码中比较的是 methodTree.root 字段。 它代表是的表示一个请求的方法,例如 GET,POST等的字符串。
- 在http1.0时。只有 GET,POST,OPTIONS 三种
- 1.1时有了九种,PUT,DELETE等等。
- 但是一个网站常用的请求,也就四五种,不会太多。所以这里采用线性遍历寻找,也是可以接受的! 数据规模不大,且可以
节省内存的开销
。 - 另外,t := engine.trees。如何验证这个 t 是在何时进行初始化的? 内存如何分配呢? 这就又引出来一个问题了。
-
t(engine.trees) 肯定是Engine初始化的时候,进行申请的。
-
Engine 初始化有两种方法,
gin.Default() 和 gin.New()
-
gin.Default 内部其实是使用了 gin.New() 并且增加了 两个中间件,日志和恢复。
-
来看下Default 源码:
// Default returns an Engine instance with the Logger and Recovery middleware already attached. func Default() *Engine { debugPrintWARNINGDefault() engine := New() engine.Use(Logger(), Recovery()) return engine }
-
查看对应的New()源码,就能看到 对 Engine对象的各种初始化工作!其中有很关键的一句:
trees: make(methodTrees, 0, 9),
证明了 路由方法树,不是运行时动态申请内存的,而是初始化的时候,就申请了9个单位的空间,初始化长度为 0!
-
- 这里遍历 t的过程,有个小技巧, 先进行计算出来 len(t), 后续比较直接使用变量就可以了, 避免了每次动态重新计算 len(t)。 当然,如果遍历的过程中需要 更改这个t, 就不能这样了, len() 每次执行都会动态的重新获取 len() 长度。
- 首先,这里 t 是个 slice结构,那么for遍历它寻找对应方法的操作就是 线性的!为啥要用线性的浪费时间呢? 而不使用 map 结构?
- (4) 这里讲了三点,还没分析完这个函数逻辑功能。再第四:路由匹配规则中会继续介绍。
-
- 那么主要就是看
3. 注册路由的过程
- 同样参照开篇中图片的那篇文章,调理很清晰! https://zhuanlan.zhihu.com/p/102303084,可以先看下路由大致的内容。
- 注册路由的主要逻辑有俩函数 addRoute函数 和 insertChild 方法
- 从最开始的注册路由函数看起:
// 创建路由组 v1 := app.Group("/") // 首页 rg.GET("/", indexCol.Get) // 加不加斜线都行 rg.GET("/login", indexCol.GetLogin) rg.GET("logout", indexCol.GetLogout) 简单的还可以直接创建添加路由 app := gin.Default app.bubble.GET("/todo", fun HandlerFunc)...
-
路由和路由组添加调用的都是同一个 GET 方法。 还有POST,DELETE等方法,我们只看 GET,POST,都是一样的:
-
翻译中说了 : POST是路由器的一种快捷方式, 内部是调用 Handle(“POST”, path, handle) 来进行处理。
// POST is a shortcut for router.Handle("POST", path, handle). func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle(http.MethodPost, relativePath, handlers) } // GET is a shortcut for router.Handle("GET", path, handle). func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes { return group.handle(http.MethodGet, relativePath, handlers) }
-
那么这是咋回事儿? 怎么突然从 Engin 的Default 对象就到了 (group *RouterGroup) 对象了呢? GET, POST, 还有创建路由组Group,都是 (group *RouterGroup)提供的! 内部还调用它的 handle 进行处理。
我们再次返回前文观察到: Engine 对象的结构中,第一行就出现了:RouterGroup
这是使用了 Embedding: 内嵌 的方式扩展已有类型。-
一共有三种,前面的博客中已经讲解过了扩展已有类型:
别名,内嵌,组合
三种方式。 -
所以我们创建路由和路由组的时候,使用的是 RouterGroup提供的方法!
-
那么 RouterGroup 对象的简答初始化,其实也是gin.New() 中了
RouterGroup: RouterGroup{ Handlers: nil, basePath: "/", root: true, },
-
-
继续说回处理路由,POST交给 handle方法,传递了三个参数:
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes { absolutePath := group.calculateAbsolutePath(relativePath) handlers = group.combineHandlers(handlers) group.engine.addRoute(httpMethod, absolutePath, handlers) return group.returnObj() }
-
POST 请求传递的多个 handlers …HandlerFunc HandlerFunc类型函数, 被handle 函数的 HandlersChain 类型接收。
-
HandlersChain 实际上就是:
type HandlersChain []HandlerFunc
,还有HandlerFunc。// HandlerFunc将gin中间件使用的处理器定义为返回值 type HandlerFunc func(*Context) // HandlersChain定义一个HandlerFunc数组。 type HandlersChain []HandlerFunc
-
剩下的两个参数: 一个是 http 请求方法, 一个是 相对路径。
-
分析handle 函数都干了什么:
- 1 calculateAbsolutePath通过名字可以看出来,这是计算绝对的路径地址,通过相对路径。
- 2 combineHandlers 合并group 自带的和 开发者注册的函数(handler)
func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { // 将开发者注册的 handlers 和 group 系统的一些处理handler 进行拼接。 finalSize := len(group.Handlers) + len(handlers) // 超出指定长度报错 if finalSize >= int(abortIndex) { panic("too many handlers") } // 合并操作,主要就是 copy 和 make 函数的使用 mergedHandlers := make(HandlersChain, finalSize) copy(mergedHandlers, group.Handlers) // 这一点可以,切片的切片 去复制。是个不错的技巧。 copy(mergedHandlers[len(group.Handlers):], handlers) return mergedHandlers }
- 注意最后一步复制的顺序和技巧
- 另外, group 对象的 handlers是从哪里来的呢? 有个对外的方法:
Use()
实际上就是添加中间件的函数。func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes { group.Handlers = append(group.Handlers, middleware...) return group.returnObj() }
-
3 下一步拼接好的 处理handlers, 加上http 方法名字, 绝对路径传递给
addRoute
方法。注意这个addRoute方法,是调用的engine 对象的
。原因就是,最开始讲 engine对象的时候,其结构体中包含 trees, 路由方法树。 用于添加路由和路由方法。func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { // 一系列断言操作 assert1(path[0] == '/', "path must begin with '/'") assert1(method != "", "HTTP method can not be empty") assert1(len(handlers) > 0, "there must be at least one handler") debugPrintRoute(method, path, handlers) // 通过get方法,内部也是线性for循环遍历,找到对应方法的 方法树 root := engine.trees.get(method) // 不存在就添加新的方法树根节点 if root == nil { root = new(node) // 默认其实 root 的字符必须是 "/" root.fullPath = "/" engine.trees = append(engine.trees, methodTree{method: method, root: root}) } // 调用节点的 addRoute 方法,往对应方法树内插入 root.addRoute(path, handlers) // Update maxParams // 更新 maxParams 参数 if paramsCount := countParams(path); paramsCount > engine.maxParams { engine.maxParams = paramsCount } }
-
其中到了最核心的步骤,找到对应的方法树后(root,方法树的根节点,node类型)下面的插入操作就交给了 node 类中的 addRoute方法实现了。
传递参数:
path: 绝对路径地址; handlers: 组合的注册handle。 -
到了这里,其实就是底层实现的Radix树, 传进去一个字符串(完整路由路径),添加到Radix树中。 源码函数比较长, 大致过程理解,但是细节还理解不是很到位~
(这一点没有理解明白)
-
相关的函数功能都有在文档中解释,比较难得,重要的放到这个源码后面再去拿出来分析。
// addRoute将一个具有给定句柄的节点添加到路径。 // 不是concurrency-safe (不是并发安全的) func (n *node) addRoute(path string, handlers HandlersChain) { // 补充fullpath属性 fullPath := path // priority++ 优先级加1 n.priority++ // Empty tree // 如果是空的树,或还没有子节点:初始化是一个 "/",node类型, 调用insertChild插入 if len(n.path) == 0 && len(n.children) == 0 { // "/" 中插入path n.insertChild(path, fullPath, handlers) // 因为是空树这里将调用的节点(“/”)标记为 根节点类型。 n.nType = root return } parentFullPathIndex := 0 walk: for { //查找最长的公共前缀。 //这也暗示了公共前缀不包含':'或'*' // 因为现有的键不能包含这些字符。 // 这个 函数非常简单,就是 for循环,找到两个path的公共最长前缀,返回其长度。 // 比如 /index , /index/a 公共长度就是 /index i := longestCommonPrefix(path, n.path) // Split edge: 分裂,找到每一部分 // 分类的是当前节点。 比方当前是 "login" 新增是 “logout” // 那么当前节点按照最长前缀进行分裂 log, 增加 in, out 两个子节点。 if i < len(n.path) { // 先将当前 node 自己分开,后半截作为子节点 child := node{ path: n.path[i:], wildChild: n.wildChild, indices: n.indices, children: n.children, handlers: n.handlers, priority: n.priority - 1, fullPath: n.fullPath, } n.children = []*node{&child} // []byte for proper unicode char conversion, see #65 // 改变 当前节点为前半段 // 保存子节点的 path 首字母 n.indices = bytesconv.BytesToString([]byte{n.path[i]}) n.path = path[:i] n.handlers = nil n.wildChild = false n.fullPath = fullPath[:parentFullPathIndex+i] } // Make new node a child of this node // 和上面的逻辑是相似的,比较 len(path) // i < len(path) 说明需要进行拆分 if i < len(path) { path = path[i:] c := path[0] // '/' after param // 处理参数后面有斜杠的情况 if n.nType == param && c == '/' && len(n.children) == 1 { parentFullPathIndex += len(n.path) n = n.children[0] n.priority++ continue walk } // Check if a child with the next path byte exists // 进行检测分配, 遍历indices,看看当前节点的子节点那个满足开头匹配。 // 满足匹配后,再跳到子节点,n = n.children[i], 再次进行相关操作。 分裂或者创建。 for i, max := 0, len(n.indices); i < max; i++ { if c == n.indices[i] { parentFullPathIndex += len(n.path) i = n.incrementChildPrio(i) n = n.children[i] continue walk } } // Otherwise insert it // 子节点找到不的话,就尝试插入进去。 if c != ':' && c != '*' && n.nType != catchAll { // []byte for proper unicode char conversion, see #65 // 拼接第一个字符到 indices n.indices += bytesconv.BytesToString([]byte{c}) child := &node{ fullPath: fullPath, } // 追加子节点,做了一个append操作内部 n.addChild(child) n.incrementChildPrio(len(n.indices) - 1) n = child // 如果是 参数节点进行相应的处理 } else if n.wildChild { // inserting a wildcard node, need to check if it conflicts with the existing wildcard // 插入通配符节点时,需要检查是否与现有通配符冲突 n = n.children[len(n.children)-1] n.priority++ // Check if the wildcard matches if len(path) >= len(n.path) && n.path == path[:len(n.path)] && // Adding a child to a catchAll is not possible n.nType != catchAll && // Check for longer wildcard, e.g. :name and :names (len(n.path) >= len(path) || path[len(n.path)] == '/') { continue walk } // Wildcard conflict pathSeg := path if n.nType != catchAll { pathSeg = strings.SplitN(pathSeg, "/", 2)[0] } prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path panic("'" + pathSeg + "' in new path '" + fullPath + "' conflicts with existing wildcard '" + n.path + "' in existing prefix '" + prefix + "'") } n.insertChild(path, fullPath, handlers) return } // Otherwise add handle to current node // 这个节点已经注册过了。 if n.handlers != nil { panic("handlers are already registered for path '" + fullPath + "'") } n.handlers = handlers n.fullPath = fullPath return } }
-
核心操作就是:
(我觉得)主要的就是进行树的一个 分裂,调整,查找节点, 插入的操作由insertChild 函数完成,并对相应的通配符节点进行处理
- 1 存在寻找位置插入
- 不断的判断,通过indices 字段查找,然后或者分裂当前 path。重新调整 radix树。
- 2 不存在先创建
- 3 各种判断:比如节点存在,就不用创建
- 4 对不同类型的节点进行特殊处理,比如param类型。
- 5 相应的进行各种节点的信息初始化 与 更新
-
注意:里面有个核心的插入操作:
insertChild 函数
。func (n *node) insertChild(path string, fullPath string, handlers HandlersChain) { for { // Find prefix until first wildcard // 找到第一个通配符参数,一直查找前缀 // 搜索通配符段并检查名称中是否有无效字符。 // 如果没有找到通配符,则返回-1作为索引。 wildcard, i, valid := findWildcard(path) if i < 0 { // No wildcard found break } // The wildcard name must not contain ':' and '*' // 通配符名称不能包含':'和'*' if !valid { panic("only one wildcard per path segment is allowed, has: '" + wildcard + "' in path '" + fullPath + "'") } // check if the wildcard has a name // 通配符是否有名字,小于2长度没有名字, :name if len(wildcard) < 2 { panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") } if wildcard[0] == ':' { // param // 对正常通配符进行相关处理 if i > 0 { // Insert prefix before the current wildcard n.path = path[:i] path = path[i:] } child := &node{ nType: param, path: wildcard, fullPath: fullPath, } n.addChild(child) n.wildChild = true n = child n.priority++ // if the path doesn't end with the wildcard, then there // will be another non-wildcard subpath starting with '/' //如果路径没有以通配符结束,那么就在那里 //将是另一个以'/'开头的非通配符子路径 if len(wildcard) < len(path) { path = path[len(wildcard):] child := &node{ priority: 1, fullPath: fullPath, } n.addChild(child) n = child continue } // Otherwise we're done. Insert the handle in the new leaf // 到这里就完成了,插入节点完成 n.handlers = handlers return } // catchAll if i+len(wildcard) != len(path) { panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") } if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") } // currently fixed width 1 for '/' i-- if path[i] != '/' { panic("no / before catch-all in path '" + fullPath + "'") } n.path = path[:i] // First node: catchAll node with empty path child := &node{ wildChild: true, nType: catchAll, fullPath: fullPath, } n.addChild(child) n.indices = string('/') n = child n.priority++ // second node: node holding the variable child = &node{ path: path[i:], nType: catchAll, handlers: handlers, priority: 1, fullPath: fullPath, } n.children = []*node{child} return } // If no wildcard was found, simply insert the path and handle // 如果没有通配符,只需要插入路径和句柄 n.path = path n.handlers = handlers n.fullPath = fullPath }
- 最主要的操作就是对通配符的处理。完成插入操作。
-
1
-
-
1
-
-
4. 路由匹配
-
之前 2 一步步推理讲到 Engine 实现 ServerHTTP接口,代替 http包下的默认 muxer。 context 是在这里做的。然后讲解了核心路由匹配处理的函数
handleHTTPRequest(c)
讲了部分内容,现在继续讲解,理解有限很多地方还很简陋,理不顺,后续再来补充吧: -
其中的 getvalue 函数:getValue(path string, params *Params, unescape bool),通过传递的相关参数,从路由树查找,返回了一个: nodeValue 类型的对象,而不是节点对象。
type nodeValue struct { handlers HandlersChain // 注册的路由 params *Params // 参数 tsr bool fullPath string } nodeValue holds return values of (*Node).getValue method
-
后面的一段主要执行:
if value.handlers != nil { c.handlers = value.handlers c.fullPath = value.fullPath c.Next() // next函数,继续执行 c.writermem.WriteHeaderNow() return }
5. 中间件
- 其实根据上面的分析,中间件的设计已经很清晰了,比较巧妙的设计。
- gin.Default() 内部默认开启两个中间件:日志 和 恢复
-
启用中间件的函数时 Use(), 它来自Engin 对象内部的 RouterGroup对象。
-
具体干了啥呢?打开源码看一下
// Use attaches a global middleware to the router. ie. the middleware attached though Use() will be // included in the handlers chain for every single request. Even 404, 405, static files... // For example, this is the right place for a logger or error management middleware. //将全局中间件附加到路由器ie。通过Use()附加的中间件将是 //每个请求都包含在处理程序链中。甚至404,405,静态文件… //例如,这是一个日志或错误管理中间件的正确位置。 func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes { engine.RouterGroup.Use(middleware...) // 整合 engin.noRout, engine.noMethod ,他俩其实就是 一个注册的 handler的slice engine.rebuild404Handlers() engine.rebuild405Handlers() return engine }
-
最终调用的 路有对象的 Use方法:意图很清晰,加入到 路由对象的 Handlers里去。 当调用路由的时候,有个合并操作! 将这些方法合并到对应子路由中去执行。
// Use adds middleware to the group, see example code in GitHub. func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes { group.Handlers = append(group.Handlers, middleware...) return group.returnObj() }
-
4.中介绍了 getValue,并将相应的数据,赋值给了 context对象。然后调用了一个 Next函数。 next主要做了什么?
-
next函数:按照 Handlers HandlersChain 注册函数链,进行依次调用执行。 顺序执行。
// Next should be used only inside middleware. // It executes the pending handlers in the chain inside the calling handler. // See example in GitHub. // 依次取出 注册的函数,包括视图逻辑处理函数 以及注册的中间件函数。 // 顺序执行 func (c *Context) Next() { c.index++ for c.index < int8(len(c.handlers)) { c.handlers[c.index](c) c.index++ } }
- 小技巧: 如何实现非线性的调用呢?
- 注意到 Next源码里有个开始的 ++ 操作。可以再某个处理逻辑handle 里调用 c.Next()函数! 此时会依次向下执行完剩余的 handle,后才接着处理本次请求。当然了这个 调用next函数,可以一直嵌套, 后面执行的 handle 里面还可以 调用c.next(Context对象)。改变执行顺序!
- 小技巧: 如何实现非线性的调用呢?
-
讲了Next函数后, 必须要提一下Abort() 函数。代码实现超级简单,阅读过前面这里就很容易懂了! 就是更改 c.index = abortIndex。 index 是遍历 Handlers HandlersChain 的索引
- abortIndex: 是一个常量:
const abortIndex int8 = math.MaxInt8 / 2
终止后序的handle的执行。
// Abort prevents pending handlers from being called. Note that this will not stop the current handler. // Let's say you have an authorization middleware that validates that the current request is authorized. // If the authorization fails (ex: the password does not match), call Abort to ensure the remaining handlers // for this request are not called. //中止处理程序被调用。注意,这不会停止当前的处理程序。 //假设你有一个授权中间件来验证当前请求是否被授权。 //如果授权失败(例如:密码不匹配),调用Abort以确保剩余的处理程序 //这个请求没有被调用。 func (c *Context) Abort() { c.index = abortIndex } func (c *Context) Abort() { c.index = abortIndex }
- abortIndex: 是一个常量:
-
再来讲解 c.Set: 向接口中添加相应的数据,因为
Context对象是贯穿整个流程的,所以后面执行的 handle 可以调用当前设置的这个值,为接口类型。
// Set is used to store a new key/value pair exclusively for this context. // It also lazy initializes c.Keys if it was not used previously. // Set用于为这个上下文专门存储一个新的键/值对。 //如果c.Keys以前没有被使用过,它也会延迟初始化。 func (c *Context) Set(key string, value interface{}) { // 还是线程安全的添加,加了读写锁的。 c.mu.Lock() if c.Keys == nil { c.Keys = make(map[string]interface{}) } c.Keys[key] = value c.mu.Unlock() }
-
除此之外还有一个 c.Get 可以获取相应的,就不再分析了。
-
-
6. 关于context对象 和 bind
- 直接查看大佬文章吧~~~ 我还是菜鸡,这篇文章已经瞎写的有点长了,电脑太卡了。
- https://zhuanlan.zhihu.com/p/102438603
- 其实上面的 中间件后面已经讲到了一些context对象的内容了。
-
-
web框架安全概览--你所使用的web框架是否安全?
2020-05-12 09:02:29文章目录web框架安全MVC框架实现安全方案模板引擎与XSS防御web框架与CSRF防御HTTP Headers管理数据持久层与SQL注入web框架自身安全struts2命令执行漏洞spring MVC命令执行Django命令执行漏洞shiro反序列化漏洞... -
基于SSM框架的高校竞赛报名系统
2021-04-13 15:51:53组织者登录创建多人比赛,管理员登录通过创建比赛申请,学生登录报名比赛,管理员登录通过学生报名申请,学生登录提交作品,组织者登录下载报名学生名单,在名单中成绩列打分,上传成绩,管理员登录查看成绩,发送... -
.NET平台常见技术框架整理汇总
2021-01-05 15:01:28使用.NET平台开发有段时间了,在网上资料的基础上做了进一步整理,汇集了.NET平台常见的技术框架。 参考资料: 基于.NET平台常用的框架整理.NET开源框架列表 1.知识网络 2.分类清单 2.1.分布式缓存框架 ... -
常用的技术框架和工具
2021-05-08 19:01:48分布式缓存框架: Microsoft Velocity:微软自家分布式缓存服务框架。 Memcahed:一套分布式的高速缓存系统,目前被许多网站使用以提升网站的访问速度。 Redis:是一个高性能的KV数据库。 它的出现很大程度补偿了... -
Web前端框架(JavaScript,CSS)、html组件、CSS规范与第三方库
2022-02-06 14:45:091、什么是前端框架 前端框架一般指用于简化网页设计的框架,使用广泛的前端开发套件,比如,jquery,extjs,bootstrap等等,这些框架封装了一些功能,比如html文档操作,漂亮的各种控件(按钮,表单等等),使用前端... -
Asp.net平台常用的框架整理[转载]
2021-09-07 06:46:13分布式缓存框架: Microsoft Velocity:微软自家分布式缓存服务框架。 Memcahed:一套分布式的高速缓存系统,目前被许多网站使用以提升网站的访问速度。 Redis:是一个高性能的KV数据库。它的出现很大程度补偿了... -
.NET平台常用的框架整理
2018-05-16 21:31:01从学习.NET以来,优雅的编程风格,极度简单的可扩展性,足够强大开发工具,极小的学习曲线,让我对这个平台产生了浓厚的...分布式缓存框架:Microsoft Velocity:微软自家分布式缓存服务框架。Memcahed:一套分布式... -
Sentinel 限流框架
2021-06-02 13:57:26第二个参数代表调用方标识 origin,目前它有两个作用,一是用于黑白名单的授权控制,二是可以用来统计诸如从应用 application-a 发起的对当前应用 interfaceXxx() 接口的调用,目前这个数据会被统计,但是 ... -
「第十二章」Web框架安全
2021-08-11 11:15:15一组不依赖分辨率的社交网络图标(可缩放矢量图标) * 响应式表格 Foundation框架中响应式表格的实现机制是固定表格的左边第一列,然后表格的其他列可以通过滚动条拖拉进行访问 * 关闭帆布布局(Off-... -
.NET常用框架学习经历
2019-05-08 16:43:15.NET常用框架学习经历 偶尔看到了一个叫做 基于.NET平台常用的框架整理 的页面,地址:https://www.cnblogs.com/hgmyz/p/5313983.html,里面介绍了许多跟.NET有关的知识。 根据他的列表,就开始挨个了解,刚开始的... -
.Net平台常用框架
2019-09-21 15:40:23分布式缓存框架: Microsoft Velocity:微软自家分布式缓存服务框架。 Memcahed:一套分布式的高速缓存系统,目前被许多网站使用以提升网站的访问速度。 Redis:是一个高性能的KV数据库。它的出现很大程度补偿了... -
web安全渗透框架梳理
2021-07-17 11:59:01os-cmd=ifconfig dumfile\outfile outfile函数会在行末端写入新行,会转义换行符 dumpfile函数不对任何列或行进行终止,也不执行任何转义处理 工具:sqlmap ** webshell 提权过程分析 ?cmd=system("find/ -user root ... -
如何管理安卓权限:Xposed框架+X隐私模块
2021-05-27 05:02:25如何管理安卓权限:Xposed框架+X隐私模块Xposed框架是一款可以被称之为“神器”的安卓系统类工具,为什么说Xposed神呢?这款App可以说能够让安卓脱胎换骨,它为安卓提供了一个全新的应用平台,用户可以通过安装... -
基于.NET平台常用的框架整理
2020-08-02 13:48:41自从学习.NET以来,优雅的编程风格,极度简单的可扩展性,足够强大开发工具,极...开源的作业调度和自动任务框架:Quartz.NET 用于大数据搜索引擎的全文检索框架:Lucene.net 微软针对企业级应用开发的最佳实践组件:E -
基于.Net开源框架
2018-11-26 08:40:02自从学习.NET以来,优雅的编程风格,极度简单的可扩展性,足够强大开发工具,极小的学习曲线,让我对这个平台产生了浓厚的兴趣,在工作和学习中也积累了一些开源的组件,就目前...分布式缓存框架: Microsoft V... -
Laravel5.1 框架模型创建与使用方法实例分析
2021-04-22 00:48:05本文实例讲述了Laravel5.1 框架模型创建与使用方法。分享给大家供大家参考,具体如下:Laravel的模型也是访问数据库的,它更加面向对象,一个模型对应着一张表 我们可以使用模型对数据做一些增删改查的操作。1 创建... -
Egg.js 开发常用框架整合
2021-10-09 10:23:45Egg.js 开发常用框架整合概述起步api 开发配置路由api版本控制resfulApi 路由版本控制错误和异常全局异常处理egg resfulAPI 全局异常处理统一错误处理数据库egg.js 数据库迁移sequelize 数据库迁移命令配置创建数据... -
web前端开源框架_9个用于前端Web开发的开源JavaScript框架
2020-07-16 09:35:19web前端开源框架 大约十年前,JavaScript开发人员社区开始目睹JavaScript框架之间出现的激烈战斗。 在本文中,我将介绍一些最著名的框架。 需要特别注意的是,这些都是开放源代码JavaScript项目,这意味着您可以在... -
Web应用框架汇总
2018-03-05 14:23:17Web应用框架(Web application framework)是一种开发框架,用来支持动态网站、网络应用程序及网络服务的开发。其类型有基于请求的和基于组件的两种框架,Web应用框架有助于减轻网页开发时共通性活动的工作负荷,...