题目一
本机运行结果
cmd截图
聊天记录截图
联机运行结果(聊天记录忘记截图了)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T00GwPs2-1686640907026)(D:\经管大三\现代程序设计\week13\微信图片_20221129094214.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UtfXlxfq-1686640907026)(D:\经管大三\现代程序设计\week13\微信图片_20221129094234.png)]
完整代码
首先说明一下该代码的运行方式,cmd命令行中调用该文件,输入服务器ip,服务器端口号,服务端/客户端
其次在进行联机操作的时候,开始连接校园网时,出现了无法ping通的情况
ping不通的可能原因:
1、路由不通(如不同网段且无路由);
2、路由通但对方防火墙拦截icmp;
3、路由通且无防火墙但对方设置为不响应ping;
4、处在不通的vlan;
5、ip间被隔离(常见于wlan及dslam环境);
6、中间路由过多(ttl不够大)。
可能是由于校园网局域网有ip隔离
于是采用网络热点的方式,应注意,需要关闭电脑防火墙拦截icmp协议,否则也是ping不通的
from socket import *
from threading import Thread,Lock
import queue
import sys
import time
import pickle
import re
BUFFER=1024
#对于server类,需要ip和端口号,设置ip和端口号
users={}#用于统计聊天的人数,将昵称与用户对应,构成为用户名:conn,ip,port,这样可以做到转发的效果
Record=[]#用来保存聊天记录,这是一个聊天室存档
MAX_L=10
sign=1#当线程byebye以后变为0
'''对于server类,首先需要创建一个socket用于监听整个过程
其次接收一个client的连接请求以后,建立一个专门用于通讯的socket,并且通过一个线程来控制
其次,我们希望做到的聊天室是可以进行广播和私聊的,显然,服务器起到一个转发的作用,因此消息通知应该比较有针对性,针对某一用户进行
其次需要设置一个队列供线程调用,需要设置一个线程锁,用来保护聊天记录
'''
Q=queue.Queue()#保存聊天语句
lock=Lock()#线程锁保护列表
class server():#manager类
def __init__(self,post,port):
self._post=post
self._port=port
self.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 sign
while True:
data=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服务端已经关闭了")
break
else:#接下来对正常的语句进行解析,分为广播和私信
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:
continue
users[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:
continue
users[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 sign
print("欢迎{}进入聊天室...".format(name))
while True:
try:
msg = conn.recv(BUFFER)
if not msg:
break
str=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=0
break#跳出循环
break#退出一个以后就需要关闭响应端口,否则只有在接受了非正常信息或者都退出聊天室以后才会关闭端口,端口需要两头都关闭才行
except Exception as e:
print("server error %s" % e)
break
conn.close()
print(f"关闭{name}的通道")
def run(self):#是开始这个线程的运行标志,当接收到一个用户时,就生成一个线程,可以参照mtserver文件
global sign
sen=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和port
t = Thread(target=self.speak, args=(name,conn))#注意类内调用函数前面需要加self,此时生成了一个线程专门用于处理该用户的通信
t.start()
#t.join()#这边如果不阻塞的话,就会直接判断sign语句,因此无效,但这个时候需要接入另一个进程,因此不能阻塞
#对于while循环来说可以看做一个主线程,那么子线程在跑的同时,主线程直接跑到while循环的位置,开始下一步监听,因此无法break
# if sign==0:
# print("确认关闭服务器")
# break
except:
print("连接失败或服务器已关闭")
'''对于client需要两个线程用于收发消息
所以定义两个函数,一个recv,一个send
两个函数常开,当读入一个时,就发送
'''
class client():#chatter类
def __init__(self,ip,port):
self.ip=ip
self.port=port
self.client=socket(AF_INET,SOCK_STREAM)
self.client.connect((self.ip,self.port))#发送连接请求
def recv(self,locky,lis):
global sign
while True:
data = self.client.recv(BUFFER)
locky.acquire()
lis.append(data.decode('utf-8'))
locky.release()
if sign==0:
break
else:
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:
continue
self.client.send(msg.encode('utf-8'))
if msg == 'byebye':
sign=0
break
def run(self):
list=[]#保存该成员聊天记录
locky=Lock()#用于保护该成员聊天记录的锁
tr = Thread(target=self.recv, args=(locky,list))#由于接收转发的信息来自服务器,因此没有意义去打印ip,port
tr.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()
服务端类的设置
服务端的操作思路
需要实现的功能:
- 客户端请求与服务端建立连接
- 管理成员的进入和离开
- 接收消息,实现私聊和广播
- 保存聊天记录
实现思路:
-
如何判断成员进入离开,聊天室关闭时刻?
设置一个users列表,当一开始为空的时候,不认为此时的聊天室为空,因为大家还没进来。当一个聊天成员发出byebye信号的时候,users减少一个人,在这个时刻判断用户数量,如果此时用户数量为0,则说明用户都已离开,则此时关闭聊天室,然后关闭服务器的监听socket,此时结束所有程序。
-
如何与用户建立连接?
我的想法是,需要用户在进入聊天室的时候,输入自己的昵称,如果昵称不重复,则允许进入聊天室,并且为其单独开辟一个线程用于通讯,否则拒绝为其开辟通讯线程
-
如何实现私聊和广播?
我的想法是,服务端统一接收所有成员的发送的信息,然后统一处理,如果有@的则解析文本,对于存在的用户则进行私聊,对于不存在的用户则改为广播。
那么如何实现私聊的功能呢?我们知道每一个用户在于服务端产生连接的时候,会有一个专门服务于用户端和服务端的一个socket,这是在accept()接收到一个连接请求的时候产生的,那么就需要将这个socket保存到对应用户的字典内,通过这种手段,在对用户进行私聊时,只需要使用对应的socket进行分发即可。
-
保存聊天记录的机制?
每个用户离开需要在服务器保存一次。
为了保证系统的安全性,我们需要在分发信息前就将信息写入日志文件中,这样才能保证系统的可靠性。
我采用两种保存方式:一种是保存为txt的文本文件形式,供管理员查看;一种是保存为序列化。
同时,需要处理cmd运行python时,相对路径无法调用的问题,一般被默认保存在了C盘的用户文件夹中,这跟工作路径有关。
我最初的解决方法是,写一个绝对路径的保存方式,但是,如果采用这种方法,那么对于多人联机的使用,就需要修改文件中的保存路径了,这是不安全的。
于是我查找了资料,除了采用cd的方式改变工作目录,还可以使用如下代码
import os #获取py 文件所在目录 current_path = os.path.dirname(__file__) #把这个目录设置成工作目录 os.chdir(current_path)
from socket import *
from threading import Thread,Lock
import queue
import sys
import time
import pickle
import re
BUFFER=1024
#对于server类,需要ip和端口号,设置ip和端口号
users={}#用于统计聊天的人数,将昵称与用户对应,构成为用户名:conn,ip,port,这样可以做到转发的效果
Record=[]#用来保存聊天记录,这是一个聊天室存档
MAX_L=10
sign=1#当线程byebye以后变为0
'''对于server类,首先需要创建一个socket用于监听整个过程
其次接收一个client的连接请求以后,建立一个专门用于通讯的socket,并且通过一个线程来控制
其次,我们希望做到的聊天室是可以进行广播和私聊的,显然,服务器起到一个转发的作用,因此消息通知应该比较有针对性,针对某一用户进行
其次需要设置一个队列供线程调用,需要设置一个线程锁,用来保护聊天记录
'''
Q=queue.Queue()#保存聊天语句
lock=Lock()#线程锁保护列表
class server():#manager类
def __init__(self,post,port):
self._post=post
self._port=port
self.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 sign
while True:
data=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服务端已经关闭了")
break
else:#接下来对正常的语句进行解析,分为广播和私信
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:
continue
users[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:
continue
users[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 sign
print("欢迎{}进入聊天室...".format(name))
while True:
try:
msg = conn.recv(BUFFER)
if not msg:
break
str=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=0
break#跳出循环
break#退出一个以后就需要关闭响应端口,否则只有在接受了非正常信息或者都退出聊天室以后才会关闭端口,端口需要两头都关闭才行
except Exception as e:
print("server error %s" % e)
break
conn.close()
print(f"关闭{name}的通道")
def run(self):#是开始这个线程的运行标志,当接收到一个用户时,就生成一个线程,可以参照mtserver文件
global sign
sen=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和port
t = Thread(target=self.speak, args=(name,conn))#注意类内调用函数前面需要加self,此时生成了一个线程专门用于处理该用户的通信
t.start()
#t.join()#这边如果不阻塞的话,就会直接判断sign语句,因此无效,但这个时候需要接入另一个进程,因此不能阻塞
#对于while循环来说可以看做一个主线程,那么子线程在跑的同时,主线程直接跑到while循环的位置,开始下一步监听,因此无法break
# if sign==0:
# print("确认关闭服务器")
# break
except:
print("连接失败或服务器已关闭")
客户端类的设置
客户端需要实现的功能:
两个线程:
- 一个线程负责发送
- 一个线程负责接收
'''对于client需要两个线程用于收发消息
所以定义两个函数,一个recv,一个send
两个函数常开,当读入一个时,就发送
'''
class client():#chatter类
def __init__(self,ip,port):
self.ip=ip
self.port=port
self.client=socket(AF_INET,SOCK_STREAM)
self.client.connect((self.ip,self.port))#发送连接请求
def recv(self,locky,lis):
global sign
while True:
data = self.client.recv(BUFFER)
locky.acquire()
lis.append(data.decode('utf-8'))
locky.release()
if sign==0:
break
else:
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:
continue
self.client.send(msg.encode('utf-8'))
if msg == 'byebye':
sign=0
break
def run(self):
list=[]#保存该成员聊天记录
locky=Lock()#用于保护该成员聊天记录的锁
tr = Thread(target=self.recv, args=(locky,list))#由于接收转发的信息来自服务器,因此没有意义去打印ip,port
tr.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')
难点
其实功能实现比较简单
难点在于,梳理明白各个线程之间的关系,以及while循环什么时候跳出,如何控制while循环的跳出,怎么控制,信号在控制的时候,有没有起到作用。
其次就是正则表达式的写法,试了有点时间
解决:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-X0zFoe2u-1686640907028)(C:\Users\kerrla\AppData\Roaming\Typora\typora-user-images\image-20221129113110876.png)]
可以看到两个线程是相对独立运行的,因此需要在sen_client内关闭服务器,另一侧无法控制
ient.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’)
### 难点
其实功能实现比较简单
难点在于,梳理明白各个线程之间的关系,以及while循环什么时候跳出,如何控制while循环的跳出,怎么控制,信号在控制的时候,有没有起到作用。
其次就是正则表达式的写法,试了有点时间
**解决:**
![](https://img-blog.csdnimg.cn/875c0e7d287f43138a2b496bf8330c8a.png)
可以看到两个线程是相对独立运行的,因此需要在sen_client内关闭服务器,另一侧无法控制