Python中的Socket模块通过UDP协议实现网络通信(第3节)


在上一节教程中,已经介绍了使用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连接发送消息给服务器端,服务器端都会自动帮我们把信息同时转发给多个客户端,这样就简单实现了群聊功能的用户体验,也方便我们进行消息的同步。