[Socket]Python用UDP协议建立带有私聊功能的网络聊天室-建立聊天工具

文章目录

  • 前言
  • 1、网络聊天室的基本架构是什么?
    • 1.1 客户端和服务器的架构
    • 1.2 通信协议的选择以及多线程通信
      • 1.2.1 多线程通信
      • 1.2.2 通信协议选择
    • 1.3 前后端功能设计思路
      • 1.3.1 前端
      • 1.3.2 后端
        • 客户端
        • 服务器
          • 服务器接收用户信息线程的实现:
          • 服务器发送聊天内容的线程实现:
  • 二、总体代码
    • 2.1 如何在同一台电脑运行服务器和客户端:
    • 2.2 如何在多台电脑运行服务器和客户端:
    • 2.3 Client服务器代码
    • 2.4 Server服务器代码
  • 总结

前言

前些天实习面试的时候被面试官问到Socket编程的问题,即“Socket创建和释放的具体过程是什么”,当时答不上来,似乎是涉及到发送和接收缓冲区的问题。由于自己在Socket编程这一块知识较为薄弱,于是写下这篇文章,当作复习下Socket编程知识。

首先,该实验是我在大三上学期上“泛在网技术”这门课时完成的,实验题目如下所示:

实验3:建立聊天工具
  1、实验目的:
      要求学生掌握Socket编程中流套接字的技术
  2、实验内容:
      i.要求学生掌握利用Socket进行编程的技术
      ii.必须掌握多线程技术,保证双方可以同时发送
      iii.建立聊天工具
      iv.可以和多个人同时进行聊天
      v.必须使用图形界面,显示双方的语录

我们可以看到实验要求,即利用Socket编程技术建立带有图形界面的聊天工具(一个像QQ群且带私聊功能的聊天室)。

最开始我看到这个题目时比较懵,毕竟先前只是学习了基本的通信协议,结果实验里突然就要实战Socket编程,而且还是个群聊聊天室。故零基础的我也是在网上先看了很久的资料才开始有思路。

首先初学者写Socket程序的话,一般都是Python语言写,方便快捷。我这里建议先去B站看下Python语言的Socket教学视频进行快速入门下,至少理解端口绑定,等待,接收,发送等socket操作的概念。

了解完概念后,我们就可以着手构建我们的网络聊天室。(代码放在文章末尾)

提示:以下是本篇文章正文内容,仅供参考

1、网络聊天室的基本架构是什么?

1.1 客户端和服务器的架构

作为一个多人聊天室,我们要支持多个用户同步信息。
那么怎么实现多用户信息的同步呢?
比如我们现在有3个用户A,B,C,当A发送一句“Hello”的时候,怎么让B和C都能收到“Hello”呢?

那么最容易想到的方法就是,我们让ABC两两之间互相建立socket连接,当A发送信息的时候,我们让A对两个socket连接都发送一次“Hello”信息,这样用户B和C就都能接收到“Hello”了。

但其实这样有一个缺点,随着用户的增加,我们每个用户所要建立的socket连接就越来越多。比如群里有100个用户,那一个用户要发送消息的时候,就要遍历99个连接来发送信息,这样对用户的机器开销是很大的。

故我们很容易想到一个改进的方法:利用服务器转发数据。


我们让ABC三个用户分别和服务器建立socket连接。然后A只要发送一次“Hello”给服务器的socket连接,然后服务器会自动帮我们把信息同时转发给B和C,这样大大提升了A的用户体验,也方便我们进行消息的同步。

当然了,在这次实验中,我把我的电脑既当作服务器,又当作客户端,其实开销还是一样大的。但是以这种思路来写这个工具会让难度减小很多,代码冗余量也会减少很多。

1.2 通信协议的选择以及多线程通信

上述提到了我们是以客户端-服务器的方式进行通信,那么在通信过程中,我们还会遇到什么样的问题呢?

1.2.1 多线程通信

假设我们服务器用的是TCP协议(后面会解释TCP和UDP区别),且服务器只负责接收用户A的消息,并将A的消息转发给B和C的话,那服务器只要和A建立连接,然后用while()循环一直监听该连接否有接收到来自A的新数据,一旦接收到A发出的数据我们就迅速将其转发给BC即可。

但实际上我们的服务器要同时监听多个用户的信息,比如在本文例子中,我们要同时监听ABC三个用户是否有数据发送。
较容易想到的方法是,在while()循环里把ABC的socket连接同时监听,同时处理。


但这会带来一个问题,倘若ABC在同一时间发送数据了,那服务器还是只能先处理A再处理B再处理C,并不能达到群聊中我们想要的那种真正多人同步的效果。

所以这里我们要用到一个并行技术叫线程thread:

上面这个图是我找的一个网图,虽然看起来这个函数很陌生,但其实很好理解,大家看一下网上文章教程就能很容易使用起来了。简单来说,就是每当主程序新接收到一个socket请求,我们就创建一个线程(Thread)来处理该请求,创建线程的方法为threading.Thread(target=该线程要进行的函数的函数名,args=(该函数的输入参数))

其主要功能就是能让我们的服务器能同时进行多个循环,服务器每收到一个socket请求我们就建立一个线程去处理。该线程可以独立自主地进行自己的事务,即独立自主的循环监听socket信息,这样就不用像之前的服务器一样只用一个循环依次监听ABC用户。

以下图片是不用线程技术,以及用了线程技术(且用的是TCP协议)的流程图对比:

故我们在聊天室代码中也是加入了线程这个技术(几行代码就行,不难)。

除了服务器要为每个socket建立一个线程,客户端本身也要启动两个线程,一个线程用于让自己发送数据给服务器,一个线程用于监听服务器是否有发送数据给自己,这样就能做到边发信息边收信息了。

1.2.2 通信协议选择

解决完多用户通信的问题后,我们要选择我们的通信协议。
最常见的是TCP和UDP协议。

TCP协议
优点:信息传输可靠,不会出现A用户发出的数据丢失,导致B和C收不到的情况。
缺点:TCP要求服务器要和用户建立连接且不允许临时断开,故我们为了保持用户能一直处于连接状态,就要让服务器为每一个socket连接开一个线程让它不停的跑(用while不停的监听信息)。所以当用户连接越多,服务器要添加的线程也就越多。比如上述图中,监听ABC用户需要用到3个线程,比较占用资源(当然你也可以检测哪个socket长时间不工作就销毁该线程)。

UDP协议
优点:传输速度快,不用管信息是否送达,即不用维持连接,所以用户发完数据就不用管了。这种情况下服务器只要建立两个线程,一个线程用于接收用户发送的数据,另一个线程用于发送数据,而不用额外的线程来维护与用户的连接。
缺点:消息无法确保可靠送达。

按理来说,作为网络聊天室,我们对信息的传输速度要求没那么高,对信息可靠性传输反而高一点,所以网上大部分教程所用的协议也都是TCP协议。
比如本篇文章参考的教程: 用Python怎么实现一个网络聊天室功能

但由于TCP实现网络聊天室的教程较多,我自己就想尝试利用UDP协议进行实现,试试新的方法。如果你想用TCP来做聊天室的话,下面的教程你同样可以参考。UDP和TCP协议的不同主要就在服务器所要开设线程数目上,其余思路基本一致。

下图即为UDP服务器的流程图:
客户端往服务器地址发送数据后就不用管了,服务器的接收信息线程会自己把数据存入到缓冲区中。
然后服务器转发信息线程检测到缓冲区有数据了,就会把该数据群发到所有客户端IP地址(客户端在登录到服务器的时候会携带自己的客户端IP信息,服务器会将这个信息储存到列表里,后面转发时再遍历该列表依次发送信息,从而达到群发的效果)。

1.3 前后端功能设计思路

选择好通信协议后,我们就要进行下一步的具体设计。

首先,我们需要带有图形界面的聊天室,那我们就需要学习如何制作前端基本界面,如何将前端界面显示内容与后端数据(如聊天信息,聊天室里的群友名)绑定起来。

这里我用的图形界面工具是tkinter,其很容易上手,不需要多少时间就能做出一个简易界面。

1.3.1 前端

前端界面包括三个页面:

  1. 登录窗口
  2. 聊天窗口
  3. 私聊窗口

1.登录窗口要能进行用户登录,比如下图所示:
目的IP地址即为服务器的IP地址,因为我们这里把自己电脑既当作服务器又当作客户端,故目的IP地址就是127.0.0.1(如果你要实现不同电脑间通信的话,则需要连接到同一局域网,并在目的IP地址一栏填入另一台作为服务器的电脑IP地址)。


点击登录按钮后,客户端会将登录窗口里输入的目的IP地址目的端口号,找到对应的服务器进行连接。在这个连接过程中,客户端还会隐式地将自身的用户名信息用户端IP信息用户所绑定端口号等登录信息发给后端,这样后端就能将用户端IP信息用户名信息存储到服务器列表里,并供与后续使用。
所以前端的 “登录” 按键要绑定一个触发函数,当我们点击“登陆”按钮后,该函数会触发,其能将我们白色输入框里的数据打包并发送后端。

2.聊天窗口要能显示服务器转发来的数据:

原理是服务器先接收到其他用户发送的信息,并将信息群发到各个客户端,客户端再根据服务器转发的信息,将信息插入在自己的聊天室界面中,从而达到别人的用户“发送”信息到自己聊天框的效果。

3.私聊窗口用于跟服务器告诉要私聊的对象和内容:
由于服务器本身已经用列表存储了用户名和用户名对应IP地址的映射关系,故我们只要告诉服务器我们要私聊的用户b,服务器就会根据该名字找到用户b对应IP,然后就将这条信息私发给b,而不是群发给所有人。


点击确定按钮,就会触发私聊函数,该函数用于将私聊用户名称和私聊内容打包发送给服务器。

总的简易流程图如下所示:

1.3.2 后端

客户端

以下为登录窗口的实现:

labelIP = tkinter.Label(root0, text='目的IP地址', bg="#F5DE83")   #LightBlue
labelIP.place(x=20, y=5, width=100, height=40)
entryIP = tkinter.Entry(root0, width=60, textvariable=IP)
entryIP.place(x=120, y=10, width=100, height=30)

labelPORT = tkinter.Label(root0, text='目的端口号', bg="#F5DE83")
labelPORT.place(x=20, y=40, width=100, height=40)
entryPORT = tkinter.Entry(root0, width=60, textvariable=PORT)
entryPORT.place(x=120, y=45, width=100, height=30)

labelUSER = tkinter.Label(root0, text='用户名', bg="#F5DE83")
labelUSER.place(x=20, y=75, width=100, height=40)
entryUSER = tkinter.Entry(root0, width=60, textvariable=USER)
entryUSER.place(x=120, y=80, width=100, height=30)

def Login():
    global IP, PORT, user
    IP = entryIP.get()	#得到用户输入的服务器IP
    PORT = entryPORT.get()	#得到用户输入的端口号
    user = entryUSER.get()	#得到用户输入的用户名
#UDP连接部分
ip_port = (IP, int(PORT))		#得到IP和PORT后就可以进行socket连接
s = socket(AF_INET, SOCK_DGRAM)
if user:
    s.sendto(user.encode(), ip_port)  # 发送用户名

以下为聊天窗口的实现:

进入聊天室后,输入用户想发送的数据,点击发送按钮。发送按钮会触发所绑定的send()函数:

def send():
    message = entryIuput.get() + '~' + user + '~' + chat
    s.sendto(message.encode(), ip_port)		#信息要先编码再发送
    print("already_send message:",message)
    INPUT.set('')
    return 'break'  #按回车后只发送不换行

