Pharo使用笔记之http、websocket与mqtt

前言

Smalltalk提供了绝佳的可探索的沉浸式编程环境(live),这是一种"红药丸",一旦尝试就再也回不去了。于是决定将原型构建相关的工作都放到Pharo上。

许多业务相关的代码可能以服务的方式跑在外部,于是进程间通信(IPC)就成了实际需要。本文关注http、websocket与mqtt这几种我常用的通信策略。 (本文只关注在Pharo中运行客户端)

日后有时间可能会加上Socket、ZeroMQ、RPC。

以及是我的使用笔记。

环境

目前我在MacOS 10.13.5下使用Pharo 7.0.3.

http

关于http的知识在此就不多说了,如果不熟悉可以参考wikipedia http

在Pharo中,我倾向于使用Zinc作为http client。

Zinc文档很齐全。Zinc既可以作为http server,也可以作为http client

如果你希望构建web server,Seaside可能会更合适。我们接下来关注如何将Zinc用作http client。

我们将httpbin.org用作测试服务器。

安装依赖

Zinc已经内置在Pharo7 (更早的版本不大清楚)。所以无需安装依赖。

实验

让我们向https://httpbin.org/get发送请求。

response
ZnClient new
   url: 'https://httpbin.org/get';
   get;
   response.

输出response

将response输出到Transcript中。

| response |
response := (ZnClient new)
   url: 'https://httpbin.org/get';
   get;
   response.
response writeOn: Transcript.
Transcript flush.

request
| request |
request := (ZnClient new)
   url: 'https://httpbin.org/get';
   get;
   request.
request writeOn: Transcript.
Transcript flush.

超时
ZnClient new
   timeout: 1;
   get: 'https://httpbin.org/get'.
retry
ZnClient new
   logToTranscript;
   numberOfRetries: 3;
   retryDelay: 2;
   get: 'https://httpbin.org/get'.
URL构造
ZnClient new
   https;
   host: 'httpbin.org';
   addPath: 'get';
   get.
ZnUrl new
   scheme: #http;
   host: 'www.google.com';
   port: 80;
   addPathSegment: 'search';
   queryAt: 'q' put: 'Pharo Smalltalk';
   yourself.
提交HTML表单
<form action="search-handler" method="POST" enctype="application/x-www-form-urlencoded">
   Search for: <input type="text" name="search-field"/>
   <input type="submit" value="Go!"/>
</form>

表单使用默认编码application/x-www-form-urlencoded

ZnClient new
   url: 'https://httpbin.org/post';
   formAt: 'search-field' put: 'Pharo Smalltalk';
   post.

上传文件

<form action="upload-handler" method="POST" enctype="multipart/form-data">
   Photo file: <input type="file" name="photo-file"/>
   <input type="submit" value="Upload!"/>
</form>
ZnClient new
   url: 'https://httpbin.org/post';
   addPart: (ZnMimePart
                fieldName: 'photo-file'
                fileNamed: '/tmp/test.jpeg'); "todo:有问题?"
   post.
Headers

ZnClient new request headers at: 'accept' put: 'text/*'.

提交json数据

jwt认证

|cli headers|
cli := ZnClient new.
headers := cli request headers.
headers at: 'Content-Type' put: 'application/json'.
headers at: 'Authorization' put: 'Bearer ACCESS_TOKEN'.
cli
    url: 
'https://httpbin.org/post';
    entity: (ZnEntity 
        with:'{"hello": "world"}'
        type: ZnMimeType applicationJson);
    post;
    response.

websocket

安装依赖

Gofer new
   smalltalkhubUser: 'SvenVanCaekenberghe' project: 'ZincHTTPComponents';
   package: 'ConfigurationOfZincHTTPComponents';
   load.
