#!/usr/bin/env python # coding: utf-8 # # Kivy指南-4-Kivy网络编程 # # > 实现聊天app的客户端-服务器架构,利用Twisted框架实现服务器 # # - toc: true # - badges: true # - comments: true # - categories: [jupyter,Kivy,Android,iOS] # - image: kbpic/4.7chatlast.png # 前面我们尝试过单一平台Android的Kivy开发,通过原生的底层API来实现。下面我们探索另外一种天生具有跨平台能力的工具来做app——网络。在这一章,我们做一个聊天app,类似于QQ,但是简单版。 # # 当然这个应用不能替代QQ,不过支持类似QQ群聊天功能,方便临时性的会议建群聊天。 # # 为了简化,我们不实现认证授权功能,这是为彼此都很熟悉的用户设计的。如果你想让app更安全,自己可以实现一下。 # # 为了在服务器端支持最大兼容性,这个app用**Telnet**收发消息。虽然不是Kivy的app的图形用户界面,Telnet可以在Windows 95和MS-DOS完美运行。 # >更严谨的考证一下,其实Telnet在1973年就标准化了,因此它甚至可以在8086 CPU上运行。Windows 95和MS-DOS相比之下已经很新了。 # # 本章教学大纲如下: # # - 用Python的Twisted框架实现服务器端。 # - 在不同的抽象层面上开发两个客户端,一个是通过套接字实现命令行程序,一个是通过Twisted的事件驱动客户端实现的程序 # - 用Kivy的`ScreenManager`更容易的实现UI # - 做一个`ScrollView`容器实现消息的无限长度 # # 我们的应用将使用中心化的客户端-服务器架构,很多网站和应用都用这种主流的互联网方法论。与去中心化的P2P网络相比,你很快会发现这种方法是多么的容易。 # >这里没有区分互联网与局域网(local area network,LAN),但是两者在底层上没啥关系。但是,如果你要把应用发布到应用商店,你还需要准备很多其他的内容,比如设置一个安全网络服务器,配置防火墙来保证你的代码可以扩展到多核处理器和其他设备。实际上这并没有多可怕,但是仍然需要一些努力。 # ## 实现聊天服务器 # 开始写客户端之前我们先把服务器端做出来。我们用**Twisted**框架来提高效率,通过Python来实现,这样可以避开许多常见的、底层的网络任务。 # >**兼容性提示** # >Twisted目前仍然不能很好的不支持Python3,所以这里得使用Python2.7。其实2to3很容易移植,因为没多少不兼容的设计方式。(不过,我们完全忽视了Unicode相关的问题,因为Python2和Python3的处理方式不同。如果是中文,还是Python3方便。) # # Twisted是一个事件驱动的底层的服务器框架,不像**Node.js**(实际上,Node.js的设计受到Twisted的影响)。与Kivy很类似,事件驱动的架构意味着我们不需要把代码构建一个无限循环,相反我们只要为app绑定大量的事件监听器就行。许多底层的网络处理,都可以通过Twisted方便的实现。 # >和其他Python包一样,用pip就可以安装Twisted: # >**pip install -U twisted** # ### 协议定义 # 现在我们来看下聊天服务器即将使用的通信协议,因为我们的app并不复杂,所以我们不用XMPP那样主流的、功能丰富的程序,我们自己实现一个简单的就行。 # # 我们实现的协议层就是两条信息在客户端和服务器传递——连接服务器(进入聊天室),对其他人说话。服务器会反馈客户端传递的每件事情;服务器自己不会产生任何事件。 # # 我们的协议执行原文传递,和很多其他的应用层如HTTP协议一样。这样做很合理,因为调试和实现起来都很方便。字符串协议比二进制码协议更具扩展性和前瞻性(future-proof)。缺点就是包压缩率底,占用资源多;二进制协议可以更紧凑。不过在这个app讨论这样不太重要,也可以通过压缩技术缓解这个不足(这就是为啥很多服务器都是HTTP协议)。 # # 现在,让我们梳理一下每条消息在应用协议的思路: # # - 连接到服务器的信息只有用户现在是否在聊天室,每次就用一个单词`CONNECT`来检测。这个信息不需要参数化,直接用单词。 # - 在聊天室说话更有趣。有两个参数:用户名和文字信息。让我们把格式定义为`A:B`,`A`就是用户名(我们要求用户名不能包含分号`:`字符)。 # # 根据这个思路,我们写出下面的算法(伪代码): # # ``` # if ':' not in message # then # // it's a CONNECT message # add this connection to user list # else # // it's a chat message # nickname, text := message.split on ':' # for each user in user list # if not the same user: # send "{nickname} said: {text}" # ``` # 对同一个用户的测试就是把向用户自己传递的信息去掉。 # ### 服务器代码 # 有了Twisted的帮助,我们的伪代码可以很直接的写出Python代码。下面就是我们应用的`server.py`文件: # In[ ]: from twisted.internet import protocol, reactor transports = set() class Chat(protocol.Protocol): def dataReceived(self, data): transports.add(self.transport) if ":" not in data: return user, msg = data.split(":", 1) for t in transports: if t is not self.transport: t.write("{0} says: {1}".format(user, msg)) class ChatFactory(protocol.Factory): def buildProtocol(self, addr): return Chat() reactor.listenTCP(9096, ChatFactory()) reactor.run() # ### 设计原理 # 下面的操作流程可以帮助你理解上面的代码: # # - 最后一行`reactor.run()`开启`ChatFactory`服务器监听9096端口 # - 当服务器收到请求,就调用`dataReceived()`响应 # - `dataReceived()`方法就是前面伪代码的实现,会把消息发送给其他用户 # # 每个到客户端的连接构成集合`transports`。我们无条件的把当前的传递`self.transport`加入集合。 # # 然后就是算法的实现。最后,聊天室内除了发消息的每个用户都会收到提示,< **username**>说了<**message text**>。 # >注意我们并没有用`CONNECT`来检查连接的信息。这是按照1980年Jon Postel在TCP说明书里面提出的*网络稳健性(network robustness)*原则设计的:保守的发送,自由的接收。 # >另外通过简化代码,我们还获得了更好的兼容性。在未来要发布新版本客户端时,如果我们给协议增加一个新消息,假设叫`WHARRGARBL`消息,名字看着就很酷。没有崩溃是因为虽然收到来一个格式错误的消息(这是由于版本不匹配),老版本的服务器会直接忽略这些消息继续工作。 # >这些具体的版本兼容性问题可以通过许多策略来纠正。但是,还有更多来自网络尤其是公网的难题,包括用户恶意攻击拖垮服务器等等。所以,实际中并没有服务器非常稳定这种可能。 # ### 服务器测试 # 直接运行Python文件就可以测试服务器: # ```sh # python server.py # ``` # 这个命令的结果不会直接看到。服务器开始运行,等待客户请求。但是,我们还没客户端程序。那怎么测试呢? # # 这种先有服务器还是先有客户端的经典问题有很多方法可以解决——向服务器发信息,然后显示服务器反馈的信息。 # # 处理字符串协议服务器的一个标准化工具就是Telnet,一般都是命令行,没有图像界面。很多操作系统都带有Telnet。在Windows系统中,打开“控制面板|程序和功能|启动或关闭Windows功能”就可以找到Telnet。 # # ![Telnet](kbpic/4.1Telnet.png) # # Telnet有两个参数:服务器地址和端口。为了连接到Telnet,你需要先启动`server.py`,然后再打开命令行输入: # ```sh # telnet 127.0.0.1 9096 # ``` # # 另外,你也可以用`localhost`代替`127.0.0.1`,在`hosts`文件中是默认设置。 # # 现在就可以测试服务器了。你可以根据前面设计流程,向服务器发送内容进行实现测试: # # ``` # CONNECT # User A:Hello, world! # ``` # # 没有出现任何反馈,因为我们没有让服务器向原作者反馈信息。因此,我们需要打开另外一个命令行,然后以一个新的用户登录。就可以看到`User A`发送的信息了。如下图所示: # # ![servertest](kbpic/4.2servertest.png) # # >如果你没法儿正常测试Telnet也甭灰心,这不影响咱们app的顺利完成。 # >如果用Windows的话,给一点小建议:最好给电脑装个Mac OS或Linux,双系统更适合研发工作,推荐使用虚拟机,切换方便。 # # 这样,我们就知道服务器可以正常工作了。现在我们来做客户端系统的GUI。 # ## 屏幕管理器 # 现在我们用一个新概念来设计UI,叫屏幕管理器。用来设计我们的app合适。一共是两个UI状态: # # - 登录界面:包括服务器地址、用户名、登录按钮 # ![uilogin](kbpic/4.3uilogin.png) # - 聊天界面:包括信息显示、信息输入、发送信息按钮和端口服务器按钮 # ![uilogin](kbpic/4.4uichat.png) # # 从理论上说,我们的app界面就是这样。这种UI分离的设计方法涉及到,对不同UI状态里可见的与隐藏的控件的管理。这样可以快速的组合一堆部件,而不要写任何代码。 # # Kivy为我们提供`ScreenManager`来实现UI设计。另外,`ScreenManager`还提供了屏幕切换的动态过程,以及大量的内置转换方式。可以完全通过Kivy语言来实现,不需要任何Python代码。 # 下面让我们来实现,首先建立`chat.kv`文件: # ```yaml # ScreenManager: # Screen: # name: 'login' # # BoxLayout: # # other UI controls -- not shown # Button: # text: 'Connect' # on_press: root.current = 'chatroom' # # Screen: # name: 'chatroom' # # BoxLayout: # # other UI controls -- not shown # Button: # text: 'Disconnect' # on_press: root.current = 'login' # ``` # 这是程序的基本结构:我们建立`ScreenManager`根部件,并为每个状态建立一个`Screen`容器。容器里面有上面看到的布局、按钮。后面我们会继续完善。 # # 代码里面看到还包括`Screen`的按钮。为了切换应用的状态,我们还需要设置`ScreenManager`的`current`属性。 # ### 屏幕切换动作 # 前面提到,屏幕可以通过切换动作的动态显示。Kivy提供了许多切换功能,在`kivy.uix.screenmanager`包里面: # | Transition class name | Visual effect | # |::|::| # | `NoTransition` | 没有动画,直接显示屏幕 | # | `SwapTransition` | 滑到下一屏幕,用上、下、左(默认)、右。 | # | `SwapTransition` | iOS屏幕切换效果 | # |`FadeTransition` | 褪色方式切换 | # |`WipeTransition` | 用1px的遮挡实现平滑的定向切换 | # |`FallOutTransition` | 将旧屏幕缩小到屏幕中间,渐渐透明,再出现新屏幕 | # |`RiseInTransition` | 与`FallOutTransition`完全相反,新屏幕从中间出现,放大直到遮住旧屏幕 | # 在`.lv`文件里面设置这些切换动作时需要注意一旦:切换需要手动导入。 # ```yaml # #:import RiseInTransition kivy.uix.screenmanager.RiseInTransition # ``` # 现在,你就可以配置`ScreenManager`了。注意这些动作都是Python类的实例,所以后面要加括号: # ```yaml # ScreenManager: # transition: RiseInTransition() # ``` # ### 登录界面布局 # 登录界面布局和前一章的录音app类似:用一个`GridLayout`把各个元素按照网格排列。 # # 还没用过的部件就是文本框`TextInput`。Kivy的文本框和按钮基本完全一样,区别就是可以输入文字。默认情况下是多行显示,因为在聊天app里面多行少见(如微信、QQ),所以要设置`multiline`为`False`。 # # 在无键盘设备上运行时,Kivy会调用虚拟键盘,和原生应用一样。下面的代码就是登录界面布局: # # ```yaml # Screen: # name: 'login' # BoxLayout: # orientation: 'vertical' # GridLayout: # Label: # text: 'Server:' # TextInput: # id: server # text: '127.0.0.1' # Label: # text: 'Nickname:' # TextInput: # id: nickname # text: 'Kivy' # Button: # text: 'Connect' # on_press: root.current = 'chatroom' # ``` # # 这里,我们增加了`Server`和`Nickname`两个文本框,对应的标签和按钮。按钮的事件handler还有没有任何功能,后面会实现。 # # 可以让单行的`TextInput`更好看点,我们让文本框里面的文字垂直居中: # ```yaml # : # multiline: False # padding: [10, 0.5 * (self.height – self.line_height)] # ``` # `padding`属性设置了左右的边距都是10,上面和下面的边距是文本框高度与一行文本高度之差的一半。下面就是效果图,可前面app的界面类似: # # ![loginscreen](kbpic/4.5loginscreen.png) # # 现在我们可以写代码来连接服务器了,不过之前我们先把聊天主界面做出来。这样我们就可以直接在上面测试了。 # ### 聊天界面布局 # 聊天界面布局使用了`ScrollView`实现对话长度的切换,因为是第一次说这个空间,下面会详细介绍: # ```yaml # : # text_size: (self.width, None) # Step 1 # halign: 'left' # valign: 'top' # size_hint: (1, None) # Step 2 # height: self.texture_size[1] # Step 3 # ScrollView: # ChatLabel: # text: 'Insert very long text with line\nbreaks' # ``` # # 文字满屏之后,滚动条会出现,类似于在Android和iOS里面看到的。具体的设计流程如下: # # 1. 我们用`text_size`属性设置`Label`子类的宽度,把第二个参数高度设置成`None`,允许无限长度 # 2. 把`size_hint`属性第二个参数设置为`None`,允许无限长度,迫使其高度与它的容器`ScrollView`独立。但是,它的长度会受到上一层的元素的限制,因此不会滚动。 # 3. 设置部件的高度等于`texture_size`属性高度(注意索引都从0开始,因此第二个元素是`texture_size[1]`)。这就迫使`ChatLabel`比包含它的`ScrollView`部件大 # 4. 当`ScrollView`部件发现它的子部件比它的空间大时,滚动条就出现了。这和手机上看到的一样,桌面系统也支持鼠标滚轮操作。 # #### 滚动模式 # 你还可以为`ScrollView`设置滚动的效果来模仿原生平台的特点(不过和原生的效果还是有差别)。可以实现的效果如下: # # - `ScrollEffect`:当触及最底部的时候可以停止滚动,类似于桌面应用常见的功能 # - `DampedScrollEffect`:这是默认的效果,类似于iOS,非常适合移动设备 # - `OpacityScrollEffect`:与`DampedScrollEffect`效果类似,滚动时增加了滚动条的透明度,不会遮挡内容 # # 要使用这些效果,从`kivy.effects`模块导入效果,然后配置到`ScrollView.effect_cls`属性,与`ScreenManager`切换效果类似。本章app不改,就用默认效果,可以自行设置。 # # 把上述内容综合起来,`chat.kv`文件代码如下: # # ```yaml # Screen: # name: 'chatroom' # BoxLayout: # orientation: 'vertical' # Button: # text: 'Disconnect' # on_press: root.current = 'login' # ScrollView: # ChatLabel: # id: chat_logs # text: 'User says: foo\nUser says: bar' # BoxLayout: # height: 90 # orientation: 'horizontal' # padding: 0 # size_hint: (1, None) # TextInput: # id: message # Button: # text: 'Send' # size_hint: (0.3, 1) # ``` # # 最后一行的`size_hint`属性设置了按钮的水平比例为0.3,默认的是1。这就会让发送按钮比文本框小。 # 为了把消息的背景色设置成白色的,我们可以这样: # ```yaml # : # canvas.before: # Color: # rgb: 1, 1, 1 # Rectangle: # pos: self.pos # size: self.size # ``` # # 这就在其他元素绘制之前为`ScrollView`铺上了白色。别忘记了调整一下``类,把背景色设置成浅色背景: # ```yaml # #:import C kivy.utils.get_color_from_hex # # : # color: C('#101010') # ``` # 效果图如下: # ![Chatroomscreen](kbpic/4.6Chatroomscreen.png) # # 这里Disconnect按钮就是断开网络连接。这是下一章的内容,到时候你会发现,用Python实现简单网络程序的难度,与用Kivy建立简单的UI没啥区别。 # ## 连接网络 # 下面我们连接服务器来收发消息,并显示给用户。 # # 首先,我们看一个纯Python实现的聊天客户端,用套接字就可以实现。不过还是推荐用高级工具,如Twisted;如果你对这类知识没有一点儿概念,可能有点小困难,坚持一下就会好,多试几次准明白。 # ### 一个简单Python客户端 # 下面我们将用`readline()`函数读取用户的消息,然后通过`print()`函数显示在命令行上。这和Telnet没啥区别——都是命令行界面显示消息——只是我们从底层的套接字开始做起。 # # 这需要一些标准模块:`socket`,`sys`(`sys.stdin`提供输入文件接口)和`select`模块等待消息出现。新建一个客户端文件`client.py`: # In[ ]: import select, socket, sys # 这个程序没有其他依赖关系;所有平台的Python都支持。 # >不过Windows里面的`select`,由于其代码实现方式不同,不能把文件描述器调整为套接字接受的样式。所以这个客户端就不能运行了,不过这个客户端我们最后也不会用,所以不要担心,如果你用的是Windows。 # # 现在,我们来连接服务然后用`CONNECT`对接: # In[ ]: s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect(("127.0.0.1", 9096)) s.send("CONNECT") # 然后就是等待两类消息,一类是标准输入(用户输入的文字),一类`s`套接字(服务器发送消息)。等待可以用`select.select()`实现: # In[ ]: rlist = (sys.stdin, s) while True: read, write, fail = select.select(rlist, (), ()) for sock in read: if sock == s: # receive message from server data = s.recv(4096) print(data) else: # send message entered by user msg = sock.readline() s.send(msg) # 然后,服务器根据收到的最新数据进行反馈,我们可以把服务器发送的信息显示到屏幕上,或者如果是用户发送的消息就发送到服务器。其实这就是Telnet做的事情,只是缺少错误异常检测部分。 # # 你会发现底层的模块实现客户端并没有我们想象的困难。但是相比高级模块如Twisted,原始套接字代码还是冗长的。但是这里显示了客户端工作的原理,其他任何高级工具都是这里实现的,高级工具也不过是通过底层的套接字实现,然后加上方便使用的API而已。 # # >注意这里我们没有加异常检测部分,因为代码可能增加2-3倍,感兴趣的可以练习一下。 # >网络是非常容易出错的;比其他IO都脆弱。因此,如果你打算做一个类似于Skype那样的商业软件,你的代码里面将充斥异常检测和测试:比如丢包,防火墙问题等等。不论你的架构设计的多么好,网络服务想获得极高的可靠性很难。 # ### 用Twisted实现 # 纯Python代码写的客户端不太适合Kivy,原因就是主循环部分(`while True:`)。要让这个循环与Kivy的事件循环协同运作,还得做些事情。 # # 不过,Twisted的优势可以很好的弥补这一点,实现同样的网络模块可以同时作用于服务器和客户端,使得代码更统一。关键在于Twisted可以与Kivy的事件循环协同运作,首先让我们把Twisted相关模块导入: # In[ ]: from kivy.support import install_twisted_reactor install_twisted_reactor() from twisted.internet import reactor, protocol # 这段代码要放在`main.py`文件的最上面。下面我们用Twisted来实现: # #### ChatClient和ChatClientFactory # 用Twisted实现工作量很少,因为这个框架为网络相关的每一件事情都做了很好的接口,这些类通过简单的连接就可以完成工作。 # # ` ClientFactory`的子类`ChatClientFactory`可以在初始化阶段储存Kivy app的实例,这样我们就可以向它传递事件。代码如下: # In[ ]: class ChatClientFactory(protocol.ClientFactory): protocol = ChatClient def __init__(self, app): self.app = app # `ChatClient`类监听Twisted的`connectionMade`和`dataReceived`事件,然后发送到Kivy app: # In[ ]: class ChatClient(protocol.Protocol): def connectionMade(self): self.transport.write("CONNECT") self.factory.app.on_connect(self.transport) def dataReceived(self, data): self.factory.app.on_message(data) # 注意那个无所不在的`CONNECT`握手信号。 # # 这和前面的套接字写法很不同,是吧?而且和前面的`server.py`很像。但是,我们只是把事件传递给aoo对象,并没有处理事件。 # ### 加入UI # 要看到app的全貌,我们还要把UI也加进来。`chat.kv`文件如下: # ```yaml # Button: # Connect button, found on login screen # text: 'Connect' # on_press: app.connect() # Button: # Disconnect button, on chatroom screen # text: 'Disconnect' # on_press: app.disconnect() # TextInput: # Message input, on chatroom screen # id: message # on_text_validate: app.send_msg() # Button: # Message send button, on chatroom screen # text: 'Send' # on_press: app.send_msg() # ``` # # 注意按钮不会再切换屏幕了,相反它们调用`app`的方法,类似于`ChatClient`事件处理。 # # 完成这些之后,我们需要实现Kivy应用类里面的5个方法:两个是Twisted代码中的服务器生成事件(`on_connect`和`on_message`),三个是用户接口事件(`connect`,`disconnect`和`send_msg`)。这样才能让聊天app真正可以用。 # ### 客户端的设计思路 # 当我们简单写一些程序运行逻辑:从`connect()`到`disconnect()`。 # # 在`connect()`方法里面,我们把**Server**和**Nickname**参数作为用户输入。**Nickname**参数被储存到`self.nick`,Twisted客户端连接到具体的服务器地址: # In[ ]: class ChatApp(App): def connect(self): host = self.root.ids.server.text self.nick = self.root.ids.nickname.text reactor.connectTCP(host, 9096, ChatClientFactory(self)) # 调用`ChatClient.connectionMade()`函数,把控件传递到`on_connect()`方法上。我们将用事件来储存`self.conn`连接,然后切换屏幕。前面提到过,按钮不再直接切换屏幕,而且通过事件handler实现: # In[ ]: # From here on these are methods of the ChatApp class def on_connect(self, conn): self.conn = conn self.root.current = "chatroom" # 现在主要部分就是收发信息。很简单,就是从`TextInput`发信息,把`self.nick`加上,发送给服务器。最后把信息显示出来,并且清空`TextInput`。 # In[ ]: def send_msg(self): msg = self.root.ids.message.text self.conn.write("%s:%s" % (self.nick, msg)) self.root.ids.chat_logs.text += "%s says: %s\n" % (self.nick, msg) self.root.ids.message.text = "" # 接受消息更简单,因为我们不会保持这些内容,所有就是把消息显示到屏幕上: # In[ ]: def on_message(self, msg): self.root.ids.chat_logs.text += msg + "\n" # 最后一个方法就是`disconnect()`:关闭连接,清理所有内容回到初始界面。这样用户就可以重新连接其他服务器了。 # In[ ]: def disconnect(self): if self.conn: self.conn.loseConnection() del self.conn self.root.current = "login" self.root.ids.chat_logs.text = "" # 这样程序就搞定了。 # >提示: # >测试的时候,`server.py`文件应该持续运行,但是我们的app就不能终止连接了。最终结果就是app停留在登录界面,不再调用`on_connect()`,用户也不能到聊天室界面。 # >还有,在Android上面测试的时候,确定你的服务器IP地址,不是`127.0.0.1`,只要局域网设备才这样,在Android设备上不一样。可以用`ifconfig`查询(Windows上是`ipconfig`)。 # ### 客户端交互 # 前面做的Telnet、两个客户端虽然实现方式不同,却可以通信,因为其底层的原理基本一致。 # # 类似于互联网的处理方式:只要你用HTTP协议,相关的服务器和客户端就可以交互:网页服务器、浏览器、搜索引擎等等。 # # 协议是更高级的API,与语言、系统无关,应该选一个流行的用。并不是每个网络开发者都熟悉微软2007年发布的Silverlight协议,但大家都知道1991年发布的HTTP。 # ## 增强视觉体验 # 现在app已经可以运行了,我们把聊天窗口改善一下。可以用Kivy的**BBCode**来修饰。 # # 让我们给每个用户加个颜色,这样方便用户区分所有人。我们同样使用**扁平化UI**的配色方式。 # # 当前用户发送的信息不会从服务器发给自己,是通过客户端代码添加到对话内容里的。所以,我们要把当前用户名加一个固定的颜色。 # In[ ]: colors = ["7F8C8D", "C0392B", "2C3E50", "8E44AD", "27AE60"] class Chat(protocol.Protocol): def connectionMade(self): self.color = colors.pop() colors.insert(0, self.color) # 通过一个无限循序,我们把颜色依次加到用户名上,循环使用。如果你熟悉Python的`itertools`模块,你可以这么写: # In[ ]: import itertools colors = itertools.cycle(("7F8C8D", "C0392B", "2C3E50", "8E44AD", "27AE60")) def connectionMade(self): self.color = colors.next() # next(colors) in Python 3 # 现在,我们再把颜色添加到用户名上,很简单,就是`[b][color]Nickname[/ # color][/b]`。 # In[ ]: for t in transports: if t is not self.transport: t.write("[b][color={}]{}:[/color][/b] {}".format(self.color, user, msg)) # `main.py`里面的客户端也同时更新了。我们还要为当前发消息的用户增加一个固定的颜色: # In[ ]: def send_msg(self): msg = self.root.ids.message.text self.conn.write("%s:%s" % (self.nick, msg)) self.root.ids.chat_logs.text += "[b][color=2980B9]{}:[/color][/b] {}\n".format( self.nick, msg ) # 然后,我们把聊天记录部件`ChatLabel`的`markup`属性设置为`True`: # ```yaml # : # markup: True # ``` # 这样就可以了。 # ## 转义字符处理 # 和HTML一样,用户发送的消息可以会出现转义字符。比如BBCode之类的符号。要解决这个问题,我们可以用Kivy的`kivy.utils.escape_markup`来解决。但是还不是很完整,我们可以稍微调整一下: # In[ ]: def esc_markup(msg): return msg.replace("&", "&").replace("[", "&bl;").replace("]", "&br;") # 这样所有的Kivy里面的转义字符转变成HTML字符替代,这样遇到这些字符时就会不会发生转义。在`server.py`文件里面,相应的代码需要改变: # In[ ]: t.write("[b][color={}]{}:[/color][/b] {}".format(self.color, user, esc_markup(msg))) # 在`main.py`里面,实现是类似的: # In[ ]: self.root.ids.chat_logs.text += "[b][color=2980B9]{}:[/color][/b] {}\n".format( self.nick, esc_markup(msg) ) # 现在bug修复了,用户可以安全的发从BBCode消息了。 # # ![chatlast](kbpic/4.7chatlast.png) # # >其实这类bug在互联网产品中很常见。类似于跨站脚本(cross-site # scripting,XSS),可以造成比修改字体更恐怖的结果。 # >不要忘了净化所有产品的用户输入,因为用户一不小心用了命令行就麻烦了。 # ### 后续任务 # 这些都只是开始,还有很多必须的事情没做。比如用户名唯一,没有历史记录和离线消息的支持。如果网络质量不好的话,消息总会丢失。 # # 但是更重要的是,这些问题都可以解决,我们已经有了产品原型。在产品开始阶段,原型是非常具有吸引力,先让轮子转起来,就有了动力;如果你因为好玩而编程,这种感觉更明显,因为你看到了一个可以使用的产品了(相反如果只是一块块代码,不能使用,那感觉很糟糕)。 # ## 总结 # 这一章我们看到了CS架构的应用开发(其实就网络编程)其实也不复杂。甚至底层的套接字代码也很容易搞定。 # # 当然,涉及到网络时会遇到很多灰色地带不容易搞定。包括高时延的处理,中断连接的恢复和多节点的数据同步等(尤其是点对点或多主机时,每一个机器只有一部分数据)。 # # 另一个目前比较新的网络问题就是政治方面的,政府已经开始实施互联网管制,包括出于安全的原因(比如,封杀恐怖主义网络资源)到完全无厘头(封杀教育网站像维基百科,主要的新闻网站或视频游戏网站)。这种连接问题会产生很高的间接伤害,如果CDN(content delivery network)挂了,很多使用CDN链接的网站就不能正常显示了。你懂的。 # # 但是,只要踏踏实实的坚持下去,一定可以克服重重困难把优质产品发布给客户。而且,Python丰富的特性可以减轻你的负担,本章的聊天app已经充分体现了这点:很多底层的细节都可以通过Kivy和Twisted轻松搞定。 # # 互联网领域有无限可能,永无止境。下一章我们继续网络相关的尝试,敬请期待。