使用socket通道和多线程创建多人对话聊天室

题目一 本机运行结果
cmd截图
聊天记录截图
联机运行结果(聊天记录忘记截图了)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img--26)(D:\经管大三\现代程序设计\\微信图片_214.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img--26)(D:\经管大三\现代程序设计\\微信图片_234.png)]
完整代码
首先说明一下该代码的运行方式,cmd命令行中调用该文件,输入服务器ip,服务器端口号,服务端/客户端
其次在进行联机操作的时候,开始连接校园网时,出现了无法ping通的情况
ping不通的可能原因:
1、路由不通(如不同网段且无路由);
2、路由通但对方防火墙拦截icmp;
3、路由通且无防火墙但对方设置为不响应ping;
4、处在不通的vlan;
5、ip间被隔离(常见于wlan及dslam环境);
6、中间路由过多(ttl不够大) 。
可能是由于校园网局域网有ip隔离
于是采用网络热点的方式,应注意,需要关闭电脑防火墙拦截icmp协议,否则也是ping不通的

使用socket通道和多线程创建多人对话聊天室

文章插图
from socket import *from threading import Thread,Lockimport queueimport sysimport timeimport pickleimport reBUFFER=1024#对于server类,需要ip和端口号,设置ip和端口号users={}#用于统计聊天的人数,将昵称与用户对应,构成为用户名:conn,ip,port,这样可以做到转发的效果Record=[]#用来保存聊天记录,这是一个聊天室存档MAX_L=10sign=1#当线程byebye以后变为0'''对于server类,首先需要创建一个socket用于监听整个过程其次接收一个client的连接请求以后,建立一个专门用于通讯的socket,并且通过一个线程来控制其次,我们希望做到的聊天室是可以进行广播和私聊的,显然,服务器起到一个转发的作用,因此消息通知应该比较有针对性,针对某一用户进行其次需要设置一个队列供线程调用,需要设置一个线程锁,用来保护聊天记录'''Q=queue.Queue()#保存聊天语句lock=Lock()#线程锁保护列表class server():#manager类def __init__(self,post,port):self._post=postself._port=portself.server = socket(AF_INET, SOCK_STREAM)#生成一个socket实例self.server.bind((post,port))#绑定地址和端口号self.server.setsockopt(SOL_SOCKET, SO_REUSEADDR,1)#设置参数self.server.listen(MAX_L)#设置最大监听数print("聊天室已开启,等待用户进入......")'''speak函数用于接收各个线程发送的语句,我想的是将语句存在一个队列中,这样就不需要上锁了其次,我希望设置一个发送语句的函数send_client,根据有无@某一用户来选择是广播还是私信解析格式为 时间 发送的用户名(按照规定不能以数字开头,只能以字符开头):需要发送的结果(如果有@则解析,规定@之后需要加空格)另外需要一个列表,用来保存已经发送的语句,并将其保存在硬盘中,每次有用户离开保存一次'''def send_client(self):#始终打开,我们设置当读到\eof时认为是结束了运行global signwhile True:data=http://www.kingceram.com/post/Q.get()if data =="\eof":#说明聊天室已经关闭,则没有必要进行聊天结果的分发,结束该进程print("开始关闭服务器")self.server.close()time.sleep(0.5)print("服务端状态如下:")if (getattr(self.server, '_closed') == False):print("当前socket服务端正在运行中")elif (getattr(self.server, '_closed') == True):print("当前socket服务端已经关闭了")breakelse:#接下来对正常的语句进行解析,分为广播和私信content = re.sub(r"\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\s", "", data)# 匹配语句正文内容,\s表示空格reciever= re.search("@\w*\s",content)# 匹配@对象,如果未匹配成功是无法使用group()调用结果的,注意空格也被匹配进去了,由于 . 表示除了换行符的任意字符因此无法得到单独的人名sender=re.search("\w*:",content).group()[:-1]#匹配发送方if reciever is None:#说明是广播for item in list(users.keys()):if item==sender:continueusers[item]["conn"].send(content.encode("utf-8"))else:rec=reciever.group()[1:-1]#注意receiver是一个匹配函数返回结果,而不是字符串,字典里不要搞错了if rec in list(users.keys()):users[rec]["conn"].send(content.encode("utf-8"))else:#找不到对象,提醒发送端的用户无该用户,并且转为广播users[sender]["conn"].send(f"server:sorry can not find {rec}".encode("utf-8"))#转广播for item in list(users.keys()):if sender==item:continueusers[item]["conn"].send(content.encode("utf-8"))def cun(self):with lock:#当保存list的时候,list被保护起来,不允许被操作with open("D:/经管大三/现代程序设计/week13/序列化_聊天记录.txt","wb") as f:#需要设置完整的存储路径才行pickle.dump(Record,f)#将列表序列化保存with open("D:/经管大三/现代程序设计/week13/聊天记录.txt","w") as f:for item in Record:f.write(item+'\n')def speak(self,name,conn):#该函数负责接收与分发global signprint("欢迎{}进入聊天室...".format(name))while True:try:msg = conn.recv(BUFFER)if not msg:breakstr=f"{time.strftime('%Y-%m-%d %H:%M:%S')} {name}:{msg.decode('utf-8')}"#格式化聊天信息print(str)with lock:Record.append(str)Q.put(str)# 将聊天语句放入队列中,如果所有的用户退出,就没有必要进行byebye语句的分发了if msg.decode('utf-8') == 'byebye':#由于删除了一个用户,因此需要将# 相应的用户从users中删除print("{}离开了聊天室...".format(name))users.pop(name)#将相应的user给删除self.cun()#每一次退出一个用户,就进行一次保存if len(users) == 0:print("聊天室关闭")Q.put("\eof")sign=0break#跳出循环break#退出一个以后就需要关闭响应端口,否则只有在接受了非正常信息或者都退出聊天室以后才会关闭端口,端口需要两头都关闭才行except Exception as e:print("server error %s" % e)breakconn.close()print(f"关闭{name}的通道")def run(self):#是开始这个线程的运行标志,当接收到一个用户时,就生成一个线程,可以参照mtserver文件global signsen=Thread(target=self.send_client)#始终打开,用于运行分发,当然这需要是一个新的线程sen.start()while sign:#每进行一次循环就会进入一个新用户//经过多次实践,发现从循环内部使用break指令已不显示,从while条件入手try:time.sleep(1)#进入一个用户则增加一个缓冲时间conn, addr = self.server.accept()# conn表示系统为新连入的客户端分配的socket,addr表示IP和端口号ci, cp = addr# 启动一个线程处理该连接,主线程继续处理其他连接conn.send("Hello,请输入您的昵称".encode("utf-8"))name=conn.recv(1024).decode("utf-8")#编码解码while name in list(users.keys()):conn.send("please choose another name".encode("utf-8"))name=conn.recv(1024).decode("utf-8")conn.send("welcome!!".encode("utf-8"))#将用户保存在users文件中dic={"conn":conn,"ip":ci,"port":cp}#保存接口和ip,cp,便于之后进行广播和users[name]=dic#用户,及其ip和portt = Thread(target=self.speak, args=(name,conn))#注意类内调用函数前面需要加self,此时生成了一个线程专门用于处理该用户的通信t.start()#t.join()#这边如果不阻塞的话,就会直接判断sign语句,因此无效,但这个时候需要接入另一个进程,因此不能阻塞#对于while循环来说可以看做一个主线程,那么子线程在跑的同时,主线程直接跑到while循环的位置,开始下一步监听,因此无法break# if sign==0:#print("确认关闭服务器")#breakexcept:print("连接失败或服务器已关闭")'''对于client需要两个线程用于收发消息所以定义两个函数,一个recv,一个send两个函数常开,当读入一个时,就发送'''class client():#chatter类def __init__(self,ip,port):self.ip=ipself.port=portself.client=socket(AF_INET,SOCK_STREAM)self.client.connect((self.ip,self.port))#发送连接请求def recv(self,locky,lis):global signwhile True:data = http://www.kingceram.com/post/self.client.recv(BUFFER)locky.acquire()lis.append(data.decode('utf-8'))locky.release()if sign==0:breakelse:print(data.decode('utf-8'))def send(self,locky,lis):global sign #赋值需要声明全局变量while True:msg = input("")locky.acquire()lis.append(msg)locky.release()if not msg:continueself.client.send(msg.encode('utf-8'))if msg == 'byebye':sign=0breakdef run(self):list=[]#保存该成员聊天记录locky=Lock()#用于保护该成员聊天记录的锁tr = Thread(target=self.recv, args=(locky,list))#由于接收转发的信息来自服务器,因此没有意义去打印ip,porttr.start()ts = Thread(target=self.send,args=(locky,list))ts.start()tr.join()ts.join()name=self.client.getsockname()[1]#getsockname返回ip地址和端口号名称self.client.close()with open(f"D:/经管大三/现代程序设计/week13/{name}_序列化.txt","wb") as f:pickle.dump(list,f)with open(f"D:/经管大三/现代程序设计/week13/{name}.txt", "w") as f:for item in list:f.write(item+'\n')def main():post=sys.argv[1]port=int(sys.argv[2])if sys.argv[3]=="client":c=client(post,port)c.run()elif sys.argv[3]=="server":s=server(post,port)s.run()if __name__=='__main__':main()