曲径通幽论坛

标题: Luci 实现框架 [打印本页]

作者: easy    时间: 2014-12-16 11:04
标题: Luci 实现框架
上一篇总结了 uhttpd 的工作方式,openwrt中利用它作为web服务器,实现客户端web页面配置功能。对于request处理方式,采用的是 cgi,而所用的 cgi 程序就是luci,工作框架如下图所示:

[attach]3941[/attach]

Client 端和 serv 端采用 cgi 方式交互,uhttpd 服务器的 cgi 方式中,fork 出一个子进程,子进程利用 execl 替换为 luci 进程空间,并通过 setenv 环境变量的方式,传递一些固定格式的数据(如PATH_INFO)给luci。另外一些非固定格式的数据(post-data)则由父进程通过一个w_pipe 写给 luci 的 stdin,而 luci 的返回数据则写在 stdout 上,由父进程通过一个 r_pipe 读取。

下面的图描述了web配置时的数据交互:
[attach]3942[/attach]


1. 首次运行时,是以普通的 file 方式获得 docroot/index.html,该文件中以 meta 的方式自动跳转到 cgi 的 url,这是 web 服务器的一般做法。


2.然后第一次执行 luci,path_info='/',会 alise 到'/admin'('/'会索引到 tree.rootnode,并执行其 target 方法,即alise('/admin'),即重新去索引adminnode,这在后面会详细描述),该节点需要认证,所以返回一个登录界面。


3.第3次交互,过程同上一次的,只是这时已post来了登录信息,所以serv端会生成一个session值,然后执行'/admin'的target(它的target为firstchild,即索引第一个子节点),最终返回/admin/status.html,同时会把session值以cookie的形式发给client。这就是从原始状态到得到显示页面的过程,之后主要就是点击页面上的连接,产生新的request。


4.每个链接的url中都会带有一个stok值(它是serv生成的,并放在html中的url里),并且每个新request都要带有session值,它和stok值一起供serv端联合认证。

2. luci程序流程


    前面已经说明了,luci 作为web服务器的 cgi 程序,是通过 execl 函数替换到进程空间的,并且详细说明了它与其它进程的交互方法。另外上一节给出了初始阶段 http 报文,可以看到从第 2 次交互开始,所有 request 都是 cgi 方式(除一些css、js 等 resource 文件外),且执行的cgi程序都是luci,只是带的参数不同,且即使所带参数相同(如都是'/'),由于需要认证,执行的过程也是不同的。

    正是由于多种情况的存在,使得 luci 中需要多个判断分支,代码多少看起来有点乱,但 openwrt 还是把这些分支都糅合在了一个流程线中。下面首先给出整体流程,首先介绍一下 lua 语言中一个执行方式 coroutine,它可以创造出另一个执行体,但却没有并行性,如下图所示,每一时刻只有一个执行体在执行,通过 resume、yield 来传递数据,且数据可以是任意类型,任意多个的。
[attach]3943[/attach]

Luci 正是利用了这种方式,它首先执行的是 running() 函数,其中create出另一个执行体 httpdispatch,每次 httpdispatch 执行 yield返 回一些数据时,running()函数就读取这些数据,做相应处理,然后再次执行 resume(httpdispath),……如此直到 httpdispatch 执行完毕,如下图所示:
[attach]3944[/attach]

  如上图所示,其实 luci 真正的主体部分正是 dispatch,该函数中有多个判断分支,全部糅合在一起,代码比较烦,总体上有4个部分,下面对它们进行一些描述。

    首先说明一下代码组成,在openwrt文件系统中,lua语言的代码不要编译,类似一种脚本语言被执行,还有一些 uhttpd 服务器的主目录,它们是:

/www/index.html

     /cgi-bin/luci

     /luci-static/xxx/xx.css、js、gif

/usr/lib/lua/nixio.so、uci.so

         /luci/http.lua、dispatcher.lua、core…

              /controller/xxx.lua

             /model/xxx.lua

             /view/xxx.lua