(Smalltalk globals at: #ConfigurationOfZincHTTPComponents) project latestVersion load: 'WebSocket'.

实验

选择www.websocket.org作为测试服务器。 也可以使用wssh帮助测试,wssh类似netcat。

让我们向wss://echo.websocket.org发送请求。

| webSocket |
webSocket := ZnWebSocket to: 'wss://echo.websocket.org'.
[ webSocket
   sendMessage: 'Pharo Smalltalk using Zinc WebSockets !';
   readMessage ] ensure: [ webSocket close ].

做实验的时候,可能希望sendMessagereadMessage是分别手动操作的。

对于sendMessage比较简单:

[ webSocket
   sendMessage: 'Pharo Smalltalk using Zinc WebSockets !'; ] value.

readMessage时可能会阻塞整个软件。推荐使用taskit来并行执行。

首先安装依赖:

Metacello new
  baseline: 'TaskIt';
  repository: 'github://sbragagnolo/taskit';
  load.

taskit的使用非常简单:[ 1 second wait. 'Waited' logCr ] schedule.,打开Transcript,运行后,可以看到1秒后有一条消息打印在Transcript上。

使用taskit来运行readMessage

[ (webSocket
   readMessage) logCr ]  schedule.

webSocket readMessage每次读取一条消息,当没有消息时,会阻塞。

MQTT

测试服务器使用iot.codelab.club,测试账号为guest:test.

安装依赖

第一个例子里,我们准备使用Pharo的MQTT client发布一个消息:hello from pharo, 使用hbmqtt订阅它。

首先让我们在Pharo中安装MQTT client, 我偏好svenvc/mqtt, svenvc是smalltalk社区的活跃用户,维护了好几个重要的库,代码质量非常高,由于smalltalk社区很小,所以不要在意star数。 使用iceberg安装它,记得load相应的package(全部load即可)。

接着在系统命令行里安装Python库hbmqtt,用于配合测试: pip install hbmqtt

实验

发布消息

在命令行里运行hbmqtt_sub --url mqtt://guest:test@iot.codelab.club -t "/pharo_mqtt"

接着在Pharo里发布消息,

|client|
client := MQTTExperimentalClient new
    atLeastOnce; "QoS level 1"
    host: 'iot.codelab.club';
    username: 'guest';
    password: 'test';
    open;
    sendMessage: 'hello from pharo' asByteArray toTopic: '/pharo_mqtt';
    close.

消息发布成功!

订阅消息
|client|
client := MQTTExperimentalClient new
    atLeastOnce; "QoS level 1"
    host: 'iot.codelab.club';
    username: 'guest';
    password: 'test';
    open;
    subscribeToTopic: '/hello_pharo';
    readMessage.

hbmqtt_pub --url mqtt://guest:test@iot.codelab.club -t "/hello_pharo" -m "hello from hbmqtt"

前头的程序接受1条消息就退出(阻塞式的)

写一个接收3条消息才退出的程序

|client|
Array streamContents: [ :stream | | count |
   count := 1.
   MQTTExperimentalClient new
    host: 'iot.codelab.club';
    username: 'guest';
    password: 'test';
    open;
    subscribeToTopic: '/3_messages';
    runWith: [ :message |
        stream nextPut: message.
        (count := count + 1) > 3 ifTrue: [ ConnectionClosed signal ] ] ].

连续发三条消息:

hbmqtt_pub --url mqtt://guest:test@iot.codelab.club -t "/3_messages" -m "hello1"
hbmqtt_pub --url mqtt://guest:test@iot.codelab.club -t "/3_messages" -m "hello2"
hbmqtt_pub --url mqtt://guest:test@iot.codelab.club -t "/3_messages" -m "hello3"

提醒

smalltalk社区习惯在class里加上comment和tests,而不是单独构建文档,所以你在svenvc/mqttreadme里基本读不到文档,在Pharo编程环境里去通过阅读、批注和tests去学习使用方法吧。这种方式充满乐趣,你可以拆开任何黑盒子,一探究竟。




Fork me on GitHub