學習完網絡套接字之后,我產生了寫一個聊天程序的想法。思路很簡單,首先創建一個套接字,客戶端和服務器可以通過套接字通信;然后,為了使通信變為全雙工,接收信息和發送信息由兩個線程分別完成;最后,我還給客戶端加了一個圖形界面,使它看起來不是那么丑陋。
得益於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()