该程序由客户端与服务器构成,使用UDP服务,服务器端绑定本地IP和端口,客户端由系统随机选择端口。
实现了群发、私发、点对点文件互传功能。
客户端自建了一个类继承了Cmd模块,使用自定义的命令command进行操作,调用相应的do_command方法。
使用json模块进行消息的封装序列化,在接收方进行解析。
客户端代码如下:
import socket
import threading
import json
import os
from cmd import Cmdclass Client(Cmd):"""客户端"""prompt = '>>>'intro = '[Welcome] 简易聊天室客户端(Cli版)\n' + '[Welcome] 输入help来获取帮助\n'buffersize = 1024def __init__(self, host):"""构造"""super().__init__()self.__socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)# self.__id = Noneself.__nickname = Noneself.__host = hostself.thread_recv = Noneself.threadisalive = False# 是否在接收文件self.recvfile = False# 是否在发送文件self.sendfile = Falseself.filesize = Noneself.sendfilesize = None# 接收文件包计数self.filecount = None# 接收文件名self.filename = None# 发送文件名self.sendfilename = None# 发送者self.filefrom = None# 接收者self.fileto = None# 接收文件流self.file_recv = None# 发送文件流self.file_send = None# 接收文件地址self.filefrom_addr = None# 发送文件地址self.fileto_addr = Nonedef __receive_message_thread(self):"""接受消息线程"""while self.threadisalive:# noinspection PyBroadExceptiontry:buffer, addr = self.__socket.recvfrom(1024)'''文件流由发送端直接发送,不经过服务器,故当发送端发来的消息时,将收到的数据存入文件'''if (addr != self.__host) & (addr == self.filefrom_addr) & self.recvfile:self.file_recv.write(buffer)self.filecount += 1if self.filecount * 1024 >= self.filesize:self.file_recv.close()print(self.filename, 'is received.')self.recvfile = Falsecontinuejs = json.loads(buffer.decode())# 若接收的数据为消息信息,则显示if js['type'] == 'message':print(js['message'])# 若接收的数据为文件发送请求,则存储文件信息,并显示elif js['type'] == 'filequest':if self.recvfile:self.__socket.sendto(json.dumps({'type': 'fileres','fileres': 'no','nickname': self.__nickname,'who': js['nickname'],'errormessage': 'is transfroming files.',}).encode(), self.__host)continuefilename = js['filename']who = js['nickname']filesize = js['filesize']self.recvfile = Trueself.filesize = filesizeself.filename = filenameself.filecount = 0self.filefrom = whoself.filefrom_addr = (js['send_ip'], js['send_port'])print('[system]:', who, ' send a file(',filename, ') to you. receive? ')# 接受的数据为请求回复,若同意接收则存储服务器发来的接收方的地址,并开启发送线程elif js['type'] == 'fileres':if js['fileres'] == 'yes':print(js['recv_ip'], js['recv_port'])self.fileto_addr = (js['recv_ip'], js['recv_port'])thread = threading.Thread(target=self.__send_file_thread)thread.start()else:print(js['nickname'], js['errormessage'])self.sendfile = Falseexcept Exception as e:print(e)print('[Client] 无法从服务器获取数据')def __send_broadcast_message_thread(self, message):"""发送广播消息线程:param message: 消息内容"""self.__socket.sendto(json.dumps({'type': 'broadcast','nickname': self.__nickname,'message': message,}).encode(), self.__host)def __send_file_thread(self):"""发送文件线程:param message: 消息内容"""filecount = 0print('[system]', 'sending the file...')while filecount * 1024 <= self.sendfilesize:self.__socket.sendto(self.file_send.read(1024), self.fileto_addr)filecount += 1self.file_send.close()self.sendfile = Falseprint('[system]', 'the file is sended.')def __send_whisper_message_thread(self, who, message):"""发送私发消息线程:param message: 消息内容"""self.__socket.sendto(json.dumps({'type': 'sendto','who': who,'nickname': self.__nickname,'message': message}).encode(), self.__host)def send_exit(self):self.__socket.sendto(json.dumps({'type': 'offline','nickname': self.__nickname,}).encode(), self.__host)def start(self):"""启动客户端"""self.cmdloop()def do_login(self, args):"""登录聊天室:param args: 参数"""nickname = args.split(' ')[0]# 将昵称发送给服务器,获取用户idself.__socket.sendto(json.dumps({'type': 'login','nickname': nickname,}).encode(), self.__host)# 尝试接受数据buffer = self.__socket.recvfrom(1300)[0].decode()obj = json.loads(buffer)if obj['login'] == 'success':self.__nickname = nicknameprint('[Client] 成功登录到聊天室')self.threadisalive = True# 开启子线程用于接受数据self.thread_recv = threading.Thread(target=self.__receive_message_thread)self.thread_recv.setDaemon(True)self.thread_recv.start()else:print('[Client] 无法登录到聊天室', obj['errormessage'])def do_send(self, args):"""发送消息:param args: 参数"""if self.__nickname is None:print('请先登录!login nickname')returnmessage = args# 开启子线程用于发送数据thread = threading.Thread(target=self.__send_broadcast_message_thread, args=(message, ))thread.setDaemon(True)thread.start()def do_sendto(self, args):"""发送私发消息:param args: 参数"""if self.__nickname is None:print('请先登录!login nickname')returnwho = args.split(' ')[0]message = args.split(' ')[1]# # 显示自己发送的消息# print('[' + str(self.__nickname) + '(' + str(self.__id) + ')' + ']', message)# 开启子线程用于发送数据thread = threading.Thread(target=self.__send_whisper_message_thread, args=(who, message))thread.setDaemon(True)thread.start()def do_catusers(self, arg):if self.__nickname is None:print('请先登录!login nickname')returncatmessage = json.dumps({'type': 'catusers'})self.__socket.sendto(catmessage.encode(), self.__host)def do_catip(self, args):if self.__nickname is None:print('请先登录!login nickname')returnwho = argscatipmessage = json.dumps({'type': 'catip', 'who': who})self.__socket.sendto(catipmessage.encode(), self.__host)def do_help(self, arg):"""帮助:param arg: 参数"""command = arg.split(' ')[0]if command == '':print('[Help] login nickname - 登录到聊天室,nickname是你选择的昵称')print('[Help] send message - 发送消息,message是你输入的消息')print('[Help] sendto who message - 私发消息,who是用户名,message是你输入的消息')print('[Help] catusers - 查看所有用户')print('[Help] catip who - 查看用户IP,who为用户名')print('[Help] sendfile who filedir - 向某用户发送文件,who为用户名,filedir为文件路径')print('[Help] getfile filename who yes/no - 接收文件,filename 为文件名,who为发送者,yes/no为是否接收')elif command == 'login':print('[Help] login nickname - 登录到聊天室,nickname是你选择的昵称')elif command == 'send':print('[Help] send message - 发送消息,message是你输入的消息')elif command == 'sendto':print('[Help] sendto who message - 发送私发消息,message是你输入的消息')else:print('[Help] 没有查询到你想要了解的指令')def do_exit(self, arg): # 以do_*开头为命令print("Exit")self.send_exit()try:self.threadisalive = Falseself.thread_recv.join()except Exception as e:print(e)# self.__socket.close()def do_sendfile(self, args):who = args.split(' ')[0]filepath = args.split(' ')[1]filename = filepath.split('\\')[-1]# 判断是否在发送文件if self.sendfile:print('you are sending files, please try later.')returnif not os.path.exists(filepath):print('the file is not exist.')returnfilesize = os.path.getsize(filepath)# print(who, filename, filesize)self.sendfile = Trueself.fileto = whoself.sendfilename = filenameself.sendfilesize = filesizeself.file_send = open(filepath, 'rb')self.__socket.sendto(json.dumps({'type': 'filequest','nickname': self.__nickname,'filename': self.sendfilename,'filesize': self.sendfilesize,'who': self.fileto,'send_ip': '','send_port': '',}).encode(), self.__host)print('request send...')# fileres = self.__socket.recvfrom(1024)[0].decode()# js = json.loads(fileres)def do_getfile(self, args):filename = args.split(' ')[0]who = args.split(' ')[1]ch = args.split(' ')[2]# print(self.filename is not None, filename, self.filename, who, self.filefrom)if (self.filename is not None) & (filename == self.filename) & (who == self.filefrom):if ch == 'yes':self.file_recv = open(self.filename, 'wb')self.__socket.sendto(json.dumps({'type': 'fileres','fileres': 'yes','nickname': self.__nickname,'who': who,'recv_ip': '','recv_port': '',}).encode(), self.__host)print('you agree to reveive the file(', filename, ') from', who)else:self.__socket.sendto(json.dumps({'type': 'fileres','fileres': 'no','nickname': self.__nickname,'errormessage': 'deny the file.','who': who,'recv_ip': '','recv_port': '',}).encode(), self.__host)print('you deny to reveive the file(', filename, ') from', who)self.recvfile = Falseelse:print('the name or sender of the file is wrong.')c = Client(('127.0.0.1', 12346))
c.start()
server:
import socket
import jsonobj = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
obj.bind(('127.0.0.1', 12346))conn_list = []
user_list = []while True:try:receive_data, client_address = obj.recvfrom(1024)js = json.loads(receive_data.decode())# 登录消息if js['type'] == 'login':nickname = str(js['nickname'])if nickname in user_list:obj.sendto(json.dumps({'login': 'fail','errormessage': 'the nickname is exists'}).encode(),client_address)else:# 向其他用户发送通知for i in range(len(conn_list)):obj.sendto(json.dumps({'type': 'message','message': '[system]' + nickname + '已登录.'}).encode(), conn_list[i])user_list.append(nickname)conn_list.append(client_address)print(nickname, client_address, '登录成功!')obj.sendto(json.dumps({'login': 'success','nickname': nickname}).encode(), client_address)# 群发消息elif js['type'] == 'broadcast':message = js['message']nickname = js['nickname']for i in range(len(conn_list)):obj.sendto(json.dumps({'type': 'message','message': nickname + ':' + message}).encode(), conn_list[i])# 私发消息elif js['type'] == 'sendto':who = js['who']nickname = js['nickname']message = js['message']# 检查用户是否存在if who not in user_list:obj.sendto(json.dumps({'type': 'message','message': who + ' not exist or not online.please try later.'}).encode(),client_address)else:obj.sendto(json.dumps({'type': 'message','message': nickname + ' whisper to you: ' + message}).encode(),conn_list[user_list.index(who)])# 查看用户列表elif js['type'] == 'catusers':users = json.dumps(user_list)obj.sendto(json.dumps({'type': 'message','message': users,}).encode(),client_address)# 查看用户IPelif js['type'] == 'catip':who = js['who']if who not in user_list:obj.sendto(json.dumps({'type': 'message','message': who + ' not exist or not online.please try later.'}).encode(),client_address)else:addr = json.dumps(conn_list[user_list.index(who)])obj.sendto(json.dumps({'type': 'message','message': addr,}).encode(),client_address)# 离线消息elif js['type'] == 'offline':user_list.remove(js['nickname'])conn_list.remove(client_address)obj.sendto((js['nickname'] + 'offline.').encode(),client_address)# 向其他用户发送通知for i in range(len(conn_list)):obj.sendto(json.dumps({'type': 'message','message': '[system]' + nickname + '下线了.'}).encode(), conn_list[i])# 发送文件请求elif js['type'] == 'filequest':who = js['who']if who not in user_list:obj.sendto(json.dumps({'type': 'message','message': who + ' not exist or not online.please try later.'}).encode(),client_address)else:js['send_ip'] = client_address[0]js['send_port'] = client_address[1]obj.sendto(json.dumps(js).encode(),conn_list[user_list.index(who)])print(js['nickname'], 'request to send file to', who)# 发送文件请求回复elif js['type'] == 'fileres':who = js['who']if js['fileres'] == 'yes':js['recv_ip'] = client_address[0]js['recv_port'] = client_address[1]print(js['nickname'], 'agree to receive file from', js['who'])else:print(js['nickname'], 'deny to receive file from', js['who'])obj.sendto(json.dumps(js).encode(),conn_list[user_list.index(who)])except Exception as e:print(e)