receive()函数会循环监听是否接收到服务器转发来的数据,接收到消息时,会把信息显示到聊天窗口中(比如下方的显示群里的所有用户名):

def receive():
    global uses
    while True:
        data = s.recv(1024)
        data = data.decode()			#接收服务器信息
        print("rec_data:",data)
        try:
            uses = json.loads(data)
            listbox1.delete(0, tkinter.END)
            listbox1.insert(tkinter.END, "       当前在线用户:")
            
            #用Insert函数将接收到的用户信息插入到聊天界面右侧一栏中
            for x in range(len(uses)):
                listbox1.insert(tkinter.END, uses[x])
            users.append('------Group chat-------')

以下为私聊窗口的功能实现:

def Priva_window():
    chat_pri = entryPriva_target.get()	#获取私聊对象名称
    message = entryPriva_talk.get()		#获取私聊内容
    if not chat_pri:
        tkinter.messagebox.showwarning('warning', message='私聊目标名称为空!')  # 目的IP地址为空则提示
    else:
        root3.destroy()					#关闭私聊窗口
        print("chat_pri", chat_pri)
        message = message + '~' + user + '~' + chat_pri
        #将用户名和私聊内容打包成 信息~自己用户名~私聊对象名,以方便服务器根据 ~ 符号进行信息切割
        s.sendto(message.encode(), ip_port)	#发送私聊信息
        INPUT.set('')
服务器

服务器要创建两个线程,一个用于接收数据,一个用于发送数据:

    def run(self):
        self.s.bind((IP, PORT))         #绑定监听端口
        q = threading.Thread(target=self.sendData)  
        q.start()	#开启发送数据线程
        t = threading.Thread(target=self.receive)  
        t.start()	# 开启接收信息进程
服务器接收用户信息线程的实现:

主要功能是接收用户发送的数据。比如接收到“你好~用户b~0”,并分割成
“你好”,“用户b”,“0”三个信息
第一个字符串是用户发送的聊天内容
第二个字符串是发送该聊天内容的用户名
第三个字符串的0代表是群发的意思,非私聊

    def receive(self):  # 接收消息,b'用户数据,(用户地址)
        while True:
            print('a')
            Info, addr = self.s.recvfrom(1024)  # 收到的信息
            print('b')
            Info_str = str(Info, 'utf-8')
            userIP = addr[0]
            userPort = addr[1]
            print(f'Info_str:{Info_str},addr:{addr}')
            if '~0' in Info_str:# 群聊
                data = Info_str.split('~')	#根据 ~ 符号进行信息分割
                print("data_after_slpit:", data)  # data_after_slpit: ['cccc', 'a', '0']
                message = data[0]   # data
                userName = data[1]  # name
                chatwith = data[2]  # 0
                message = userName + '~' + message + '~' + chatwith  # 界面输出用户格式
                print("message:",message)
                self.Load(message, addr)
服务器发送聊天内容的线程实现:
    def sendData(self):  # 发送数据
        print('send')
        while True:
            if not messages.empty(): #如果接收到用户的信息不为空
                message = messages.get()#获取该数据
                print("messages.get()",message)
                if isinstance(message[1], str):#判断类型是否为字符串
                    print("send str")
                    for i in range(len(users)):#遍历所有用户,进行消息群发
                        data = ' ' + message[1]
                        print("send_data:",data.encode()) 
                        self.s.sendto(data.encode(),(users[i][1],users[i][2])) #聊天内容发送过去

二、总体代码

项目分为两部分代码,一部分是UDPServer.py,一部分是UDPClient.py

2.1 如何在同一台电脑运行服务器和客户端:

如果你只有一台电脑来运行程序,那么使用步骤如下:

先启动UDPServer.py,再启动UDPClient.py。
如果你要用多个用户登录到聊天室,只要将UDPClient.py复制多几份,再分别运行即可。(Client连接的IP地址填127.0.0.1即可,即自己本地IP)

2.2 如何在多台电脑运行服务器和客户端:

如果你有多台电脑来运行程序,那么使用步骤如下:

假设有3台电脑A,B,C
其中A作为服务器,B和C作为客户端
那我们首先要将ABC连接到同一个局域网或者热点中。

注:如果你在自己用自己电脑运行2.1节成功,但在多台电脑运行失败,那么原因一般是你服务器IP地址没填对,或者没将各个电脑的防火墙关闭。
我记得我之前演示的时候是将三台电脑防火墙都关了,并将网络设置成公开还是共享才能让电脑B和C连接上服务器A。

然后服务器A在自己的cmd窗口中输入ipconfig获取自己的IP地址(获取到的地址可能有很多个,用无线WIFI热点的话一般是WLAN那个地址),然后A在UDPServer.py中将IP更改成自己查到的IP。


端口自定义即可,但建议不要用8080等其他软件可能占用的端口。

接下来在电脑B和C运行UDPClient.py并将连接的服务器目的IP地址填192.168.124.18(你查到的A地址)即可

2.3 Client服务器代码

代码如下(示例):

from socket import *
import time
import tkinter
import tkinter.messagebox
import threading
import json
import tkinter.filedialog
from tkinter.scrolledtext import ScrolledText

IP = '127.0.0.1'
SERVER_PORT = 50000
user = ''
listbox1 = ''  # 用于显示在线用户的列表框
show = 1  # 用于判断是开还是关闭列表框
users = []  # 在线用户列表
chat = '0'  # 聊天对象
chat_pri = ''



# 登陆窗口的界面实现
root0 = tkinter.Tk()
root0.geometry("300x150")
root0.title('用户登陆窗口')
root0.resizable(0, 0)
one = tkinter.Label(root0, width=300, height=150, bg="#F5DE83")
one.pack()
IP = tkinter.StringVar()
IP.set('')
PORT = tkinter.StringVar()
PORT.set('')
USER = tkinter.StringVar()
USER.set('')
##将填空处内容和实际参数绑定起来 比如将输入的IP地址绑定到entryIP,以供后续使用
labelIP = tkinter.Label(root0, text='目的IP地址', bg="#F5DE83")   #bg代表颜色
labelIP.place(x=20, y=5, width=100, height=40)
entryIP = tkinter.Entry(root0, width=60, textvariable=IP)
entryIP.place(x=120, y=10, width=100, height=30)

labelPORT = tkinter.Label(root0, text='目的端口号', bg="#F5DE83")
labelPORT.place(x=20, y=40, width=100, height=40)
entryPORT = tkinter.Entry(root0, width=60, textvariable=PORT)
entryPORT.place(x=120, y=45, width=100, height=30)

labelUSER = tkinter.Label(root0, text='用户名', bg="#F5DE83")
labelUSER.place(x=20, y=75, width=100, height=40)
entryUSER = tkinter.Entry(root0, width=60, textvariable=USER)
entryUSER.place(x=120, y=80, width=100, height=30)


#界面完成后,以下就是编写实际的登录函数
def Login():
    global IP, PORT, user
    IP = entryIP.get()	#获取前面绑定的IP地址,PORT,user信息
    PORT = entryPORT.get()
    user = entryUSER.get()
    if not IP:
        tkinter.messagebox.showwarning('warning', message='目的IP地址为空!')  # 目的IP地址为空则提示
    elif not PORT:
        tkinter.messagebox.showwarning('warning', message='目的端口号为空!')  # 目的端口号为空则提示
    elif not user:
        tkinter.messagebox.showwarning('warning', message='用户名为空!')     # 客户端用户名为空则提示
    else:
        root0.destroy()	#提交后,登录窗口要自己销毁,以便进入登录成功后的界面

#登录按钮的实现
loginButton = tkinter.Button(root0, text="登录", command=Login, bg="#FF8C00")
loginButton.place(x=135, y=120, width=40, height=25)
root0.bind('<Return>', Login)	#将按钮与Login()函数绑定

root0.mainloop()


# 聊天窗口界面的实现
root1 = tkinter.Tk()
root1.geometry("640x480")
root1.title('聊天工具')
root1.resizable(0, 0)

## 聊天窗口中的消息界面的实现
listbox = ScrolledText(root1)
listbox.place(x=5, y=0, width=485, height=320)
listbox.tag_config('tag1', foreground='blue', backgroun="white")
listbox.insert(tkinter.END, '欢迎用户 '+user+' 加入聊天室!', 'tag1')
listbox.insert(tkinter.END, '\n')
# 聊天窗口中的在线用户列表界面的实现
listbox1 = tkinter.Listbox(root1)
listbox1.place(x=490, y=0, width=140, height=320)
# 聊天窗口中的聊天内容输入框界面的实现
INPUT = tkinter.StringVar()
INPUT.set('')
entryIuput = tkinter.Entry(root1, width=120, textvariable=INPUT)
entryIuput.place(x=5, y=330, width=485, height=140)



#UDP连接部分
ip_port = (IP, int(PORT))
s = socket(AF_INET, SOCK_DGRAM)
if user:
    s.sendto(user.encode(), ip_port)  # 发送用户名
else:           #e这部分else可删除,因为已经确保用户名不为空了
    s.sendto('用户名不存在', ip_port)
    user = IP + ':' + PORT

#发送聊天内容的函数实现,与下面的“发送按钮”绑定起来
def send():
    message = entryIuput.get() + '~' + user + '~' + chat
    s.sendto(message.encode(), ip_port)
    print("already_send message:",message)
    INPUT.set('')
    return 'break'  #按回车后只发送不换行

# 私聊窗口的函数实现
def Priva_window():
    chat_pri = entryPriva_target.get()
    message = entryPriva_talk.get()
    if not chat_pri:
        tkinter.messagebox.showwarning('warning', message='私聊目标名称为空!')  # 目的IP地址为空则提示
    else:
        root3.destroy()
        print("chat_pri", chat_pri)
        #print("message", message)message
        message = message + '~' + user + '~' + chat_pri
        #message = entryIuput.get() + '~' + user + '~' + chat_pri
        s.sendto(message.encode(), ip_port)
        INPUT.set('')

# 私聊窗口的界面实现。为什么私聊窗口界面要在函数里实现?因为他是要点击后自己跳出来,而不是一开始就存在的。
def Priva_Chat():
    global chat_pri,root3,window,Priva_target,labelPriva_target,entryPriva_target,Priva_talk,labelPriva_talk,entryPriva_talk
    root3 = tkinter.Toplevel(root1)
    root3.geometry("300x150")
    root3.title('私聊对象')
    root3.resizable(0, 0)
    window = tkinter.Label(root3, width=300, height=150, bg="LightBlue")
    window.pack()
    Priva_target = tkinter.StringVar()
    Priva_target.set('')
    labelPriva_target = tkinter.Label(root3, text='私聊用户名称', bg="LightBlue")
    labelPriva_target.place(x=20, y=5, width=100, height=40)
    entryPriva_target = tkinter.Entry(root3, width=60, textvariable=Priva_target)
    entryPriva_target.place(x=120, y=10, width=100, height=30)

    Priva_talk = tkinter.StringVar()
    Priva_talk.set('')
    labelPriva_talk = tkinter.Label(root3, text='私聊内容', bg="LightBlue")
    labelPriva_talk.place(x=20, y=40, width=100, height=40)
    entryPriva_talk = tkinter.Entry(root3, width=60, textvariable=Priva_talk)
    entryPriva_talk.place(x=120, y=45, width=100, height=30)

    Priva_targetButton = tkinter.Button(root3, text="确定", command=Priva_window, bg="Yellow")
    Priva_targetButton.place(x=135, y=120, width=40, height=25)

