本篇文章分享如何用相当简洁的 Python 代码制作一个简单的聊天应用程序。更重要的是,我已经实现了没有任何第三方依赖的代码!
首先,我创建了一个聊天服务器,通过它可以接收来自希望进行通信的客户机的传入请求。为此,我使用了很好的 ole’sockets 和一些多线程。使用像 Twisted 和 SocketServer 这样的框架是一种选择,但是对于像我们这样简单的软件来说,功能似乎有点太庞大了。
服务器
以下是我们如何开始我们的服务器脚本(对于这个应用程序,只有两个脚本:一个用于服务器,另一个用于客户端)):
#!/usr/bin/env python3
"""Server for multithreaded (asynchronous) chat application."""
from socket import AF_INET, socket, SOCK_STREAM
from threading import Thread
为此我们将使用 TCP sockets,因此我们使用 AF_INET 和 SOCK_STREAM 标志。我们通过 UDP sockets 使用它们,因为它们更像电话通讯,在通信开始之前接收者必须批准传入的连接,而 UDP sockets 更像是邮件类型(任何人都可以向任何地址为他/她知道的接收者发送邮件) ,所以它们在通信开始之前并不真正需要建立连接。显然,TCP 比 UDP 更适合我们的目的,因此我们使用它们。
在导入之后,我们设置了一些常量供以后使用:
clients = {}
addresses = {}
HOST = ''
PORT = 33000
BUFSIZ = 1024
ADDR = (HOST, PORT)
SERVER = socket(AF_INET, SOCK_STREAM)
SERVER.bind(ADDR)
现在,我们将服务分解为接受新的连接、广播消息和处理特定的客户端。让我们从接受新的连接开始:
def accept_incoming_connections():
"""Sets up handling for incoming clients."""
while True:
client, client_address = SERVER.accept()
print("%s:%s has connected." % client_address)
client.send(bytes("Greetings from the cave!"+
"Now type your name and press enter!", "utf8"))
addresses[client] = client_address
Thread(target=handle_client, args=(client,)).start()
这只是一个永远等待传入连接的循环,一旦得到一个连接,它就记录连接(打印一些连接细节)并向连接的客户端发送一条欢迎消息。然后它将客户端的地址存储在字典 addresses 中,然后启动该客户端的处理线程。当然,我们还没有为此定义目标函数 handle_client() ,但是我们是这样做的:
def handle_client(client): # Takes client socket as argument.
"""Handles a single client connection."""
name = client.recv(BUFSIZ).decode("utf8")
welcome = 'Welcome %s! If you ever want to quit, type {quit} to exit.' % name
client.send(bytes(welcome, "utf8"))
msg = "%s has joined the chat!" % name
broadcast(bytes(msg, "utf8"))
clients[client] = name
while True:
msg = client.recv(BUFSIZ)
if msg != bytes("{quit}", "utf8"):
broadcast(msg, name+": ")
else:
client.send(bytes("{quit}", "utf8"))
client.close()
del clients[client]
broadcast(bytes("%s has left the chat." % name, "utf8"))
break
当然,在我们给新客户发送欢迎信息后,它会回复一个他/她想用来进一步交流的名字。在 handle_client()函数中,我们要做的第一个任务是保存这个名称,然后根据进一步的指示向客户机发送另一条消息。在这之后是通信的主循环:在这里我们接收来自客户端的进一步消息,如果消息中没有包含退出指令,我们只需将消息广播给其他连接的客户端(我们将在稍后定义广播方法)。如果我们确实遇到带有退出指令的消息(例如,客户端发送{ quit }) ,我们将相同的消息回发给客户端(它在客户端触发关闭操作) ,然后关闭它的连接套接字。然后,我们通过删除客户端的条目来进行一些清理,最后给这个特定的人已经离开会话的其他联系人一个响应。
现在来看看我们的 broadcast()函数:
def broadcast(msg, prefix=""): # prefix is for name identification.
"""Broadcasts a message to all the clients."""
for sock in clients:
sock.send(bytes(prefix, "utf8")+msg)
这几乎是不言自明的,它只是将 msg 发送给所有连接的客户机,并在必要时添加一个可选的 prefix。我们确实在 handle_client() 函数中为 broadcast() 传递了一个 prefix,我们这样做是为了让人们能够准确地看到特定消息的发送者是谁。
这就是我们服务器所需的全部功能。最后,我们放入一些代码来启动我们的服务器并侦听传入的连接:
if __name__ == "__main__":
SERVER.listen(5) # Listens for 5 connections at max.
print("Waiting for connection...")
ACCEPT_THREAD = Thread(target=accept_incoming_connections)
ACCEPT_THREAD.start() # Starts the infinite loop.
ACCEPT_THREAD.join()
SERVER.close()
我们 join() ACCEPT_THREAD,这样主脚本就会等待它完成,而不会跳到下一行,下一行将关闭服务器。
客户端
这是更有趣的,因为我们将编写一个 GUI!我们使用 Tkinter,Python 的“batteries included”GUI 构建工具来实现我们的目的。让我们先做一些导入:
#!/usr/bin/env python3
"""Script for Tkinter GUI chat client."""
from socket import AF_INET, socket, SOCK_STREAM
from threading import Thread
import tkinter
现在我们将编写处理消息发送和接收的函数:
def receive():
"""Handles receiving of messages."""
while True:
try:
msg = client_socket.recv(BUFSIZ).decode("utf8")
msg_list.insert(tkinter.END, msg)
except OSError: # Possibly client has left the chat.
break
为什么又是一个无限循环?因为我们将不确定地接收信息,并且与我们如何以及何时发送信息无关。我们不希望这是一个对讲机聊天应用程序,只能发送或接收在一个时间; 我们希望接收消息时,我们可以,并发送他们当我们想。循环中的功能非常简单; recv()是阻塞部分。它会停止执行,直到它接收到一条消息,当它接收到消息时,我们继续前进,并将消息附加到 msg list 中。我们将很快定义 msg_list,它基本上是 Tkinter 的一个特性,用于在屏幕上显示消息列表。
接下来,我们定义 send() 函数:
def send(event=None): # event is passed by binders.
"""Handles sending of messages."""
msg = my_msg.get()
my_msg.set("") # Clears input field.
client_socket.send(bytes(msg, "utf8"))
if msg == "{quit}":
client_socket.close()
top.quit()
我们使用 event 作为参数,因为当图形用户界面上的 send 按钮被按下时,Tkinter 会隐式地传递事件。my_msg 是 GUI 上的输入字段,因此我们提取要发送给我们的消息 g msg = my_msg.get()。之后,我们清除输入字段,然后将消息发送到服务器,正如我们之前看到的,服务器将此消息广播到所有客户机(如果它不是退出消息)。如果是退出消息,我们关闭 socket,然后关闭 GUI 应用程序(通过 top.close())
我们定义了另一个函数,当我们选择关闭 GUI 窗口时,它将被调用。这是一个类似于关闭前清理的函数,应该在 GUI 关闭前关闭 socket 连接:
def on_closing(event=None):
"""This function is to be called when the window is closed."""
my_msg.set("{quit}")
send()
这将输入字段设置为{ quit } ,然后调用 send() ,这样可以按预期工作。现在我们开始在主命名空间(即任何函数之外)构建 GUI。我们首先定义顶级小部件并设置它的标题:
top = tkinter.Tk()
top.title("Chatter")
然后我们创建一个框架来保存消息列表。接下来,我们创建一个字符串变量,主要用于存储我们从输入字段获得的值(我们将很快定义这个值)。我们将该变量设置为“在这里键入您的消息”提示用户写下他们的信息。然后,我们创建一个滚动条来滚动这个消息框。下面是代码:
messages_frame = tkinter.Frame(top)
my_msg = tkinter.StringVar() # For the messages to be sent.
my_msg.set("Type your messages here.")
scrollbar = tkinter.Scrollbar(messages_frame) # To navigate through past messages.
现在我们定义消息列表,它将存储在 messages_frame 中,然后将我们创建的所有内容(在适当的位置)打包:
msg_list = tkinter.Listbox(messages_frame, height=15, width=50, yscrollcommand=scrollbar.set)
scrollbar.pack(side=tkinter.RIGHT, fill=tkinter.Y)
msg_list.pack(side=tkinter.LEFT, fill=tkinter.BOTH)
msg_list.pack()
messages_frame.pack()
然后,我们创建输入字段,让用户输入他们的消息,并将其绑定到上面定义的字符串变量。我们还将其绑定到 send()函数,以便每当用户按下 return 键时,消息就被发送到服务器。接下来,如果用户希望通过单击发送消息,我们将创建 send 按钮。同样,我们将单击此按钮绑定到 send()函数。同时我们也包装我们刚刚创建的所有东西。此外,不要忘记使用 on_closing() 的清理函数,当用户希望关闭 GUI 窗口时,应该调用这个函数。我们使用 top 的 protocol 方法来实现。下面是所有这些的代码:
entry_field = tkinter.Entry(top, textvariable=my_msg)
entry_field.bind("<Return>", send)
entry_field.pack()
send_button = tkinter.Button(top, text="Send", command=send)
send_button.pack()
top.protocol("WM_DELETE_WINDOW", on_closing)
现在差不多完成了。我们还没有编写连接到服务器的代码。为此,我们必须向用户询问服务器的地址。我只需要使用 input()就可以做到这一点,因此在 GUI 开始之前,用户会收到一些命令行提示,询问主机地址。这可能有点不方便,您可以为此添加图形用户界面。下面是我的代码:
HOST = input('Enter host: ')
PORT = input('Enter port: ')
if not PORT:
PORT = 33000 # Default value.
else:
PORT = int(PORT)
BUFSIZ = 1024
ADDR = (HOST, PORT)
client_socket = socket(AF_INET, SOCK_STREAM)
client_socket.connect(ADDR)
一旦我们得到地址并创建一个 socket 连接到它,我们就开始线程接收消息,然后我们的 GUI 应用程序的主循环:
receive_thread = Thread(target=receive)
receive_thread.start()
tkinter.mainloop() # Starts GUI execution.
这样!我们已经编写了聊天应用程序。
演示
在多台计算机上进行测试感觉很棒。当然,您可以在同一台机器上运行服务器和客户机进行测试(在客户端中使用127.0.0.1 for HOST) ,但是看到不同计算机之间实时进行通信感觉非常棒。服务器脚本将记录访问它的 IP 地址,客户端脚本将生成一个 GUI (在询问主机地址之后) ,类似于下面的屏幕截图:
客户端 GUI
连接到同一服务器的另一个客户端
· END ·
HAPPY LIFE