Python实现基于C/S架构的聊天室功能详解

时间:2022-10-07 09:19:11

本文实例讲述了Python实现基于C/S架构的聊天室功能。分享给大家供大家参考,具体如下:

一、课程介绍

 

1.简介

本次项目课是实现简单聊天室程序的服务器端和客户端。

2.知识点

服务器端涉及到asyncoreasynchatsocket这几个模块,客户端用到了telnetlibwxtimethread这几个模块。

3.所需环境

本次课中编写客户端需要用到wxPython,它是一个GUI工具包,请先使用下面的命令安装:

?
1
$ sudo apt-get install python-wxtools

密码为shiyanlou

4.项目效果截图

登录窗口

Python实现基于C/S架构的聊天室功能详解

聊天窗口

Python实现基于C/S架构的聊天室功能详解

5.源代码下载

git clone https://github.com/shiyanlou/pythonchat.git

说明:如果你不理解上述代码的下载方式或者下载后在环境中找不到代码,可以点击查看这里

二、项目实战(服务器端)

 

1.服务器类

首先需要一个聊天服务器,这里继承asyncore的dispatcher类来实现,代码如下

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ChatServer(dispatcher):
  """
  聊天服务器
  """
  def __init__(self, port):
    dispatcher.__init__(self)
    self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
    self.set_reuse_addr()
    self.bind(('', port))
    self.listen(5)
    self.users = {}
    self.main_room = ChatRoom(self)
  def handle_accept(self):
    conn, addr = self.accept()
    ChatSession(self, conn)

2.会话类

有了服务器类还需要能维护每个用户的连接会话,这里继承asynchat的async_chat类来实现,代码如下:

?
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
class ChatSession(async_chat):
  """
  负责和单用户通信
  """
  def __init__(self, server, sock):
    async_chat.__init__(self, sock)
    self.server = server
    self.set_terminator('\n')
    self.data = []
    self.name = None
    self.enter(LoginRoom(server))
  def enter(self, room):
    '从当前房间移除自身,然后添加到指定房间'
    try:
      cur = self.room
    except AttributeError:
      pass
    else:
      cur.remove(self)
    self.room = room
    room.add(self)
  def collect_incoming_data(self, data):
    '接受客户端的数据'
    self.data.append(data)
  def found_terminator(self):
    '当客户端的一条数据结束时的处理'
    line = ''.join(self.data)
    self.data = []
    try:
      self.room.handle(self, line)
    except EndSession:
      self.handle_close()
  def handle_close(self):
    async_chat.handle_close(self)
    self.enter(LogoutRoom(self.server))

3.命令解释器

现在就需要一个命令解释器能够解释用户的命令,例如登录、查询在线用户和发消息等,代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class CommandHandler:
  """
  命令处理类
  """
  def unknown(self, session, cmd):
    '响应未知命令'
    session.push('Unknown command: %s\n' % cmd)
  def handle(self, session, line):
    '命令处理'
    if not line.strip():
      return
    parts = line.split(' ', 1)
    cmd = parts[0]
    try:
      line = parts[1].strip()
    except IndexError:
      line = ''
    meth = getattr(self, 'do_' + cmd, None)
    try:
      meth(session, line)
    except TypeError:
      self.unknown(session, cmd)

4.房间

接下来就需要实现聊天室的房间了,这里我们定义了三种房间,分别是用户刚登录时的房间、聊天的房间和退出登录的房间,这三种房间都有一个公共的父类,代码如下:

?
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
class Room(CommandHandler):
  """
  包含多个用户的环境,负责基本的命令处理和广播
  """
  def __init__(self, server):
    self.server = server
    self.sessions = []
  def add(self, session):
    '一个用户进入房间'
    self.sessions.append(session)
  def remove(self, session):
    '一个用户离开房间'
    self.sessions.remove(session)
  def broadcast(self, line):
    '向所有的用户发送指定消息'
    for session in self.sessions:
      session.push(line)
  def do_logout(self, session, line):
    '退出房间'
    raise EndSession
class LoginRoom(Room):
  """
  刚登录的用户的房间
  """
  def add(self, session):
    '用户连接成功的回应'
    Room.add(self, session)
    session.push('Connect Success')
  def do_login(self, session, line):
    '登录命令处理'
    name = line.strip()
    if not name:
      session.push('UserName Empty')
    elif name in self.server.users:
      session.push('UserName Exist')
    else:
      session.name = name
      session.enter(self.server.main_room)
class ChatRoom(Room):
  """
  聊天用的房间
  """
  def add(self, session):
    '广播新用户进入'
    session.push('Login Success')
    self.broadcast(session.name + ' has entered the room.\n')
    self.server.users[session.name] = session
    Room.add(self, session)
  def remove(self, session):
    '广播用户离开'
    Room.remove(self, session)
    self.broadcast(session.name + ' has left the room.\n')
  def do_say(self, session, line):
    '客户端发送消息'
    self.broadcast(session.name + ': ' + line + '\n')
  def do_look(self, session, line):
    '查看在线用户'
    session.push('Online Users:\n')
    for other in self.sessions:
      session.push(other.name + '\n')
class LogoutRoom(Room):
  """
  用户退出时的房间
  """
  def add(self, session):
    '从服务器中移除'
    try:
      del self.server.users[session.name]
    except KeyError:
      pass

5.服务器端完整代码

?
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#!/usr/bin/python
# encoding: utf-8
from asyncore import dispatcher
from asynchat import async_chat
import socket, asyncore
PORT = 6666 #端口
class EndSession(Exception):
  """
  自定义会话结束时的异常
  """
  pass
class CommandHandler:
  """
  命令处理类
  """
  def unknown(self, session, cmd):
    '响应未知命令'
    session.push('Unknown command: %s\n' % cmd)
  def handle(self, session, line):
    '命令处理'
    if not line.strip():
      return
    parts = line.split(' ', 1)
    cmd = parts[0]
    try:
      line = parts[1].strip()
    except IndexError:
      line = ''
    meth = getattr(self, 'do_' + cmd, None)
    try:
      meth(session, line)
    except TypeError:
      self.unknown(session, cmd)
class Room(CommandHandler):
  """
  包含多个用户的环境,负责基本的命令处理和广播
  """
  def __init__(self, server):
    self.server = server
    self.sessions = []
  def add(self, session):
    '一个用户进入房间'
    self.sessions.append(session)
  def remove(self, session):
    '一个用户离开房间'
    self.sessions.remove(session)
  def broadcast(self, line):
    '向所有的用户发送指定消息'
    for session in self.sessions:
      session.push(line)
  def do_logout(self, session, line):
    '退出房间'
    raise EndSession
class LoginRoom(Room):
  """
  刚登录的用户的房间
  """
  def add(self, session):
    '用户连接成功的回应'
    Room.add(self, session)
    session.push('Connect Success')
  def do_login(self, session, line):
    '登录命令处理'
    name = line.strip()
    if not name:
      session.push('UserName Empty')
    elif name in self.server.users:
      session.push('UserName Exist')
    else:
      session.name = name
      session.enter(self.server.main_room)
class ChatRoom(Room):
  """
  聊天用的房间
  """
  def add(self, session):
    '广播新用户进入'
    session.push('Login Success')
    self.broadcast(session.name + ' has entered the room.\n')
    self.server.users[session.name] = session
    Room.add(self, session)
  def remove(self, session):
    '广播用户离开'
    Room.remove(self, session)
    self.broadcast(session.name + ' has left the room.\n')
  def do_say(self, session, line):
    '客户端发送消息'
    self.broadcast(session.name + ': ' + line + '\n')
  def do_look(self, session, line):
    '查看在线用户'
    session.push('Online Users:\n')
    for other in self.sessions:
      session.push(other.name + '\n')
class LogoutRoom(Room):
  """
  用户退出时的房间
  """
  def add(self, session):
    '从服务器中移除'
    try:
      del self.server.users[session.name]
    except KeyError:
      pass
class ChatSession(async_chat):
  """
  负责和单用户通信
  """
  def __init__(self, server, sock):
    async_chat.__init__(self, sock)
    self.server = server
    self.set_terminator('\n')
    self.data = []
    self.name = None
    self.enter(LoginRoom(server))
  def enter(self, room):
    '从当前房间移除自身,然后添加到指定房间'
    try:
      cur = self.room
    except AttributeError:
      pass
    else:
      cur.remove(self)
    self.room = room
    room.add(self)
  def collect_incoming_data(self, data):
    '接受客户端的数据'
    self.data.append(data)
  def found_terminator(self):
    '当客户端的一条数据结束时的处理'
    line = ''.join(self.data)
    self.data = []
    try:
      self.room.handle(self, line)
    except EndSession:
      self.handle_close()
  def handle_close(self):
    async_chat.handle_close(self)
    self.enter(LogoutRoom(self.server))
