之前一段时间,我们都在研究做天气预报程序,最后的成品本质上还是文本的,有的朋友问我,为什么不根据天气,展示不同的 Gif 动图啊,那样就很好用了啊!
这是一个非常好的建议,与我的想法不谋而合,但是在 PySimpleGUI 中显示图片略微复杂一些,我打算将显示图片的工作推迟到一个更适合的时候。
当下,让我们继续回到上一节的内容,发现一些更有趣的事。
上一节介绍了网络服务器的一些基本知识,IP、端口之类,希望你已经对网络通信的大体逻辑清楚了。然后用 bottle 库实现了简单的 web 服务,这是一个会根据不同姓名,向你 Say hi~ 的网页服务。
其实沿着上一节的前进方向,稍微阅读一点 bottle 的文档,很容易编写出更加复杂的 web 服务,作出类似之前天气预报一样的服务也是早晚的事,只要你真的有这个愿望,并付诸实施就一定可以。对服务器方面的开发,我作为 “引路人” 的工作到上一节就算完成了。
然而,一切并不止这么简单!魔改一下 web 服务,做个简单的聊天工具吧!像这样:
这魔改,分两步走
在网络中,各个节点是对等的,一台电脑可以同时扮演客户端和服务端。上一节已经实现了 web 服务端,它可以接受来自客户端(上一节是浏览器)发来的消息。在此基础上增加两步:
第一步,如果我们将收到的消息显示在视窗里,就是一个只能收不能发的聊天软件了。
客户端发送 Hello 给服务端的端口,这里以 HTTP 协议的默认端口 80 为例。还是使用刚认识的 bottle 库来接收客户端的数据 Hello。然后 bottle 库需要以某种方式将数据显示到视窗中的一个组件中。我们将用多行文本 Multiline,而非单行 Input 组件,以便显示多行信息记录。
这样离一个可用的聊天软件还有很大的距离,现在只能 “听”,但还是个哑巴,要是服务端能和客户端一样,向客户端发送消息就好了!
好办,第二步,我们让双方都拥有 web 服务 和 客户端功能。
任意一方都可以用自己的客户端功能,向对方的 web 服务发送消息,然后由 Multiline 组件显示在视窗中。
基建工作,从 print 开始
先从最简单的功能开始,利用 bottle 接收客户端数据,暂时用 print 打印出来,至于怎么显示在视窗上,我们稍后再介绍,这会是一个比较复杂的问题!
回顾一下上一节的代码:
from bottle import route, run, template
@route('/hello/<name>')
def index(name):
return template('<b>Hello {{name}}</b>!', name=name)
run(host='localhost', port=8080)
这段代码可以让我们在自己本机浏览器中输入 http://localhost:8080/hello/Jiangchuan,看到包含 Hello Jiangchuan 字样的网页。这至少说明一件事:
- 服务端知道客户端的名字
通过 @route('/hello/
') 这行,bottle 库会将浏览器地址框中 hello/ 以后的字符串(比如例子里的 Jiangchuan)赋值给变量 name,在 index(name) 函数中就可以使用变量 name。
用户不会仅限于发送比如 “Jiangchuan” 之类的短词。如果我们希望客户端能向服务端传递更加复杂的消息,比如一大段文字,甚至一张照片,就不能填充在 Url(浏览器的地址栏里),毕竟地址栏就那么点地方。 web 服务的 HTTP 协议中还支持另外一种方式,便于客户端向服务端发送大数据:POST 方法。
TIPS
HTTP 协议有 6 种方法:HEAD、GET、PUT、POST、DELETE、OPTIONS 等。每一种方法的数据格式都有差异,其中 POST 方法可以携带更多数据给服务端。详细介绍可以百度之。
为了方便组织信息,通常会把数据以 Json 格式组织,以类似 Python 字典的形式发给服务端处理。比如将数据这样组织:
{
"from" : "张三",
"msg” : “明天张学友演唱会门票你要么?50 一张!"
}
非常清晰,是吧!一组数据同时包含了发送者和消息。
如果你使用 Mac 或者 Linux,可以在命令行中这样提交 Json 数据:
curl -X POST -d '{"from" : "张三", "msg" : "明天张学友演唱会门票你要么?50 一张!"}' http://localhost:8080/msg
那么服务端的 bottle 库如何获得 POST 数据呢?
首先要用使用 @post('/msg') 替换 @route('/hello/\<name>'),让这个 URL 支持 POST 方法。另外我们不需要用户在 URL 中提供他的名字,他的名字直接在消息的 “from” 字段指明就好了。
然后在 index() 函数中,访问 request.body 就可以得到原始的客户端消息。如果使用 type(request.body) 会发现 request.body 是一个 BytesIO 对象,要调用它的 read 方法才能获得其中的二进制数据,再对其进行 “utf-8” 解码就能看到它的内容。
记住 request.body 中获取的数据是 Json 格式是 String,一定要用 json 库转换为 Python 字典才能直接访问。
实践一下,新建一个 recive_post_and_print.py 保存下面的代码。先完成服务端接收客户端消息的功能,就简单的把消息 print 就好了。
from bottle import route, run, template, request, post
@post('/msg')
def index():
print(request.body.read().decode("utf-8"))
return "<h1>OK</h1>"
run(host='localhost', port=8080)
TIPS
编程有点像做雕塑,先做出一个最简单的功能,称为 MVP (minimum viable product, 最小可行产品)。 再逐步添加新功能、调整代码,逼近最终的产品。伟大的罗马城也是从最简单的土坯开始的,对吧!
我用 visual studio code 编辑这些代码,运行之后输出框中提示:
Listening on http://localhost:8080/
这说明 web 服务已经正常启动了,服务的 IP 是 localhost,端口是 8080。下来向它发送 POST 请求,如果正常的话,在输出框中可以看到发送它的消息。
我试着发送了多次消息,在输出框中都可以看到:服务器收到我的消息了。
TIPS
目前,发送消息的客户端和服务器必须在同一局域网或者本机,关于网络和 IP 可以看一下上一篇文章。
那些准备知识都是有用的。
这样我们就完成第一步的一大部分了,还有个问题就是如何将消息显示到视窗中去,我们搞 PySimpleGUI 不就是为了摆脱黑框文本嘛。
视窗中显示文字,不就用组件的 update 方法嘛?!
没错,但不能直接这么做。由于一些情况变得有点复杂,主要是因为:
- 服务端不知道什么时候客户端会发送数据,可能在任意时刻;
- PySimpleGUI 的 “事件循环” 是一个死循环;
- bottle 为了能随时接收消息,也可以认为是一个 “事件循环“,在 run 函数中等待客户端的消息触发;
- 无论程序进入上述哪个循环,就意味着另一个事件循环里的逻辑永远无法执行;
所以我们会用到多线程技术,让一个程序运行多个 “事件循环”,或者说 ”死循环“。
TIPS 多线程是并行计算技术之一,除此之外还有多进程、协程等技术,可以预先了解一下。
如果使用多线程技术,一个循环等待客户端的消息,一个循环在视窗中显示消息,就有下面的问题待解决:
- PySimpleGUI 的所有调用只能在主线程;
- 那就不能在 bottle 线程中更新视窗了;
- 只能让 bottle 收到消息,并通知视窗线程,如何做到这一点?
下一节我们会一一解决这些问题。
总结
天气预报程序先放一放,顺势搞一搞网络应用。我们制定了分两步走的计划,实现一个简单的聊天软件,目前第一步已经实现了 50%(虽然已经实现的部分占据了示意图的一大半,但真的只实现了 50% 不到),起码服务端在输出框可以打印出收到的消息了。
不用太沮丧,虽然看起来不怎么 “高端”,起码逻辑跑通了,会慢慢好起来的。
下一节会用到的新知识:
- 多线程技术,
- 多线程之间的通信技术,
- 实时更新 PySimpleGUI 组件的方法。
明天见~