乍听之下,不无道理;仔细揣摩,胡说八道

0%

最近用Common Lisp开发一个个人项目,需要记录发出的HTTP请求的参数,包括了目标地址、HTTP body,以及HTTP头部等多种信息。为了可以结构化地存储这些数据(比如HTTP头部是由多个键值对组成的),我选择将它们保存到MongoDB中。Google一番后,我找到了cl-mongo这个库,可以在Common Lisp中读写MongoDB,尝试了一下也确实可以满足自己的需求。为了方便自己查阅,也为了方便有相同需求的人了解如何使用cl-mongo,于是写了这篇文章。

首先使用CL-MONGO:MONGO函数连接MongoDB的服务器程序(mongod)。因为在我的系统上mongod进程监听的是27017这个端口,并且我希望使用的数据库名为test,因此键入如下代码来连接数据库

1
2
3
(cl-mongo:mongo :db "test"
:host "127.0.0.1"
:port 27017)

连接上了数据库后,首先尝试往其中写入一个文档。假设现在要记录的是发出的HTTP请求的信息,那么一个可以写入的基本信息就是请求的目标地址。假设在命令行的mongo shell中输入的内容如下

1
db.http_request.insert({uri: 'http://example.com'});

那么使用cl-mongo提供的DB.INSERT函数达到上述效果的代码如下

1
2
(cl-mongo:db.insert "http_request"
(cl-mongo:kv "uri" "http://example.com"))

求值上述代码后返回值为NIL。为了将上述写入的文档重新查询出来,需要使用cl-mongo提供的DB.FIND函数。因为只有一个文档,所以直接查询就可以查看到结果了。在mongo shell中我们可以使用如下代码查询

1
db.http_request.find();

使用DB.FIND函数的话编写的代码可能会长得像下面这样

1
(cl-mongo:db.find "http_request" :all)

在我的系统上求值了上述代码后在REPL中输出的内容如下

