上一节已经实现了聊天软件的最关键部分:消息的接收和显示。其实我们已经实现了一个单向的聊天工具,如果把 HTTP 调试工具可以算作聊天发送端的话。
当然,这样的作品交差肯定是不行的,今天只剩下最后一哆嗦,继续加油了。
界面调整
一个经典的聊天软件有哪些功能呢?
- 一个多行文本框显示聊天记录,最新消息追加在聊天记录的最后一行。
- 一个编辑框,用于输入要发送的消息。
- 一个发送按钮
这一节几乎没有新知识,调整视窗这种事咱们也是一回生、二回熟、三回旧相识,不多啰嗦,直接在上一节代码的基础上,加代码:
# 继续增加一个 Input 输入消息,一个 Button 发送消息
# ###就显示一个多行文本框,大小是 50 * 5,单位是应为字符,并通过 disabled 禁止用户编辑内容,使其只读
layout = [
[sg.Multiline( size = (50, 5), key = "-MSG_SCREEN-", do_not_clear=True, disabled=True)],
[sg.Input(key="-EDIT-", size = (50, 1))],
[sg.Button("Send", key = "-SEND-")]
]
注意第二行注释是上一节的遗迹,我们在 sg.Multiline 下面又增加了 sg.Input 和 sg.Input。注意,在 PySimpleGUI 中一行用一个 list 表示,即使一行只有一个组件,它也得在 list 中,所以这两个组件必须分别在一个 list 中,这样它们才能垂直排列。
直接运行,就可以看到效果,非常简单。
已经达到要求了。
实现消息发送
上一节我们用的是 HTTP 调试工具发送消息。现在就需要将发送 POST 请求的功能,增加到当前代码中。
之前已经约定服务端接收数据有这些要求:
- web 服务端在 IP:PORT 等待请求;
- 客户端向服务端发送 POST 请求,请求 Url 是 http://IP:PORT/msg;
- POST 请求的数据是 {"from" : "sender name", "msg" : "what you say"},这是一个 Json 格式的数据。
触发发送事件的是 ”-SEND-“ 按钮,要发送的 msg 是视窗中 ”-EDIT-“ 组件中的文字。至于 IP、PORT 和发送者的名称,暂时硬编码在代码中。
TIPS
在半成品的时候,将临时数据编码在代码中是一种非常方便的做法。 随着功能的完善,这些硬编码的数据会被变量取代。
# HOST 和 PORT 在编写 web 服务的时候,就已经定义过了
HOST = "localhost"
PORT = 8080
# 硬编码发送者姓名
SENDER = "Jiangchuan"
发送消息的时机是 “-SEND-” 按钮被按下之时。立刻想到,发送消息的代码应该在 event == “-SEND-” 的事件处理中。
不过在此之前,先把发送消息的功能,编写为一个函数,不然 “-SEND-” 消息处理代码就会很长,可读性不太好。
TIPS
适当的时候,将一组相关的代码整合为函数,即能提高代码复用,又能使代码整洁。
要发送一条消息,至少需要:
- web 服务的 ip,web 服务有时也称为 host、
- port、
- 发送者的姓名、
- 及消息正文
所以发小消息的函数应当有 4 个参数,实现如下:
# 在代码开头,导入 requests 库
import requests
def send_msg(host, port, name, msg):
# 按照约定,POST 数据是这样格式的字典,发送的时候会被转换为 Json 格式的字符串
msg_obj = {"msg" : msg, "from" : name}
# 使用 requests 库的 post 方法,向 http://host:port/msg 发送 msg_obj 转换成的 json 字符串,
# 注意 post 函数的 data 参数就是是 msg_obj 经过 json 库转换后的结果。
# 这里没有处理发送异常,如果希望完善一些,你应当在调用 send_msg 的时候捕获可能发生的异常。
r = requests.post("http://{}:{}/msg".format(host, port) , data = json.dumps(msg_obj))
好了,以后我们就可以通过 send_msg,以 name 的身份,向 host:port 上的 web 服务发送消息 msg 了。
下面来处理 "-SEND-" 事件。基本的逻辑是:
其中,应当捕获 send_msg 可能触发的异常,比如对方的 web 服务联系不上,或者其他什么错误。不处理的话,会有一些问题,但是能节省不少精力,这个工作就留给你来处理了,姑且让我偷个懒。
TIPS
Python 的异常处理可以参考这里。
继续上代码,这些代码可以放在 "-TIMEOUT-" 事件处理之后:
if event == "-SEND-":
old_sreen = values["-MSG_SCREEN-"]
# 获得你输入的内容
your_msg = values["-EDIT-"]
# 发送消息
send_msg(HOST, PORT, SENDER, your_msg)
# 将发送出去的消息,拼接为一条新的本地聊天记录,格式是 you: 消息
new_msg_line = "{}: {}".format("you", your_msg)
# 将新记录追加到聊天记录尾部
new_screen = "{}{}".format(old_sreen, new_msg_line) if old_sreen != "" else new_msg_line
# 更新本地聊天记录框
window["-MSG_SCREEN-"].update(new_screen)
# 清空编辑框
window["-EDIT-"].update("")
continue
运行一下看看效果:
太棒了,发送一次消息,可以看到聊天记录里先有一条已发记录,还有一条一摸一样的消息,只不过显示的是 Jiangchuan 发来的,这是新收的消息。
这是为什么呢?现在代码里 HOST、PORT、SENDER 都是硬编码的,消息被发送到自己的 web 服务上,所以自己能收到自己的消息。
询问用户信息
如果两个人希望用这个工具互相沟通,如果每次都要用户自己改代码里的 HOST、IP、SENDER 信息肯定是不合理的。
还记得最开始用过的 One-shot window 吗?此时正是需要它的时候,我们需要一次性的窗口,获得用户自己 web 服务端口、对方的 IP 及 端口,还有用户的姓名。
迅速的设计一个 5 行的蓝图:
hostInfoLayout = [
[sg.Text("local port"), sg.Input("8080", size=(20, 1), justification="center", key = "-LPORT-")],
[sg.Text("remote IP"), sg.Input("localhost", size=(20, 1), justification="center", key = "-HOST-")],
[sg.Text("remote port"), sg.Input("8080", size=(20, 1), justification="center", key = "-PORT-")],
[sg.Text("your name"), sg.Input(size=(20, 1), justification="center", key = "-NAME-")],
[sg.Button("OK"), sg.Button("Cancel")]
]
我指定了一些值的默认值,希望可以减少一些用户输入工作。
然后,依次完成以下操作:
- 创建 window;
- 调用 window.read();
- 取得所有 Input 的值;
- 关闭 window。
这个流程我们应该很熟悉了,在此不再展开,大家直接查看最终源码即可。
通过上面的工作,就可以得到:
- 本地 web 服务
- 对方 IP
- 对方 端口
- 本地用户名
记得在创建本地 web 服务和发送消息的时候,使用相应的值。
看一下最终效果,开了一对聊天工具:
- Jiangchuan 在 localhost:8080 接收消息,
- Lucy 在 localhost:8081 接收消息。
对照之前的构想图,更容易理解两个客户端之间的工作原理。
一点补充
为什么在我的例子中,我配置的 web 服务是
- localhost:8080
- localhost:8081
都用一个端口多方便啊!是啊,但是操作系统不允许同一台计算机多个程序使用相同的 IP:PORT,所以在自己的电脑上 IP 都是一样的,只能通过使用不同的端口绕开这个限制咯。
如果在不同的计算机上运行这个程序,就可以使用相同的端口,因为它们的 IP 肯定不同。
两台机器要用现在的工具通信,要求它们:
- 要么两台机器都在公网;
- 要么两台机器都在 同一 局域网;
否则不能互相通信,至于原因在 6.1 有详细解释。
安全性如何?
我们用的微信、QQ 实际上除了聊天双方外,还有腾讯公司作为中间人。某种角度来说,腾讯知道我们所说的每一句话、发的每一段语音、图片,一切的一切。如果你发现只是在微信里聊了几句 Python,网页、知乎都开始给你推送相关广告,你大概也猜得到背后的逻辑吧。
我们做的小聊天工具是真正点对点的(peer to peer),没有中间人,这一点上隐私性比微信、QQ 等 IM 高。
但是我们的通信使用的是明文 HTTP 协议,很容易被有恶意的人截获,比较安全的做法是在通信中使用加密技术,比如使用 HTTPS 代替 HTTP,或者在聊天双方之间建立加密隧道,都是可以的。有兴趣可以了解一些常见的加密技术,尤其是非对称加密。
想作出真正安全的聊天工具,还有很多工作要做。
总结
最近刚好团队招了新人,采购了几本 Python 教材,我的文员惊叹道:“ 吓我一跳,一本书 700 页!”
是的,其实我还没告诉他这书还有上下两册!
我回过头一看,也被吓了一跳————真厚!
当真正潜入到一个技术中的时候,才发现它有这么多错综复杂的细节。一个非常简单的聊天工具竟然也写了 5 节!我自己也觉得有点不可思议。
编程确实会牵扯到各种行当的知识。也许遇到问题的时候,很多知识就信手拈来。平时没有觉得有多少,罗列开一看,还真是一大堆让人头疼的东西。这正是长久学习积累的作用,长久坚持的东西,不知不觉就会会带来扎实的回报。
即便这么难的东西,最后不也被我们搞定了么?!做到少数人能做到的事,感觉就很棒。
下一节我打算暂停脚步,稍微总结一下这一段学习到的东西,以求融会贯通,顺便琢磨点有趣的新项目。
明天见咯~
本节的代码在这里 https://github.com/JiangChuanGo/examples/tree/master/PySimpleGUIDemos/messenger_demo