在上一节教程中,已经介绍了使用Socket模块通过TCP协议实现网络通信,包括建立客户端和服务器端的网络连接,进行数据的发送和接收。Socket模块同样提供了UDP协议实现网络通信,与TCP协议不同,UDP是一种简单的无连接的协议。在UDP通信中,不需要在客户和服务器之间建立连接,而是通过recvfrom()函数和sendto()函数直接接收和发送数据,且没有超时重发等机制,故而传输速度很快,适用于一些对传输速度要求较高,可以容忍一定数据丢失的场景。
1、Socket模块通过UDP协议实现简单的网络通信
UDP协议实现起来相对比较简单,同样我们分别创建一个服务器端(server.py)文件和一个客户端(client.py)文件。打开Pyhint编辑器,然后在黑色的cmd终端窗口中,分别运行两个程序,就可以在服务器和客户端之间进行通信了。
(1)服务器端(server.py)
server.py:
import socket
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.bind(('127.0.0.1', 9999))
while True:
data, addr = server.recvfrom(1024)
print("客户端{}:{}发来消息:".format(*addr), data.decode('utf-8'))
if data.decode('utf-8') == 'quit':
break
server.sendto(data.decode('utf-8').upper().encode('utf-8'), addr)
server.close()
(2)客户端(client.py)
client.py:
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
while True:
data = input("请输入>>")
client.sendto(data.encode('utf-8'), ('127.0.0.1', 9999))
if data == 'quit':
break
recv, addr = client.recvfrom(1024)
print(recv.decode('utf-8'))
client.close()
2、Socket模块通过UDP协议模拟聊天软件
在设计聊天软件时,往往需要完成单人或多人之间的聊天通信。为了实现群聊功能,可以使用多线程来处理每个客户端的连接。Python的threading模块提供了多线程支持,可以很方便地实现多线程网络通信。
模拟聊天软件同样需要分别创建一个服务器端(server.py)文件和一个客户端(client.py)文件,服务器端程序主要用于接收用户信息,消息接收与转发;客户端程序实现用户注册登录,聊天信息显示与信息输入。
设计聊天软件步骤如下所示:
(1)服务器端(server.py)
首先,打开第2章【安装和运行Python】教程里面介绍的Pyhint编辑器,在Pyhint\Learn-Python\test文件夹下面新建一个文件server.py,输入以下内容并保存。然后,打开Pyhint编辑器的黑色cmd终端窗口,输人“python server.py”并按下Enter回车键即可打开服务器端程序,并提示“聊天服务器已开启”,最后等待客户端的连接请求(注意:不要关闭cmd终端窗口)。
server.py:
from socket import *
import threading
import queue
import json
# 设定地址和端口
IP = "127.0.0.1"
# 自定义端口,但建议不要用8080等其他软件可能占用的端口。
PORT = 9999
#存放总体数据
messages = queue.Queue()
# 定义一个空列表
users = []
#线程锁,防止多个线程占用同个资源时导致资源不同步的问题
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)
# 通过UDP协议创建一个socket对象
self.s = socket(AF_INET, SOCK_DGRAM)
# 接收消息
# 接受来自客户端的用户名,如果用户名为空,使用用户的IP与端口作为用户名。
# 如果用户名出现重复,则在出现的用户名依此加上后缀“2”、“3”、“4”……
def receive(self):
while True:
# 收到的字符串和地址
Info, addr = self.s.recvfrom(1024)
Info_str = str(Info, "utf-8")
userIP = addr[0]
userPort = addr[1]
print(f"客户端信息:{Info_str},地址:{addr}")
if "~0" in Info_str:# 群聊
data = Info_str.split("~")
print("分割之后的数据:", data)
message = data[0] # data
userName = data[1] # name
chatwith = data[2] # 0
# 界面输出用户格式
message = userName + "~" + message + "~" + chatwith
print("用户信息:",message)
self.Load(message, addr)
elif "~" in Info_str and "0" not in Info_str:# 私聊
data = Info_str.split("~")
print("分割之后的数据:", data)
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)
# 当前用户列表
Info_str = Current_users()
print("当前用户列表:", Info_str)
self.Load(Info_str, addr)
# 在获取用户名后便会不断地接受用户端发来的消息(即聊天内容),结束后关闭连接。
# 将地址与数据(需发送给客户端)存入messages队列。
def Load(self, data, addr):
lock.acquire()
try:
messages.put((addr, data))
print(f"加载,地址:{addr},数据:{data}")
finally:
lock.release()
# 服务端在接受到数据后,会对其进行一些处理后再发送给客户端
# 对于聊天内容,服务端直接发送给客户端,而对于用户列表,便由json.dumps处理后发送。
def sendData(self): # 发送数据
while True:
#如果信息不为空
if not messages.empty():
message = messages.get()
print("messages.get()",message)
#判断类型是否为字符串
if isinstance(message[1], str):
print("发送字符串")
for i in range(len(users)):
data = " " + message[1]
print("发送信息:",data.encode('utf-8'))
#聊天内容发送过去
self.s.sendto(data.encode('utf-8'),(users[i][1],users[i][2]))
# 判断是否为列表
if isinstance(message[1], list):
print("message[1]",message[1])
data = json.dumps(message[1])
for i in range(len(users)):
try:
self.s.sendto(data.encode('utf-8'), (users[i][1], users[i][2]))
print("已发送")
except:
pass
def run(self):
# 绑定地址和端口
self.s.bind((IP, PORT))
#开启发送数据线程
q = threading.Thread(target=self.sendData)
q.start()
# 开启接收信息进程
t = threading.Thread(target=self.receive)
t.start()
# 该“.py”文件作为脚本直接执行
if __name__ == "__main__":
print("聊天服务器已开启")
cserver = ChatServer()
cserver.start()
(2)客户端(client.py)
接着,我们创建一个客户端程序。同样打开Pyhint编辑器,在Pyhint\Learn-Python\test文件夹下面新建一个文件client.py,输入以下内容并保存,然后再次点击Pyhint编辑器上面的“打开终端”按钮,在弹出的黑色cmd终端窗口中输人“python client.py”并按下Enter回车键即可打开客户端程序。
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"
PORT = 9999
user = ""
listbox1 = "" # 用于显示在线用户的列表框
show = 1 # 用于判断是开还是关闭列表框
users = [] # 在线用户列表
chat = "0" # 聊天对象
chat_pri = ""
# 登录窗口的界面实现
root0 = tkinter.Tk()
root0.geometry("300x120")
root0.title("用户登录")
root0.resizable(0, 0)
one = tkinter.Label(root0, width=300, height=150, bg="#69d5ff")
one.pack()
USER = tkinter.StringVar()
USER.set("")
labelUSER = tkinter.Label(root0, text="创建用户名", bg="#69d5ff")
labelUSER.place(x=20, y=35, width=100, height=40)
entryUSER = tkinter.Entry(root0, width=60, textvariable=USER)
entryUSER.place(x=120, y=40, width=100, height=30)
#界面完成后,以下就是编写实际的登录函数
def Login():
global user
user = entryUSER.get()
if not user:
# 客户端用户名如果为空则警告
tkinter.messagebox.showwarning("警告", message="用户名为空!")
else:
root0.destroy() #提交后,登录窗口要自己销毁,以便进入登录成功后的界面
#登录按钮的实现
loginButton = tkinter.Button(root0, text="登录", command=Login, bg="#FF8C00")
loginButton.place(x=135, y=80, width=40, height=25)
# 绑定按钮与Login()函数
root0.bind("<Return>", 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协议创建一个socket对象
ip_port = (IP, PORT)
s = socket(AF_INET, SOCK_DGRAM)
if user:
# 发送用户名
s.sendto(user.encode('utf-8'), ip_port)
else:
s.sendto("用户名不存在", ip_port)
user = IP + ":" + PORT
# 点击按钮,发送聊天内容
def send():
message = entryIuput.get() + "~" + user + "~" + chat
s.sendto(message.encode('utf-8'), ip_port)
print("已发送消息:",message)
INPUT.set("")
return "break" #按回车后只发送不换行
# “发送按钮”的界面实现,与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)
# 接收信息的函数实现
def receive():
global uses
while True:
data = s.recv(1024)
data = data.decode('utf-8')
print("接收消息:",data)
try:
uses = json.loads(data)
listbox1.delete(0, tkinter.END)
# 往用户列表插入信息
listbox1.insert(tkinter.END, f"当前在线用户{len(uses)}个:")
for x in range(len(uses)):
listbox1.insert(tkinter.END, uses[x])
users.append("------群聊-------")
except:
data = data.split("~")
print("分割之后的数据:",data)
userName = data[0] #data
userName = userName[1:] #获取用户名
message = data[1] #信息
chatwith = data[2] #判断是群聊还是私聊
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()
执行以上代码,会弹出“登录”窗口,输入任意一个用户名就可以登录聊天群。如果想要模拟多人之间的聊天通信,可以按上面的方法同时打开多个客户端程序文件(client.py),也就是多次点击Pyhint编辑器上面的“打开终端”按钮,并输人“python client.py”按下Enter回车键。然后,在每个客户端程序中输入不同的用户名并点击登录,如果用户名出现重复,则程序会自动在重复的用户名后面依次加上后缀“2”、“3”、“4”……。
以上实现群聊功能的主要原理是,利用服务器转发数据。我们让每个客户端都和服务器端建立Socket连接。然后任意一个客户端通过Socket连接发送消息给服务器端,服务器端都会自动帮我们把信息同时转发给多个客户端,这样就简单实现了群聊功能的用户体验,也方便我们进行消息的同步。