# “发送按钮”的界面实现,与send()函数绑定
sendButton = tkinter.Button(root1, text="发送", anchor='n', command=send, font=('Helvetica', 18), bg='white')
sendButton.place(x=535, y=350, width=60, height=40)
# “私聊发送按钮”的界面实现,与send()函数绑定,send通过text内容判断是私聊还是群发
PrivaButton = tkinter.Button(root1, text="私聊", anchor='n', command=Priva_Chat, font=('Helvetica', 18), bg='white')
PrivaButton.place(x=535, y=400, width=60, height=40)
root1.bind('<Return>', send)

# 接收信息的函数实现
def receive():
    global uses
    while True:
        data = s.recv(1024)
        data = data.decode()
        print("rec_data:",data)
        try:
            uses = json.loads(data)
            listbox1.delete(0, tkinter.END)
            listbox1.insert(tkinter.END, "       当前在线用户:")	#往用户列表插入信息
            #listbox1.insert(tkinter.END, "------Group chat-------")
            for x in range(len(uses)):
                listbox1.insert(tkinter.END, uses[x])
            users.append('------Group chat-------')
        except:
            data = data.split('~')
            print("data_after_slpit:",data) #data_after_slpit: ['cccc', 'a', '0/1']
            userName = data[0]   #data 
            userName = userName[1:]	#获取用户名
            message = data[1]  #信息
            chatwith = data[2]  #destination 判断是群聊还是私聊
            message = '  ' + message + '\n'
            recv_time = " "+userName+"   "+time.strftime ("%Y-%m-%d %H:%M:%S", time.localtime()) + ': ' + '\n'	#信息发送时间
            listbox.tag_config('tag3', foreground='green')
            listbox.tag_config('tag4', foreground='blue')
            if chatwith == '0':  # 群聊
                listbox.insert(tkinter.END, recv_time, 'tag3')
                listbox.insert(tkinter.END, message)
            elif chatwith != '0':  # 私聊别人或是自己发出去的私聊
                if userName == user:                     #如果是自己发出去的,用私聊字体显示
                    listbox.insert(tkinter.END, recv_time, 'tag3')
                    listbox.insert(tkinter.END, message, 'tag4')
                if chatwith == user:                                    #如果是发给自己的,用绿色字体显示
                    listbox.insert(tkinter.END, recv_time, 'tag3')
                    listbox.insert(tkinter.END, message, 'tag4')

            listbox.see(tkinter.END)


r = threading.Thread(target=receive)
r.start()  # 开始线程接收信息

root1.mainloop()
s.close()

2.4 Server服务器代码

代码如下(示例):

import tkinter as tk
from socket import *
import threading
import queue
import json  # json.dumps(some)打包  json.loads(some)解包
import os
import os.path
import sys

IP = '127.0.0.1'
#IP = '192.168.1.103'(如果是多台主机,将IP改为服务器主机的地址即可)
PORT = 8087  # 端口
messages = queue.Queue()    #存放总体数据
users = []  # 0:userName 2:str(Client_IP)  3:int(Client_PORT)定义一个二维数组
lock = threading.Lock()             #线程锁,防止多个线程占用同个资源时导致资源不同步的问题
BUFLEN=512

def Current_users():  # 统计当前在线人员,用于显示名单并发送消息
    current_suers = []
    for i in range(len(users)):
        current_suers.append(users[i][0])      #存放用户相关名字
    return  current_suers

