服务端类的设置
服务端的操作思路
需要实现的功能:
客户端请求与服务端建立连接管理成员的进入和离开接收消息,实现私聊和广播保存聊天记录
【使用socket通道和多线程创建多人对话聊天室】实现思路:
如何判断成员进入离开,聊天室关闭时刻?
设置一个users列表,当一开始为空的时候,不认为此时的聊天室为空,因为大家还没进来 。当一个聊天成员发出信号的时候,users减少一个人,在这个时刻判断用户数量,如果此时用户数量为0,则说明用户都已离开,则此时关闭聊天室,然后关闭服务器的监听,此时结束所有程序 。
如何与用户建立连接?
我的想法是,需要用户在进入聊天室的时候,输入自己的昵称,如果昵称不重复,则允许进入聊天室,并且为其单独开辟一个线程用于通讯,否则拒绝为其开辟通讯线程
如何实现私聊和广播?
我的想法是,服务端统一接收所有成员的发送的信息,然后统一处理,如果有@的则解析文本,对于存在的用户则进行私聊,对于不存在的用户则改为广播 。
那么如何实现私聊的功能呢?我们知道每一个用户在于服务端产生连接的时候,会有一个专门服务于用户端和服务端的一个,这是在()接收到一个连接请求的时候产生的,那么就需要将这个保存到对应用户的字典内,通过这种手段,在对用户进行私聊时,只需要使用对应的进行分发即可 。
保存聊天记录的机制?
每个用户离开需要在服务器保存一次 。
为了保证系统的安全性,我们需要在分发信息前就将信息写入日志文件中,这样才能保证系统的可靠性 。
我采用两种保存方式:一种是保存为txt的文本文件形式,供管理员查看;一种是保存为序列化 。
同时,需要处理cmd运行时,相对路径无法调用的问题,一般被默认保存在了C盘的用户文件夹中,这跟工作路径有关 。
我最初的解决方法是,写一个绝对路径的保存方式,但是,如果采用这种方法,那么对于多人联机的使用,就需要修改文件中的保存路径了,这是不安全的 。
于是我查找了资料,除了采用cd的方式改变工作目录,还可以使用如下代码
importos#获取py 文件所在目录current_path = os.path.dirname(__file__)#把这个目录设置成工作目录os.chdir(current_path)
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("连接失败或服务器已关闭")
- Streamlit应用程序使用Streamlit
- A星算法代码
- ZYNQ:MIO、EMIO、IO的区别和灵活使用
- 使用Samba实现文件共享:Windows和Linux之间
- 使用Keras,TensorFlow.js,Node
- 10、帧动画
- 解决项目版本冲突——maven-shade插件使用
- 提示 “需要使用新应用以打开此windowsdefender链接“
- Win11解决需要使用新应用以打开此WindowsDefender链接
- 显示需要使用新应用打开此ms-windows-store链接的解决办法