<jupyter_start><jupyter_text>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`文件：<jupyter_code>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()<jupyter_output><empty_output><jupyter_text>设计原理 下面的操作流程可以帮助你理解上面的代码：- 最后一行`reactor.run()`开启`ChatFactory`服务器监听9096端口- 当服务器收到请求，就调用`dataReceived()`响应- `dataReceived()`方法就是前面伪代码的实现，会把消息发送给其他用户每个到客户端的连接构成集合`transports`。我们无条件的把当前的传递`self.transport`加入集合。然后就是算法的实现。最后，聊天室内除了发消息的每个用户都会收到提示，说了。>注意我们并没有用`CONNECT`来检查连接的信息。这是按照1980年<NAME>在TCP说明书里面提出的*网络稳健性（network robustness）*原则设计的：保守的发送，自由的接收。>另外通过简化代码，我们还获得了更好的兼容性。在未来要发布新版本客户端时，如果我们给协议增加一个新消息，假设叫`WHARRGARBL`消息，名字看着就很酷。没有崩溃是因为虽然收到来一个格式错误的消息（这是由于版本不匹配），老版本的服务器会直接忽略这些消息继续工作。>这些具体的版本兼容性问题可以通过许多策略来纠正。但是，还有更多来自网络尤其是公网的难题，包括用户恶意攻击拖垮服务器等等。所以，实际中并没有服务器非常稳定这种可能。  服务器测试 直接运行Python文件就可以测试服务器：```shpython server.py```这个命令的结果不会直接看到。服务器开始运行，等待客户请求。但是，我们还没客户端程序。那怎么测试呢？这种先有服务器还是先有客户端的经典问题有很多方法可以解决——向服务器发信息，然后显示服务器反馈的信息。处理字符串协议服务器的一个标准化工具就是Telnet，一般都是命令行，没有图像界面。很多操作系统都带有Telnet。在Windows系统中，打开“控制面板|程序和功能|启动或关闭Windows功能”就可以找到Telnet。![Telnet](kbpic/4.1Telnet.png)Telnet有两个参数：服务器地址和端口。为了连接到Telnet，你需要先启动`server.py`，然后再打开命令行输入：```shtelnet 127.0.0.1 9096```另外，你也可以用`localhost`代替`127.0.0.1`，在`hosts`文件中是默认设置。现在就可以测试服务器了。你可以根据前面设计流程，向服务器发送内容进行实现测试：```CONNECTUser 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`文件：```yamlScreenManager:    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类的实例，所以后面要加括号：```yamlScreenManager:    transition: RiseInTransition()```  登录界面布局 登录界面布局和前一章的录音app类似：用一个`GridLayout`把各个元素按照网格排列。还没用过的部件就是文本框`TextInput`。Kivy的文本框和按钮基本完全一样，区别就是可以输入文字。默认情况下是多行显示，因为在聊天app里面多行少见（如微信、QQ），所以要设置`multiline`为`False`。在无键盘设备上运行时，Kivy会调用虚拟键盘，和原生应用一样。下面的代码就是登录界面布局：```yamlScreen:    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 3ScrollView:    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`文件代码如下：```yamlScreen:    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`：<jupyter_code>import select, socket, sys<jupyter_output><empty_output><jupyter_text>这个程序没有其他依赖关系；所有平台的Python都支持。>不过Windows里面的`select`，由于其代码实现方式不同，不能把文件描述器调整为套接字接受的样式。所以这个客户端就不能运行了，不过这个客户端我们最后也不会用，所以不要担心，如果你用的是Windows。现在，我们来连接服务然后用`CONNECT`对接：<jupyter_code>s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 9096))
s.send("CONNECT")<jupyter_output><empty_output><jupyter_text>然后就是等待两类消息，一类是标准输入（用户输入的文字），一类`s`套接字（服务器发送消息）。等待可以用`select.select()`实现：<jupyter_code>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)<jupyter_output><empty_output><jupyter_text>然后，服务器根据收到的最新数据进行反馈，我们可以把服务器发送的信息显示到屏幕上，或者如果是用户发送的消息就发送到服务器。其实这就是Telnet做的事情，只是缺少错误异常检测部分。你会发现底层的模块实现客户端并没有我们想象的困难。但是相比高级模块如Twisted，原始套接字代码还是冗长的。但是这里显示了客户端工作的原理，其他任何高级工具都是这里实现的，高级工具也不过是通过底层的套接字实现，然后加上方便使用的API而已。>注意这里我们没有加异常检测部分，因为代码可能增加2-3倍，感兴趣的可以练习一下。>网络是非常容易出错的；比其他IO都脆弱。因此，如果你打算做一个类似于Skype那样的商业软件，你的代码里面将充斥异常检测和测试：比如丢包，防火墙问题等等。不论你的架构设计的多么好，网络服务想获得极高的可靠性很难。  用Twisted实现 纯Python代码写的客户端不太适合Kivy，原因就是主循环部分（`while True:`）。要让这个循环与Kivy的事件循环协同运作，还得做些事情。不过，Twisted的优势可以很好的弥补这一点，实现同样的网络模块可以同时作用于服务器和客户端，使得代码更统一。关键在于Twisted可以与Kivy的事件循环协同运作，首先让我们把Twisted相关模块导入：<jupyter_code>from kivy.support import install_twisted_reactor

install_twisted_reactor()

from twisted.internet import reactor, protocol<jupyter_output><empty_output><jupyter_text>这段代码要放在`main.py`文件的最上面。下面我们用Twisted来实现：  ChatClient和ChatClientFactory 用Twisted实现工作量很少，因为这个框架为网络相关的每一件事情都做了很好的接口，这些类通过简单的连接就可以完成工作。` ClientFactory`的子类`ChatClientFactory`可以在初始化阶段储存Kivy app的实例，这样我们就可以向它传递事件。代码如下：<jupyter_code>class ChatClientFactory(protocol.ClientFactory):
    protocol = ChatClient

    def __init__(self, app):
        self.app = app<jupyter_output><empty_output><jupyter_text>`ChatClient`类监听Twisted的`connectionMade`和`dataReceived`事件，然后发送到Kivy app：<jupyter_code>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)<jupyter_output><empty_output><jupyter_text>注意那个无所不在的`CONNECT`握手信号。这和前面的套接字写法很不同，是吧？而且和前面的`server.py`很像。但是，我们只是把事件传递给aoo对象，并没有处理事件。  加入UI 要看到app的全貌，我们还要把UI也加进来。`chat.kv`文件如下：```yamlButton:  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客户端连接到具体的服务器地址：<jupyter_code>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))<jupyter_output><empty_output><jupyter_text>调用`ChatClient.connectionMade()`函数，把控件传递到`on_connect()`方法上。我们将用事件来储存`self.conn`连接，然后切换屏幕。前面提到过，按钮不再直接切换屏幕，而且通过事件handler实现：<jupyter_code># From here on these are methods of the ChatApp class
def on_connect(self, conn):
    self.conn = conn
    self.root.current = "chatroom"<jupyter_output><empty_output><jupyter_text>现在主要部分就是收发信息。很简单，就是从`TextInput`发信息，把`self.nick`加上，发送给服务器。最后把信息显示出来，并且清空`TextInput`。<jupyter_code>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 = ""<jupyter_output><empty_output><jupyter_text>接受消息更简单，因为我们不会保持这些内容，所有就是把消息显示到屏幕上：<jupyter_code>def on_message(self, msg):
    self.root.ids.chat_logs.text += msg + "\n"<jupyter_output><empty_output><jupyter_text>最后一个方法就是`disconnect()`：关闭连接，清理所有内容回到初始界面。这样用户就可以重新连接其他服务器了。<jupyter_code>def disconnect(self):
    if self.conn:
        self.conn.loseConnection()
        del self.conn
    self.root.current = "login"
    self.root.ids.chat_logs.text = ""<jupyter_output><empty_output><jupyter_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**的配色方式。当前用户发送的信息不会从服务器发给自己，是通过客户端代码添加到对话内容里的。所以，我们要把当前用户名加一个固定的颜色。<jupyter_code>colors = ["7F8C8D", "C0392B", "2C3E50", "8E44AD", "27AE60"]


class Chat(protocol.Protocol):
    def connectionMade(self):
        self.color = colors.pop()
        colors.insert(0, self.color)<jupyter_output><empty_output><jupyter_text>通过一个无限循序，我们把颜色依次加到用户名上，循环使用。如果你熟悉Python的`itertools`模块，你可以这么写：<jupyter_code>import itertools

colors = itertools.cycle(("7F8C8D", "C0392B", "2C3E50", "8E44AD", "27AE60"))


def connectionMade(self):
    self.color = colors.next()
    # next(colors) in Python 3<jupyter_output><empty_output><jupyter_text>现在，我们再把颜色添加到用户名上，很简单，就是`[b][color]Nickname[/color][/b]`。<jupyter_code>for t in transports:
    if t is not self.transport:
        t.write("[b][color={}]{}:[/color][/b] {}".format(self.color, user, msg))<jupyter_output><empty_output><jupyter_text>`main.py`里面的客户端也同时更新了。我们还要为当前发消息的用户增加一个固定的颜色：<jupyter_code>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
    )<jupyter_output><empty_output><jupyter_text>然后，我们把聊天记录部件`ChatLabel`的`markup`属性设置为`True`：```yaml:    markup: True```这样就可以了。  转义字符处理 和HTML一样，用户发送的消息可以会出现转义字符。比如BBCode之类的符号。要解决这个问题，我们可以用Kivy的`kivy.utils.escape_markup`来解决。但是还不是很完整，我们可以稍微调整一下：<jupyter_code>def esc_markup(msg):
    return msg.replace("&", "&amp;").replace("[", "&bl;").replace("]", "&br;")<jupyter_output><empty_output><jupyter_text>这样所有的Kivy里面的转义字符转变成HTML字符替代，这样遇到这些字符时就会不会发生转义。在`server.py`文件里面，相应的代码需要改变：<jupyter_code>t.write("[b][color={}]{}:[/color][/b] {}".format(self.color, user, esc_markup(msg)))<jupyter_output><empty_output><jupyter_text>在`main.py`里面，实现是类似的：<jupyter_code>self.root.ids.chat_logs.text += "[b][color=2980B9]{}:[/color][/b] {}\n".format(
    self.nick, esc_markup(msg)
)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.13012852199703412, 'fraction_numerical': 0.011925358378645575, 'mean_word_length': 6.906692721055203, 'pattern_counts': {'":': 2, '<': 78, '<?xml version=': 0, '>': 96, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 5, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '19', 'dir': 'jupyter-structured-clean-dedup', 'id': '532913', 'license': 'Apache-2.0', 'n_tokens_mistral': 9756, 'n_tokens_neox': 10626, 'n_words': 572, 'path': '_notebooks/2019-05-01-kivy-ch4-chat-app.ipynb', 'repo_name': 'CyberSciTech/air', 'size': '16188'}