Tinyfox 用户手册

TINYFOX WEB APPLICATION FRAMEWORK USER MANUAL


一、概述

Tinyfox 是一款自带 HTTP 服务器的以 WebApi、WebSocket 及“动态HTML”为核心功能的超轻量级的同时具有良好扩展力的 HTTP Web 应用程序基础框架。Tinyfox 灵活易用,性能强劲,跨平台,既支持 Linux 也支持 Windows,既支持 X86 硬件环境,也支持 ARM、LongArch 等 CPU 环境。

Tinyfox 本身没有使用任何动态反射功能和运行期动态编译功能,所以支持 NativeAOT,能够通过 .NET NativeAOT 技术将其编译成与 c/c++ 程序相类似的“原生”程序,为基于 Tinyfox 开发高性能、高价值的商用程序提供了关键性的基础条件。

OVERVIEW:

Tinyfox is an ultra-lightweight, highly scalable service framework with WebApi, WebSocket, and "Dynamic HTML" as the core functions with its own HTTP server. Tinyfox is flexible, easy-to-use, powerful, and cross-platform, supporting both Linux and Windows, supporting both x86 hardware environments and CPU environments such as ARM and LongArch.

Tinyfox itself does not use any reflection function and runtime dynamic compilation function, so it supports NativeAOT, which can be compiled into native programs similar to c/c++ programs through .NET NativeAOT technology, which provides key basic support for the development of high-performance and high-value commercial programs based on Tinyfox.

(The following content is in Chinese)

1、Tinyfox 的特点:

2、Tinyfox 使用场景:

根据 Tinyfox 的自身特点,Tinyfox 适合于在下列场景中使用:

二、获取

Tinyfox 是一款既支持 .NET Framework v4.x 这样的“.NET 传统版本”同时也支持 .NET6 以上的“.NET新世界”的免费组件,它通过 Nuget 向全球公开发布。所以,获取 TinyFox 是一件非常简单的事情:您只需要在 .NET 项目中添加名为“Tinyfox”v6.0 以上版本的 nuget 包引用就行了。

除了 Tinyfox 本身外,您还可以从如下几个方面获取关于 Tinyfox 的技术支持:

三、开始

Tinyfox 的使用很简单,大致包括如下几个大的步骤:

  1. 用 Microsoft Visual studio(或其他编辑工具)创建一个 .NET 控制台项目(桌面程序项目也行);
  2. 在项目中,打开“管理Nuget程序包”,浏览并安装“Tinyfox”,选6.0以上版本;
  3. 在程序的 Main 方法中对 Tinyfox 的运行参数进行设置,包括服务端口号、静态文件路径、静态文件标签处理器、路由参数等;
  4. 在程序 Mian 方法的最后,用 Fox.Start() 方法启动 Tinyfox,用 Fox.WaitExit() 等候 Tinyfox 退出;
  5. 运行这个程序;
  6. 用浏览器访问“http://localhost:8088/”,查看效果。

下边是一个较为完整的简明示例,就从这儿开始吧:

using Tinyfox;
using Tinyfox.WebApiEngine;
using Tinyfox.WebApiEngine.Results;

//......

static void Main(string[] args){
    
    // 设置服务端口(默认8088)
    Fox.Port = 800;

    // 配置路由关系
    // 如果路由条目很多,可以单独放到一个静态方法中集中处理
    var RT = (HttpRoute)Fox.Router;
    RT.GET["/"] = _ => new TextResult("Hello, World!");
    // .....
    RT.OnNotFound = _ => new NotFoundResult("找不到你希望获取的资源...");
    
    // 启动并阻止程序退出
    Fox.Start();
    Fox.WaitExit();
}

运行这个程序,然后打开浏览器,在地址栏中键入“http://localhost:800/”开始访问,就能在浏览器中看到“Hello, World!”这样的应答内容。

从以上示例可以看出,Main() 方法中,主要就是通过一个名叫“Fox”的静态类所公开的属性、方法对程序的运行参数进行设置,然后启动 Tinyfox 服务并阻止程序退出。

