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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YjUNhL4W-1686640907023)(C:\Users\kerrla\AppData\Roaming\Typora\typora-user-images\image-20221125203617637.png)]

题目一

本机运行结果

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()

服务端类的设置

服务端的操作思路

需要实现的功能:

  1. 客户端请求与服务端建立连接
  2. 管理成员的进入和离开
  3. 接收消息,实现私聊和广播
  4. 保存聊天记录

实现思路:

  1. 如何判断成员进入离开,聊天室关闭时刻?

    设置一个users列表,当一开始为空的时候,不认为此时的聊天室为空,因为大家还没进来。当一个聊天成员发出byebye信号的时候,users减少一个人,在这个时刻判断用户数量,如果此时用户数量为0,则说明用户都已离开,则此时关闭聊天室,然后关闭服务器的监听socket,此时结束所有程序。

  2. 如何与用户建立连接?

    我的想法是,需要用户在进入聊天室的时候,输入自己的昵称,如果昵称不重复,则允许进入聊天室,并且为其单独开辟一个线程用于通讯,否则拒绝为其开辟通讯线程

  3. 如何实现私聊和广播?

    我的想法是,服务端统一接收所有成员的发送的信息,然后统一处理,如果有@的则解析文本,对于存在的用户则进行私聊,对于不存在的用户则改为广播。

    那么如何实现私聊的功能呢?我们知道每一个用户在于服务端产生连接的时候,会有一个专门服务于用户端和服务端的一个socket,这是在accept()接收到一个连接请求的时候产生的,那么就需要将这个socket保存到对应用户的字典内,通过这种手段,在对用户进行私聊时,只需要使用对应的socket进行分发即可。

  4. 保存聊天记录的机制?

    每个用户离开需要在服务器保存一次。

    为了保证系统的安全性,我们需要在分发信息前就将信息写入日志文件中,这样才能保证系统的可靠性。

    我采用两种保存方式:一种是保存为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("连接失败或服务器已关闭")

客户端类的设置

客户端需要实现的功能:

两个线程:

  1. 一个线程负责发送
  2. 一个线程负责接收
'''对于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内关闭服务器,另一侧无法控制
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值