HTTP WEB基本功

我发觉很多程序员,即便是有很多年经验的,也会对HTTP基本原理的理解不够透彻,特别明显的是对资源的理解。具体问题体现在服务器中的临时文件的处理。以下让我描述一下某些错误观点。

错误一:某些动态生成的资源(如PDF,验证码图片等等),需要写在文件系统中,而该目录需要在应用服务器中的某个公开目录,才可以让用户访问

我们以前做的JSP,放在webapp目录中的文件都是可以直接访问的(或ASP项目中的目录也是可以访问的)。这个概念一致根深蒂固在每一个程序员中:要让用户访问,必须有一个实际文件存在,所以在项目需求中一旦出现某些动态生成的东西,用浏览器访问的模式(如下载),程序员第一反应就是:写一个临时文件在应用服务器中。

这个做法有几个问题:

  1. 安全性问题:应用服务器中,代码和临时生成的资源混杂了,会导致一定风险。通常应用服务器的权限很低,对部署了的应用目录,不可以作修改,以免造成黑客的攻击点。但如果你要在应用服务中的某个目录写入文件,代表需要有写入权限,违背了【代码应该和运行数据隔离】这个基本原则
  2. 删除临时文件问题:当临时文件有一定量的时候,我们需要想办法删掉他们,以保持系统的稳定性。删除临时文件导致系统额外的运维工作量。如果有自动删除的程序策略,也无形增加了一些功能代码。假如忘记处理,文件越来越多,导致系统资源耗尽
  3. 额外消耗:当你写了一个临时文件,通常下一步就是做个【重定向】到该文件地址,让浏览器下载。这重定向让访问多了一个往返,造成额外消耗
  4. 新环境或集群部署问题:生成的文件是在本地文件系统中,在集群服务器中,这需要做特殊考虑,要么做共享目录,要么做同步,都是麻烦事。而就算跑单机,如果我们升级应用,这个特殊目录也得做保护,以免升级过程中造成【文件丢失】

错误二:服务代码运行时所产生的数据结果,只能是JSON、XML、HTML等类型,其他如PDF,Excel等复杂一点的东西,需要写一个文件在某个应用服务器开放的某个目录中

此问题是第一个问题的详细展开版,因为一般程序员在整个学习过程中,都只认为Servlet/JSP/ASP等的结果都是HTML,而并没有体会到这些动态的HTML,其实也是一个资源文件。所以偶尔遇到一个需求需要产生PDF的话,可能使用了PDFBox生成一个PDF文件,就完成了任务了。

错误三:服务器生成PDF、Excel等比较复杂的结果,需要比较长时间和耗费资源,所以要写一个本地文件存下来

这个观点的逻辑性不太严格,因为是否要存下来取决于有没有这个需要,和耗费资源多少没有直接关系。如果觉得花那么多资源来生成一个东西,不存下来可惜,那只是一种个人愿望。

纠正概念

入正题:一个HTTP请求,比如GET,所代表的的是取服务器中的某个资源,而这个资源是按其【路径】区分。

比如:http://test.server.com/order/listing

意思是在服务器 test.server.com 中,取出资源名称叫 “/order/listing” 的东西来。至于取出来的东西是什么类型,我们暂时不知道,直到服务器响应了这个请求,返回 Content-Type 这个HTTP头信息为止。

转化为协议层的交互,大致会是这么一回事:

浏览器请求:

GET /order/listing HTTP/1.1
Host: test.server.com

服务器返回

200 OK
Content-Type: text/html
Content-Length: 66

<html>
<body>
<h1>Hello</h1>
<p>Order Listing</p>
</body>
</html>

通常这个服务器的返回结果怎样产生的呢?

如果是Servlet/JSP,你得在web.xml中定义一个路径 /order/listing, 并写一个Servlet返回一个JSP运行的结果。

应用服务器再把问题简化一下,如果你请求的路径是 /order/listing.jsp 的话(.jsp 后缀),在服务器中的应用主目录下的 /order/listing.jsp 会被编译和执行,并输出结果。

以上说的都是服务器如何实现,目的是为返回 200 OK 之后的结果,包括HTTP返回头部,和内容体。

所以,如果我们能按需要返回结果,包括返回状态,HTTP头信息和内容,我们就可以和浏览器正常交互,而不用关注到底服务器是否真的有这个本地文件存在。因为服务器和浏览器真正的交互的,只会按照HTTP协议,而浏览器也不管服务器是不是真正物理上存在这个文件资源

再举个例子:

一个图片地址:http://test.server.com/company/logo.png

浏览器请求:

GET /company/logo.png HTTP/1.1
Host: test.server.com

服务器返回

200 OK
Content-Type: image/png
Content-Length: 8902

...PNG格式的数据,字节数为8902...

一般实现就是把logo.png这个文件丢在/company目录下。

如果我们按节假日来替换这个logo呢?在假日时替换这个 logo.png 文件吗?那我假日需要加班,就是为了替换这个Logo吗?

如果用JAX-RS实现,我们需要绑定这个路径 /company/logo.png,为它写一个方法,返回一个Response。类似这样:

@Path("/company")
public class Company {

    @GET
    @Path("/logo.png")
    public Response logo() {
        if (checkHoliday()) {
            return Response.ok(getImage("holiday")).type("image/png").build();
        } else {
            return Response.ok(getImage("normal")).type("image/png").build();
        }
    }

    private byte[] getImage(String type) {
        // 返回相应的logo字节码
    }

    private boolean checkHoliday() {
        // 判断今天是否节假日
    }
}

如果你还没体会这个思维转变的好处的话,再来一个例子:

假如我们需要生成一个订单的打印PDF格式,我们大概会这样设计我们的URL路径:

http://test.server.com/order/{orderId}/pdf

例如:

http://test.server.com/order/SO1806309991/pdf

这个路径说明了几点:

  1. PDF资源是直接在服务器中取出来的
  2. 路径不关心生成PDF的过程,因为此URL路径只表达了:我想要订单SO1806309991的PDF
  3. 符合REST规范
  4. 先设计路径,再想办法如何把东西搞出来,符合结果导向的原则(如 TDD)

有人会把【生成PDF】看的很重,把生成PDF设计成一个POST请求:

http://test.server.com/order/SO1806309991/generatePDF (POST请求)

这样的设计有几个问题:

  1. POST目的是改变系统状态,虽然你生成了一个PDF文件,如果实际上没对业务数据发生改变的话(如订单状态变更),不应该用POST请求
  2. 路径用了动词,并不符合REST规范
  3. 生成PDF变成重点,而并非取PDF,所以程序员有足够的诱惑生成文件并存下来

这样就导致了本文初所说的问题,因此不建议大家这样设计。

如何实现 http://test.server.com/order/{orderId}/pdf 输出PDF呢。
方法就如上面生成 logo.png 例子差不多,只不过采用别的工具生成PDF的字节码而已,同时改一下返回的 Content-Type 类型。这里不展开具体内容的字节码的生成方式,也不是本文的目的。

总结

本文主要想纠正HTTP服务端编程的一个基本概念:资源

服务器中的资源应该是由服务端全权控制的,采用本地文件只是一种方式,而很多时候并非是最佳的方式。而在浏览器的角度,你只要把资源的内容传递出来就是了,服务器怎样架构这个内容,是按需求由服务端的自主设计。大家别被某些应用服务器的常用处理方式蒙蔽了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值