Fox 静态类包括如下属性:

Fox 静态类包括如下方法:

四、路由

在 Tinyfox 中,最必要最重要的设置是对“路由”的设置。它反映的是远端浏览器等各种客户端访问本程序所请求的 URL PATH 与本程序所使用的具体的处理方法(函数)之间的对应关系。

每一个或每一类不同的访问路径(URL PATH)应该有一个对应的“处理函数(c#称方法)”与之对应,如果某个路径没有配置“处理器方法”,那么 tinyfox 就会向访问者返回“找不到文件”的异常,即返回“404”状态码。

强调:“静态文件”不受路由的约束,比如 html、txt、jpg、js、css、ico 等类型的文件,这些静态文件一般存放在程序所在路径下的一个名叫“wwwroot”的子目录中(也可以使用其它的子目录名),这个文件夹中的所有内容都将被视为静态文件,Tinyfox 将以“根路径”为基准响应用户请求发送对应文件,即,这个“wwwroot”其实就是所有静态文件相对于URL根路径的“根文件夹”。

(一)路由语法

路由配置的具体语法是:“路由器.请求方法["URL路径"] = 会话处理函数”

Tinyfox 路由配置示例:


// 获取Tinyfox内置的默认路由器,以方便后续操作
var rt = (HttpRoute)Fox.Router;

// 客户端GET方法请求某个固定路径
rt.GET["/hello"] = _ => new TextResult("Hello");
// 或者:rt.GET["/hello"]=_=>TextResult.FromHtmlRoot("hello.html");

// 把URL路径的一部分作参数:
rt.GET["/api/value/{id}"] = c => new TextResult($"得到的id值是:{c.Request["id"]}, 原始URL路径是:{c.Request.Path}");

// 配置找不到文件或路由关系时的处理器
rt.OnNotFound = _ => NotFoundResult.FromHtmlRoot("404.html"); 
// 或者:rt.OnNotFound = _ => new NotFoundResult("不小心跑丢了....");

// 配置“URL重写”
rt.REWRITE["^/sitemap$"] = "/SiteMap.html";

(二)路由的处理函数(会话处理函数)

“会话处理函数”(c#称为“方法”)是用来具体处理某一个路径上的用户请求。该函数由一个类型为 HttpContext 的“上下文对象”作为参数和一个继承于 ResultBase 类型的“应答对象”作为返回值构成,形如:

public static TextResult Process(HttpContext context)
{
    var hello = "Hello, Welcome!";
    hello += $" Your IP address: {context.RemoteIpAddress}";

    // ... 其他各种处理细节 ...
    
    // 返回一个应答对象
    return new TextResult(hello);
}

又如:

public static class MyRequestProcessor
{

    // 浏览器用GET访问访问网站根路径的应答,
    // 对应路由是:router.GET["/"] = MyRequestProcessor.GetWebRoot;
    public static TextReult GetWebRoot(HttpContext c) => new TextResult("Hello, world!");


    // 浏览器用GET方法访问某个路径,路径中带有参数
    // 路由类似:route.GET["/api/value/{id}"] = MyRequestProcessor.GetApiValue;
    public static TextResult GetApiValue(HttpContext c) {

        // 浏览器请求的 URL PATH
        var url_path = c.Request.Path;

        // 获取名为 "id" 的参数
        var id_val = c.Request.GetQuery("id");

        // Session测试
        // 从Session中读取当前访客的一个名叫“count”的数字值,并递增1
        var count = c.Session.Int("count") + 1;
        // 保存回去,更新session内容
        c.Session.Int("count", count);

        // 准备要输出的应答内容
        // 当前是拼接一个普通文本字串,
        // 当然,你也可以将这些数据序列化为 json 字串返回给访问者,但不要忘了设置 json 对应的 ContentType 属性
        var s = $"原始请求路径是: {url_path};路径参数 'id' 的值是: {id_val};你的访问次数是: {count}";

        //创建并返回一个文本类应答对象
        return new TextResult(s);
    }

}

*“处理函数”可以是静态方法,也可以是实例方法,还可以是匿名函数,具体怎么处理,取决于实际场景。比如上例中对访问“/”根路径的应答,由于处理逻辑很简单,所以直接用一个匿名函数就行了。

* 强调:处理函数一定不要有明显的耗时操作,要尽可能及时地返回应答对象。如果不可避免地有非常耗时处理过程,应当使用 StreamResult 作为应答对象,并另起一个线程对其进行操作(同时将该应答对象作为参数传给该线程以便后续进行数据输出)。

(三)静态文件与路由的关系

Tinyfox 不但能处理 WebApi,而且能高效怎理 html、js、css、jsp 等各种静态文件资源,是非常迅捷的静态文件服务器。

当一个基于 tinyfox 开发的应用程序,同时有 html 之类的“静态文件”又有路由逻辑时,Tinyfox 会按如下规则进行处理:

  1. Tinyfox 只会直接处理“wwwroot”或用户指定的静态文件夹中的文件,“静态文件夹”在整个程序中具有唯一性,Tinyfox 只会将该文件夹中的文件视其为可以由自己作主的、可以直接处理的“静态文件”;
  2. Tinyfox 只会处理静态文件夹中真实存在的文件,如果这个文件夹中没有用户访问的资源,Tinyfox 就会脱离静态处理环节而进入路由匹配链;
  3. Tinyfox 只会处理客户端通过 GET 方法提交的静态文件请求,其它 HTTP 方法,比如 POST、PUT 等请求方法,即使静态文件夹中存在对应文件,Tinyfox 也不会直接处理,而是提交到路由层面,由应用程序处理;
  4. Tinyfox 会将静态文件中的 index.html、default.html 视为网站的首页,如果静态文件夹存在这种命名的“首页”文件,当用户访问“/”路径时,Tinyfox 会视之为访问的是“/index.html”或“/default.html”,并直接向访问者返回该首页内容,不会进入路由管道。
  5. 符合静态文件处理规则的请求,不受用户定制的路由规则约束。

* Tinyfox 对请求的处理流程:首先适配静态资源,如果静态资源匹配不上,就进入应用层的路由环节,如果路由环节也匹配不上,就触发路由的 OnNotFound 事件,如果 OnNotFound 也没有配置处理方法,就直接向客户端应答 HTTP 404 状态码。

五、上下文对象:HttpContext

HttpContext 是 Tinyfox 中的一个重要对象,用于向应用层处理函数提供客户端的请求信息和环境变量的实体,是客户端“请求”与应用层“处理”之间的信息通道,是“路由处理函数”的关键参数。

HttpContext 包括两大对象:Request 对象和 Session 对象。

(一)Request 对象

Request 对象是对客户端请求信息的包装,包括如下常用内容:

(二)Session 对象

Session “会话对象”是一个在服务器端关联和存贮每个访问者的会话数据的缓存对象,包括如下内容:

六、应答对象

路由配置的处理函数(会话处理函数)都必须返回一个基于 ResultBase 基类派生的“应答对象”,该对象告诉 Tinyfox 下一步应该做什么,应该怎么做。

Tinyfox 内置了如下 6 个基础应答类型:

还基于 StatusResult 基础类型派生了 3 个常用的状态类型:

注:如果 Tinyfox 使用者认为上述应答类型不足使用,也可以基于上述基础类型或派生类型继续派生拓展新的应答类型,以适应具体开发场景上更广泛的实际需求。但需要注意的是,按目前的设计规范,开发者不要基于 ResutBase 这个基类去派生新的应答类型。

七、动态 HTML

扩展名为“.html”或“.htm”的文件是 Web 程序中最常见、最常用的网页文件类型,但是它是“静态文件”,也就是说,原生的 html 文件的内容是固定不变的,它不能反映服务器端数据的变化。

让内容不变的 html 变成能反映服务器端数据变化的“动态HTML”,常用的方法有如下两种:

  1. 客户端处理:在 html 中通过 javascript 脚本向服务器发起 WebApi 数据请求,得到服务器返回的数据后通过 html 显示出来;
  2. 服务器处理:在 Html 中添加特殊标记,服务器在发送这个 html 文件之前对这些标记进行解析并填充具体内容。

Tinyfox 的“动态HTML”其实就属于上述第2类处理方式,即,在 HTML 文件中添加特定的“标签”,Tinyfox 在向浏览器发送这个 html 文件前对这些标签进行解析并填入当前的实时数据。

Tinyfox的“HTML标签”样式是:

${ 变量 或 标签函数 }

“标签”的特点: 以“$”号开头,以花括号“{}”容纳具体内容,“$”号与“{”号之间不能留空格,不能换行书写。

“标签”中的内容可以包括两种:一种是“变量”,一种是“标签函数”:

如:

<html>

<head>
    <meta charset="utf-8" />
</head>

<body>
    <!--变量赋值与取值显示-->
    ${$Area=中国区}
    当前地区是:${$Area}

    <!--调用c#中的名叫“GetUserName”的处理方法并显示其返回值-->
    您的姓名是:${GetUserName()}
</body>

</html>

如何在HTML中获取URL的查询参数?

答:在“Tinyfox动态Html”机制中,浏览器地址栏中的URL查询参数会映射成 html 的一个“变量”,如这个“/test.html?abc=111”的查询参数“abc”,它在 test.html 中所对应的变量就是“$abc”,其值为“111”。

能不能用标签变量作为标签函数的参数

答:能:

八、响应动态 HTML“标签函数”的伺服器对象

动态 HTML 可以通过“标签函数”向应用程序发起调用它内置的某个c#方法的请求,为了应对这种请求,我们就需要为之配备一个“伺服器”对象负责处理相关调用。

处理 HTML 标答函数调用的“伺服器”(一个类对象)必须继承自 Tinyfox 提供的 HtmlMethodBase 基类,继承者必需实现一个名叫“Invoke”方法。该方法的定义是:

public abstract string Invoke(string methodName, params string[] args);

比如,针对上一个html示例中的“GetUserName()”调用,我们的“伺服器”类可以这样写:

class MyHtmlMethodHelper : HtmlMethodBase
{
    public override string Invoke(string methodName, params string[] args)
    {
        switch (methodName.ToLower()){
            case "getusername":
                return GetUserName();

            default: break;
        }
        return "";
    }

    string GetUserName()
    {
        retrun "张三峰";
    }
}

这个响应 HTML 函数调用的伺服对象,应该在 Tinyfox 启动之前,通过 Fox.HtmlMethodProvider 属性提供给 Tinfox,否则 Html 文件中的所有标签函数都将被忽略。

下边的示例是将上例这个“MyHtmlMethodHelpar”类提供给 Tinyfox,以便 Tinyfox 响应 HTML 中的“标签函数”的调用:

  Fox.HtmlMethodProvider = new MyHtmlMethodHelper();

九、动态 HTML 中的“布局页”与“内容页”

如同 ASP.NET MVC,Tinyfox 也支持“布局页”与“内容页”这样的概念,但它们都是 html 文件而不是 MVC 那样的 cshtml 文件。

“布局页”是父级页,“内容页”是布局页“<body>”节之内某个位置上的具体内容,是布局页的“子级页”。“布局页”与“内容页”共同构成一个完整的 HTML 页面。

为了网站安全和方便站内各内容页共享,Tinyfox 规定“布局页”不能放在“wwwroot”这种保存静态文件的开放文件夹中,必须放在主程序所在文件夹内的一个名称叫“html_data”的子文件夹中,该文件夹属保留文件夹,禁止WEB用户直接请求。

关键点:布局页与内容页如何结合?

  1. 布局页中,在需要嵌入内容页的位置作如下固定的标签标记:
${IncludeBody()}
  1. 在内容页的第一行用下边这个标记设定该内容页要使用的布局页。
<!-- @Layout("布局页名称.html") -->

强调:内容页引用布局页的标签,必须独占内容页的第一行。

1、布局页(母版页)示例

假设该母版页的文件名为“_layout_main.html”,存放在程序文件夹的一个专用子目录“html_root”文件夹中(该文件夹禁止外部直接访问)。

<!DOCTYPE html>

<html lang="en">

<head>
    <meta charset="utf-8" />
    <title>${$TITLE}</title>
</head>

<body>
    <div>
       公共页头,导航栏等等.....
    </div>

    <!--下边是内容页嵌入位置-->
    <div>
${IncludeByoe()}
    </div>

    <div id="foot">
       公共页脚.....
    </div>
</body>

</html>

2、内容页示例

假设该内容页文件名为“index.html”,存放在程序文件夹下的的专用子目前“wwwroot”中。注意首行的写法。

<!--@Layout("_layout_main.html")-->
${$TITLE="XX官网 - 首页"}

<style>

</style>

各种正式的文字/图片内容....
<div>
    1.....
</div>

<div>
    2.....
</div>

由于内容页只是被嵌入到布局页“<body>”节之内的一个部分,共享布局页的所有的元素,所以,内容页不能有body之外的任何HTML页节点,比如不能有“<head></head>”节点

十、WebSocket

Tinyfox 原生支持 WebSocket 服务。

响应客户端WebSocket连接请求的应答类是 WebSocketResult,如果要接受客户端的 WebSocket 服务请求,需要在配置 Tinyfox 路由的时候,在 GET 对象(只能是GET)的路由属性中设置,如:

Fox.HttpRouter.GET["/myws"] = _ =>{
   ver ws = new MyWebSocket();
   return new WebSocketResult(ws);
}

WebSocket服务类

上边这个示例中,WebSocketResult 响应类需要一个 MyWebSocket 服务对象作参数,这个“MyWebSocket”对象就是真正用于WebSocket服务的工作对象(名字并非要命名为“MyWebSocket”),关键一点在于:它必须继承自 WebSocketBase 这个基类。

简单地说:要承载 WebSocket 服务,用户必须基于 WebSocketBase 基类开发一个具体的服务类。

设计要点如下:

WebSocketBase 基类包括如下需要用户具体实现的响应 WebSocket 事件的方法:

WebSocketBase 提供如下方法给继承者调用:

WebSocketBase 向派生类公开如下属性:

* 以下是一个简单的 WebSocket 服务类(工作类)示例:


using Tinyfox.WebApiEngine;
using Tinyfox.WebApiEngine.Results;
using Tinyfox.WebSocketEngine;

class MyWebSocket : WebSocketBase
{
    // 与客户端连接成功,可以开始数据交互了
    protected override void OnAccepted()
    {
        Console.WriteLine("一个WebSocket客户接入成功");
        
        // 发个问候
        Send("Tinyfox 欢迎你!你贵姓?");
    }

    // 同意了客户端连接请求,但发送应答信息时失败
    protected override void OnAcceptFailed()
    {
        Console.WriteLine("与客户端握手失败,连接已对断开");
    }

    // 发生了客户端关闭事件
    protected override void OnClose()
    {
        Console.WriteLine("客户端已关闭,网络连接已断开");
    }

    // 收到了客户端发送过来的文本数据
    protected override void OnMessage(string message)
    {
        // 显示客户端发送过来的内容
        Console.WriteLine($"收到客户端数据: {message}");

        // 回应对方
        Send("你发送的是:" + message);
    }

    // 收到客户端发送过来的二进制数据(基本不存在)
    protected override void OnReadyBytes(ArraySegment<byte> buffer, bool endOfMessage)
    {
        // 在Web方面,限于浏览器的功能所限,一般都不会进行二进制数据收发
    }

    // 收到发送成功的消息
    protected override void OnSendComplete()
    {
        Console.WriteLine("上一个内容已发送成功");
    }

}



以上是《Tinyfox 用户手册》第一版的全部内容,对应 Tinyfox v6.0 以上的版本,欢迎大家使用和交流,您的参与就是对 Tinyfox 作者(宇内流云)的最大的支持。

在使用过程中,不管是针对该手册,还是针对Tinyfox本身,如果您有什么好的提议或者发现了BUG,请及时与作者取得联系。