2.1 节点树node-tree


    在 controller 目录下,每个 .lua 文件中,都有一个 index() 函数,其中主要调用 entry() 函数,形如 entry(path,target,title,order),path 形如 {admin,network,wireless},entry() 函数根据这些创建一个node,并把它放在全局 node-tree 的相应位置,后面的参数都是该 node 的属性,还可以有其他的参数。其中最重要的就是 target 。

    Createtree() 函数就是要找到controller目录下所有的 .lua 文件,并找到其中的 index() 函数执行,从而生成一个 node-tree。这样做的 io 操作太多,为了效率,第一次执行后,把生成的 node-tree 放在/tmp/treecache文件中,以后只要没有更新(一般情况下,服务器里的.lua文件是不会变的),直接读该文件即可。生成的 node-tree 如下:

[attach]3945[/attach]

     这里要注意的是,每次 dispatch() 会根据path_info逐层索引,且每一层都把找到的节点信息放在一个变量 track 中,这样做使得上层 node 的信息会影响下层 node,而下层 node 的信息又会覆盖上层node。比如{/admin/system},最后的auto=false,target=aa,而由于 admin 有sysauth值,它会遗传给它的子节点,也即所有 admin 下的节点都需要认证。

2.2 target简介


    对每个节点,最重要的属性当然是 target,这也是 dispatch() 流程最后要执行的方法。target主要有:alise、firstchild、call、cbi、form、template。这几个总体上可以分成两类,前两种主要用于链接其它 node,后一个则是主要的操作、以及页面生成。下面分别描述。

    链接方法:在介绍初始登录流程时,已经讲到了这种方法。比如初始登录时,url中的path_info仅为'/',这应该会索引到 rootnode 节点。而该节点本身是没有内容显示的,所以它用 alias('admin') 方法,自动链接到 admin 节点。再比如,admin 节点本身也没有内容显示,它用firstchild() 方法,自动链接到它的第一个子节点 /admin/status。

    操作方法:这种方法一般用于一个路径的叶节点 leaf,它们会去执行相应的操作,如修改interface参数等,并且动态生成页面html文件,传递给 client。这里实际上是利用了所谓的 MVC 架构,这在后面再描述,这里主要描述 luci 怎么把生成的 html 发送给client端。

    Call、cbi、form、template这几种方法,执行的原理各不相同,但最终都会生成完整的http-response报文(包括html文件),并调用luci.template.render(),luci.http.redirect()等函数,它们会调用几个特殊的函数,把报文内容返回给luci.running()流程。

[attach]3946[/attach]

如上图所示,再联系luci.running()流程,就很容易看出,生成的完整的http-response报文会通过io.write()写在stdout上,而uhttpd架构已决定了,这些数据将传递给父进程,并通过tcp连接返回给client端。


3. sysauth用户认证


    2.1节已描述了,由于节点是由上而下逐层索引的,所以只要一个节点有 sysauth 值,那么它所有的子节点都需要认证。不难想象,/admin节点有 sysauth 值,它以下的所有子节点都是需要认证才能查看、操作的;/mini节点没有sysauth值,那么它以下的所有子节点都不需要认证。

    luci中关于登陆密码,用到的几个函数为:

[attach]3947[/attach]

    可以看出它的密码是用的linux的密码,而openwrt的精简内核没有实现多用户机制,只有一个root用户,且开机时自动以root用户登录。要实现多用户,必须在web层面上,实现另外一套(user、passwd)系统。

    另外,认证后,serv端会发给client一个session值,且它要一直以cookie的形式存在于request报文中,供serv端来识别用户。这是web服务器的一般做法,这里就不多讲了。

4. MVC界面生成


    这其实是 luci 的精华所在,第二节开始介绍/usr/lib/lua/luci/下有三个目录model、view、controller,它们对应M、V、C。第2.2节介绍了生成的界面怎么传递给client,下面简单介绍生成界面的方法。

Call() 方法会调用controller里的函数,主要通过 openwrt 系统的uci、network、inconfig等工具对系统进行设置,如果需要还会生成新界面。动态生成界面的方法有两种,一是通过cbi()/form()方法,它们利用model中定义的模板map,生成html文件;另一种是通过template()方法,利用view中定义的htm(一种类似html的文件),直接生成界面。

[attach]3948[/attach]

上面的标题是由node-tree生成的,下面的内容由每个node通过上面的方法来动态生成。这套系统是很复杂的,但只要定义好了,使用起来就非常方法,增加页面,修改页面某个内容等操作都非常简单。

转载自:http://www.cnblogs.com/zmkeil/archive/2013/05/14/3078774.html




欢迎光临 曲径通幽论坛 (http://www.groad.net/bbs/) Powered by Discuz! X3.2