我正在尝试通过套接字将数据发送到同一 IP,但通过不同的端口。这些是我迄今为止开发的测试脚本:
服务器:
# test_server.py
import socket
import select
# module-level variables ##############################################################################################
HOST1 = '127.0.0.1'
PORT1 = 65432
HOST2 = '127.0.0.1'
PORT2 = 65433
#######################################################################################################################
def main():
sock1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock1.bind((HOST1, PORT1))
sock1.listen()
conn1, addr1 = sock1.accept()
sock2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock2.bind((HOST2, PORT2))
sock2.listen()
conn2, addr2 = sock2.accept()
conns = [ conn1, conn2 ]
while True:
readyConns, _, _ = select.select(conns, [], [])
for conn in readyConns:
data = conn.recv(1024)
if not data:
print('no data received')
else:
print('received: ' + data.decode("utf-8"))
# end if
conn.sendall(bytes('acknowledgement from server', 'utf-8'))
# end for
# end while
# end main
#######################################################################################################################
if __name__ == '__main__':
main()
客户:
# test_client.py
import socket
import time
# module-level variables ##############################################################################################
HOST1 = '127.0.0.1'
PORT1 = 65432
HOST2 = '127.0.0.1'
PORT2 = 65433
#######################################################################################################################
def main():
myCounter = 1
sock1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock1.connect((HOST1, PORT1))
sock2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock2.connect((HOST2, PORT2))
while True:
# sock1 ############################################
# send the original message
messageAsStr1 = 'message 1-' + myCounter
sock1.sendall(bytes(messageAsStr1, 'utf-8'))
# receive the acknowledgement
ack1 = sock1.recv(1024)
if ack1 is None:
print('error receiving acknowledgement on port 1')
else:
print('received: ' + ack1.decode('utf-8'))
# end if
time.sleep(2)
# sock2 ############################################
# send the original message
messageAsStr2 = 'message 2-' + myCounter
sock2.sendall(bytes(messageAsStr2, 'utf-8'))
# receive the acknowledgement
ack2 = sock2.recv(1024)
if ack2 is None:
print('error receiving acknowledgement on port 2')
else:
print('received: ' + ack2.decode('utf-8'))
# end if
time.sleep(2)
myCounter += 1
# end while
# end main
#######################################################################################################################
if __name__ == '__main__':
main()
如果我启动test_server.py
,然后test_client.py
,test_server.py
将成功启动,但是在启动test_client时。 py
我得到:
$ python3 test_client.py
Traceback (most recent call last):
File "test_client.py", line 66, in <module>
main()
File "test_client.py", line 23, in main
sock2.connect((HOST2, PORT2))
ConnectionRefusedError: [Errno 111] Connection refused
我不明白为什么第二个连接不会通过,b/c,如果我将 test_client.py
分成两个单独的程序,如下所示:
# test_client1.py
import socket
import time
# module-level variables ##############################################################################################
HOST1 = '127.0.0.1'
PORT1 = 65432
#######################################################################################################################
def main():
myCounter = 1
sock1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock1.connect((HOST1, PORT1))
while True:
# sock1 ############################################
# send the original message
messageAsStr1 = 'message 1-' + str(myCounter)
sock1.sendall(bytes(messageAsStr1, 'utf-8'))
# receive the acknowledgement
ack1 = sock1.recv(1024)
if ack1 is None:
print('error receiving acknowledgement on port 1')
else:
print('received: ' + ack1.decode('utf-8'))
# end if
myCounter += 1
time.sleep(2)
# end while
# end main
#######################################################################################################################
if __name__ == '__main__':
main()
和:
# test_client2.py
import socket
import time
# module-level variables ##############################################################################################
HOST2 = '127.0.0.1'
PORT2 = 65433
#######################################################################################################################
def main():
myCounter = 1
sock2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock2.connect((HOST2, PORT2))
while True:
# sock2 ############################################
# send the original message
messageAsStr2 = 'message 2-' + str(myCounter)
sock2.sendall(bytes(messageAsStr2, 'utf-8'))
# receive the acknowledgement
ack2 = sock2.recv(1024)
if ack2 is None:
print('error receiving acknowledgement on port 2')
else:
print('received: ' + ack2.decode('utf-8'))
# end if
myCounter += 1
time.sleep(2)
# end while
# end main
#######################################################################################################################
if __name__ == '__main__':
main()
然后按照test_server.py
、test_client1.py
、test_client2.py
的顺序启动它们,我得到了预期的结果:
(第一个命令提示符):
$ python3 test_server.py
received: message 1-1
received: message 2-1
received: message 1-2
received: message 2-2
received: message 1-3
received: message 2-3
received: message 1-4
received: message 2-4
(第二个命令提示符):
$ python3 test_client1.py
received: acknowledgement from server
received: acknowledgement from server
received: acknowledgement from server
received: acknowledgement from server
(第三个命令提示符):
python3 test_client2.py
received: acknowledgement from server
received: acknowledgement from server
received: acknowledgement from server
received: acknowledgement from server
此时我的问题是:
1) 为什么第一种方式(在 test_client.py
中使用不同的端口号)不起作用,但如果我将其分成两个脚本,它就可以工作?
2)是否有更优雅/稳健的方法来实现相同的目标?我应该提到,在我的最终程序中,我绝对需要确认内容以及使用不同端口号和 IP 的灵 active 。
3)上述的另一个限制是在第二种情况下,它起作用,我必须按特定顺序启动程序test_server.py
,test_client1.py
,test_client2.py
。在生产版本中,我最终的订单会有所不同。是否有建议的更改来优雅地处理这个问题?
最佳答案
我明白为什么这会令人困惑。幸运的是,解释很简单。
问题 1 - 阻塞调用和竞争条件
在您的服务器脚本中,当您告诉它接受()时,您的脚本“等待”(我们说这个函数调用是“阻塞”)。您的服务器脚本的其余部分甚至还没有被查看。现在您运行客户端脚本,它连接到第一个端口,这会导致服务器脚本中的调用解除阻止并继续。现在“竞赛”开始了,您的服务器脚本会在客户端下一次调用 connect() 之前执行对 Listen() 的调用,然后执行 Accept() 吗?或许!但不太可能,除非你让你的客户睡着了。然后你每次都会看到它有效。但这不是正确的解决方案。
问题 2 - “优雅”方式
您确实希望同时等待 X 个端口上的连接,而不是连续等待。执行此操作的简单方法是使用 python 多处理模块来启动并行服务器执行线程。但是,这是一个沉重的解决方案。
正确的方法确实是使用 select(),这是表达“我只是一个进程,但我想监听多个事物(端口)上的等待事件”的官方方式。因此,您将设置多个套接字,然后选择您想要等待的事件列表。它将阻塞,直到其中任何一个出现事件为止。您执行处理该操作的工作,然后再次循环回 select() 以阻止并等待更多事件(也许下次在不同的端口上,或者可能是在同一端口上的新连接)。
有一个重要的警告,可能需要选择“高级”工具。当您处理请求时,请注意不要花费太多时间来处理,因为所有其他连接都在等待。有一些技术可以正确地做到这一点,例如使用 libevent 管理所有套接字和文件 io。
使用多处理更简单,但它的扩展性不如架构良好的基于事件的设计。 Here's有关此示例的讨论,Apache 与 Nginx。基于事件处理架构的解决方案的强大功能的另一个例子是 NodeJS,它以在事件循环中运行所有内容而闻名。
所有这些细节都是为了强调,因为您说过接下来要进入生产实现,所以有很多事情需要考虑。
最佳解决方案:专注于解决您的实际问题,并让其他人设计服务器。学习使用gunicorn或wsgi(已经很好的服务器)并将您的请求处理放在那里。
问题 3 - 与客户联系的正确方式
当然,您也想让您的客户端变得更加漂亮。做客户的正确方法当然是不要期望一切都是完美的。您正在尝试连接到远程计算机。网络可能会出现故障,服务器可能会离线等等。因此,请从一般客户的流行选择中选择您的策略:
- 失败(在脚本之外),并给用户带来明显的错误,他们将在需要时通过重新运行您的客户端脚本来重试
- 通知用户您正在暂停,并将在 10 秒后重试。进行 X 次尝试(例如 10 次尝试),然后从(客户端)脚本失败。
- 作为一个有多种选择的客户端,自动尝试其他服务器。如果您的问题领域有必要,您可以计划构建一个带有应急计划的宏伟解决方案。如果第一个服务器没有应答,优秀的客户端会自动尝试其他服务器(并且优秀的服务会提供多个可用服务器)。
所以,一般答案:捕获该异常,然后决定你想要做什么。等待并循环重试、保释或“其他”?
关于Python程序具有来自同一脚本的多个具有不同端口号的套接字?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58795028/