用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  阅读(7644)  评论(0编辑  收藏  举报

导航