HTTP方法的深度探源与RESTful API的设计思想

即使写程序很多年的老鸟,能真正说清楚道明白HTTP方法的也不多,多数都只会使用而不知道所以然。每个人都知道使用GET、POST方法,使用也很熟练,但是底层设计思想并不一定了解。这里,来认真梳理一下HTTP方法背后设计的逻辑。

一. HTTP方法常见10种

HTTP 标准里常见的10 个:

方法主要作用
GET获取资源
POST提交数据 / 创建资源
PUT完整替换资源
PATCH部分修改资源
DELETE删除资源
HEAD获取响应头,不返回正文
OPTIONS查询服务器支持哪些方法/能力
CONNECT建立隧道
TRACE请求回显,用于诊断
QUERY用于查询搜索(2026年RFC10008新增)

(实际上HTTP方法数量多达几十种,这里只是列举常见的方法,详见文末)

二.HTTP方法两个核心设计理念:安全(Safe) 与 幂等(Idempotent)


注意:以上的标准是2014年颁布的RFC 7231,QUETY标准出自2026颁布的RFC 10008

FastAPI + HarfBuzz + Redis实现动态字体子集化实战

字形编译引擎(Text Shaping Engine)当今的绝对王者HarfBuzz,拥有极强性能,Android、Chromium、Firefox等主流应用底层都在用它。HarfBuzz有一个hb-subset 子集化模块,是专业的字体子集化引擎:

当你给它一段文本(比如 "动态字体子集化")和原字体时,HarfBuzz 会:
自动分析这几个汉字对应的 Glyph ID。
保留这几个字所需的矢量描边数据。
剔除其余上万个未用到的字形数据
重构并修复 .ttf 文件的 OpenType 内部索引表,确保输出的小字体符合国际标准,能在浏览器中被完美加载。

直观了解:HarfBuzz是做什么的

官方是这样介绍的:


harfbuzz-world.cc is a single-file build of HarfBuzz that drops into your own C or C++ project without any build-system glue, plus a live playground for it right here in your browser:

harfbuzz-world.cc 提供了一个单文件版的 HarfBuzz(HarfBuzz 整个库整合成一个可以直接编译的 .cc 文件。),可以直接放进你自己的 C/C++ 项目中,无需额外处理构建系统;同时,它还在浏览器中提供了一个可以实时操作的 HarfBuzz 实验场。


shape — the shaped glyph stream as a table, alongside an SVG preview.(字形整形 / 字形塑形)
subset — produce a font subset for the current text and download it.(子集化)
raster — pixel-perfect BGRA rendering at any size, blitted to a canvas.(光栅化/位图化)
vector — the same shaped text rendered to SVG and downloadable PDF.
gpu — slug-based GPU rendering via WebGL2.(GPU 渲染)

官网地址:https://harfbuzz-world.cc/



OpenType(包括排版规则、连字、变形、字形定位表等)也是一套极其复杂的标准规范,而 HarfBuzz 就是这套标准在全世界最权威、最完美的执行引擎(“编译器/解析引擎”)。 很多人以为:计算机显示文本 = 查字典(给一个字符编码,找到对应的字体形状画出来)。 如果全世界只有英文字母(A-Z),这确实很简单。但在现实世界中,文字的显示极其复杂:

—— 复杂连字(Ligatures)与形态变化:阿拉伯文同一个字母,在词首、词中、词尾、单独存在时,形状完全不同,而且必须连笔。印地文/泰文: 辅音和元音组合时,元音符号可能会跑到辅音的上方、下方甚至左边。英文在优雅的排版中,f 和 i 连着写时,f 的横线和 i 的点会融合成一个整体(fi 连字)。

—— 字形(Glyph)与字符(Character)不是一回事。字符(Unicode):是逻辑上的编码(如 U+0061 代表 a)。字形(Glyph):是字体文件(.ttf)中实际绘制出来的矢量图形。一个字符可能对应多个字形,多个字符也可能合成一个字形。

HarfBuzz 的核心工作,这串文本在当前字体下,应该使用字体库里的哪几个字形(Glyph ID)?每个字形应该放在什么坐标位置?字形之间的间距(Kerning)是多少?

说得更直白一些,HarfBuzz就是把字体规则给完美呈现出来,就像浏览器,要把庞大复杂的HTML/CSS/Javascript这些规则给视觉实现,HarfBuzz就是字体界的浏览器。

由于项目需要,我们需要做动态字体子集化,HarfBuzz是不二之选。我们采用的是Python FastAPI + HarfBuzz + Redis+Nginx+Vue3+pdf-lib前后端技术栈来实现。以下的记录是其中一些操作要点。

字体子集化优化在线动态渲染PDF

最近做了一个在线生成PDF,适时预览并可在线打印的系统。使用了pdf-lib.js(https://pdf-lib.js.org),作为前端创建PDF的框架,虽然很方便,但遇到一个棘手的问题:如何解决中文显示。

PDF文件不同于web文件(HTML+CSS),它的字体是嵌入文件的,而由于安全性和PDF文件本身机制的问题,并不能读取本地字体文件。而国际PDF阅读器规范中的 “Standard 14 Fonts”(14 种标准字体)中并没有中文字体,这就导致了在不嵌入中文字体的情况下,出现中文就会无法渲染。

Standard 14 Fonts

内置的 14 种标准字体 这 14 种字体全部属于 拉丁字母(英文) 系列,分为四大类:
1.无衬线 (Sans Serif)字体: Helvetica, Helvetica-Bold, Helvetica-Oblique, Helvetica-BoldOblique
2.衬线 (Serif/Roman) 字体:Times-Roman, Times-Bold, Times-Italic, Times-BoldItalic
3.等宽 (Monospace)字体: Courier, Courier-Bold, Courier-Oblique, Courier-BoldOblique
4.符号 (Symbol) :Symbol, ZapfDingbats

PDF 的设计初衷是 “所见即所得” (WYSIWYG)。它的目标是:不管你把这个文件发给谁,在手机上、打印机上还是 20 年后的电脑上,看到的排版必须一模一样。

封闭性:PDF 不信任阅读者的系统。它认为:“万一对方电脑里没有微软雅黑,我的排版不就乱了吗?”

机制:为了保证绝对一致,pdf-lib 必须执行 “字体嵌入 (Embedding)”。它需要把字体文件的每一个二进制数据“缝进” PDF 文件里。

面临的难点

这个系统需要兼容正体中文(繁体)和简体中文,而这种字体体量大得惊人。兼容性最好的Adobe全字符体——Super OTC 体积高达100M+,师出同门的Google Noto也差不多这个体量,如放到Web项目,简直是灾难,浏览器瞬间会被卡死。

于是只能放弃使用这种大字符集的字体,选择性地支持正体或简体。

但是单独的简体字体,体积也很大,思源Source Han Serif,单独一种字重的字体,也高达20M+,Noto也要10M-20M。如果要加载这么大的字体,配置一般的电脑尤其是内存小的电脑,会频繁出现假死的情况。

于是,只能想办法减少字体的体积。

通过万能的Gemini,终于有了解决办法:字体子集化。

说白了,就是从原字体中提取一部分字符,只要能覆盖自己常用的字符就行了。

方案:

1. 用集成工具做子集化。这是最常用的方式,工具例如,Fontmin(https://ecomfe.github.io/fontmin/)。

这种方式就是要准备“字体白名单”,也就是你要把那些你需要的字符一一列出来,让它把你需要的字符从庞大字库中“抠”出来。

可以到GitHub上下载常用中文字符3500字等整理好的字符集,导入Fontmin中。也可以自己整理用到的文字再加进去。

2.用 Python 做字体子集化。

#安装fonttools库
pip install fonttools brotli

#命令执行子集化
pyftsubset original.ttf --text-file=chars.txt --output-file=sub.woff2 --flavor=woff2
'''
其中
original.ttf:原始字体,如思源宋体
--text-file:字符白名单文件
--output-file输出字体
'''

#不指定字符白名单,只按照某个标准来子集化(如变成GB2312标准字符集)
pyftsubset NotoSerifSC-Medium.ttf --unicodes="U+0020-007E,U+00A0-00FF,U+2000-206F,U+3000-303F,U+4E00-9FA5" --output-file=label_gb2312.ttf --layout-features='*'

3.在线动态字体子集化

① Node.js 生态方案:Fontmin,基于 fonteditor-core,支持 TTF、EOT、WOFF、WOFF2 之间的转换。它支持插件机制,可以方便地动态剪裁。示例代码:

const Fontmin = require('fontmin');

const fontmin = new Fontmin()
    .src('fonts/SourceHanSansCN-Regular.ttf') // 原始 15MB 字体
    .use(Fontmin.glyph({
        text: '张三的PDF发票金额: ¥100.00' // 前端传过来的动态文本
    }))
    .use(Fontmin.ttf2woff2()) // 转换为更小的 woff2 格式
    .dest('dist/fonts');

fontmin.run((err, files) => {
    // 这里的 files[0].contents 就是剪裁后只有几 KB 的字体二进制流
});

Firebase JWT库Token验证的使用

项目中使用Firebase JWT库做Token验证,确实很好用,减少了很多繁琐的判断,不过要使用它自带的验证方法,命名就必须符合它的标准。

如,起始时间,过期时间等都需要按照它的标准命名:

$tokenData = [
'iat' => $issuedAt, //Token核发时间,标准写法,名称固定为 iat
'exp' => $expirationTime, //Token过期时间 ,标准写法,名称固定为 exp
];

验证非常方便,只需要调用相应的异常结果就可以了,不必自己拆解出来做复杂的判断。

 

try {
                $decoded = JWT::decode($token, new Key($this->secretKey, 'HS256'));
                $decodeToken = (array)$decoded;
            } catch (ExpiredException $e) {//过期异常
                $message = 'Token has expired';
                $outData = ['code' => 401, 'message' => $message];
                return json($outData);
            } catch (SignatureInvalidException $e) { //签名不正确,被篡改
                 $message = 'Invalid token signature';
                $outData = ['code' => 401, 'message' => $message];
                return json($outData);
            } catch (Exception $e) { //格式不符或者被篡改
                $message = 'Token validation failed';
                $message = 'Verify Fail: permission denied';
                $outData = ['code' => 401, 'message' => $message];
                return json($outData);
            }


 

 

 

使用Fabric的一些心得

最近项目中使用Fabric.js做图像动态标记,被有些小问题缠住,花了很多时间去琢磨,有些甚至弄了一两天才理明白,特此记录。

1.canvas的width、height与style.width、style.height须一致,否则画出图形会变形或只显示图像的一部分。

canvasRef.value.width = img.naturalWidth;
canvasRef.value.height = img.naturalHeight;
canvasRef.value.style.width = img.naturalWidth + 'px';
canvasRef.value.style.height = img.naturalHeight + 'px';

看资料,fabric的画布实际上有三层(下图来自:jb51)

2.可以创建离屏/静态画布,不必在页面中显示,整体绘制完毕后再通过图像对象输出。

 

const canvas = new fabric.StaticCanvas(null, {
width: originWidth,
height: originHeight
});

离屏画布的节点引用设置为null,一样可以设置宽高。

3. 如果有背景图片,所有创建的元素都需要在创建背景图片的函数体内完成,不能在函数体外创建,否则导出的图片将背景不可见。就是这个问题,我困扰了两天,开始以为是跨域问题,背景图片格式的问题等等,后来发现原因在此。

nextTick(
      () => {
        canvasRef.value.width = img.naturalWidth;
        canvasRef.value.height = img.naturalHeight;
        canvasRef.value.style.width = img.naturalWidth + 'px';
        canvasRef.value.style.height = img.naturalHeight + 'px';
        let currentCanvas = new fabric.Canvas(canvasRef.value, {
          selectionColor: '#4fb8d3',
          selectionLineWidth: 0
        });

        fabric.Image.fromURL(imgUrl, (img) => {  //必须将绘制的文本和图形放到这个背景图函数体内,否则无法显示背景图
          currentCanvas.setBackgroundImage(
            img,
            currentCanvas.renderAll.bind(currentCanvas)
          );
          currentCanvas.backgroundImage.scaleToWidth(currentCanvas.width);
          currentCanvas.backgroundImage.scaleToHeight(currentCanvas.height);
          for (let key in LocationObj) {
            let locationNum = LocationObj[key][0].bbox;
            let arr = locationNum.split(',');
            let numberArray = arr.map(Number);
            let rect = new fabric.Rect({
              left: numberArray[0],
              top: numberArray[1],
              width: numberArray[2] - numberArray[0],
              height: numberArray[3] - numberArray[1],
              fill: 'transparent',
              stroke: 'rgb(235,64,64,0.9)',
              strokeWidth: 3,
              strokeDashArray: [5, 5]
            });
            let text = new fabric.Text(
              ' ' +
                messageMap.value[key] +
                ' - ' +
                Number(LocationObj[key][0].conf) * 100 +
                '%' +
                ' ',
              {
                left: numberArray[0], // 文本的X坐标  
                top: numberArray[1] - 22,
                fontSize: 22, // 字体大小
                textBackgroundColor: '#eb4040', // 文字背景颜色
                textBackgroundColor: 'rgb(235,64,64,0.8)',
                hasControls: false, // 不显示控件
                hasBorders: false, // 不显示边框
                fontWeight: 'normal',
                fill: 'white',
                padding: 20,
                lineHeight: 3,
                textAlign: 'right',
                fontFamily: 'Arial,Microsoft YaHei'
              }
            );
            currentCanvas.add(rect);
            currentCanvas.add(text);
            currentCanvas.renderAll();  
            // rect.set('selectable', false);
            // text.set('selectable', false);
          }

          downURL.value = currentCanvas.toDataURL({
            format: 'png',
            quality: 0.9
          });
          downLink.value.download = 'X_RayImg.png';
        });
      },
      { crossOrigin: 'Anonymous' }
    );

 

 

 

OracleCloud的VPS问题汇总

Oracle的云服务较之国内有很多便利,也没那么多套路,但新上手还是有很多不顺之处。

1.OracleCloud创建的主机,是不能自己配置公网IP的,也看不到绑定公网IP的网卡,只能看到一块ens3的默认网卡,绑定的是内网IP。

[root@instance-heiry ~]# [root@instance-heiry ~]# ifconfig -a
ens3: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 9000
        inet 10.0.0.86  netmask 255.255.255.0  broadcast 10.0.0.255
        inet6 fe80::17ff:fe00:7ee5  prefixlen 64  scopeid 0x20<link>
        ether 02:00:17:00:7e:e5  txqueuelen 1000  (Ethernet)
        RX packets 42717  bytes 79644892 (75.9 MiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 39782  bytes 17319192 (16.5 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

2.默认以opc+key登录,root被禁用,每次SSH之后都要sudo -i 才能执行相应操作,非常麻烦,需要手动更改:

sudo -i
sudo passwd root
Changing password for user root.
New password:
#打开PasswordAuthentication及 PermitRootLogin功能
vim /etc/ssh/sshd_config
#把值设为yes,去掉注释
PasswordAuthentication yes
PermitRootLogin yes
#重启SSH服务
service sshd restart

3.默认有security list 控制入站规则,除了在服务器设定开放端口外, security list 也需要设置Ingress Rules才能访问。

4.使用ubuntu 系统默认装 iptables-persistent,需要手动设置。centOS8下安装mysql8.0,出现莫名其妙的问题, security list开放了3306访问,服务器防火墙也开启了3306,mysql服务器权限都设置了,但是在启用服务器防火墙情况下,始终无法远程访问mysql服务器,如关闭,则可以访问。即使mysqld服务占用的3306/33060端口都开放,监听了所有IP,仍然无法访问。

发现3306端口只是在TCP6下监听,于是将

vim /etc/mysql/my.cnf 
#加上参数
bind-address = 0.0.0.0

设置IPv4转发状态

sysctl net.ipv4.ip_forward
vim /etc/sysctl.conf
#添加以下参数
net.ipv4.ip_forward = 1

发现,3306转到tcp下监听,可是打开防火墙,远程依然是被blocked状态。

 

vim高频实用命令整理

进入编辑/输入模式

快捷命令 功能描述
i 在当前光标所在位置插入随后输入的文本,光标后的文本相应向右移动
I 在光标所在行的行首插入随后输入的文本,行首是该行的第一个非空白字符,相当于光标移动到行首执行 i 命令
o 在光标所在行的下面插入新的一行。光标停在空行首,等待输入文本
O 在光标所在行的上面插入新的一行。光标停在空行的行首,等待输入文本
a 在当前光标所在位置之后插入随后输入的文本
A 在光标所在行的行尾插入随后输入的文本,相当于光标移动到行尾再执行 a 命令

光标移动操作

快捷命令 功能描述
w 向前移动,光标停留在下个单词词首,每次一个单词(单词中不含其他字符如:- \ = +等)在当前光标所在位置插入随后输入的文本,光标后的文本相应向右移动
nw 向后移动n个word(n可以是任意整数数字,如2w)后续W、b、B、e、E都可这样使用
W 向前移动,光标停留在下个单词词首,每次一个字符串(字符串可以含有其字符如:- \ = +等,is-a word/and/bog 这种都是字串)
b 向后移动,光标停留在前个单词词首,每次一个单词
B 向后移动,光标停留在前个单词词首,每次一个字符串在光标所在行的上面插入新的一行。光标停在空行的行首,等待输入文本
e 向前移动,光标停留在前个单词词尾,每次一个单词
E 向前移动,光标停留在前个单词词尾,每次一个字符串在光标所在行的行尾插入随后输入的文本,相当于光标移动到行尾再执行 a 命令
Home 快速移动光标至当前行首
End 快速移动光标至当前行尾
:n 移动到第n行,如,:10可快速跳转到10行
gg 移动到文本开始处
G 移动到文本末尾处
nG 光标移动到第 n 行,n 为数字
0 或 ^ 移动至当前行的行首
$ 移动至当前行的行尾
n$ 移动至当前行只有 n 行的行尾,n为数字
fx 移动至当前行中下一个 x 字符处,如,fe,则移动到下一个e字母处
Fx 移动至当前行中上一个 x 字符处,如,fe,则移动到下上个e字母处
nj 向下移动n行,如,10j表示向下移动10行
ctrl+b 向上翻页
ctrl+f 向下翻页

撤销操作

快捷命令 功能描述
u 撤销上一次的操作
U 行撤销,撤销所有在前一个编辑行上的操作(行恢复)。

查找操作

快捷命令 功能描述
/str 在后面的文本中查找str。
?str 在前面的文本中查找str。
n 向后查找下一个。
N 向前查找下一个。

复制粘贴

快捷命令 功能描述
yy 单行复制
dd 单行删除(剪切)
p 粘贴
按esc键后,先按gg(到达顶部),然后dG 全部删除
按esc键后,先按gg,然后ggyG 全部复制
n 向后查找下一个。
N 向前查找下一个。