一、基本工作原理
UOJ 主要由两部分组成:网页端和测评端。顾名思义,网页端就是用来通过网页与用户交互的,测评端则是在用户发出测评请求时负责测评并将结果发送给网页端的。
网页端
什么是网页端?顾名思义,就是管网页的部分。网页端又分为网页前端和网页后端。所谓网页前端,就是你打开浏览器能看到的那部分。不过浏览器是如何知道自己要显示什么东西的呢?这就是因为服务器上还有网页后端,会以 HTML 代码的形式告诉浏览器它要显示些什么。
下面我们来理一下你从输入网址到看到网页的全过程。首先,你输入了 UOJ 的网址,然后浏览器就会根据你的网址去找到对应的服务器,并发一个消息给该服务器。这种消息称为 HTTP 请求(HTTP Request)。当然了,这取决于你的网址是以什么开头的。如果是 https 开头那么就是个加了密的 HTTP 请求,称为 HTTPS 请求。一个 HTTP 请求报文会包含很多很多信息,例如你输入的网址啦,例如你的 IP 啦。
服务器收到 HTTP 请求之后,会根据请求的内容进行相应的处理,并生成一个 HTTP 响应(HTTP Response),然后发回给你的浏览器。HTTP 响应报文也会包括很多信息。比如会包括状态码,正常情况下是 200,但如果对应的网址上没有东西可显示,那么会返回一个喜闻乐见的 404。当然最重要的是 HTTP 响应报文里面会包含一份 HTML 代码,这份代码会指示你的浏览器如何渲染出一个完整的网页。只要你在浏览器里鼠标右键,点击审查元素,就能看到这份 HTML 代码了。实际上,HTML 代码中可能还会内嵌有其他的元素,例如内嵌一些 JavaScript 代码就可以做一些动态的页面效果,内嵌一些 CSS 代码就可以更好的控制网页的排版。
总而言之,想要理解网页端,就得理解两部分:第一,浏览器是如何把 HTML 代码变成你所看到的网页的;第二,服务器是如何根据 HTTP 请求报文生成 HTML 代码的。前者属于网页前端的范畴,而后者属于网页后端的范畴。
测评端
光有网页端是不够的,因为如果用户提交了一份代码,UOJ 就得负责评测,而这往往不是几秒钟内能完成的,所以不能用 HTTP 响应的方式快速返回结果。因此,UOJ 的网页端服务器得把该测评请求发给另一个被我们称之为测评端的服务器,让它测评之后将测评结果发回,再通过网页端告知用户。
UOJ 官网的网页端和测评端服务器是分别部署在两台机器上的,不过为了方便起见,在我们发布的这版 UOJ 里网页端和测评端是放在同一台机器上的。
测评端又分为负责通信的部分和(真正)负责测评的部分。顾名思义,负责通信的代码控制的是和网页端服务器的交互,而负责测评的代码则是给选手交过来的文件打分的,最后负责通信的部分会将测评结果发回网页端。
二、网页端代码简介
下面我们稍微展开讲一讲 UOJ 网页前端和网页后端分别是如何实现的。
网页后端
UOJ 使用了一款开源的网页服务器软件,叫作 Apache。Apache 支持使用 PHP 语言来实现网页后端。进入 UOJ 的 docker,网页后端的代码就位于 /opt/uoj/web/ 目录下。Apache 收到一个 HTTP 请求之后,会运行 index.php(这一行为其实是由同目录下的 .htaccess 指定的)。index.php 会加载所需的函数库和类库,然后根据路由文件(route file) 去给请求中的网址匹配用于生成响应报文的 PHP 代码。所谓路由文件其实也是 PHP 代码,只不过这种文件记录了一个从网址到代码文件路径的映射。控制主站路由的是 app/route.php,控制博客路由的是 app/controllers/subdomain/blog/route.php。
我们称路由文件中被映射到的代码为控制器(controller),位于 app/controllers 目录下。index.php 给请求中的网址匹配到对应的控制器后,控制器会根据用户的 HTTP 请求报文的具体内容生成一个 HTTP 响应报文,交由 Apache 负责发回给作出 HTTP 请求的浏览器。
早期 UOJ 的网页后端是我们一点一点自己写的,架构上没有参考任何的现有开源项目。后来我们发现有些架构的设计思路理不太清楚,于是去学习了一下著名的 Laravel 框架。所以代码架构实则是介于早期 UOJ 架构和 Laravel 之间的一种架构,没有像早期架构那么乱,也没用像专业的 Laravel 那样包装得很深,希望这样能兼顾代码的可维护性和可阅读性吧。
数据库
数据库是网页后端的一部分,但又是较为独立的一部分。通常我们在 OI 中会使用数组来存储数据,但一旦你把程序掐了,或者电脑关机了,数组里的内容就丢失了。这是因为数组通常是存储在内存里的,而非机械硬盘、固态硬盘等可持久存储的介质上。那么如何把数据保存在硬盘上呢?你可能已经开始摩拳擦掌准备写个 B 树了,但是且慢 —— 如今有很多数据库软件已经帮你写好了。
UOJ 使用的是 MySQL 数据库。在 UOJ 的后端 PHP 代码里,你可以通过访问 DB 这个静态类来跟数据库进行交互。交互使用的是 SQL 语言,一种专门为数据库中数据的查询、修改、插入、删除而设计的语言,你可能需要稍微学习一番这种特殊的语言。DB 是对 SQL 的一种简单包装,请参照现有代码调用 DB 的方式来更好的理解其用法。
网页前端
大部分的网页前端代码实际上是由 PHP 一行一行输出出来的。负责这种输出的代码一部分位于各个控制器代码中,一部分 app/views 目录下。理想情况下,如果一段 PHP 代码能生成一长段 HTML 代码,且生成的过程不包含任何控制逻辑或者数据库查询,那么该 PHP 代码就应该放在 app/views 里作为一种视图(view);否则就应该放在 app/controllers 下作为一种控制器。例如,每个页面都会包含的页头和页尾就写在了 app/views/page-header.php 和 app/views/page-footer.php 这两个视图文件里。当然了,严格遵守这样的规则会让一些简单的代码变得结构很复杂,所以目前的 UOJ 代码并没有严格遵守这一点。
其实,并非所有前端代码都是 PHP 一行行输出出来的。HTML 代码中通常会内嵌一些 CSS 和 JavaScript 来控制页面排版和制造一些动态页面效果,而这些部分往往是跟页面具体内容独立的。所以有很多优秀的前端框架,例如 Bootstrap。UOJ 目前使用的是 Bootstrap 3。所以,要想完全理解前端的代码,你不仅需要阅读对应的控制器和视图的内容,还要学习一下 Bootstrap 的使用方式。如果你尚未接触过前端开发,不妨先找点前端开发教程,试着做一点不需要 PHP 的静态小网站,然后再回过头来阅读 UOJ 的代码。
重要文件目录
网页端的代码和相关文件主要在 /opt/uoj/web/ 目录下。下面列出了一些重要的文件和子目录:
app/:UOJ 主要的后端 PHP 代码目录
controllers/:存放控制器文件的目录
locale/:存放页面上的文字在不同语言下的翻译的目录
models/:UOJ 运行所需的一些 PHP 类
storage/:存储一些文件数据的目录
submissions/:存放用户提交的测评请求中附带的文件的目录
tmp/:存放临时文件的目录
views/:存放视图文件的目录
route.php:主站路由文件
uoj-*-lib.php:一些 UOJ 运行所需的函数库文件
vendor/:一些 UOJ 使用的第三方 PHP 代码库,由 Composer 管理。
public/:一些可以不经过 PHP 直接访问的资源
css/:存放部分 CSS 代码文件的目录
fonts/:存放字体文件的目录
js/:存放部分 JavaScript 代码文件的目录
libs/:一些 UOJ 使用的前端库,例如 Bootstrap,jQuery 等等
pictures/:存放部分图片文件的目录