class ChatServer(dispatcher):
  """
  聊天服务器
  """
  def __init__(self, port):
    dispatcher.__init__(self)
    self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
    self.set_reuse_addr()
    self.bind(('', port))
    self.listen(5)
    self.users = {}
    self.main_room = ChatRoom(self)
  def handle_accept(self):
    conn, addr = self.accept()
    ChatSession(self, conn)
if __name__ == '__main__':
  s = ChatServer(PORT)
  try:
    asyncore.loop()
  except KeyboardInterrupt:
    print

三、项目实战(客户端)

 

完成了服务器端后,就需要实现客户端了,这里客户端连接服务器使用了telnetlib模块。

1.登录窗口

这里的图形界面包选择了wxPython,前面有安装说明,登录窗口通过继承wx.Frame类来实现,代码如下:

?
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
class LoginFrame(wx.Frame):
  """
  登录窗口
  """
  def __init__(self, parent, id, title, size):
    '初始化,添加控件并绑定事件'
    wx.Frame.__init__(self, parent, id, title)
    self.SetSize(size)
    self.Center()
    self.serverAddressLabel = wx.StaticText(self, label = "Server Address", pos = (10, 50), size = (120, 25))
    self.userNameLabel = wx.StaticText(self, label = "UserName", pos = (40, 100), size = (120, 25))
    self.serverAddress = wx.TextCtrl(self, pos = (120, 47), size = (150, 25))
    self.userName = wx.TextCtrl(self, pos = (120, 97), size = (150, 25))
    self.loginButton = wx.Button(self, label = 'Login', pos = (80, 145), size = (130, 30))
    self.loginButton.Bind(wx.EVT_BUTTON, self.login)
    self.Show()
  def login(self, event):
    '登录处理'
    try:
      serverAddress = self.serverAddress.GetLineText(0).split(':')
      con.open(serverAddress[0], port = int(serverAddress[1]), timeout = 10)
      response = con.read_some()
      if response != 'Connect Success':
        self.showDialog('Error', 'Connect Fail!', (95, 20))
        return
      con.write('login ' + str(self.userName.GetLineText(0)) + '\n')
      response = con.read_some()
      if response == 'UserName Empty':
        self.showDialog('Error', 'UserName Empty!', (135, 20))
      elif response == 'UserName Exist':
        self.showDialog('Error', 'UserName Exist!', (135, 20))
      else:
        self.Close()
        ChatFrame(None, -2, title = 'ShiYanLou Chat Client', size = (500, 350))
    except Exception:
      self.showDialog('Error', 'Connect Fail!', (95, 20))
  def showDialog(self, title, content, size):
    '显示错误信息对话框'
    dialog = wx.Dialog(self, title = title, size = size)
    dialog.Center()
    wx.StaticText(dialog, label = content)
    dialog.ShowModal()

2.聊天窗口

聊天窗口中最主要的就是向服务器发消息并接受服务器的消息,这里通过子线程来接受,代码如下:

?
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
class ChatFrame(wx.Frame):
  """
  聊天窗口
  """
  def __init__(self, parent, id, title, size):
    '初始化,添加控件并绑定事件'
    wx.Frame.__init__(self, parent, id, title)
    self.SetSize(size)
    self.Center()
    self.chatFrame = wx.TextCtrl(self, pos = (5, 5), size = (490, 310), style = wx.TE_MULTILINE | wx.TE_READONLY)
    self.message = wx.TextCtrl(self, pos = (5, 320), size = (300, 25))
    self.sendButton = wx.Button(self, label = "Send", pos = (310, 320), size = (58, 25))
    self.usersButton = wx.Button(self, label = "Users", pos = (373, 320), size = (58, 25))
    self.closeButton = wx.Button(self, label = "Close", pos = (436, 320), size = (58, 25))
    self.sendButton.Bind(wx.EVT_BUTTON, self.send)
    self.usersButton.Bind(wx.EVT_BUTTON, self.lookUsers)
    self.closeButton.Bind(wx.EVT_BUTTON, self.close)
    thread.start_new_thread(self.receive, ())
    self.Show()
  def send(self, event):
    '发送消息'
    message = str(self.message.GetLineText(0)).strip()
    if message != '':
      con.write('say ' + message + '\n')
      self.message.Clear()
  def lookUsers(self, event):
    '查看当前在线用户'
    con.write('look\n')
  def close(self, event):
    '关闭窗口'
    con.write('logout\n')
    con.close()
    self.Close()
  def receive(self):
    '接受服务器的消息'
    while True:
      sleep(0.6)
      result = con.read_very_eager()
      if result != '':
        self.chatFrame.AppendText(result)