class ChatServer(threading.Thread):
    global users, que, lock

    def __init__(self):  # 构造函数
        threading.Thread.__init__(self)
        self.s = socket(AF_INET, SOCK_DGRAM)      #用UDP连接

    # 接受来自客户端的用户名,如果用户名为空,使用用户的IP与端口作为用户名。如果用户名出现重复,则在出现的用户名依此加上后缀“2”、“3”、“4”……
    def receive(self):  # 接收消息,b'用户数据,(用户地址)
        while True:
            print('a')
            Info, addr = self.s.recvfrom(1024)  # 收到的信息
            print('b')
            Info_str = str(Info, 'utf-8')
            userIP = addr[0]
            userPort = addr[1]
            print(f'Info_str:{Info_str},addr:{addr}')
            if '~0' in Info_str:# 群聊
                data = Info_str.split('~')
                print("data_after_slpit:", data)  # data_after_slpit: ['cccc', 'a', '0']
                message = data[0]   # data
                userName = data[1]  # name
                chatwith = data[2]  # 0
                message = userName + '~' + message + '~' + chatwith  # 界面输出用户格式
                print("message:",message)
                self.Load(message, addr)
            elif '~' in Info_str and '0' not in Info_str:# 私聊
                data = Info_str.split('~')
                print("data_after_slpit:", data)  # data_after_slpit: ['cccc', 'a', 'destination_name']
                message = data[0]  # data
                userName = data[1]  # name
                chatwith = data[2]  # destination_name
                message = userName + '~' + message + '~' + chatwith  # 界面输出用户格式
                self.Load(message, addr)
            else:# 新用户
                tag = 1
                temp = Info_str
                for i in range(len(users)):  # 检验重名,则在重名用户后加数字
                    if users[i][0] == Info_str:
                        tag = tag + 1
                        Info_str = temp + str(tag)
                users.append((Info_str, userIP, userPort))
                print("users:", users)  # 用户名和信息[('a', '127.0.0.1', 65350)]
                Info_str = Current_users()  # 当前用户列表
                print("USERS:", Info_str)  # ['a']
                self.Load(Info_str, addr)
        # 在获取用户名后便会不断地接受用户端发来的消息(即聊天内容),结束后关闭连接。
    # 将地址与数据(需发送给客户端)存入messages队列。
    def Load(self, data, addr):
        lock.acquire()
        try:
            messages.put((addr, data))
            print(f"Load,addr:{addr},data:{data}")
        finally:
            lock.release()

    # 服务端在接受到数据后,会对其进行一些处理然后发送给客户端,如下图,对于聊天内容,服务端直接发送给客户端,而对于用户列表,便由json.dumps处理后发送。
    def sendData(self):  # 发送数据
        print('send')
        while True:
            if not messages.empty():                        #如果信息不为空
                message = messages.get()
                print("messages.get()",message)
                if isinstance(message[1], str):             #判断类型是否为字符串
                    print("send str")
                    for i in range(len(users)):
                        data = ' ' + message[1]
                        print("send_data:",data.encode())         #send_data:b' a:cccc~a~------Group chat-------'
                        self.s.sendto(data.encode(),(users[i][1],users[i][2])) #聊天内容发送过去

                if isinstance(message[1], list):        #是否为列表
                    print("message[1]",message[1])      #message[1]为用户名 message[0]为地址元组
                    data = json.dumps(message[1])
                    for i in range(len(users)):
                        try:
                            self.s.sendto(data.encode(), (users[i][1], users[i][2]))
                            print("send_already")
                        except:
                            pass
        print('out_send_loop')
    def run(self):
        self.s.bind((IP, PORT))         #绑定端口
        q = threading.Thread(target=self.sendData)  #开启发送数据线程
        q.start()
        t = threading.Thread(target=self.receive)  # 开启接收信息进程
        t.start()

#入口
if __name__ == '__main__':
    print('start')
    cserver = ChatServer()
cserver.start()
#netstat -an|find /i "50000"

总结

该教程也是参考网上的TCP网络聊天室教程写的,主要是解释UDP的实现思路为主,如有错误,欢迎指正。

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
xiaoxingxing的头像xiaoxingxing管理团队
上一篇 2023年12月26日
下一篇 2023年12月26日

相关推荐