聊天室程序编写小结

老师布置作业编写一个聊天室软件,本着我不喜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

 

wxPython界面布局小经验

年初第一次做wxPython程序的时候都是使用size和position参数对界面元素进行了固化操作。最近看《Python基础教程(第2版)》时发现了一个更好的界面布局方法:使用proportion参数使得界面元素自行根据窗口大小进行调整,再配合与flag中各类Style,就可以轻松写出布局干净的程序界面。

参数:proportion

功能:根据窗口改变大小时所分配的空间设置界面元素比例。

所属方法:wx.BoxSizer

 

参数:flag

功能:设置界面元素的Style

所属方法:wx.BoxSizer

 

下面时一段小例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/usr/bin/env python
#coding:utf-8
 
import wx
import sys
 
class LoginDialog(wx.Dialog):
    def __init__(self):
        wx.Dialog.__init__(self,None,title="Login",size=(280,140))
        self.InitUI()
        self.ShowModal()
 
    def InitUI(self):
        vbox = wx.BoxSizer(wx.VERTICAL)
 
        nameLabel = wx.StaticText(self,label="      Name:")
        nameText = wx.TextCtrl(self)
        ipLabel = wx.StaticText(self,label="Server IP:")
        ipText = wx.TextCtrl(self)
 
        loginButton = wx.Button(self,label="Login")
        exitButton = wx.Button(self,label="Exit")
 
        loginButton.Bind(wx.EVT_BUTTON,self.OnLogin)
        exitButton.Bind(wx.EVT_BUTTON,self.OnExit)
 
        hbox_1 = wx.BoxSizer()
        hbox_1.Add(nameLabel,proportion=0,flag=wx.ALL | wx.EXPAND | wx.ALIGN_LEFT,border=2)
        hbox_1.Add(nameText,proportion=2,flag=wx.ALL | wx.EXPAND |wx.ALIGN_CENTER,border=2)
 
        hbox_2 = wx.BoxSizer()
        hbox_2.Add(ipLabel,proportion=0,flag=wx.ALL | wx.EXPAND | wx.ALIGN_LEFT,border=2)
        hbox_2.Add(ipText,proportion=2,flag=wx.ALL | wx.EXPAND | wx.ALIGN_CENTER,border=2)
 
        hbox_3 = wx.BoxSizer()
        hbox_3.Add(loginButton,proportion=0,flag=wx.ALL | wx.EXPAND | wx.ALIGN_CENTER,border=2)
        hbox_3.Add(exitButton,proportion=0,flag=wx.ALL | wx.EXPAND | wx.ALIGN_CENTER,border=2)
 
        vbox.Add(hbox_1,proportion=1,flag=wx.ALL | wx.EXPAND,border=2)
        vbox.Add(hbox_2,proportion=1,flag=wx.ALL | wx.EXPAND,border=2)
        vbox.Add(hbox_3,proportion=0,flag=wx.ALL | wx.ALIGN_CENTER,border=2)
 
        self.SetSizer(vbox)
 
    def OnLogin(self,e):
        self.Destroy()
 
    def OnExit(self,e):
        self.Destroy()
        sys.exit()
 
if __name__ == '__main__':
    app = wx.App()
    dlg = LoginDialog()
    app.MainLoop()

解释1:proportion是指BoxSizer绑定界面元素时界面元素所占BoxSizer的比例,默认proportion的值为0。在同一个BoxSizer中的界面元素会根据proportion的值进行自动调整元素的大小。也就是说proportion值越大,该元素所占空间就越多。那么当我们进行为界面布局进行编写代码时,只需合理的安排名proportion值即可实现各元素间的布局。

解释2:为了使proportion参数能够其作用,实现界面元素的自动大小调整,flag参数的wx.EXPAND值就非常重要。若proportion的值大于0(不包括0),那么flag参数中就应该有wx.EXPAND值。

下面是本例的运行图:

 

有任何疑问可评论探讨。欢迎指错。

猎人笔记

我的猎人笔记复活了。经历了3个多月的时间,今晚我重新建了个博客。这大概是我建立的第四个博客了吧(或者是第五个),从之前使用博客服务商到现在自己用Wordpress架设。其实这两年已经没有很多的动力去写以前的那种文字了,是因为我懒了,懒得去思考了,也是因为自己变得浮躁了,变得更加世俗了。我一直认为以前的那个自己,有一种别样的特别,而现在没有了。

自己也挺蠢的,在建立主机的时候把主域名tarkrul.tk写成了tarkrul.dk,还好没什么影响,不知道为什么想的是.dk。域名用了个免费的.tk,我觉得挺适合我选择的这个名字:tarkrul,应该很好记。

说说这博客名的来源吧,很简单,有本随笔小说叫《猎人笔记》,是俄罗斯作家屠格涅夫的作品。我很少看随机类的文章,但这部我觉得不错,因为我喜欢看18世纪19世纪普通百姓的故事。我用这个名字的另一个原因是猎人二字,因为高中时玩魔兽世界时我选择的是个猎人也职业,而那段游戏经历值得我记住一辈子。这就是博客名的由来,我想不会再变了。