3.客户端完整代码

?
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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
#!/usr/bin/python
# encoding: utf-8
import wx
import telnetlib
from time import sleep
import thread
class LoginFrame(wx.Frame):
  """
  登录窗口
  """
  def __init__(self, parent, id, title, size):
    '初始化,添加控件并绑定事件'
    wx.Frame.__init__(self, parent, id, title)
    self.SetSize(size)
    self.Center()
    self.serverAddressLabel = wx.StaticText(self, label = "Server Address", pos = (10, 50), size = (120, 25))
    self.userNameLabel = wx.StaticText(self, label = "UserName", pos = (40, 100), size = (120, 25))
    self.serverAddress = wx.TextCtrl(self, pos = (120, 47), size = (150, 25))
    self.userName = wx.TextCtrl(self, pos = (120, 97), size = (150, 25))
    self.loginButton = wx.Button(self, label = 'Login', pos = (80, 145), size = (130, 30))
    self.loginButton.Bind(wx.EVT_BUTTON, self.login)
    self.Show()
  def login(self, event):
    '登录处理'
    try:
      serverAddress = self.serverAddress.GetLineText(0).split(':')
      con.open(serverAddress[0], port = int(serverAddress[1]), timeout = 10)
      response = con.read_some()
      if response != 'Connect Success':
        self.showDialog('Error', 'Connect Fail!', (95, 20))
        return
      con.write('login ' + str(self.userName.GetLineText(0)) + '\n')
      response = con.read_some()
      if response == 'UserName Empty':
        self.showDialog('Error', 'UserName Empty!', (135, 20))
      elif response == 'UserName Exist':
        self.showDialog('Error', 'UserName Exist!', (135, 20))
      else:
        self.Close()
        ChatFrame(None, -2, title = 'ShiYanLou Chat Client', size = (500, 350))
    except Exception:
      self.showDialog('Error', 'Connect Fail!', (95, 20))
  def showDialog(self, title, content, size):
    '显示错误信息对话框'
    dialog = wx.Dialog(self, title = title, size = size)
    dialog.Center()
    wx.StaticText(dialog, label = content)
    dialog.ShowModal()
class ChatFrame(wx.Frame):
  """
  聊天窗口
  """
  def __init__(self, parent, id, title, size):
    '初始化,添加控件并绑定事件'
    wx.Frame.__init__(self, parent, id, title)
    self.SetSize(size)
    self.Center()
    self.chatFrame = wx.TextCtrl(self, pos = (5, 5), size = (490, 310), style = wx.TE_MULTILINE | wx.TE_READONLY)
    self.message = wx.TextCtrl(self, pos = (5, 320), size = (300, 25))
    self.sendButton = wx.Button(self, label = "Send", pos = (310, 320), size = (58, 25))
    self.usersButton = wx.Button(self, label = "Users", pos = (373, 320), size = (58, 25))
    self.closeButton = wx.Button(self, label = "Close", pos = (436, 320), size = (58, 25))
    self.sendButton.Bind(wx.EVT_BUTTON, self.send)
    self.usersButton.Bind(wx.EVT_BUTTON, self.lookUsers)
    self.closeButton.Bind(wx.EVT_BUTTON, self.close)
    thread.start_new_thread(self.receive, ())
    self.Show()
  def send(self, event):
    '发送消息'
    message = str(self.message.GetLineText(0)).strip()
    if message != '':
      con.write('say ' + message + '\n')
      self.message.Clear()
  def lookUsers(self, event):
    '查看当前在线用户'
    con.write('look\n')
  def close(self, event):
    '关闭窗口'
    con.write('logout\n')
    con.close()
    self.Close()
  def receive(self):
    '接受服务器的消息'
    while True:
      sleep(0.6)
      result = con.read_very_eager()
      if result != '':
        self.chatFrame.AppendText(result)
'程序运行'
if __name__ == '__main__':
  app = wx.App()
  con = telnetlib.Telnet()
  LoginFrame(None, -1, title = "Login", size = (280, 200))
  app.MainLoop()

四、小结

 

最后就可以运行程序进行聊天了,注意需要先启动服务器再启动客户端。这个项目中使用了asyncore的dispatcher来实现服务器,asynchat的asyn_chat来维护用户的连接会话,用wxPython来实现图形界面,用telnetlib来连接服务器,在子线程中接受服务器发来的消息,由此一个简单的聊天室程序就完成了。

希望本文所述对大家Python程序设计有所帮助。

原文链接:https://www.cnblogs.com/rrxc/p/4530626.html