聊天室程序编写小结

老师布置作业编写一个聊天室软件,本着我不喜VC++、不碰Java、拒绝C的原则,只剩下Python这条路供我走下去了。于是操起Vi,背上Python API,wxPython API/doc,Twisted API/doc,开始写我的PyChatRoom。经过艰苦卓绝的战斗,终于我解决了众多名为error,bug的敌人,来到目的地–它可以基本无错的运行起来!————废话说完,进入正题。

考虑到聊天室的多人聊天需求,自然想到服务器端需要多线程处理。但我自知自己在多线程上经验甚少,而且对服务器程序了解更少,那么索性就使用现成的技术吧。Twisted是一个Python实现的异步网络编程框架,有了它我就不需要为多线程而烦恼了:Twisted已经完成了一切操作!Twisted的入门教程可以看这里:。当然我们不能忘记最强大的官方文件和示例:http://twistedmatrix.com/trac/

鄙人不才,看了2个入门教程还是没有写服务器的头绪,看了官方教程才有所领悟:因为官方的示例正好是一个聊天室啦!于是在此基础上进行二次开发,使之成为满足我需求的服务器端程序。我的服务器程序使用示例构架,按照标准的Twisted方式编写。首先是创建一个自己的Protocol:

1
2
3
4
5
6
7
8
9
10
11
12
class ChatProtocol(Protocol):
    def __init__(self):
        pass
 
    def connectionMade(self):
        pass
 
    def connectionLost(self,reason):
        pass
 
    def dataReceived(self,data):
        pass

第二步创建一个ServerFactory,用于处理每一个Protocol

1
2
3
4
5
class ChatServerFactory(ServerFactory):
    def __init__(self):
        pass
    def buildProtocol(self,addr):
        pass

第三步实现一个reactor,它是Twisted真正的关键:

1
2
reactor.listenTCP(port,ChatServerFactory())
reactor.run()

Twisted的构架看上去很棒,而且它确实很棒!虽然它看上去很简单,但是即使我完成了这个聊天室服务器端的程序,我还是认为我不会写Twisted程序。

好,其他问题咱们放到最后说。下面来看看客户端程序吧。

这次我想让我的程序看上去有个清晰的结构,因为我发觉年初我写的第一个Python程序的结构实在太糟糕了:所有的代码都写在了一个文件当中!足足1500行代码都堆叠在一起,又脏又乱!最丑的要属SQL处理代码了,足足有十个独立的函数,那时我没有写一个类来统一它们,实在是太糟糕了。年中为了写一个串口程序作业而看到了PySerial模块作者写的那个示例代码,真让我体会到了阅读拥有清晰结构的程序代码时那种愉悦的心情!这次在编写客户端时我就努力让自己的程序结构变得清晰可读。虽然没达到非常棒的效果,但比年初那个代码的结构是好了很多!

我把程序分成三块来处理,分别是三个不同的类:Chat类、MainFrame类和Login类。其中MainFrame类是聊天客户端的主界面,Login类从名字上就可以判断出它是用于用等的,而Chat类起了衔接MainFrame和Login的功能。socket的初始化和等入的验证也都是在Chat中完成的。对于客户端的结构部分就不多叙述了,它的代码可以在本文末尾处下载到。我想直接进入问题讨论部分。

面对新的技术,第一次接触总会遇到很多问题。网络编程我本就接触不多,这次果然遇到多多的问题。有些技术问题可以通过查月资料来解决,而有些则属于特定问题,不论是好的方法还是不好的方法,最终还是解决了吧。本文只列出我遇到的最大问题。

问题:socket buffer中的数据不是立即发送,导致多条transport.write()中的数据在一个数据包中发送。好吧,这个问题还是我自己在设定协议时造成的麻烦。最初我并没考虑很多,对socket机制也了解不够。所以我就按照一次recv产生一次数据处理的方法来编写协议代码了。而socket对buffer的管理机制应该是当buffer中数据达到一定量后发送一个数据包清空buffer,或者是存在一个计数器在buffer没有达到一定数据量的情况下当计数器计数完成则强制发送一个数据包清空buffer。Python的socket中貌似没有提供可令程序员强制flush buffer的方法(可能有,但我不知道),那么同样的,Twisted这个基于Python的框架自然也没有提供类似的方法了。其他语言比如Java中是有提供这类操作的方法。那么我只能通过其他方法来解决这个问题了。这个问题我在2中情况下都有遇到:

情况一是欢迎信息和登入之后的成员更新信息被封装在一个数据包中发送了,在我这种数据处理方法中直接把成员更新数据给当作欢迎信息发送到屏幕了。我的处理方法是把欢迎信息写成独立函数,把成员更新写成独立函数,然后在收到欢迎信息后客户端返回一个值,而收到更远更新之后客户端也返回一个值。

1
2
3
4
5
6
7
8
9
10
11
#服务器代码
def GetReady(self):
    """Client is ready. Server send welcome info."""
    self.transport.write("99Welcome to py chatroom")
 
def update_Member(self):
    """update the member list."""
    for name,protocol in self.users.iteritems():
        if not name == self.name:
            self.transport.writeSequence('03'+name)
            protocol.transport.write('03'+self.name)

或许是Python的效率救了我,让我能够用这个方法解决问题。有时候慢也有好处吗?再想想这个方法应该是有点问题的,只是在我自己这种测试中没暴露出来而已。

情况二:当聊天室同时在线人数在3人及以上时成员更新又出现了成员更新数据被封装成一个数据包发送。这次我的解决方法时从客户端入手,手动分割这个包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#客户端代码
if data[:2] == '03':
    #new user coming message
    new_user_count = len(data) / 18
    while new_user_count:
        new_user = data[:18]
        if len(data) > 18:
            data = data[18:]
        name = self.PrintName(new_user)
        if not name[2:] in self.member:
            self.memberText.AppendText(name[2:]+'\n')
            self.member.append(name[2:])
            self.memberBox.Append(name[2:])
            self.messageText.AppendText(name[2:]+'加入了聊天室\n')
        new_user_count = new_user_count - 1
    self.socket.send('96')

这个方法时不得已的,因为我实在想不出一个在服务器端解决的方法。

其实要真正解决这个问题,还是要从协议上解决。我应该在每个数据段中增加一个结束标识符,而客户端每次recv之后按照结束标识符来分割数据段来处理。这个方法就能彻底解决此问题。

从这次编程中我所获得的启示:

  1. 自己很懒,花了近一个月才完成。
  2. 网络编程中要正确处理自己的协议:要有一个好的协议头和一个正确的协议结束标识符,这点非常重要!
  3. 更多更全面的测试,这是完成一个程序所必须的步骤。
  4. 更好的注释,更多的草稿,让自己看的懂。
  5. 之前我还想到什么来的,为什么现在想不起来了?

新问题:刚才测试了以下,客户端忘记强制强制指定为utf-8编码,导致中文无法输入。好,我懒的更改了。

Python小白,欢迎吐槽。(我还在用print来debug!!!)

最后是下载源代码下载链接:http://sdrv.ms/YYS6FC    (使用MIT License)。

服务器端运行环境:Python 2.7,Twisted 12.2

客户端运行环境:Python 2.7,wxPython 2.8

 

2 thoughts on “聊天室程序编写小结”

  1. 最近也在写wxPython socket程序,中文资料比较少……..非常受用,多谢了

    1. 不客气。其实看看自己的代码,总觉得对socket处理这一块写的不好。

Leave a Reply to Tristan Cancel reply