您需要在服务器脚本中将
socket.gethostname()
更改为空字符串(或直接调用
socket.bind(('', port))
)。
您的问题不在于 Python,而是在于套接字的使用。当您创建一个套接字时,只是准备好了您的进程来接收/发送一些数据到/从另一个进程。
服务器
创建套接字的第一步是指定将用于这些进程之间通信的协议类型。在您的情况下,它是
socket.AF_INET
,这是用于 IP 协议的常量,
socket.SOCK_STREAM
指定可靠的面向流服务。可靠的面向流服务意味着您希望确保每个发送的字节都会被传递到另一端,在通信过程中不会丢失任何东西(底层操作系统将使用 TCP 协议)。从这一点开始,我们使用 IPv4 协议(因为我们设置了
socket.AF_INET
)。
第二步是将其绑定到一个地址。bind过程会分配一个地址,你期望客户端将加入(使用套接字的设置,它是IP地址和TCP端口)。您的PC有多个IP地址(至少两个)。它始终有127.0.0.1,这被称为“回调”,仅在应用程序在同一台PC上通信时起作用(即您在问题中的Linux-Linux场景),然后您有外部IP地址,用于与其他计算机通信(假装它是10.0.0.1)。
当您调用socket.bind(('127.0.0.1', 5555))时,您正在设置套接字仅监听来自同一PC的通信。如果您调用socket.bind(('10.0.0.1', 5555)),则套接字设置已准备好接收针对10.0.0.1地址的数据。
但是,如果您有10个或更多IP,并且希望接收所有内容(具有正确的TCP端口)?对于这些情况,可以将bind()中的IP地址留空,它会完全按照您的要求执行。
使用Python中的
bind()
版本,您还可以输入“计算机名称”,而不是具体的IP地址。调用
socket.gethostname()
返回计算机的名称。问题在于将“计算机名称”翻译为Python在背后执行的IP地址。翻译有一些规则,但通常您的“计算机名称”可以转换为计算机上设置的任何IP地址。在您的情况下,您的计算机名称转换为
127.0.0.1
,这就是为什么通信仅在同一台计算机上的进程之间工作的原因。
在socket.bind()
之后,您已经准备好使用套接字,但它仍然处于“非活动”状态。调用socket.listen()
激活套接字并导致它等待直到收到尝试连接的请求。当套接字接收到新的连接请求时,它将把它放入队列中并等待处理。
这就是socket.accept()
的作用。它从队列中拉出连接请求,接受它,并在服务器和客户端之间建立流(记住您设置套接字时的socket.SOCK_STREAM
)。新流实际上是一个新的套接字,但已准备好与另一侧通信。
旧套接字发生了什么?它仍然存在,您可以再次调用
socket.listen()
来获取另一个流(连接)。
如何在同一个端口上有多个套接字?
计算机网络中的每个连接都由流定义,该流是5项元组:
• L4协议(通常为TCP或UDP)
• 源IP地址
• 源L4端口
• 目标IP地址
• 目标L4端口
当您与客户端创建新连接时,流可能如下所示:
(TCP、192.168.0.1、12345、10.0.0.1、55555)
。仅供澄清,服务器响应流为
(TCP、10.0.0.1、55555、192.168.0.1、12345)
,但对我们来说并不重要。如果您使用另一台计算机创建另一个连接,则源TCP端口将不同(如果您从另一台计算机执行此操作,则源IP也将不同)。只有从这些信息中,您才能区分出发送到您的计算机的每个连接。
当您在代码中创建服务器套接字并调用
socket.listen()
时,它会监听任何符合此模式的流动
(TCP, *, *, *, 55555)
(星号表示“匹配所有内容”)。因此,当您获得一个连接
(TCP, 192.168.0.1, 12345, 10.0.0.1, 55555)
时,
socket.accept()
将创建另一个仅与此具体流量一起工作的套接字,而旧套接字继续接受尚未建立的新连接。
当操作系统接收到数据包时,它会查看数据包并检查流量。此时,可能会发生几种情况:
- 数据包的流量完全匹配所有5个项目(不使用
*
)。然后将数据包的内容传递到与该套接字关联的队列中(调用socket.recv()
时读取队列)。
- 数据包的流量与带有关联流量的套接字匹配
*
,则将其视为新连接,并可以调用scoket.accept()
。
- 操作系统不包含与流量匹配的打开套接字。在这种情况下,操作系统拒绝连接(或只是忽略数据包,这取决于防火墙设置)。
也许通过一个示例可以更清晰地解释这些场景。操作系统有一种类似于表格的东西,它将流映射到套接字上。当您调用socket.bind()
时,它会为套接字分配一个流。调用之后,该表格可能如下所示:
+=====================================+========+
| Flow | Socket |
+=====================================+========+
| (TCP, *, *, *, 55555) | 1 |
+-------------------------------------+--------+
当它接收到一个带有流量
(TCP, 1.1.1.1, 10, 10.0.0.1, 10)
的数据包时,它将不会匹配任何流量(最后一个端口不匹配)。因此,连接被拒绝。如果它收到一个带有流量
(TCP, 1.1.1.1, 10, 10.0.0.1, 55555)
的数据包,则该数据包将发送到套接字
1
中(因为存在匹配项)。
socket.accept()
调用将创建一个新的套接字并在表中记录。
+=====================================+========+
| Flow | Socket |
+=====================================+========+
| (TCP, 1.1.1.1, 10, 10.0.0.1, 55555) | 2 |
+
| (TCP, *, *, *, 55555) | 1 |
+
现在您有1个端口的2个套接字。与套接字2相关联的流匹配的每个接收数据包也与套接字1相关联的流匹配(相反则不适用)。这不是问题,因为套接字2具有更精确的匹配(它不使用*),因此任何具有该流的数据将被传递到套接字2。
如何服务多个连接
如果您想要实现一个“真正”的服务器,您的应用程序应该能够在不重新启动的情况下处理多个连接。有两种基本方法:
Sequential processing
try:
l = prepare_socket()
while True:
l.listen()
s, a = socket.accept()
process_connection(s)
except KeyboardInterrupt:
l.close()
In this case, you can process only one client while others clients have to wait for accept. If the process_connection()
takes too long, then others clients will timeout.
Parallel processing
import threading
threads = []
try:
l = prepare_socket()
while True:
l.listen()
s, a = socket.accept()
t = threading.Thread(target=process_connection, s)
threads.append(t)
t.start()
except KeyboardInterrupt:
for t in threads:
t.join()
l.close()
Now when you receive a new connection, it will create a new thread so that every connection is processed in parallel. The main disadvantage of this solution is that you have to solve common troubles with threading (like access to shared memory, deadlocks etc.).
注意,上述代码片段仅为示例,不完整!例如,它们不包含关于意外异常优雅退出的代码。
Python还包含一个名为
socketserver
的模块,其中包含用于在Python中创建服务器的快捷方式。您可以在
此处找到如何使用它的示例。
客户端比服务器简单得多。您只需使用一些设置(与服务器端相同)创建套接字,然后告诉它服务器在哪里(其IP和TCP端口是什么)。这通过
socket.connect()
调用实现。作为奖励,它还建立了客户端和服务器之间的流,因此从此时起,您可以进行通信。
您可以在Beej的网络编程指南中找到有关套接字的更多信息。该指南是用C语言编写的,但概念相同。
socket.gethostname()
替换为空字符串。 - Qeek