用Python实现一个简单的聊天程序
学习完网络套接字之后,我产生了写一个聊天程序的想法。思路很简单,首先创建一个套接字,客户端和服务器可以通过套接字通信;然后,为了使通信变为全双工,接收信息和发送信息由两个线程分别完成;最后,我还给客户端加了一个图形界面,使它看起来不是那么丑陋。
得益于Python的强大,所有这些实现起来都不是特别困难。比如Python中的很多数据结构,像列表,都是线程安全的,这样就免去了处理一大堆线程锁的烦恼;Python提供了方便的图形界面接口,tkinter,使得像我这种从来没有过图形界面编程经验的人,也可以在短时间内创建一个还说得过去的界面;同时,Python还拥有大量high-level Interface,相比起那些更贴近系统的低级接口,尤其是对于那些对底层操作系统不太熟的人来说,使用起来更加方便,并且在大部分情况下,这些高级接口都能满足你的需求;Python中的垃圾回收机制,让程序员从内存管理这项繁重的劳动中解脱出来,程序员再也不用像在C中那样小心谨慎,释放掉每一块不用的内存;最后不得不提到Python简洁灵活的语法,它使得代码more readable and more close to humanity,很多语法即使你以前从来没有用过,你也很有可能猜对它该怎麽用!
下面来介绍一下程序的基本功能。
客户端提供了简单的用户登录、登出、注册、发送信息、选择联系人等功能。首先,输入服务器的IP地址,点击connect(或按回车键),在客户端与服务器之间建立连接;然后,输入用户账号(只支持6位数字),点击log in(或按回车键),如果登录成功,log in 按钮会变为log out,再点击即位登出操作。登录成功之后,用户可以在contacts一栏里边选择联系人发送信息。
服务器主要用来处理用户连接以及信息的转发。服务器为每个用户提供一个user-buffer,所有发给该用户的信息都先存在这个缓冲区中,由服务器统一进行转发;每一个登录的用户都享有一个独立的线程,该线程时刻监听用户的输入并把它存入合适的缓冲区中。
程序运行截图
1 ''' 2 TCP Client Version 2.2 3 2015.12.19 4 ''' 5 6 import selectors 7 import queue 8 import re 9 import threading 10 from socket import * 11 from tkinter import * 12 from time import ctime 13 14 BUFSIZE = 1024 15 16 #lock = threading.Lock() # Global Lock 17 que = queue.Queue(4096) 18 19 class GUI(object): 20 ''' 21 This is the top module. It interacts with users. When a button is clicked 22 or Return is pressed, a corresbonding event trigered. Connect is used to 23 estanblish a TCP connection with server while Log In tells server a user 24 comes. We also provide an Add button, which allows user to add contacts. 25 All the contacts will be displayed in a list box, and a double click on 26 each contact will direct the user's message to a specific contact. 27 Note that this module is based on other modules like Send and Recv, so it 28 is not concerned with send and receive details. In fact it is wisdom to 29 throw this burden to others. We just do what we can and do it perfectly. 30 ''' 31 def __init__(self): 32 self.root = Tk() 33 self.root.title('Chat') 34 35 self.frame_lft = Frame(self.root) 36 self.frame_rgt = Frame(self.root) 37 self.frame_lft.grid(row=0, column=0) 38 self.frame_rgt.grid(row=0, column=1) 39 40 self.entry_msg = Entry(self.frame_lft, width=46) # entry, collect input 41 self.entry_msg.grid(row=1, column=0) 42 self.entry_msg.bind('<Return>', self.send_method) 43 44 self.scrollbar_txt = Scrollbar(self.frame_lft, width=1) 45 self.scrollbar_txt.grid(row=0, column=2, sticky=W+N+S) 46 47 #self.button_qit = Button(self.root, text='Quit', command=self.root.quit) 48 #self.button_qit.pack() 49 50 self.text_msg = Text(self.frame_lft, state=DISABLED, width=49, wrap=WORD) # text, display message 51 self.text_msg.config(font='Fixedsys') 52 self.text_msg.grid(row=0, column=0, columnspan=2, sticky=W+N+S+E) 53 self.text_msg.config(yscrollcommand=self.scrollbar_txt.set) 54 self.scrollbar_txt.config(command=self.text_msg.yview) 55 56 self.entry_IP = Entry(self.frame_rgt, width=14) # an IP address is supposed to input here 57 self.entry_IP.grid(row=0, column=0, padx=5, pady=0) 58 self.entry_IP.bind('<Return>', self.connect_method) 59 60 self.button_cnt = Button(self.frame_rgt, text='connect', command=self.connect_method) # click this button to connect 61 self.button_cnt.config(height=1, width=8) 62 self.button_cnt.grid(row=0, column=1, padx=0, pady=0) 63 64 self.button_snd = Button(self.frame_lft, height=1, text='send', command=self.send_method) 65 self.button_snd.config(width=8) 66 self.button_snd.grid(row=1, column=1) 67 68 ## New features in version 2.2 ## 69 self.entry_log = Entry(self.frame_rgt, width=14) # log in 70 self.entry_log.grid(row=1, column=0, padx=5, pady=0) 71 self.entry_log.bind('<Return>', self.login_method) 72 73 self.button_log = Button(self.frame_rgt, text='log in', command=self.login_method) # first click means log in, second means log out 74 self.button_log.config(width=8) 75 self.button_log.grid(row=1, column=1, sticky=W) 76 77 self.listbox_cat = Listbox(self.frame_rgt, height=17, width=24) 78 self.listbox_cat.insert(END, '000000') 79 self.listbox_cat.grid(row=3, column=0, columnspan=2, padx=5, sticky=N+S+E) 80 self.listbox_cat.bind('<Double-1>', self.contact_method) 81 82 self.scrollbar_cat = Scrollbar(self.frame_rgt, width=1) 83 self.scrollbar_cat.grid(row=3, column=2, sticky=W+N+S) 84 self.scrollbar_cat.config(command=self.listbox_cat.yview) 85 self.listbox_cat.config(yscrollcommand=self.scrollbar_cat.set) 86 87 self.entry_add = Entry(self.frame_rgt, width=14) 88 self.entry_add.grid(row=4, column=0, padx=5, pady=0) 89 self.entry_add.bind('<Return>', self.add_method) 90 91 self.button_add = Button(self.frame_rgt, width=8, text='add') 92 self.button_add.config(command=self.add_method) 93 self.button_add.grid(row=4, column=1) 94 95 self.label_cat = Label(self.frame_rgt, text='Contacts') 96 self.label_cat.grid(row=2, column=0, sticky=W) 97 98 def send_method(self, ev=None): 99 data = self.entry_msg.get() 100 self.entry_msg.delete(0, END) 101 if not data: 102 pass 103 else: 104 self.text_msg.config(state=NORMAL) 105 self.text_msg.insert(END, data+'\n') 106 self.text_msg.config(state=DISABLED) 107 self.text_msg.see(END) 108 self.send.send(data) 109 110 def recv_method(self): 111 try: 112 data = que.get(block=False) 113 except: 114 pass 115 else: 116 self.text_msg.config(state=NORMAL) 117 self.text_msg.insert(END, data+'\n') 118 self.text_msg.config(state=DISABLED) 119 self.text_msg.see(END) 120 if re.match(r'^FROME', data): # log in failed 121 self.entry_log.config(state=NORMAL) 122 self.button_log.config(text='log in', command=self.login_method) 123 124 self.root.after(200, self.recv_method) # runs every 200ms 125 126 def connect_method(self, ev=None): 127 IP = self.entry_IP.get() 128 self.connt = Connt(IP) # make an instance of Connt class 129 self.connt() # establish connection 130 self.button_cnt.config(state=DISABLED) 131 self.entry_IP.config(state=DISABLED) 132 self.send = Send(self.connt.tcpCliSock) # make an instance of Send class 133 self.recv = Recv(self.connt.tcpCliSock) # make an instance of Recv class 134 self.recv_thread = threading.Thread(target=self.recv) # a new thread, dealing with receiving 135 self.recv_thread.daemon = True 136 self.recv_thread.start() 137 self.root.after(200, self.recv_method) 138 139 def login_method(self, ev=None): 140 ID = self.entry_log.get() 141 if re.match(r'^[0-9]{6}$', ID) == None: 142 pass 143 else: 144 self.send.send(ID) # this action is infalliable 145 self.button_log.config(text='log out', command=self.logout_method) 146 self.entry_log.config(state=DISABLED) 147 148 def logout_method(self, ev=None): 149 self.send.send('::LOG OUT') 150 self.button_log.config(text='log in', command=self.login_method) 151 self.entry_log.config(state=NORMAL) 152 153 def contact_method(self, ev=None): 154 ID = self.listbox_cat.get(self.listbox_cat.curselection()) 155 self.send.send('::'+ID) 156 self.text_msg.delete(1.0, END) # delete all text 157 self.text_msg.config(state=NORMAL) 158 self.text_msg.insert(END, '[to '+ID+' '+ctime()+']\n') 159 # if this contact action fails, server will send an error message. 160 161 def add_method(self, ev=None): 162 ID = self.entry_add.get() 163 if re.match(r'[0-9]{6}', ID) == None: 164 pass 165 else: 166 self.listbox_cat.insert(END, ID) 167 168 class Send(object): 169 ''' 170 This module deals with every detail in sending bytes through a socket, 171 such as lock, encode, blocking, etc, and provide a send interface for 172 GUI module. 173 ''' 174 def __init__(self, fd): 175 self.fd = fd 176 self.sel = selectors.DefaultSelector() 177 self.sel.register(self.fd, selectors.EVENT_WRITE) 178 179 def send(self, data): 180 self.sel.select() # wait until the socket is ready to write 181 #if lock.acquire(): 182 self.fd.send(data.encode('utf-8')) 183 #lock.release() 184 #else: 185 # pass 186 187 class Recv(object): 188 ''' 189 This module deals with every detail in receiving bytes from a socket, 190 and providing a friendly recv interface for GUI module. 191 ''' 192 def __init__(self, fd): 193 self.fd = fd 194 self.sel = selectors.DefaultSelector() 195 self.sel.register(self.fd, selectors.EVENT_READ) 196 197 def recv(self): 198 while True: 199 self.sel.select() 200 #if lock.acquire(): 201 byte = self.fd.recv(BUFSIZE) 202 que.put(byte.decode('utf-8')) 203 # lock.release() 204 #else: 205 # pass 206 207 def __call__(self): 208 self.recv() 209 210 class Connt(object): 211 ''' 212 This module deals with establishing a TCP connection with host. 213 ''' 214 def __init__(self, IP): 215 self.HOST = IP 216 self.PORT = 21567 217 self.ADDR = (self.HOST, self.PORT) 218 self.tcpCliSock = socket(AF_INET, SOCK_STREAM) 219 220 def connect(self): 221 self.tcpCliSock.connect(self.ADDR) 222 223 def __call__(self): 224 self.connect() 225 226 def main(): 227 gui = GUI() 228 gui.root.mainloop() 229 230 if __name__ == '__main__': 231 main() 232
1 ''' 2 TCP Server Version 2.2 3 2015.12.19 4 ''' 5 import selectors 6 import threading 7 import queue 8 import re 9 from socket import * 10 from time import ctime 11 12 BUFSIZE = 1024 13 HOST = input('HOST: ') 14 PORT = 21567 15 ADDR = (HOST, PORT) 16 tcpServSock = socket(AF_INET, SOCK_STREAM) 17 tcpServSock.bind(ADDR) 18 tcpServSock.listen(100) 19 20 user_list = [] # all registered users 21 user_buff = {} # each user has a buffer 22 acti_user = [] # once a user logs in, he/she becomes active 23 acti_user_list = [] 24 25 sel = selectors.DefaultSelector() 26 lock = threading.Lock() 27 28 def recv(): 29 global tcpServSock 30 while True: 31 lock.acquire(blocking=True) 32 ret = sel.select(timeout=1) 33 lock.release() 34 for key, event in ret: # some socket is readable 35 if key.fileobj == tcpServSock: # a new connection comes 36 new_socket, new_addr = tcpServSock.accept() 37 print('connected from %s [%s]' %(new_addr, ctime())) 38 lock.acquire(blocking=True) 39 sel.register(new_socket, selectors.EVENT_READ) 40 lock.release() 41 else: # some one clicked Log In 42 try: 43 ID = key.fileobj.recv(BUFSIZE).decode('utf-8') 44 except: 45 print('%s disconnected' %(key.fileobj)) 46 lock.acquire(blocking=True) 47 sel.unregister(key.fileobj) 48 lock.release() 49 continue 50 if ID not in user_list: 51 user_list.append(ID) 52 acti_user_list.append(ID) 53 user_buff[ID] = queue.Queue(4096) 54 print('%s signed up [%s]' %(ID, ctime())) 55 else: 56 if ID in acti_user_list: # already logged in, deny 57 server_msg = 'FROME SERVER: already logged in' 58 key.fileobj.send(server_msg.encode('utf-8')) 59 continue 60 else: 61 acti_user_list.append(ID) 62 print('%s logged in [%s]' %(ID, ctime())) 63 user = User(key.fileobj, ID) # create an instance for logged in user 64 acti_user.append(user) 65 user() 66 lock.acquire(blocking=True) 67 sel.unregister(key.fileobj) 68 lock.release() 69 70 71 def forward(): 72 while True: 73 for eachUser in acti_user: 74 if eachUser.zombie == True: # this user has logged out 75 lock.acquire(blocking=True) 76 sel.register(eachUser.socket, selectors.EVENT_READ) # after user's loging out, 77 # recv_thread will take over the socket and listen to it 78 lock.release() 79 acti_user.remove(eachUser) 80 del(eachUser) 81 continue 82 if eachUser.death == True: 83 acti_user.remove(eachUser) 84 del(eachUser) 85 continue 86 while user_buff[eachUser.ID].qsize(): 87 msg = user_buff[eachUser.ID].get() 88 ## some ckecking is desired, for the socket may not be writable 89 eachUser.socket.send(msg.encode('utf-8')) 90 91 92 class User(object): 93 def __init__(self, socket, ID): 94 self.socket = socket 95 self.ID = ID 96 self.zombie = False 97 self.death = False 98 self.contact = None 99 self.sel = selectors.DefaultSelector() 100 self.sel.register(self.socket, selectors.EVENT_READ) 101 def recv(self): 102 while True: 103 self.sel.select() 104 try: 105 msg = self.socket.recv(BUFSIZE).decode('utf-8') 106 except: 107 print('%s disconnected' %(self.socket)) 108 acti_user_list.remove(self.ID) 109 self.death = True 110 return None 111 if re.match(r'^::[0-9]{6}', msg): 112 # user wants to contact with some one 113 contact = msg[2:] 114 if contact not in user_list: 115 server_msg = 'FROME SERVER: no such user' 116 self.socket.send(server_msg.encode('utf-8')) 117 else: 118 self.contact = msg[2:] 119 elif msg == '::LOG OUT': 120 # user wants to log out 121 acti_user_list.remove(self.ID) 122 print('%s logged out [%s]' %(self.ID, ctime())) 123 self.zombie = True 124 print(self.zombie) 125 return None 126 else: 127 if self.contact != None: 128 msg = '[' + 'frome ' + self.ID + ' ' + ctime() + ']\n' + msg 129 user_buff[self.contact].put(msg) 130 else: 131 server_msg = 'FROME SERVER: choose a contact first' 132 self.socket.send(server_msg.encode('utf-8')) 133 134 def __call__(self): 135 self.recv_thread = threading.Thread(target=self.recv) 136 self.recv_thread.daemon = True 137 self.recv_thread.start() 138 139 140 def main(): 141 fd = open(r'.\user.txt', 'r+') 142 for eachUser in fd: 143 user_list.append(eachUser[0:6]) 144 user_buff[eachUser[0:6]] = queue.Queue(4096) 145 print(user_list) 146 147 sel.register(tcpServSock, selectors.EVENT_READ) 148 149 recv_thread = threading.Thread(target=recv) 150 recv_thread.daemon = True 151 recv_thread.start() 152 153 forward_thread = threading.Thread(target=forward) 154 forward_thread.daemon = True 155 forward_thread.start() 156 157 while True: 158 pass # infinate loop 159 160 if __name__ == '__main__': 161 main()
posted on 2015-12-23 16:21 songlingfei 阅读(7698) 评论(0) 编辑 收藏 举报