Kitura 配置小感
上回说到,环境基本配好,HTTP服务器使用NGINX,所有路由由Kitura自行调配。跑通官方示例项目后,就该正式做Ric的需求了。
当初接触Server Side Swift也是因为Ric突发奇想,希望在内网服务器上搭一个纯Swift的后台服务,从而实现从客户端到后台一门语言通吃的效果。实际上,安卓开发通过Java很早就能做到跨平台通吃的效果,不过Java的历史包袱比较沉重,之前和彭大哥做项目时候就曾了解到这一点。Windows平台上,C#似乎也有这方面的野心,但多年来UWP程序不温不火,轻量级应用又有Electron-Vue这样的前端框架异军突起,很难找到一款能够在Windows上完成原型并方便地跨平台的MSVC系工具。
一般来说,做什么工作就得选什么工具,工具不好用会浪费不少时间。这次配置编写这台服务器的服务时就体会到了这一点。原谅我水平不济,所有后台服务都想和Django比一比,比完后总会得出结论——Django真是好用啊!无论是数据库模型,还是默认的HTTP服务器,还是Python强大三方库社区的加持,还是验证管理会话安全等中间件……都非常适合对配置服务器或后台服务一知半解的小白——无需懂得太多,只需关注服务逻辑,就能很快搭出原型。想要用Kitura实现许多在Django里面半天就能搞定的功能,可算得上大费周章。
第一个服务 - Emoji Server
.
├── [1.6K] Package.swift
├── [4.0K] public
│ ├── [4.0K] css
│ │ ├── [5.1K] emoji.css
│ │ ├── [3.2K] emoji.css.map
│ │ ├── [ 17K] github-markdown.css
│ │ └── [1.6K] index.css
│ ├── [4.0K] img
│ │ ├── [ 49] blank.gif
│ │ ├── [722K] emoji_spritesheet_0.png
│ │ ├── [531K] emoji_spritesheet_1.png
│ │ ├── [956K] emoji_spritesheet_2.png
│ │ ├── [402K] emoji_spritesheet_3.png
│ │ ├── [531K] emoji_spritesheet_4.png
│ │ ├── [3.2K] IconsetSmiles_1x.png
│ │ ├── [6.8K] IconsetSmiles.png
│ │ ├── [ 13K] IconsetW_1x.png
│ │ ├── [ 26K] IconsetW.png
│ │ └── [5.5K] plusSign.jpg
│ └── [4.0K] js
│ ├── [126K] config.js
│ ├── [2.9K] emoji-picker.coffee
│ ├── [3.8K] emoji-picker.js
│ ├── [3.0K] emoji-picker.js.map
│ ├── [ 24K] jquery.emojiarea.js
│ └── [5.4K] util.js
├── [8.0K] README.md
├── [4.0K] Sources
│ ├── [4.0K] Application
│ │ ├── [2.9K] Application.swift
│ │ ├── [1.9K] InitializationError.swift
│ │ ├── [2.3K] Metrics.swift
│ │ ├── [4.0K] Models
│ │ │ ├── [5.4K] JournalEntry.swift
│ │ │ ├── [3.1K] Persistence.swift
│ │ │ └── [2.2K] UserAuth.swift
│ │ └── [4.0K] Routes
│ │ ├── [2.9K] EntryRoutes.swift
│ │ ├── [2.0K] HealthRoutes.swift
│ │ ├── [ 919] MarkdownRoutes.swift
│ │ ├── [ 543] NotFoundRoutes.swift
│ │ ├── [2.3K] UserRoutes.swift
│ │ └── [4.9K] WebClientRoutes.swift
│ └── [4.0K] EmojiJournalServer
│ └── [1.9K] main.swift
├── [4.0K] Tests
│ ├── [4.0K] ApplicationTests
│ │ └── [4.3K] RouteTests.swift
│ └── [ 102] LinuxMain.swift
└── [4.0K] Views
├── [4.0K] docs
│ ├── [ 252] 404.md
│ ├── [ 18K] 564.md
│ ├── [ 632] doc2.md
│ └── [2.5K] index.md
└── [4.3K] home.stencil
基于网上的教程,搭起了第一个服务进行测试。再次赞美Kitura,其结构设计的比较合理,MVC各个模块分割清晰,静态文件服务也很方便配置。第一个服务搭起来没有太大的困难,在解决了一些依赖的版本冲突和PostgreSQL安装配置问题后,整个服务运行的很良好。
Kitura自带的OpenAPI组件能够自动生成所有Codable Routes的API示例界面与测试功能。因此,所有基于RESTful的API全都能够在一个界面上看到和管理,非常方便,省去了用Postman一个个测试的麻烦。在此基础上,我又改进和新加了几个功能,从而完成了EmojiJournal使其真正能够使用。直到这时,我还觉得搭服务简直轻而易举,甚至一度认为Kitura跟Django的易用性差不多了。
高兴的太早了。
第二个服务- ECE564 Server
给Ric简单介绍完,他觉得看起来不错,于是我便着手开始搭建ECE564的服务器。这个服务的内容其实也很简单——CRUD数据库操作封装成RESTful API,加以简单的展示和管理界面,再加上一个文件传输存储服务便大功告成。实际上,日常大多数API服务器的工作也就是这些,如果没有高并发分布式等需求,大可以把这次的服务器当做一个范本,以后再起服务器时就不用费尽周折再配置了。在这里不得不再次说,Django真的非常方便。上述功能Django的默认组件基本都已经囊括,无需用户再去调研该引入哪些依赖。
API
Kitura的API可分为两种:Codable Route、Raw Route。
Raw Route顾名思义就是原始数据相关的API,相当于中间件仅将请求封装成一个对象而已,很多parse、decode的工作需要自行完成。在处理文件上传时,由于请求一般为form-data或者binary,所以需要用到原始路由。
Codable Route高度整合了Swift 4引入的Codable protocol,也继承了JSON的简洁与易用的特点,将较为简单的请求和响应均封装为JSON字符串进行传输。像查找数据库相关的操作,由于不需要传输二进制文件,因此可以全部写成Codable路由。另外,借助Swift的Completion Handler的概念,写起异步阻塞来轻而易举。或许是我才疏学浅,反正从正经下决心学习编程以来,Swift的线程相关的操作是最直观、封装程度最好、可预期性最强的语言。在写C/C++的socket时经常弄乱,最后把整个请求搞成全线性阻塞的导致性能极差,还有select以后处理不好,真的很纠结;写Python和Django时虽说有了请求中间件的封装方便了许多,但上次为了保持文件和数据库同步时先存再取改完再存的窒息操作仍历历在目……甚至直到大四上学期,我还搞不懂线程和进程、多线程和超线程、同步和异步这一大套之间的联系。直到现在,写了很多Swift之后,我才稍稍有了点得心应手的感觉,不得不赞美Swift简明清晰的语法。
写好API准备测试,遇到了前所未有的挑战——当我将人员条目的模型创建好数据库,POST完准备读取时,无论怎么尝试都是500.当时真是汗刷地一下就下来了。完全不合理啊,于是我左改又改,首先通过其他服务确定HTTP服务器没有问题,然后再看路由也没有走错,但始终无法解决问题。带着疑惑与不解,灰溜溜地十二点回家。
第二天重整旗鼓,梳理思路,先从最小可运行的实例开始慢慢往上加东西。当我在模型加入5个成员时,没有问题;当我加到8个,便再次500……我还以为每一行数据大小有限制呢,研究了半天PostgreSQL似乎真的有限制,但我实在不敢相信区区几个字符就能溢出。
然后开始从8个减到6个,依旧500,再减到5个,瞬间恢复。其间不知道DROP TABLE多少次,直到我看到struct里成员的命名才发现端倪:全小写时就能工作,加入camelCase的命名就炸……赶忙谷歌一搜,果然——pg列名称只能小写!
后来复盘时分析为什么花了将近6个小时才发现这个问题,原因大致有如下几个:
- 第一次学pg时用的是C++,C++约定的变量名称是snake的,一般不包含大写字母,因此没有被这个问题困扰过;
- 之后用Django时Python也是snake的,而且用的是SQLite,便以为所有数据库在这方面应该没啥区别;
- 示例程序里非数据库的struct中有使用camelCase的变量名,而存进数据库的模型中变量个数又很少,导致我想当然认为没有区别;
- 认为ORM在模型映射的过程中应该会代为解决这些问题,用户毋需关心。
总之,一个非常弱的小问题浪费了大量时间,令我很难过😢。
前端
为了使数据库展示较为美观,也是受了Django自带管理界面的影响,我决定也部署一个类似的数据库管理前端界面。
对于pg,官方推荐的自然是pgAdmin 4了。Mac上安装时自带就是这个工具,自然我也先尝试使用它。
安装好以后,在MobaXterm上输入pgadmin4
后,过了一会居然直接弹出一个Ubuntu FireFox的图形界面来,让我相当差异。之前见过的类似情景无非gedit跳出个编辑器,这一下搞出个浏览器,让我重新认识了terminal的功能。但是实在不好用,而且在Mac上不知道为什么ssh -X
并不能实现类似的效果。
尝试将pgadmin设置成headless的,无果;以pgadmin server mode
/pgadmin remote
查询了一大堆帖子,还是设置不好通过HTTP服务从外部访问。甚至还有一种说法在某个版本前,pgadmin必须代理在域名根目录下才能运行。鉴于pgadmin的界面实在不好用,遂弃之。
pgadmin在我看来就是web1.0时代人们对互联网服务的理解——桌面应用的翻版,功能庞杂而缺少设计感,大量的文字和元素充斥着页面,想看一下数据库中某张表的数据需要点击十余次鼠标,或在键盘输入SQL命令才能实现。因此只得放弃这个方案。
接着在GitHub一搜,有个叫pgweb的项目星很多。部署了一下一测试,意外的好用!一个界面,一次登录,查看所有数据库中所有的表;不提供鼠标编辑,但可以执行SQL;表结构、约束关系、历史记录、连接状态一应俱全,非常紧凑。一下子就决定用它了。
唯一的问题是Windows端必须得开cmd输入正确参数才能连上数据库,没有默认的WebUI,稍显不足。
为了不重复造轮子,就没有做前端的增加数据功能。毕竟这个服务器是给学生在客户端发请求用的,再搞个网页端的编辑功能有些画蛇添足。况且,我不太会搞前端的设计,尤其是js那一大套……
做了个卡片式的条目展示,这时候RESTful API的优势就显现出来了——ajax发个REST请求,直接就返回JSON,在页面上更新,逻辑非常清晰。但是改CSS和js还是费了不少劲,由于对于前端不太了解,想实现任何功能都要去查如何调库,费了半天劲才写出二三十行代码的感觉很不爽。
匿名请求
最初我是想在访问数据库的过程中加入登录验证的。但后来一想初学者可能弄不清这些复杂的逻辑,遂重新写了匿名请求的API。同时,还怕他们搞不清PUT和POST的区别,于是把PUT的功能也揉进POST里面了。希望我多费点儿时间能让他们少点纠结吧。
如何厘清匿名时哪些数据可写可改可删,哪些数据可查还是有点复杂。最后我设计的是
- 所有数据可通过id查询,通过netID进行过滤,或者直接查询所有数据;
- 所有匿名上传数据可通过id匿名删除和修改
- 可匿名增加数据,在数据库里上传者的字段设为一个统一的匿名用户,如果增加时id与已有条目重复则变为修改;如果违反netID的UNIQUE约束则禁止增加。
- 登录后的用户可以任意操作本账号对应的条目
如此一来,登录和匿名的范围就基本分清了。
头像图片文件上传
对于文件上传,最初我是有些纠结的。去年当我初学时,Ric的设计是一切基于JSON,所有都是字符。这样一来,上传文件就变成了先把文件数据转为base64,然后填到JSON字段里POST。虽说这样避免了表单操作,但却增添了后台的难度。究竟应该把这个base64存在数据库里,还是解码后存成文件?返回响应时是把二进制文件传回去,还是再转为base64传回去,还是直接返回一个链接?
把二进制文件存在数据库里的坏处就在于无端增大了每个条目的大小,且使条目占用空间不可控,必须的按照最大的文件分配。去年采用的就是这样的方案,每次从服务器GET的时候都要等三五秒,体验不太好,因此这次轮到我设计时想做一些改变。
一般而言,图片文件等二进制文件都该存在静态文件区,数据库中只存相对路径。请求时返回路径就行。这次我也是如此实现的。
struct Avatar : Model {
var id: String?
var netid: String
var path: String
var size: Int // size in bytes
var md5: String
}
app.router.all("/avatar", middleware: BodyParser())
guard let multipartBody: [Part] = request.body?.asMultiPart else {
next()
try response.status(.noContent).end()
return
}
通过Kitura自带的中间件分析请求,解码为multipart/form-data类型,然后将其中字段读出。md5校验后将文件存储到静态文件目录,并计算文件大小,最终存入数据库。对于重复处理,我的标准是基于netID进行判断——如果不存在则为创建;如果存在则为更新。理论上来说,每次新增或者更新前应该先把之前的文件删掉,但为了图省事暂时直接覆盖。这样会引入一个问题,就是对于同一个netID如果用户上传与上次图片文件类型不同,数据库中现有文件将无法追踪,只能手动删除。不过因为应用规模很小,到时手动清理或许比先删再加处理错误更简单。
经过测试,文件上传一切正常,整个项目也终于成了气候。下周一看看Ric怎么说吧。
示例文件编写
为了做得稍微圆满一些,我给所有需要用到的API编写了示例代码,GET/POST/PUT/DELETE分别存在playground文件中。
在写参考代码时才发现playground的window机制竟然也很hacky——是用一个iPad simulator精简后实现的。当代码崩溃时,能够看到一个iPad模拟器满桌面全是没有图标的应用😂。同时估计也是相同原因,playground的window还写死为768x1024的大小,导致许多和UI有关的测试都不太方便。Playground还是适合作为一个类似于Jupyter Notebook的工具来使用。
总结
这篇文章特地没有涉及许多技术细节,因为这次写的代码自我感觉非常良好,代码的“自明性”很高,直接看代码就能读懂逻辑,用不着记录太多细节。希望以后翻到这篇文章时不要被打脸🤣。
总体来说,搭建一个常规的后台服务,在2019年已经不是什么难事。几乎任何流行的主流互联网语言都具备这种能力,而且造好的轮子很多,只需自己实现“胶合”代码、业务逻辑即可快速建立原型。这在Web1.0时代是难以想象的。跟彭哥讨论项目时,他讲过当初在IBM做JavaEE时配置环境、部署应用是一件非常重量级的任务。几十兆的jar包搭配着WSGI、xml、SOAP等初代互联网协议栈,以及缺乏趁手的版本管理工具、API管理工具等困难,诸多因素混合在一起导致传统应用十分臃肿而错综复杂。而如今,对于Kitura、对于Django,只需一杯茶的功夫,一个后台服务就能冉冉升起,真让我觉得赶上了学编程的好时机。
20190628 Tree
.
├── [9.2K] Package.resolved
├── [1.6K] Package.swift
├── [4.0K] public
│ ├── [4.0K] avatars
│ │ ├── [ 11K] ece564.png
│ │ └── [ 67K] tc233.png
│ ├── [4.0K] css
│ │ ├── [5.1K] emoji.css
│ │ ├── [ 17K] github-markdown.css
│ │ └── [1.7K] index.css
│ ├── [4.0K] img
│ │ ├── [ 49] blank.gif
│ │ ├── [3.2K] IconsetSmiles_1x.png
│ │ ├── [6.8K] IconsetSmiles.png
│ │ ├── [ 13K] IconsetW_1x.png
│ │ └── [ 26K] IconsetW.png
│ └── [4.0K] js
│ └── [5.4K] util.js
├── [4.0K] Sources
│ ├── [4.0K] Application
│ │ ├── [1.2K] Application.swift
│ │ ├── [ 247] InitializationError.swift
│ │ ├── [ 717] Metrics.swift
│ │ ├── [4.0K] Models
│ │ │ ├── [ 265] DukePersonAvatar.swift
│ │ │ ├── [9.7K] DukePersonEntry.swift
│ │ │ ├── [2.3K] Persistence.swift
│ │ │ └── [ 870] UserAuth.swift
│ │ ├── [4.0K] Routes
│ │ │ ├── [5.5K] AnonymousRoutes.swift
│ │ │ ├── [ 11K] AvatarRoutes.swift
│ │ │ ├── [1.7K] EntryRoutes.swift
│ │ │ ├── [ 390] HealthRoutes.swift
│ │ │ ├── [ 919] MarkdownRoutes.swift
│ │ │ ├── [ 543] NotFoundRoutes.swift
│ │ │ ├── [ 713] UserRoutes.swift
│ │ │ └── [3.8K] WebClientRoutes.swift
│ │ └── [1.6K] Utils.swift
│ └── [4.0K] ECE564Server
│ └── [ 227] main.swift
├── [4.0K] Tests
│ ├── [4.0K] ApplicationTests
│ │ └── [4.3K] RouteTests.swift
│ └── [ 102] LinuxMain.swift
└── [4.0K] Views
├── [4.0K] docs
│ ├── [ 367] 404.md
│ └── [2.1K] index.md
└── [5.3K] home.stencil
14 directories, 35 files
参考链接
-
PQ field limitations need to serialize array into string due to ORM limitations
Leave a Comment