Python套接字/端口转发

6
我已经使用Python编写了服务器和客户端程序。 Server.py
import socket

sock = socket.socket (socket.AF_INET, socket.SOCK_STREAM)

host = socket.gethostname()
port = 5555

sock.bind((host, port))

sock.listen(1)

conn, addr = sock.accept()

data = "Hello!"
data = bytes(data, 'utf-8')

conn.send(data)

sock.close()

在Linux上的Client.py
import socket

sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

host = socket.gethostname()
port = 5555

sock.connect((host, port))

data = sock.recv(2048)

data = str(data, "utf-8")

print(data)

sock.close()

当我在本地机器(Linux Mint)上运行服务器,然后运行客户端时,一切正常。我在bash中得到了“Hello!”的结果,一切都很好。但是当我在另一台机器(Windows 8)上运行我的客户端程序,并运行它(先前我当然在Linux上运行了服务器,并将客户端中的IP地址更改为我的静态Linux Mint IP),它会显示:
连接被拒绝错误:[WinError 10061]由于目标计算机积极拒绝,无法建立连接
Windows上的client.py
import socket
    
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    
host = "here is my static ip"
port = 5555
    
sock.connect((host, port))
    
data = sock.recv(2048)
    
data = str(data, "utf-8")
    
print(data)
    
sock.close()

我必须说,我已经在我的路由器设置中做了端口转发,端口号为5555。之前,我对端口80做了相同的操作,我的网站能够正常工作,但现在使用Python sockets时却不能使用5555端口!为什么?我无法理解!还有一件事:我尝试在我的服务器和客户端文件中将端口改为80,但也没有成功。请帮忙。


1
尝试在服务器脚本中将socket.gethostname()替换为空字符串。 - Qeek
@Qeek非常感谢!我在Python方面非常新,所以您能否解释一下为什么空字符串可以正常工作,而主机名不行? - Alexander Mueller
如果您的两台计算机都位于私有网络中,则无需配置路由器。您是否检查过 Windows 计算机在此特定端口上是否有防火墙阻止? - Andrejs Cainikovs
2个回答

20
您需要在服务器脚本中将 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。
如何服务多个连接
如果您想要实现一个“真正”的服务器,您的应用程序应该能够在不重新启动的情况下处理多个连接。有两种基本方法:
  1. Sequential processing

    try:
        l = prepare_socket()
        while True:
            l.listen()
            s, a = socket.accept()
            process_connection(s) # before return you should call s.close()
    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.


  2. 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语言编写的,但概念相同。


但是,旧的套接字(我首先创建的)如何在某个端口上运行,而新的套接字(已处理某个连接的套接字)也存在于同一端口上?它们如何不干扰彼此? - Alexander Mueller
@AlexanderMueller 我会在回答中加入多个套接字在同一端口上如何工作的解释。 - Qeek
当我调用 socket.listen() 时,它会监听 (TCP, *, *, *, 55555) 模式。因此当前套接字位于端口55555上,因为我们在 socket.bind() 中指定了它。当有新的连接到来时,会创建一个新的套接字,并且它也在同一个端口上(如果我们查看新的特定流程新套接字使用的方式,我们可以猜测它)。好的,旧的套接字监听新的连接,但新的套接字与某个特定的套接字一起工作,但它们都在同一个端口上!端口是程序,不是吗? - Alexander Mueller
因此,一个程序同时作为某个连接的侦听器和处理程序工作。但我不这么认为:c 2. 你说:“流程与任何套接字都不匹配,因此它会拒绝连接。” 如果我们从互联网收到一些数据包,它必须具有所有目标地址(这是获取此数据包的条件),但你说,在数据包中我们看不到任何套接字。 - Alexander Mueller
另外,如果你所推荐的书中已经涵盖了我感兴趣的所有内容,那就请告诉我吧!我从现在开始会静心阅读而不再提问。再次感谢您 :) - Alexander Mueller
@AlexanderMueller 我已经尝试填写一些关于此的示例。 - Qeek

0

数月前我遇到了同样的问题,也无法进行端口转发。我找到了一个解决方案——Ngrock

关于Ngrock的作用,它是一个有用的实用程序,可以使用反向代理创建安全隧道到本地托管的应用程序。它是一个公开任何本地托管的应用程序在Web上的实用工具。

如果您想知道如何使用它,请参阅下面显示的步骤:

如果您使用Mac,请在终端中输入此命令以下载ngrock

brew install ngrok

针对Windows

choco install ngrok

安装后,您需要在Ngrok网站上注册账户。
注册成功后,您将获得Ngrock身份验证令牌,然后将此命令粘贴到终端中。

适用于MacWindows操作系统。

ngrok config add-authtoken <token>

现在Ngrock已经全部设置完成,您可以使用以下命令开始隧道:
ngrok tcp <Your Port Number Used In Server.py>
ngrok tcp 5321

注意请在Python Socket Server文件所在的目录中给出命令

这样,您的Socket就可以连接世界上任何地方的任何计算机了。

如果您仍然难以理解,请参考视频中的详细说明。
您也可以参考ngrock文档此处


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接