如何在Python中使用TCP套接字



网络套接字是跨计算机网络的进程间通信的端点。Python标准库有一个名为socket的模块,该模块提供了一个底层Internet网络接口。该接口在不同的编程语言中通用,因为它使用OS级系统调用.

socket(family,type[,protocal]) 使用给定的地址族、套接字类型、协议编号(默认为0)来创建套接字。它接受family,type和protocal参数。下面创建一个TCP套接字。

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

它返回一个具有以下主要方法的套接字对象:

  • bind() 绑定 ip 和 port
  • listen() 监听TCP传入连接
  • accept() 接受TCP连接并返回
  • connect() 连接到特定目的ip + port 的
  • send() 发送TCP数据
  • recv() 接受TCP套接字的数据

bind(),listen()和accept()特定于服务器套接字。connect()是特定于客户端套接字的。send()和recv()是两种最为常见的方法。下面是来自文档的示例:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('localhost', 60000))
s.listen(1)
conn, addr = s.accept()
while 1:
data = conn.recv(1024)
if not data:
break
conn.sendall(data)
conn.close()

在这里,我们创建一个服务器套接字,将其绑定到本地主机和60000端口,并开始侦听传入的连接。要接受传入连接,我们调用accept()方法,该方法将阻塞直到新客户端连接为止。发生这种情况时,它将创建一个新的套接字,并将其与客户端地址一起返回。然后,在无限循环中,它使用方法从套接字以1024个字节为批处理读取数据,recv()直到返回空字符串。之后,它将使用sendall()内部重复调用的便捷方法将所有传入数据发送回去send()。之后,它仅关闭客户端的连接。本示例只能服务一个传入连接,因为它没有accept()周期性地调用。

客户端代码看起来更简单:

import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('localhost', 60000))
s.sendall('Hello, world')
data = s.recv(1024)
s.close()
print 'Received', repr(data)

在这里,而不是bind()和,listen()它仅调用connect()并立即将数据发送到服务器。然后,它接收回1024个字节,关闭套接字,并打印接收到的数据。

所有套接字方法都被阻止。例如,当它从套接字读取或写入套接字时,程序将无法执行其他任何操作。一种可能的解决方案是将与客户端的工作委派给单独的线程。但是,创建线程并在它们之间切换上下文并不是一个便宜的操作。为了解决这个问题,有一种使用套接字的异步方法。主要思想是将维护套接字状态的任务委托给操作系统,并在有什么内容可以从套接字读取或何时可以进行编写时通知程序。

有许多用于不同操作系统的接口:

  • poll, epoll (linux)
  • kqueue, kevent (BSD)
  • select (crossplatform)

它们几乎相同,因此让我们使用Python select创建一个服务器。这是一个Python select示例:

import select, socket, sys, Queue
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setblocking(0)
server.bind(('localhost', 60000))
server.listen(5)
inputs = [server]
outputs = []
message_queues = {}

while inputs:
readable, writable, exceptional = select.select(
inputs, outputs, inputs)
for s in readable:
if s is server:
connection, client_address = s.accept()
connection.setblocking(0)
inputs.append(connection)
message_queues[connection] = Queue.Queue()
else:
data = s.recv(1024)
if data:
message_queues[s].put(data)
if s not in outputs:
outputs.append(s)
else:
if s in outputs:
outputs.remove(s)
inputs.remove(s)
s.close()
del message_queues[s]
<pre><code>for s in writable:
    try:
        next_msg = message_queues[s].get_nowait()
    except Queue.Empty:
        outputs.remove(s)
    else:
        s.send(next_msg)

for s in exceptional:
    inputs.remove(s)
    if s in outputs:
        outputs.remove(s)
    s.close()
    del message_queues[s]</code></pre>

如您所见,这主要是因为我们必须为不同的套接字列表维护一组队列,即写,读和为错误的套接字创建单独的列表。

创建服务器套接字的外观相同,只不过一行:server.setblocking(0)。这样做是为了使套接字无阻塞。由于该服务器可以为多个客户端提供服务,因此它是高级服务器。要在selecting套接字中:

readable, writable, exceptional = select.select(inputs, outputs, inputs)

在这里,我们select.select要求操作系统检查给定的套接字是否已准备好写入,读取或分别存在异常。这就是为什么它传递三个套接字列表来指定期望哪个套接字是可写的,可读的以及应该检查哪个套接字的错误。该调用将阻塞程序(除非传递了超时参数),直到某些传递的套接字准备就绪为止。此时,该调用将返回三个带有套接字的列表,用于指定的操作。

然后,它顺序地遍历这些列表,并且如果其中包含套接字,它将执行相应的操作。当服务器插槽插入时inputs,表示新客户端已到达。因此,它调用accept(),向其中添加返回的套接字,inputs并Queue为将被发回的传入消息添加。如果输入中还有另一个套接字,则某些消息已经到达并准备好读取,因此它将读取它们并将它们放入相应的队列中。

对于可写套接字,它获取挂起的消息(如果有)并将它们写入套接字。如果套接字中有任何错误,它将从列表中删除该套接字。