Python Socket 编程:基于TCP套接字(1)

Socket API 的调用顺序和 TCP 的数据流:
在这里插入图片描述
左边表示服务器, 右边则是客户端
服务器创建「监听」 Socket 的 API 调用:

  • socket()
  • bind()
  • listen()
  • accept()

「监听」 Socket 做的事情就像它的名字一样。 它会监听客户端的连接, 当一个客户端连接进
来的时候, 服务器将调用 accept() 来「接受」 或者「完成」 此连接

客户端调用 connect() 方法来建立与服务器的链接, 并开始三次握手。 握手很重要是因为它
保证了网络的通信的双方可以到达, 也就是说客户端可以正常连接到服务器, 反之亦然

中间部分往返部分表示客户端和服务器的数据交换过程, 调用了send() 和 recv() 方法

客户端 / 服务器打印程序

打印程序的服务端

import socket
HOST = '127.0.0.1' # 标准的回环地址 (localhost)
PORT = 65432 # 监听的端口 ( 非系统级的端口: 大于 1023)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen()
    conn, addr = s.accept()
    with conn:
        print('Connected by', addr)
        while True:
            data = conn.recv(1024)
            if not data:
                break
            conn.sendall(data)

socket.socket()创建了一个 socket 对象, 并且支持 上下文管理器, 你可以使用 with 语
句, 这样你就不用再手动调用 s.close() 来关闭 socket 了
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
调用 socket() 时传入的 socket 地址族参数socket.AF_INET表示因特网 IPv4 地址族, SOCK_STREAM表示使用 TCP 的 socket 类型, 协议将被用来在网络中传输消息

bind()用来关联 socket 到指定的网络接口( IP 地址) 和端口号:
HOST = '127.0.0.1'
PORT = 65432

s.bind((HOST, PORT))

bind() 方法的入参取决于 socket 的地址族, 在这个例子中我们使用了 socket.AF_INET
(IPv4), 它将返回两个元素的元组: (host, port)

  • host可以是主机名称、 IP 地址、 空字符串, 如果使用 IP 地址, host 就应该是 IPv4 格式的字符串, 127.0.0.1 是标准的 IPv4 回环地址, 只有主机上的进程可以连接到服务器, 如果你传了空字符串, 服务器将接受本机所有可用的 IPv4 地址

  • 端口号应该是 1-65535 之间的整数( 0 是保留的) , 这个整数就是用来接受客户端链接的
    TCP 端口号, 如果端口号小于 1024, 有的操作系统会要求管理员权限

listen() 方法调用使服务器可以接受连接请求, 这使它成为一个「监听中」 的 socket

s.listen()
conn, addr = s.accept()

listen() 方法有一个 backlog 参数。 它指定在拒绝新的连接之前系统将允许使用的 未接受的连接 数量。 从 Python 3.5 开始, 这是可选参数。 如果不指定, Python 将取一个默认值

accept() 方法阻塞并等待传入连接。 当一个客户端连接时, 它将返回一个新的 socket 对象, 对象中有表示当前连接的 conn 和一个由主机、 端口号组成的 IPv4/v6 连接的元组

这里必须要明白我们通过调用 accept() 方法拥有了一个新的 socket 对象。 这非常重要, 因为你将用这个 socket 对象和客户端进行通信。 和监听一个 socket 不同的是后者只用来授受新的连接请求

    with conn:
        print('Connected by', addr)
        while True:
            data = conn.recv(1024)
            if not data:
                break
            conn.sendall(data)

accept() 获取客户端 socket 连接对象 conn 后, 使用一个无限 while 循环来阻塞调用
conn.recv() , 无论客户端传过来什么数据都会使用 conn.sendall()打印出来

如果 conn.recv() 方法返回一个空 byte 对象( b'' ) , 然后客户端关闭连接, 循环结束,with 语句和 conn 一起使用时, 通信结束的时候会自动关闭 socket 链接

打印程序的客户端

import socket
HOST = '127.0.0.1' # 服务器的主机名或者 IP 地址
PORT = 65432 # 服务器使用的端口
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((HOST, PORT))
    s.sendall(b'Hello, world')
    data = s.recv(1024)
    
print('Received', repr(data))

与服务器程序相比, 客户端程序简单很多。 它创建了一个 socket 对象, 连接到服务器并且调用 s.sendall() 方法发送消息, 然后再调用 s.recv() 方法读取服务器返回的内容并打印出来

socket传输的数据为二进制数据(字节)

处理多个连接

打印程序的服务端肯定有它自己的一些局限。 这个程序只能服务于一个客户端然后结束。 打印程序的客户端也有它自己的局限, 但是还有一个问题, 如果客户端调用了下面的方法 s.recv() 方法将返回 b’Hello, world’ 中的一个字节 b’H’
data = s.recv(1024)
1024 是缓冲区数据大小限制最大值参数 bufsize , 并不是说 recv() 方法只返回 1024 个字节的内容
send() 方法也是这个原理, 它返回发送内容的字节数, 结果可能小于传入的发送内容, 你得处理这处情况, 按需多次调用 send() 方法来发送完整的数据

应用程序负责检查是否已发送所有数据; 如果仅传输了一些数据, 则应用程序需要尝试
传递剩余数据 引用

可以使用 sendall() 方法来回避这个过程

和 send() 方法不一样的是, sendall() 方法会一直发送字节, 只到所有的数据传输完成
或者中途出现错误。 成功的话会返回 None 引用

有两个问题:

  • 如何同时处理多个连接请求
  • 我们需要一直调用 send() 或者 recv() 直到所有数据传输完成

应该怎么做呢, 有很多方式可以实现并发。 最近, 有一个非常流程的库叫做 Asynchronous
I/O 可以实现, asyncio 库在 Python 3.4 后默认添加到了标准库里面。
用一个非常古老的系统调用:select()
select() 允许你检查多个 socket 的 I/O 完成情况, 所以你可以使用它来检测哪个 socket I/O
是就绪状态从而执行读取或写入操作, 但是这是 Python, 总会有更多其它的选择, 我们将使
用标准库中的selectors 模块, 所以我们使用了最有效的实现, 不用在意你使用的操作系统:
asyncio 使用单线程来处理多任务, 使用事件循环来管理任务。 通过使用 select() , 我们可
以创建自己的事件循环, 更简单且同步化。

多连接的客户端 / 服务器程序

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值