用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()

 

           


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM