├── ChatClient.py ├── ChatServer.py ├── README.md └── img ├── Snip20170531_1.png └── Snip20170531_4.png /ChatClient.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python2.7 2 | # -*- coding: utf-8 -*- 3 | # Author: Hurray(hurray0@icloud.com) 4 | # Date: 2017.05.28 5 | 6 | from socket import * 7 | import json 8 | from Tkinter import * 9 | import threading 10 | import ctypes 11 | import inspect 12 | import sys 13 | import struct 14 | 15 | reload(sys) 16 | sys.setdefaultencoding('utf8') 17 | 18 | class R(): 19 | SENDERPORT = 1501 20 | MYPORT = 1234 21 | MYGROUP = '224.1.1.1' 22 | 23 | class Client(): 24 | def __init__(self): 25 | self.isConnect = False 26 | 27 | def connect(self): 28 | """连接服务器""" 29 | if not self.isConnect: 30 | self.tcpCliSock = socket(AF_INET, SOCK_STREAM) 31 | self.tcpCliSock.connect(ADDR) 32 | self.isConnect = True 33 | print "连接成功" 34 | else: 35 | print "已经连接,不再重新连接" 36 | 37 | def disConnect(self): 38 | """断开服务器""" 39 | self.tcpCliSock.close() 40 | 41 | def showErr(self, info): 42 | """错误提示界面""" 43 | errTk = Tk() 44 | errTk.geometry('200x120') 45 | errTk.title("Error!") 46 | Label(errTk, text = info).pack(padx = 5, pady = 20, fill = 'x') 47 | bt = Button(errTk, text = "确定", command = errTk.destroy).pack() 48 | errTk.mainloop() 49 | 50 | class Login(): 51 | """登录界面""" 52 | def __init__(self, father): 53 | self.father = father 54 | 55 | def goLogin(self, entry, loginWindow): 56 | """登录操作""" 57 | username = entry.get() 58 | self.father.username = username 59 | data = {"type": "login", "username": username} 60 | jData = json.dumps(data) 61 | try: 62 | self.father.connect() 63 | except Exception as e: 64 | print e 65 | self.father.showErr("网络连接异常,无法连接服务器") 66 | return False 67 | else: 68 | socket = self.father.tcpCliSock 69 | socket.send(jData) 70 | recv_jData = socket.recv(BUFSIZ) 71 | recv_data = json.loads(recv_jData) 72 | if recv_data["type"] == "login" and \ 73 | recv_data["username"] == username and \ 74 | recv_data["status"] == True: 75 | # login success! 76 | print "login success" 77 | mainFrame = self.father.MainFrame(self.father) 78 | loginWindow.destroy() 79 | mainFrame.__main__() 80 | else: 81 | # login failed 82 | if recv_data["info"]: 83 | self.father.showErr(recv_data["info"]) 84 | else: 85 | self.father.showErr("未知登录错误") 86 | #self.disConnect() 87 | 88 | def window(self): 89 | """登录窗口GUI""" 90 | tk = Tk() 91 | tk.geometry('250x150') 92 | tk.title('登录界面') 93 | frame = Frame(tk) 94 | frame.pack(expand = YES, fill = BOTH) 95 | Label(frame, font = ("Arial, 15"), 96 | text = "请输入一个用户名:", anchor = 'w').pack(padx = 10, 97 | pady = 15, fill = 'x') 98 | entry = Entry(frame) 99 | entry.pack(padx = 10, fill = 'x') 100 | entry.bind("", lambda x : self.goLogin(entry, tk)) 101 | button = Button(frame, text = "登录", 102 | command = lambda : self.goLogin(entry, tk)) 103 | button.pack() 104 | 105 | tk.mainloop() 106 | 107 | def __main__(self): 108 | # 建立窗口 109 | self.window() 110 | 111 | class MainFrame(): 112 | """聊天主窗口""" 113 | def __init__(self, father): 114 | self.father = father 115 | self.socket = father.tcpCliSock # may raise a Exception 116 | self.rSocket = None 117 | self.sSocket = None 118 | 119 | class ListenThread(threading.Thread): 120 | """Socket监听线程,对收到的信息作出相应反馈""" 121 | def __init__(self, socket, father): 122 | threading.Thread.__init__(self) 123 | self.father = father 124 | self.socket = socket 125 | 126 | def run(self): 127 | while True: 128 | try: 129 | jData = self.socket.recv(BUFSIZ) 130 | data = json.loads(jData) 131 | except: 132 | break 133 | print "__receive__" + jData 134 | switch = { 135 | "list": self.list, 136 | "singleChat": self.chat, 137 | "groupChat": self.chat, 138 | "pong": self.pong 139 | } 140 | switch[data['type']](data) 141 | print "结束监听" 142 | 143 | def list(self, data): 144 | """刷新列表""" 145 | listbox = self.father.listbox 146 | list = ['群聊', '组播'] 147 | list += data['list'] 148 | listbox.delete(0, END) # 清空现有列表 149 | for l in list: 150 | listbox.insert(END, l) # 插入新列表 151 | 152 | def chat(self, data): 153 | """接收聊天信息并打印""" 154 | textArea = self.father.textArea 155 | text = ('[群聊]' if data['type'] == 'groupChat' else '') + \ 156 | data['from'] + ': ' + data['msg'] + '\n' 157 | textArea.insert(END, text) 158 | 159 | def pong(self, data): 160 | """ping pong!""" 161 | text = '[Server]pong\n' 162 | textArea.insert(END, text) 163 | 164 | class BroadListenThread(threading.Thread): 165 | """组播侦听线程""" 166 | def __init__(self, father): 167 | threading.Thread.__init__(self) 168 | self.father = father 169 | 170 | def run(self): 171 | print '开始监听组播' 172 | self.alive = True 173 | sock = self.father.rSocket 174 | while self.alive: 175 | try: 176 | jData, addr = sock.recvfrom(BUFSIZ) 177 | data = json.loads(jData) 178 | except Exception as e: 179 | pass 180 | else: 181 | textArea = self.father.textArea 182 | text = "[组播]" + data['from'] + ': ' + data['msg'] + '\n' 183 | textArea.insert(END, text) 184 | print "__receiveBroad__" + jData 185 | print '组播监听循环结束' 186 | 187 | def stop(self): 188 | self.alive = False 189 | 190 | class Window(): 191 | def __init__(self, father): 192 | self.father = father 193 | 194 | def refresh(self, socket): 195 | """点击刷新按钮""" 196 | data = {"type": "list"} 197 | jData = json.dumps(data) 198 | socket.send(jData) 199 | 200 | def changeAddr(self): 201 | def setAddr(entry1, entry2, entry3, self, tk): 202 | if self.father.rSocket: 203 | # 停止之前的地址 204 | self.father.broadListenThread.stop() 205 | self.father.sSocket.sendto("", (R.MYGROUP, R.MYPORT)) # fake send 206 | print '停止之前的地址' 207 | 208 | try: 209 | # 发送socket 210 | R.MYGROUP = entry1.get() 211 | R.MYPORT = int(entry2.get()) 212 | R.SENDERPORT = int(entry3.get()) 213 | s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) 214 | s.bind((HOST, R.SENDERPORT)) 215 | ttl_bin = struct.pack('@i', MYTTL) 216 | s.setsockopt(IPPROTO_IP, IP_MULTICAST_TTL, ttl_bin) 217 | status = s.setsockopt(IPPROTO_IP, IP_ADD_MEMBERSHIP, 218 | inet_aton(R.MYGROUP) + 219 | inet_aton(HOST))#加入到组播组 220 | self.father.sSocket = s 221 | 222 | # 监听socket 223 | so = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) 224 | so.setsockopt(SOL_SOCKET, SO_REUSEADDR, 1) 225 | so.bind((HOST, R.MYPORT)) 226 | status = so.setsockopt(IPPROTO_IP, 227 | IP_ADD_MEMBERSHIP, 228 | inet_aton(R.MYGROUP) + 229 | inet_aton(HOST)) 230 | so.setblocking(0) 231 | self.father.rSocket = so 232 | 233 | except Exception as e: 234 | print e 235 | self.father.father.showErr("该地址不可使用") 236 | else: 237 | broadListenThread = self.father.BroadListenThread( \ 238 | self.father) 239 | broadListenThread.start() 240 | self.father.broadListenThread = broadListenThread 241 | tk.destroy() 242 | 243 | def isset(v): 244 | try: 245 | type(eval(v)) 246 | except: 247 | return False 248 | else: 249 | return True 250 | 251 | """修改组播地址""" 252 | tk = Tk() 253 | tk.geometry('270x120') 254 | tk.title('请修改组播设置') 255 | Label(tk, text = "组播地址: ").grid(row = 0, column = 0) 256 | Label(tk, text = "监听端口: ").grid(row = 1, column = 0) 257 | Label(tk, text = "本地端口: ").grid(row = 2, column = 0) 258 | entry1 = Entry(tk) 259 | entry1.grid(row = 0, column = 1) 260 | entry1.insert(END, R.MYGROUP) 261 | entry2 = Entry(tk) 262 | entry2.grid(row = 1, column = 1) 263 | entry2.insert(END, R.MYPORT) 264 | entry3 = Entry(tk) 265 | entry3.grid(row = 2, column = 1) 266 | entry3.insert(END, R.SENDERPORT) 267 | bt = Button(tk, text = "确定", 268 | command = lambda : setAddr(entry1, entry2, entry3, 269 | self, tk)).grid(row = 3, column = 0, columnspan = 2) 270 | 271 | tk.mainloop() 272 | 273 | def sendBroad(self, msg, et_input, username): 274 | sSocket = self.father.sSocket 275 | if not sSocket: 276 | self.changeAddr() 277 | else: 278 | data = {'type': 'broadChat', 'msg': msg, 'from': username} 279 | jData = json.dumps(data) 280 | print (R.MYGROUP, R.MYPORT) 281 | sSocket.sendto(jData, (R.MYGROUP, R.MYPORT)) 282 | print '__sendBroad__' + jData 283 | 284 | # 清空输入框 285 | et_input.delete(0, END) 286 | 287 | def send(self, socket, lb_toName, et_input): 288 | """点击发送按钮""" 289 | text = et_input.get() 290 | toName = lb_toName['text'] 291 | username = self.father.father.username 292 | print toName 293 | if toName == '群聊': 294 | data = {'type': 'groupChat', 'msg': text, 'from': username} 295 | elif toName == '组播': 296 | self.sendBroad(text, et_input, username) 297 | return 298 | else: 299 | # 私聊 300 | data = {'type': 'singleChat', 'msg': text, 301 | 'to': toName, 'from': username} 302 | textArea = self.father.textArea 303 | t = "[->" + toName + ']' + text + '\n' 304 | textArea.insert(END, t) 305 | jData = json.dumps(data) 306 | socket.send(jData) 307 | print '__send__' + jData 308 | et_input.delete(0, END) 309 | 310 | def changeSendTo(self, listbox, lb_toName): 311 | """双击选择列表""" 312 | try: 313 | lb_toName['text'] = listbox.get(listbox.curselection()) 314 | except: 315 | pass # nothing choose 316 | 317 | def __main__(self): 318 | father = self.father 319 | tk = Tk() 320 | tk.geometry('600x400') 321 | tk.title('Chatroom') 322 | 323 | # 背景 324 | f = Frame(tk, bg = '#EEEEEE', width = 600, height = 400) 325 | f.place(x = 0, y = 0) 326 | 327 | # 聊天内容框 328 | textArea = Text(f, bg = '#FFFFFF', width = 60, 329 | height = 22, 330 | #state = DISABLED, 331 | bd = 0) 332 | textArea.place(x = 10, y = 10, anchor = NW) 333 | textArea.bind("", lambda x : "break") 334 | father.textArea = textArea 335 | textArea.focus_set() 336 | # 右侧选择聊天对象 337 | Label(f, text = "双击选择发送对象:", bg = "#EEEEEE").place( 338 | x = 460, y = 10, anchor = NW) 339 | listbox = Listbox(f, width = 13, height = 13, bg = '#FFFFFF') 340 | listbox.place(x = 460, y = 35, anchor = NW) 341 | father.listbox = listbox 342 | bt_refresh = Button(f, text = "刷新列表", bd = 0, 343 | command = lambda : self.refresh(father.socket)) 344 | bt_refresh.place(x = 515, y = 290, anchor = CENTER) 345 | # 修改组播地址 346 | bt_changeAddr = Button(f, text = "组播地址", 347 | command = self.changeAddr) 348 | bt_changeAddr.place(x = 515, y = 330, anchor = CENTER) 349 | bt_clear = Button(f, text = "清屏", 350 | command = lambda : textArea.delete(0.0, END)) 351 | bt_clear.place(x = 560, y = 372, anchor = CENTER) 352 | # 下方内容输入 353 | lb_toName = Label(f, text = "群聊", bg = '#FFFFFF', width = 8) 354 | lb_toName.place(x = 12, y = 360) 355 | listbox.bind('', 356 | lambda x : self.changeSendTo(listbox, lb_toName)) 357 | self.lb_toName = lb_toName 358 | et_input = Entry(f, width = 37) 359 | et_input.place(x = 90, y = 358) 360 | et_input.bind('', 361 | lambda x : self.send(father.socket, 362 | lb_toName, et_input)) 363 | self.et_input = et_input 364 | # 发送按钮 365 | bt_send = Button(f, text = "ENTER", 366 | command = lambda : self.send(father.socket, 367 | lb_toName, et_input)) 368 | bt_send.place(x = 480, y = 371, anchor = CENTER) 369 | 370 | # 刷新列表 371 | self.refresh(father.socket) 372 | 373 | tk.mainloop() 374 | 375 | father.socket.shutdown(2) 376 | print 'Socket 断开' 377 | try: 378 | father.broadListenThread.stop() 379 | father.sSocket.sendto("", (R.MYGROUP, R.MYPORT)) # fake send 380 | except: 381 | pass 382 | print 'rSocket 断开' 383 | 384 | def __main__(self): 385 | # 开启监听线程 386 | listenThread = self.ListenThread(self.socket, self) 387 | listenThread.start() 388 | self.listenThread = listenThread 389 | 390 | # 组播侦听线程 391 | #print '开始监听' 392 | #broadListenThread = self.BroadListenThread(self) 393 | #broadListenThread.start() 394 | #self.broadListenThread = broadListenThread 395 | 396 | # 建立窗口 397 | window = self.Window(self) 398 | window.__main__() 399 | self.window = window 400 | 401 | def __main__(self): 402 | #pass 403 | login = Client.Login(self) 404 | login.__main__() 405 | 406 | if __name__ == '__main__': 407 | global HOST 408 | global PORT 409 | global BUFSIZ 410 | global ADDR 411 | 412 | HOST = '0.0.0.0' 413 | PORT = 8945 414 | BUFSIZ = 1024 415 | ADDR = (HOST, PORT) 416 | MYTTL = 255 417 | 418 | client = Client() 419 | client.__main__() 420 | -------------------------------------------------------------------------------- /ChatServer.py: -------------------------------------------------------------------------------- 1 | #/usr/bin/python2.7 2 | # -*- coding: utf-8 -*- 3 | # Author: Hurray(hurray0@icloud.com) 4 | # Date: 2017.05.28 5 | 6 | from socket import * 7 | from time import ctime 8 | import threading 9 | import json 10 | 11 | global HOST 12 | global PORT 13 | global BUFSIZ 14 | global ADDR 15 | 16 | class User(): 17 | def __init__(self, address, tcpCliSock): 18 | self.address = address 19 | self.tcpCliSock = tcpCliSock 20 | 21 | class Handle(): 22 | usernames = {} # user: usernames 23 | 24 | def __init__(self, user): 25 | self.user = user 26 | 27 | @staticmethod 28 | def getUser(username): 29 | def getKey(list, value): 30 | return [k for k,v in d.items() if v == value][0] 31 | return getKey(Handle.usernames, username) 32 | 33 | @staticmethod 34 | def delUsername(username): 35 | try: 36 | user = Handle.getUser(username) 37 | Handle.delUser(user) 38 | except: 39 | pass 40 | 41 | @staticmethod 42 | def delUser(user): 43 | try: 44 | Handle.usernames.pop(user) 45 | except Exception as e: 46 | print e 47 | 48 | @staticmethod 49 | def sendSocketToUsers(userList, data): 50 | jData = json.dumps(data) 51 | for user in userList: 52 | user.tcpCliSock.send(jData) 53 | print "__sendToAll__" + jData 54 | 55 | @staticmethod 56 | def sendSocketToNames(usernameList, data): 57 | """向用户列表发送相同的数据包""" 58 | def getKeys(list, valueList): 59 | return [k for k,v in list.items() if v in valueList] 60 | userList = getKeys(Handle.usernames, usernameList) 61 | Handle.sendSocketToUsers(userList, data) 62 | 63 | def sendSocketToMe(self, data): 64 | """给本用户发送信息包""" 65 | jData = json.dumps(data) 66 | self.user.tcpCliSock.send(jData) 67 | print '__send__' + jData 68 | 69 | def login(self, data): 70 | """处理登录信息包""" 71 | # already login 72 | if self.user in Handle.usernames.keys(): 73 | data['status'] = False 74 | data['info'] = "您已经登录了" 75 | # username in use 76 | elif data['username'] in Handle.usernames.values(): 77 | data['status'] = False 78 | data['info'] = "该用户名已被占用" 79 | else: 80 | data['status'] = True 81 | Handle.usernames[self.user] = data['username'] 82 | self.sendSocketToMe(data) 83 | 84 | def ping(self, data): 85 | """ping pong!""" 86 | data = {'type': 'pong'} 87 | self.sendSocketToMe(data) 88 | 89 | def list(self, data): 90 | """获取在线用户列表""" 91 | nameList = Handle.usernames.values() 92 | data['list'] = nameList 93 | self.sendSocketToMe(data) 94 | 95 | def singleChat(self, data): 96 | """私聊""" 97 | toUsername = data['to'] 98 | self.sendSocketToNames([toUsername], data) 99 | 100 | def groupChat(self, data): 101 | """群聊(公共聊天)""" 102 | userList = [user for user in Handle.usernames] 103 | self.sendSocketToUsers(userList, data) 104 | 105 | def logout(self, data): 106 | """登出""" 107 | print "用户"+ Handle.usernames[self.user] +"登出" 108 | Handle.delUser(self.user) 109 | 110 | def __main__(self, data): 111 | """处理信息包""" 112 | type = data['type'] 113 | switch = { 114 | "login": self.login, 115 | "ping": self.ping, 116 | "list": self.list, 117 | "singleChat": self.singleChat, 118 | "groupChat": self.groupChat, 119 | "logout": self.logout 120 | } 121 | try: 122 | return switch[type](data) 123 | except Exception as e: 124 | print e 125 | data['status'] = False 126 | data['info'] = "未知错误" 127 | return data 128 | 129 | class ClientThread(threading.Thread): 130 | def __init__(self, user): 131 | threading.Thread.__init__(self) 132 | self.user = user 133 | 134 | def run(self): 135 | try: 136 | handle = Handle(self.user) # handle input 137 | while True: 138 | jData = self.user.tcpCliSock.recv(BUFSIZ) 139 | data = json.loads(jData) 140 | print "___receive___" + jData 141 | if data['type'] == 'logout': 142 | break 143 | else: 144 | handle.__main__(data) 145 | except Exception as e: 146 | print "连接中断" 147 | print e 148 | finally: 149 | name = Handle.usernames[self.user] 150 | print "用户"+ str(name) +"登出" 151 | Handle.delUser(self.user) 152 | self.user.tcpCliSock.close() 153 | 154 | def stop(self): 155 | try: 156 | self.user.tcpCliSock.shutdown(2) 157 | self.user.tcpCliSock.close() 158 | except: 159 | pass 160 | 161 | class Server(): 162 | def __main__(self): 163 | tcpSerSock = socket(AF_INET, SOCK_STREAM) 164 | tcpSerSock.bind(ADDR) 165 | tcpSerSock.listen(5) 166 | 167 | threads = [] 168 | 169 | while True: 170 | try: 171 | print 'Waiting for connection...' 172 | tcpCliSock, addr = tcpSerSock.accept() 173 | print '...connected from:', addr 174 | 175 | user = User(addr, tcpCliSock) 176 | clientThread = ClientThread(user) 177 | threads += [clientThread] 178 | clientThread.start() 179 | except KeyboardInterrupt: 180 | print 'KeyboardInterrupt:' 181 | for t in threads: 182 | t.stop() 183 | break 184 | 185 | tcpSerSock.close() 186 | 187 | if __name__ == '__main__': 188 | HOST = '' 189 | PORT = 8945 190 | BUFSIZ = 1024 191 | ADDR = (HOST, PORT) 192 | 193 | server = Server() 194 | server.__main__() 195 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python-Chatroom 2 | > TCP/IP课程作业@UCAS 3 | 4 | ![](./img/Snip20170531_1.png) 5 | ![](./img/Snip20170531_4.png) -------------------------------------------------------------------------------- /img/Snip20170531_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurray0/Python-Chatroom/0a4e6504fa466d08631a58850701babd2d8e7a87/img/Snip20170531_1.png -------------------------------------------------------------------------------- /img/Snip20170531_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurray0/Python-Chatroom/0a4e6504fa466d08631a58850701babd2d8e7a87/img/Snip20170531_4.png --------------------------------------------------------------------------------