实现聊天app的客户端-服务器架构,利用Twisted框架实现服务器
前面我们尝试过单一平台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相比之下已经很新了。
本章教学大纲如下:
ScreenManager
更容易的实现UIScrollView
容器实现消息的无限长度我们的应用将使用中心化的客户端-服务器架构,很多网站和应用都用这种主流的互联网方法论。与去中心化的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
文件:
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文件就可以测试服务器:
python server.py
这个命令的结果不会直接看到。服务器开始运行,等待客户请求。但是,我们还没客户端程序。那怎么测试呢?
这种先有服务器还是先有客户端的经典问题有很多方法可以解决——向服务器发信息,然后显示服务器反馈的信息。
处理字符串协议服务器的一个标准化工具就是Telnet,一般都是命令行,没有图像界面。很多操作系统都带有Telnet。在Windows系统中,打开“控制面板|程序和功能|启动或关闭Windows功能”就可以找到Telnet。
Telnet有两个参数:服务器地址和端口。为了连接到Telnet,你需要先启动server.py
,然后再打开命令行输入:
telnet 127.0.0.1 9096
另外,你也可以用localhost
代替127.0.0.1
,在hosts
文件中是默认设置。
现在就可以测试服务器了。你可以根据前面设计流程,向服务器发送内容进行实现测试:
CONNECT
User A:Hello, world!
没有出现任何反馈,因为我们没有让服务器向原作者反馈信息。因此,我们需要打开另外一个命令行,然后以一个新的用户登录。就可以看到User A
发送的信息了。如下图所示:
如果你没法儿正常测试Telnet也甭灰心,这不影响咱们app的顺利完成。 如果用Windows的话,给一点小建议:最好给电脑装个Mac OS或Linux,双系统更适合研发工作,推荐使用虚拟机,切换方便。
这样,我们就知道服务器可以正常工作了。现在我们来做客户端系统的GUI。
现在我们用一个新概念来设计UI,叫屏幕管理器。用来设计我们的app合适。一共是两个UI状态:
从理论上说,我们的app界面就是这样。这种UI分离的设计方法涉及到,对不同UI状态里可见的与隐藏的控件的管理。这样可以快速的组合一堆部件,而不要写任何代码。
Kivy为我们提供ScreenManager
来实现UI设计。另外,ScreenManager
还提供了屏幕切换的动态过程,以及大量的内置转换方式。可以完全通过Kivy语言来实现,不需要任何Python代码。
下面让我们来实现,首先建立chat.kv
文件:
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
文件里面设置这些切换动作时需要注意一旦:切换需要手动导入。
#:import RiseInTransition kivy.uix.screenmanager.RiseInTransition
现在,你就可以配置ScreenManager
了。注意这些动作都是Python类的实例,所以后面要加括号:
ScreenManager:
transition: RiseInTransition()
登录界面布局和前一章的录音app类似:用一个GridLayout
把各个元素按照网格排列。
还没用过的部件就是文本框TextInput
。Kivy的文本框和按钮基本完全一样,区别就是可以输入文字。默认情况下是多行显示,因为在聊天app里面多行少见(如微信、QQ),所以要设置multiline
为False
。
在无键盘设备上运行时,Kivy会调用虚拟键盘,和原生应用一样。下面的代码就是登录界面布局:
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
更好看点,我们让文本框里面的文字垂直居中:
<TextInput>:
multiline: False
padding: [10, 0.5 * (self.height – self.line_height)]
padding
属性设置了左右的边距都是10,上面和下面的边距是文本框高度与一行文本高度之差的一半。下面就是效果图,可前面app的界面类似:
现在我们可以写代码来连接服务器了,不过之前我们先把聊天主界面做出来。这样我们就可以直接在上面测试了。
聊天界面布局使用了ScrollView
实现对话长度的切换,因为是第一次说这个空间,下面会详细介绍:
<ChatLabel@Label>:
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里面看到的。具体的设计流程如下:
text_size
属性设置Label
子类的宽度,把第二个参数高度设置成None
,允许无限长度size_hint
属性第二个参数设置为None
,允许无限长度,迫使其高度与它的容器ScrollView
独立。但是,它的长度会受到上一层的元素的限制,因此不会滚动。texture_size
属性高度(注意索引都从0开始,因此第二个元素是texture_size[1]
)。这就迫使ChatLabel
比包含它的ScrollView
部件大ScrollView
部件发现它的子部件比它的空间大时,滚动条就出现了。这和手机上看到的一样,桌面系统也支持鼠标滚轮操作。你还可以为ScrollView
设置滚动的效果来模仿原生平台的特点(不过和原生的效果还是有差别)。可以实现的效果如下:
ScrollEffect
:当触及最底部的时候可以停止滚动,类似于桌面应用常见的功能DampedScrollEffect
:这是默认的效果,类似于iOS,非常适合移动设备OpacityScrollEffect
:与DampedScrollEffect
效果类似,滚动时增加了滚动条的透明度,不会遮挡内容要使用这些效果,从kivy.effects
模块导入效果,然后配置到ScrollView.effect_cls
属性,与ScreenManager
切换效果类似。本章app不改,就用默认效果,可以自行设置。
把上述内容综合起来,chat.kv
文件代码如下:
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。这就会让发送按钮比文本框小。
为了把消息的背景色设置成白色的,我们可以这样:
<ScrollView>:
canvas.before:
Color:
rgb: 1, 1, 1
Rectangle:
pos: self.pos
size: self.size
这就在其他元素绘制之前为ScrollView
铺上了白色。别忘记了调整一下<ChatLabel>
类,把背景色设置成浅色背景:
#:import C kivy.utils.get_color_from_hex
<ChatLabel@Label>:
color: C('#101010')
效果图如下:
这里Disconnect按钮就是断开网络连接。这是下一章的内容,到时候你会发现,用Python实现简单网络程序的难度,与用Kivy建立简单的UI没啥区别。
下面我们连接服务器来收发消息,并显示给用户。
首先,我们看一个纯Python实现的聊天客户端,用套接字就可以实现。不过还是推荐用高级工具,如Twisted;如果你对这类知识没有一点儿概念,可能有点小困难,坚持一下就会好,多试几次准明白。
下面我们将用readline()
函数读取用户的消息,然后通过print()
函数显示在命令行上。这和Telnet没啥区别——都是命令行界面显示消息——只是我们从底层的套接字开始做起。
这需要一些标准模块:socket
,sys
(sys.stdin
提供输入文件接口)和select
模块等待消息出现。新建一个客户端文件client.py
:
import select, socket, sys
这个程序没有其他依赖关系;所有平台的Python都支持。
不过Windows里面的
select
,由于其代码实现方式不同,不能把文件描述器调整为套接字接受的样式。所以这个客户端就不能运行了,不过这个客户端我们最后也不会用,所以不要担心,如果你用的是Windows。
现在,我们来连接服务然后用CONNECT
对接:
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 9096))
s.send("CONNECT")
然后就是等待两类消息,一类是标准输入(用户输入的文字),一类s
套接字(服务器发送消息)。等待可以用select.select()
实现:
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那样的商业软件,你的代码里面将充斥异常检测和测试:比如丢包,防火墙问题等等。不论你的架构设计的多么好,网络服务想获得极高的可靠性很难。
纯Python代码写的客户端不太适合Kivy,原因就是主循环部分(while True:
)。要让这个循环与Kivy的事件循环协同运作,还得做些事情。
不过,Twisted的优势可以很好的弥补这一点,实现同样的网络模块可以同时作用于服务器和客户端,使得代码更统一。关键在于Twisted可以与Kivy的事件循环协同运作,首先让我们把Twisted相关模块导入:
from kivy.support import install_twisted_reactor
install_twisted_reactor()
from twisted.internet import reactor, protocol
这段代码要放在main.py
文件的最上面。下面我们用Twisted来实现:
用Twisted实现工作量很少,因为这个框架为网络相关的每一件事情都做了很好的接口,这些类通过简单的连接就可以完成工作。
ClientFactory
的子类ChatClientFactory
可以在初始化阶段储存Kivy app的实例,这样我们就可以向它传递事件。代码如下:
class ChatClientFactory(protocol.ClientFactory):
protocol = ChatClient
def __init__(self, app):
self.app = app
ChatClient
类监听Twisted的connectionMade
和dataReceived
事件,然后发送到Kivy app:
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对象,并没有处理事件。
要看到app的全貌,我们还要把UI也加进来。chat.kv
文件如下:
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客户端连接到具体的服务器地址:
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实现:
# 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
。
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 = ""
接受消息更简单,因为我们不会保持这些内容,所有就是把消息显示到屏幕上:
def on_message(self, msg):
self.root.ids.chat_logs.text += msg + "\n"
最后一个方法就是disconnect()
:关闭连接,清理所有内容回到初始界面。这样用户就可以重新连接其他服务器了。
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的配色方式。
当前用户发送的信息不会从服务器发给自己,是通过客户端代码添加到对话内容里的。所以,我们要把当前用户名加一个固定的颜色。
colors = ["7F8C8D", "C0392B", "2C3E50", "8E44AD", "27AE60"]
class Chat(protocol.Protocol):
def connectionMade(self):
self.color = colors.pop()
colors.insert(0, self.color)
通过一个无限循序,我们把颜色依次加到用户名上,循环使用。如果你熟悉Python的itertools
模块,你可以这么写:
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]
。
for t in transports:
if t is not self.transport:
t.write("[b][color={}]{}:[/color][/b] {}".format(self.color, user, msg))
main.py
里面的客户端也同时更新了。我们还要为当前发消息的用户增加一个固定的颜色:
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
:
<ChatLabel@Label>:
markup: True
这样就可以了。
和HTML一样,用户发送的消息可以会出现转义字符。比如BBCode之类的符号。要解决这个问题,我们可以用Kivy的kivy.utils.escape_markup
来解决。但是还不是很完整,我们可以稍微调整一下:
def esc_markup(msg):
return msg.replace("&", "&").replace("[", "&bl;").replace("]", "&br;")
这样所有的Kivy里面的转义字符转变成HTML字符替代,这样遇到这些字符时就会不会发生转义。在server.py
文件里面,相应的代码需要改变:
t.write("[b][color={}]{}:[/color][/b] {}".format(self.color, user, esc_markup(msg)))
在main.py
里面,实现是类似的:
self.root.ids.chat_logs.text += "[b][color=2980B9]{}:[/color][/b] {}\n".format(
self.nick, esc_markup(msg)
)
现在bug修复了,用户可以安全的发从BBCode消息了。
其实这类bug在互联网产品中很常见。类似于跨站脚本(cross-site
scripting,XSS),可以造成比修改字体更恐怖的结果。
不要忘了净化所有产品的用户输入,因为用户一不小心用了命令行就麻烦了。
这些都只是开始,还有很多必须的事情没做。比如用户名唯一,没有历史记录和离线消息的支持。如果网络质量不好的话,消息总会丢失。
但是更重要的是,这些问题都可以解决,我们已经有了产品原型。在产品开始阶段,原型是非常具有吸引力,先让轮子转起来,就有了动力;如果你因为好玩而编程,这种感觉更明显,因为你看到了一个可以使用的产品了(相反如果只是一块块代码,不能使用,那感觉很糟糕)。
这一章我们看到了CS架构的应用开发(其实就网络编程)其实也不复杂。甚至底层的套接字代码也很容易搞定。
当然,涉及到网络时会遇到很多灰色地带不容易搞定。包括高时延的处理,中断连接的恢复和多节点的数据同步等(尤其是点对点或多主机时,每一个机器只有一部分数据)。
另一个目前比较新的网络问题就是政治方面的,政府已经开始实施互联网管制,包括出于安全的原因(比如,封杀恐怖主义网络资源)到完全无厘头(封杀教育网站像维基百科,主要的新闻网站或视频游戏网站)。这种连接问题会产生很高的间接伤害,如果CDN(content delivery network)挂了,很多使用CDN链接的网站就不能正常显示了。你懂的。
但是,只要踏踏实实的坚持下去,一定可以克服重重困难把优质产品发布给客户。而且,Python丰富的特性可以减轻你的负担,本章的聊天app已经充分体现了这点:很多底层的细节都可以通过Kivy和Twisted轻松搞定。
互联网领域有无限可能,永无止境。下一章我们继续网络相关的尝试,敬请期待。