1
2
3
4
5
6
((86 449 0 1 8 0 0 1 "http_request")
(<CL-MONGO:DOCUMENT> : {
_id : CL-MONGO::BSON-OID [#(89 52 26 160 109 156 254 71 184 115 102 30)]
elements : 1}

))

值得注意的是,DB.FIND的返回值不完全是文档组成的数组,而是在这个结果集的数组之外又多了一层列表,并且列表的第一个元素还是一个一看之下不知其所以然的子列表。由于cl-mongo的GitHub上没有提及这个玩意儿的来历,我也没有深入去了解DB.FIND函数的实现代码,因此这个元素就先忽略它吧。如果需要使用DB.FIND查询到的结果,那么开发者需要对DB.FIND的返回值应用一下函数SECOND才行,如下

1
(second (cl-mongo:db.find "http_request" :all))

返回值的列表中的每一个元素都是CL-MONGO:DOCUMENT这个类的实例对象,如果要直接使用还是稍微有点不方便的,因此我写了一个函数用来将DB.FIND函数查询到的CL-MONGO:DOCUMENT的实例对象都转换为较为熟悉,容易操作的数据类型——association list,函数的定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
(defun document-to-alist (doc)
"Convert a DOC of type CL-MONGO:DOCUMENT to a equivalent, serializable alist."
(check-type doc cl-mongo:document)
(labels ((aux (doc)
(cond ((typep doc 'cl-mongo:document)
(let ((keys (cl-mongo:get-keys doc)))
(mapcar #'(lambda (key)
(cons key
(aux (cl-mongo:get-element key doc))))
keys)))
(t doc))))
(let ((id (cl-mongo:doc-id doc)))
(append (aux doc) (list (cons "_id" id))))))

使用如下代码即可查看方才所写入的文档究竟长什么样子了

1
(document-to-alist (first (second (cl-mongo:db.find "http_request" :all))))

如果想要修改数据库中的文档,例如增加一个字段,那么可以使用cl-mongo提供的DB.UPDATE函数,用法如下

1
2
3
4
(cl-mongo:db.update "http_request"
(cl-mongo:kv "uri" "http://example.com")
(cl-mongo:kv "$set"
(cl-mongo:kv "method" "GET")))

最后如果要删除刚才所写入的这个文档,可以使用cl-mongo的DB.DELETE函数(我很好奇这个函数居然不是叫做DB.REMOVE),用法如下

1
2
(cl-mongo:db.delete "http_request"
(cl-mongo:kv "uri" "http://example.com"))

不久前在办公室抓取某网站S被对方发现,导致对方自动屏蔽了来自办公室网络的所有HTTP请求,连正儿八经地用浏览器打开也不行。为了可以摸索出“改头换面”(改HTTP头部)访问的方法,必须先成功访问至少一次,看看发出的HTTP头部是怎样的才行。恰好想起自己有一台腾讯云服务器,登上去用curl访问网站S,发现是成功的(也就是尚未被屏蔽)。既然如此,干脆在服务器上部署一套Squid作为正向代理,帮助办公网络的请求成功抵达网站S并拿到响应页面。

apt-get安装了squid软件包后启动并监听端口8321,在办公网络下将公网地址和8321端口作为代理配置传递给curl-x选项,访问网站S。不料Squid拒绝了我的请求,返回了如下内容(节选自curl -v命令的输出)

1
2
3
4
5
6
7
8
9
10
11
12
13
< HTTP/1.1 403 Forbidden
< Server: squid/3.5.12
< Mime-Version: 1.0
< Date: Wed, 17 May 2017 15:18:08 GMT
< Content-Type: text/html;charset=utf-8
< Content-Length: 3531
< X-Squid-Error: ERR_ACCESS_DENIED 0
< Vary: Accept-Language
< Content-Language: en
< X-Cache: MISS from VM-44-136-ubuntu
< X-Cache-Lookup: NONE from VM-44-136-ubuntu:8321
< Via: 1.1 VM-44-136-ubuntu (squid/3.5.12)
< Connection: keep-alive

经过一番Google,才知道原来是Squid的配置导致的。在Squid配置文件(/etc/squid/squid.conf)中,默认的acl和http_access指令的设置如下

1
2
3
4
5
6
7
8
9
10
11
12
acl SSL_ports port 443
acl Safe_ports port 80 # http
acl Safe_ports port 21 # ftp
acl Safe_ports port 443 # https
acl Safe_ports port 70 # gopher
acl Safe_ports port 210 # wais
acl Safe_ports port 1025-65535 # unregistered ports
acl Safe_ports port 280 # http-mgmt
acl Safe_ports port 488 # gss-http
acl Safe_ports port 591 # filemaker
acl Safe_ports port 777 # multiling http
acl CONNECT method CONNECT
1
2
3
4
5
6
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports
http_access allow localhost manager
http_access deny manager
http_access allow localhost
http_access deny all

由于Squid是按照第一条匹配的http_access指令来决定允许还是拒绝的,因为来自我办公网络的请求实际上命中的是

1
http_access deny all

因此被拒绝是必然的。为了可以接受来自办公网络发起的请求,首先需要新增一行acl指令。通过Squid的日志(/var/log/squid/access.log)可以查看到被拒绝的请求的IP地址是多少,此处假设IP地址为8.7.198.45,那么相应的acl指令如下

1
acl myclients src 8.7.198.45

此处的myclients为自定义的名称,顾名思义,它表示“我的客户端”;src是一种acl类型,表示客户端的IP地址;8.7.198.45是src类型下的参数,也就是我所使用的客户端发出的请求的来源IP地址。配置了acl后,还需要配置http_access指令。这个就简单多了,只要允许上面创建的这个acl的访问即可,内容如下

1
http_access allow myclients

之后再重启Squid服务即可

1
sudo service squid restart

这时候再从办公网络中以腾讯云服务器上的Squid为正向代理发出请求,就不会再被Squid拒绝了。

全文完

TL;DR;

这是一篇为了完成写作KPI而写的博客,总结起来就是提供了一种用Common Lisp实现来自于Twitter的雪花算法的实现方案。成品在这里,本文只是简单地描述一下生成雪花ID的大致思路,详细内容请各位移步代码仓库查看。

上述代码仓库中的snowflake算法——如果我的实现确实可以称作snowflake算法的话——的思路来自于下列两个地方:

  1. http://www.lanindex.com/twitter-snowflake,64位自增id算法详解/
  2. https://github.com/sony/sonyflake

如何获取时间戳

Common Lisp本身提供了一个获取时间戳的函数,也就是get-universal-time,可惜的是,这个函数所返回的并不是通常意义上的Epoch时间戳,而是自己的一套计算时间的方式中的表示时间的整数。为了获得UNIX时间戳,需要借助于第三方库local-time。为了可以获取到毫秒精度的时间戳,一个可运行的函数如下

1
2
3
4
5
6
(defun now ()
"Returns the number of milliseconds elapsed since 1 January 1970 00:00:00 UTC."
(let* ((now (local-time:now))
(seconds (local-time:timestamp-to-unix now))
(milliseconds (local-time:timestamp-millisecond now)))
(+ (* 1000 seconds) milliseconds)))

如何获取机器ID

这里参考了Sony的雪花ID算法中的思路,基于机器的内网IP地址来生成机器ID。当然了,Common Lisp标准中是没有提供获取机器的内网IP地址的方法的,这一点也可以借助于第三方库实现,选用的是ip-interfaces。通过这个库提供的get-ip-interfaces函数可以获取到机器的所有“接口”,遍历这个接口的列表后即可找出其中的内网IP。一台机器可能会有多个内网IP,我的方法是选用了第一个找到的内网IP地址。当然了,还需要一个将向量转化为数值的函数,并取出转化为数值后的IP地址的低10位,作为机器ID。

序号

如果希望生成的ID是保持递增的,那么就需要维护一个可以原子递增的数值计数器。在真实的使用中可以通过Redis的INCR指令来生成这一个ID,但是因为这里的雪花ID算法是作为一个独立的库实现的,不需要依赖于数据库等外部组建,因此这里就直接使用了Common Lisp自带的random函数来生成这个序号了。

全文完

假设有N个区间,将它们表达为,其中下标i位于区间

为了判定这组区间中是否存在两个区间是有重叠的,首先对这组区间进行排序,使得对于排序后的每一个区间而言,都有(这里的i小于N-1)。

为了说明要如何判定这些区间中是否存在重叠,首先我们假设这其中确实存在着至少两个这样的区间,假设分别是第j个和第k个(假设j小于k),它们必然会满足这样的关系

这是因为如果,那么所有位于区间中的数都将会小于 b_k,那么第j个区间与第k个区间就不可能有交集了,因此上述不等式一定成立。再加上这一组区间都是按照区间的下界递增排序的,那么必然有

假设,由于k和j都是正整数,这意味着在第j和第k个区间之间,必然还存在着一个区间l,那么这个区间的必然满足

这就意味着第j个区间和第l个区间也存在交集,它们的交集是子区间(这里假设)。这就说明了,如果可以在一组区间中找到两个不相邻的区间,它们存在重叠的部分,那么一定可以找到第三个区间,使得这个区间与其中的一个区间也存在重叠。

这表示如果我们要判定一组区间是否存在重叠,那么只需要先将它们基于区间的起点按照递增排序后,比较每一对相邻的两个区间是否存在重叠即可。

最近产品需要一个搜索商城中的商品的功能,于是接触了一下Elastic Search。虽然久仰它的大名,但一直都没有真正用过。这次稍微摸索了一下,顺便记录下来,说不定哪天就真的需要用上了。

安装

首先需要下载Elastic Search,我选择了.zip格式的安装包,下载地址在这里。下载完成后就拿到了一个5.2.2版本的Elastic Search的安装包,只需要解压即可使用。因为我喜欢把软件安装到主目录的app目录下,所以我用的命令是

1
2
cd /home/liutos/app
unzip ../installer/elasticsearch-5.2.2.zip

主目录下的installer是我习惯的用来存放软件的安装包的位置。解压后生成了一个名为elasticsearch-5.2.2的新目录。在这个目录下有一个名为bin的子目录,只需要进入该目录运行其中的elasticsearch文件即可,实例命令为

1
2
cd elasticsearch-5.2.2/bin
./elasticsearch -d

为了让Elastic Search可以不占用当前的终端,添加了-d选项,使其以后台进程(daemon)的方式运行。Elastic Search需要JVM才能运行,在执行上面的命令之前请各位自行准备好Java程序的运行环境。成功启动后,Elastic Search默认会监听9200端口,可以通过浏览器访问http://localhost:9200来确认Elastic Search是否正常启动了

使用

创建索引

遵照官方文档中的指导,先创建一个索引以便后续向这个索引中添加文档。假设要创建的索引是为商品准备的,取名为products,可以通过如下的命令创建出来

1
curl -X PUT 'http://localhost:9200/products'

创建成功后Elastic Search的响应结果为

1
{"acknowledged":true,"shards_acknowledged":true}

如果希望Elastic Search返回更可读的形式,可以添加?pretty参数到上面的URL的末尾。

文档的增删查改

索引已经创建了,就可以创建文档了。Elastic Search的文档是对象形式的,假设现在要创建的对象的类型为product,示例命令如下

1
2
3
4
5
curl -X POST 'http://localhost:9200/products/product' --data '
{
"id": 1,
"name": "Product 1"
}'

在我的机器上,Elastic Search的响应结果为

1
{"_index":"products","_type":"product","_id":"AVqpZHmVckriR6iVcbaW","_version":1,"result":"created","_shards":{"total":2,"successful":1,"failed":0},"created":true}

其中名为”_id”的字段的值为Elastic Search自动为这份新写入的文档分配的ID,通过这个ID可以从Elastic Search中取出这份文档,示例命令如下

1
curl 'http://localhost:9200/products/product/AVqpZHmVckriR6iVcbaW?pretty'

相当的RESTful的接口路径,也许你已经猜到了,删除一个文档的代码就是将请求的GET方法替换为DELETE。是的,示例代码如下

1
curl -X DELETE 'http://localhost:9200/products/product/AVqpZHmVckriR6iVcbaW?pretty'

再次查找刚才的ID的文档时,响应结果中的”_found”字段的值就已经变成了false了。关于修改文档的方法,请参考官方手册中的章节

搜索

准备数据

最简单的搜索接口的使用就是通过浏览器访问http://localhost:9200/products/_search这个地址了。在我的机器上,看到的页面内容为如下的JSON字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{

"took": 42,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 0,
"max_score": null,
"hits": [ ]
}

}

