前言
前些天实习面试的时候被面试官问到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建立一个线程,客户端本身也要启动两个线程,一个线程用于让自己发送数据给服务器,一个线程用于监听服务器是否有发送数据给自己,这样就能做到边发信息边收信息了。
Q: “不用线程是否也可以呢?”
A: 可以是可以,相当于服务器要维护一个队列,让消息依次读入依次发送,然后再频繁创建socket销毁socket,其实代码量比写几行线程要多,且不能算并行。线程在研究或工作领域都经常用到,可以趁现在学一学
1.2.2 通信协议选择
解决完多用户通信的问题后,我们要选择我们的通信协议。
最常见的是TCP和UDP协议。
TCP协议
优点:信息传输可靠,不会出现A用户发出的数据丢失,导致B和C收不到的情况。
缺点:TCP要求服务器要和用户建立连接且不允许临时断开,故我们为了保持用户能一直处于连接状态,就要让服务器为每一个socket连接开一个线程让它不停的跑(用while不停的监听信息)。所以当用户连接越多,服务器要添加的线程也就越多。比如上述图中,监听ABC用户需要用到3个线程,比较占用资源(当然你也可以检测哪个socket长时间不工作就销毁该线程)。
UDP协议
优点:传输速度快,不用管信息是否送达,即不用维持连接,所以用户发完数据就不用管了。这种情况下服务器只要建立两个线程,一个线程用于接收用户发送的数据,另一个线程用于发送数据,而不用额外的线程来维护与用户的连接。
缺点:消息无法确保可靠送达。
按理来说,作为网络聊天室,我们对信息的传输速度要求没那么高,对信息可靠性传输反而高一点,所以网上大部分教程所用的协议也都是TCP协议。
比如本篇文章参考的教程: 用Python怎么实现一个网络聊天室功能
但由于TCP实现网络聊天室的教程较多,我自己就想尝试利用UDP协议进行实现,试试新的方法。如果你想用TCP来做聊天室的话,下面的教程你同样可以参考。UDP和TCP协议的不同主要就在服务器所要开设线程数目上,其余思路基本一致。
TCP服务器所需开设的线程数=所维持的socket连接的数目(每来一个socket请求就创建一个线程,每个线程负责该socket的信息接收与发送,当某个线程长时间没有接收到数据则自己销毁)
UDP服务器所需开设的线程数=接收信息线程 + 转发信息线程(因为UDP不用维持socket连接,所以只要两个线程就够了;UDP以信息传输不可靠为代价,换来了服务器线程开销的减少)
TCP/UDP客户端所需开设的线程数=接收信息线程 + 发送信息线程(客户端只要一个线程发送数据,一个线程接收数据即可。接收到数据后再把数据插入到聊天窗口里,从而制造出别人发信息到自己窗口的效果)
下图即为UDP服务器的流程图:
客户端往服务器地址发送数据后就不用管了,服务器的接收信息线程会自己把数据存入到缓冲区中。
然后服务器转发信息线程检测到缓冲区有数据了,就会把该数据群发到所有客户端IP地址(客户端在登录到服务器的时候会携带自己的客户端IP信息,服务器会将这个信息储存到列表里,后面转发时再遍历该列表依次发送信息,从而达到群发的效果)。
1.3 前后端功能设计思路
选择好通信协议后,我们就要进行下一步的具体设计。
首先,我们需要带有图形界面的聊天室,那我们就需要学习如何制作前端基本界面,如何将前端界面显示内容与后端数据(如聊天信息,聊天室里的群友名)绑定起来。
这里我用的图形界面工具是tkinter,其很容易上手,不需要多少时间就能做出一个简易界面。
1.3.1 前端
前端界面包括三个页面:
- 登录窗口
- 聊天窗口
- 私聊窗口
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.py代码
客户端代码如下:
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.py代码
服务器代码如下:文章来源:https://www.toymoban.com/news/detail-768825.html
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的实现思路为主,如有错误,欢迎指正。文章来源地址https://www.toymoban.com/news/detail-768825.html
到了这里,关于[Socket]Python用UDP协议建立带有私聊功能的网络聊天室-建立聊天工具的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!