背景

我最近在折腾blockly4pi,这是一个教育项目,致力于将编程带入到基础教育,通过使用blockly,我们将原子操作封装为积木块,学生只需要操作积木块就能做到程序能做到的事

已经封装为积木的原子操作,除了blockly提供的基本编程要素(循环、条件、逻辑等),我们还积木化了虚拟角色的操作以及硬件操作。

硬件的积木化不只是对硬件的操作积木化,硬件本身也被设计为积木

用户通过拖拽web试验平台拖拽积木块,将生成对应程序代码(我目前选择生成python,其实js也是很棒的选择),之后这些代码在树莓派里运行以操控硬件。

我们的硬件积木与树莓派之间采用wifi通信,硬件积木自然是李老师做的(对李老师有兴趣的可以参考我之前的文章),目前的原型机便是文章开头的那张图

我们之前为了省心采用了类似http(实际是socket)的通信机制(请求-答复-中断)来做树莓派和积木节点之间的通信,虽然架构简单,不过带来了一个麻烦,实时性不高的问题,近期重新调整为长链接

问题

我们最初放弃长链接的一个原因是:对硬件积木的操作指令是用户生成的,用户每次在web界面搭建好积木点击运行,树莓派里新起python进程。如此一来长连接就不能建立在动态生成的脚本里。因为脚本是动态执行的,随时可能因为用户新的提交而中断(执行新的).

思路

于是我想到用ipc(进程间通信)来解决这个问题: 在树莓派中使用两个进程来做这件事(分别以进程一和进程二表示):

  • 用户在web页面拖拽积木块生成代码,之后代码作为脚本运行在树莓派中,此为进程一,它是动态的 (为了保证清晰和教学方便,只允许在单次提交中使用多线程,不允许多次脚本并行)
  • 进程二运行在后台(不中断),负责与硬件节点保持长连接,该进程同时作为server,等待进程二传递的控制硬件节点的消息,同时将消息传递给硬件积木

也就是说进程二是用户动态生成的脚本与硬件积木的中间人,使用这个中间人的目的是保持长链接不中断

从数据的视角来看,进程一只是个管道,它只负责传递消息,管道本是静态的,我们使用载荷来承载变化的消息

实现

一旦进入实现部分,岔路就多了,尽管可能都通往罗马,但有些路荆棘遍布,有些路一马平川,如何选择,感觉有时候比分析重要,这可能是工程问题中偏艺术的一环

就我而言,在不熟悉的领域,我便好走人多的一条路,即便迷路的话,指路人也多

尽管进程间通信的方法很多,我选择Socket(套接口),Socket为目前Linux上最为广泛使用的一种的进程间通信机制,与其他的Linux通信机制不同之处在于除了它可用于单机内的进程间通信以外,还可用于不同机器之间的进程间通信

代码

考虑到这部分可能对其他同学也有帮助,我将这部分的具体实现放过来(只展示原理)

进程二为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#!/usr/bin/env python
# encoding: utf-8
import socket
import os, os.path
import time
import settings

sockfile = "./communicate.sock"

if os.path.exists( sockfile ):
  os.remove( sockfile )

print "Opening socket..."
server = socket.socket( socket.AF_UNIX, socket.SOCK_STREAM )
server.bind(sockfile)
server.listen(5)

host_li= settings.HOST_LI# 硬件积木的host,从setting里读
port_li= settings.PORT_LI
# 关于为了硬件积木当server,有空再谈
# todo : 连接失败处理机制

socket2li = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 
socket2li.connect((host_li, port_li)) 
print "Listening..."

def send_message2server_li(message):
    socket2li.sendall(message)
    data = socket2li.recv(1024)
    return data

while True:
  conn, addr = server.accept()
  try:
        datagram =conn.recv(1024)
        if datagram == 'exit':
            break
        result = send_message2server(datagram).strip()
        conn.send(result) 
  except Exception as e :
        print(str(e))

进程一与进程二通信部分为:

1
2
3
4
5
6
7
def send_message(message="s01d"):
    client = socket.socket( socket.AF_UNIX, socket.SOCK_STREAM)
    client.connect( "./communicate.sock" )
    client.sendall(message)
    data = client.recv(1024)
    client.close()
    return data

从这里我们可以看到,我们实际把中断进行了转移,把与远程主机的中断移到了本地。如此一来延迟变得非常小

扫尾

细心的小伙伴可能会看到,我们这里没有做掉线重连、启动顺序、错误处理之类的机制,这些保证健壮性的工作我放到源码里,有兴趣的同学可以自行阅读

需要说明的是等待机器上线后连接部分的代码我用的是《python network programming cookbook》中第三章的源码,周末逛书店看到这部分代码很漂亮,就直接拿过来用,替换了我之前自己写的

FAQ

进程二中的server可以使用web server吗

可以的,不采用的原因是web server比socket啰嗦太多

为何不用zeromq

我其实偏好zeromq,关于zeromq的优点,我之前笔记里又记录:消息队列中间件学习笔记 ,zeromq 是个野心勃勃而欣欣向荣的项目

没用它的主要原因是,硬件开发者,包李老师,偏好用socket

尽管我可以在进程二与硬件积木中使用socket连接(迁就硬件这边),在进程一与进程二之间使用zeromq,不过获得的好处并不明显,还导致了不一致,感觉不划算

2017-04-29补充

目前能想到使用zeromq的一个场景是:多个用户能一起用浏览器来控制我们的硬件积木(我们在此只关注通信部分,其他方面暂不关注) ,也就是说需要1-N连接(1是server,N是client),直接使用REQUEST/RESPONSE模型就行

这里给个示例代码(照抄这里):

客户端

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import zmq  
  
context = zmq.Context()  
  
#  Socket to talk to server  
print "Connecting to hello world server..."  
socket = context.socket(zmq.REQ)  
socket.connect ("tcp://localhost:5555")  
  
#  Do 10 requests, waiting each time for a response  
for request in range (1,10):  
    print "Sending request ", request,"..."  
    socket.send ("Hello")  
      
    #  Get the reply.  
    message = socket.recv()  
    print "Received reply ", request, "[", message, "]"

服务端:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import zmq  
import time  
  
context = zmq.Context()  
socket = context.socket(zmq.REP)  
socket.bind("tcp://*:5555")  
count = 0

while True:  
    #  Wait for next request from client  
    message = socket.recv()  
    count += 1
    print "Received request: ", message, count 
  
    #  Do some 'work'  
    time.sleep (1)        #   Do some 'work'  
  
    #  Send reply back to client  
    socket.send("World")  

client可以在server没有启动的时候上线(不会报错),只要服务端上线就不会丢消息,如此一来启动问题变得十分轻松,不必保证server先上线

其次client可以有多个,server能同时处理它们,不会引起混乱

单是以上两点就省下我们许多工作

下一步打算如何改进

使用websocket,目前已经在开发环境里用了,具体原因之后有机会说

参考