为了方便接下来的演示,先通过Elastic Search的_bulk接口向其批量创建文档数据,示例代码如下

1
curl -X POST 'http://localhost:9200/_bulk?pretty' --data-binary '@docs'

其中docs文件中的内容为

1
2
3
4
5
6
7
8
{"create": {"_index": "products", "_type": "product", "_id": 1}}
{"name": "设计模式之禅"}
{"create": {"_index": "products", "_type": "product", "_id": 2}}
{"name": "失控:全人类的最终命运和结局"}
{"create": {"_index": "products", "_type": "product", "_id": 3}}
{"name": "构建高性能Web站点", "price": 75.00, "author": "郭欣"}
{"create": {"_index": "products", "_type": "product", "_id": 4}}
{"name": "大型网站技术架构:核心原理与案例分析", "price": 59.00, "author": "李智慧", "publisher": "电子工业出版社"}

尽管这里是一个文本文件,但是根据官方文档的说法,此处需要使用curl的二进制模式来发送数据,否则会报错。

个性化搜索

如果希望找到《失控》这本书的信息,那么可以根据书名进行查找,示例代码如下

1
curl 'http://localhost:9200/products/_search?q=name:失控'

Elastic Search提供了许多的搜索选项,如果全部通过URL中的query string来传递将会非常难以构造。为此,可以使用Elastic Search提供的基于HTTP body的参数传递方式,示例代码如下

1
curl -X GET 'http://localhost:9200/products/_search' --data '{"query": {"match": {"name": "失控"}}}'

Elastic Search支持相当丰富的搜索选项,这里不逐一介绍了,大家可以从官方文档的这里开始翻看。本来想在Chrome的POSTMAN插件中试验搜索功能的,结果当我选定了GET方法后,就不需要我提交HTTP body了,因此还是用curl进行演示。回到正题,如果我们搜索的是一个“站”字,那么Elastic Search会吐出两个结果,此处可以使用搜索接口的fromsize参数,分别控制返回的内容取自搜索结果中的哪一个片段。例如想要取出结果中的第二个,可以使用下列代码

1
curl -X GET 'http://localhost:9200/products/_search?pretty' --data '{"from": 1, "size": 1, "query": {"match": {"name": "站"}}}'

通过结合使用fromsize参数,可以实现许多应用中所要求的分页功能。在我厂的业务场景中,商品信息还是很多的,不可能全部放入到Elastic Search中作为文档数据存储,Elastic Search只是负责提供搜索出来的商品ID即可,之后再通过商品ID从原来的商品的数据库中按照顺序取出对应的完整的商品信息。因此,在搜索Elastic Search时实际上只需要商品的ID就足够了,可以通过Elastic Search提供的_source字段控制接口吐出的内容,示例代码如下

1
curl -X GET 'http://localhost:9200/products/_search?pretty' --data '{"query": {"match_all": {}}, "_source": ["_id"]}'

这样在吐出的内容中_source字段的值就会是一个空对象,应用程序只需要取每一个hits数组中的记录的”_id”字段即可。这样做的目的是减少Elastic Search通过网络传输了一部分毫无必要的数据,略微优化一下网络开销

全文完