Skip to content

Python 网络编程

Python 提供了两个级别访问的网络服务。:

  • 低级别的网络服务支持基本的 Socket,它提供了标准的 BSD Sockets API,可以访问底层操作系统 Socket 接口的全部方法。
  • 高级别的网络服务模块 SocketServer,它提供了服务器中心类,可以简化网络服务器的开发。

互联网上的服务,最底层都是基于 tcp/ip 协议封装,根据是要场景主要分为两种类型:C/S 架构B/S 架构

C/S 架构:c/s 架构 主要是指一个客户端(client)、一个服务器(server),像手机 app、电脑程序都是这种。需要服务器提供服务的应用,都是这种架构。

如果使用 python 进行网络编程,一般需要写两个 py 文件,一个服务器、一个客户端,才能实现数据通信。

B/S 架构:

b/s 架构 主要是指一个浏览器(browser)、一个服务器(server),使用浏览器打开的应用程序都是这种。

提示

c/s 架构:客户端(手机应用、电脑应用、需要服务器提供服务的应用)/ 服务器

b/s 架构:浏览器 / 服务器。(这种架构的优点是不需要开发自己的客户端)

服务器有时候指代很多东西:服务器(提供服务)、web 服务器(专门返回网页)、腾讯云服务器(部署写好的服务程序 物理设备)

Socket 模块

python 网络编程的主要使用模块为 socket 模块。使用socket(family, type[,protocal]) 使用给定的套接族,套接字类型,协议编号(默认为 0)就可以创建套接字

  • family:协议版本
  • protocal:通信协议

 

socket 类型描述
socket.AF_UNIX用于同一台机器上的进程通信(既本机通信)
socket.AF_INET用于服务器与服务器之间的网络通信
socket.AF_INET6基于 IPV6 方式的服务器与服务器之间的网络通信
socket.SOCK_STREAM基于 TCP 的流式 socket 通信
socket.SOCK_DGRAM基于 UDP 的数据报式 socket 通信
socket.SOCK_RAW原始套接字,普通的套接字无法处理 ICMPIGMP 等网络报文,而 SOCK_RAW 可以;其次 SOCK_RAW 也可以处理特殊的 IPV4 报文;此外,利用原始套接字,可以通过 IP_HDRINCL 套接字选项由用户构造 IP
socket.SOCK_SEQPACKET可靠的连续数据包服务

想要进行通信,就需要创建 socket 对象。

创建 TCP Socket

python
import socket

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

创建 UDP Socket

python
import socket

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

注意

  • TCP 发送数据时,已建立好 TCP 链接,所以不需要指定地址,而 UDP 是面向无连接的,每次发送都需要指定发送给谁。
  • 服务器与客户端不能直接发送列表,元素,字典等带有数据类型的格式,发送的内容必须是字符串数据。

创建 socket 对象之后,可以使用 socket 的函数进一步将其设置为客户端或者是服务器。

服务器端 Socket 函数

用于创建服务器对象的函数

Socket 函数描述
s.bind(address)将套接字绑定到地址,在 AF_INET 下,以 tuple(host, port) 的方式传入,如s.bind((host, port))
s.listen(backlog)开始监听 TCP 传入连接,backlog 指定在拒绝链接前,操作系统可以挂起的最大连接数,该值最少为 1,大部分应用程序设为 5 就够用了
s.accept()接受 TCP 链接并返回(conn, address),其中 conn 是新的套接字对象,可以用来接收和发送数据,address 是链接客户端的地址。

客户端 Socket 函数

用于创建客户端的服务器

Socket 函数描述
s.connect(address)链接到 address 处的套接字,一般 address 的格式为 tuple(host, port),如果链接出错,则返回 socket.error 错误
s.connect_ex(address)功能与 s.connect(address) 相同,但成功返回 0,失败返回 errno 的值

公共 Socket 函数

客户端、服务器都可以使用的公共函数,一般是用于获取通信对象的属性、收发数据。

Socket 函数描述
s.recv(bufsize[, flag])接受 TCP 套接字的数据,数据以字符串形式返回,buffsize 指定要接受的最大数据量,flag 提供有关消息的其他信息,通常可以忽略
s.send(string[, flag])发送 TCP 数据,将字符串中的数据发送到链接的套接字,返回值是要发送的字节数量,该数量可能小于 string 的字节大小
s.sendall(string[, flag])完整发送 TCP 数据,将字符串中的数据发送到链接的套接字,但在返回之前尝试发送所有数据。成功返回 None,失败则抛出异常
s.recvfrom(bufsize[, flag])接受 UDP 套接字的数据,与 recv() 类似,但返回值是 tuple(data, address)。其中 data 是包含接受数据的字符串,address 是发送数据的套接字地址
s.sendto(string[, flag], address)发送 UDP 数据,将数据发送到套接字,address 形式为 tuple(ipaddr, port) ,指定远程地址发送,返回值是发送的字节数
s.close()关闭套接字
s.getpeername()返回套接字的远程地址,返回值通常是一个 tuple(ipaddr, port)
s.getsockname()返回套接字自己的地址,返回值通常是一个 tuple(ipaddr, port)
s.setsockopt(level, optname, value)设置给定套接字选项的值
s.getsockopt(level, optname[, buflen])返回套接字选项的值
s.settimeout(timeout)设置套接字操作的超时时间,timeout 是一个浮点数,单位是秒,值为 None 则表示永远不会超时。一般超时期应在刚创建套接字时设置,因为他们可能用于连接的操作,如 s.connect()
s.gettimeout()返回当前超时值,单位是秒,如果没有设置超时则返回 None
s.fileno()返回套接字的文件描述
s.setblocking(flag)如果 flag 为 0,则将套接字设置为非阻塞模式,否则将套接字设置为阻塞模式(默认值)。非阻塞模式下,如果调用 recv() 没有发现任何数据,或 send() 调用无法立即发送数据,那么将引起 socket.error 异常。
s.makefile()创建一个与该套接字相关的文件

Socket 编程思想

socket 通信流程

socket 是 "打开—读/写—关闭" 模式的实现,以使用 TCP 协议通讯的 socket 为例,其交互流程大概是这样子的

assets

  1. 服务器根据地址类型(ipv4,ipv6)、socket 类型、协议创建 socket
  2. 服务器为 socket 绑定 ip 地址和端口号
  3. 服务器 socket 监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的 socket 并没有被打开
  4. 客户端创建 socket
  5. 客户端打开 socket,根据服务器 ip 地址和端口号试图连接服务器 socket
  6. 服务器 socket 接收到客户端 socket 请求,被动打开,开始接收客户端请求,直到客户端返回连接信息。这时候 socket 进入阻塞 状态,所谓阻塞即 accept() 方法一直到客户端返回连接信息后才返回,开始接收下一个客户端谅解请求
  7. 客户端连接成功,向服务器发送连接状态信息
  8. 服务器 accept 方法返回,连接成功
  9. 客户端向 socket 写入信息
  10. 服务器读取信息
  11. 客户端关闭
  12. 服务器端关闭

注意

生活中的电话机

如果想让别人能更够打通咱们的电话获取相应服务的话,需要做以下几件事情:

  1. 买个手机
  2. 插上手机卡
  3. 设计手机为正常接听状态(即能够响铃)
  4. 静静的等着别人拨打

如同上面的电话机过程一样,在程序中,如果想要完成一个 tcp 服务器的功能,需要的流程如下:

  1. socket 创建一个套接字
  2. bind 绑定 ipport
  3. listen 使套接字变为可以被动链接
  4. accept 等待客户端的链接
  5. recv/send 接收发送数据

TCP 服务器

1、创建套接字,绑定套接字到本地 ip 与端口

python
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('127.0.0.1', 7788))

函数 socket.socket 创建一个 socket,该函数带有两个参数:

  • Address Family:可以选择 AF_INET(用于 Internet 进程间通信)或者 AF_UNIX (用于同一台机器进程间通信),实际工作中常用 AF_INET
  • Type:套接字类型,可以是 SOCK_STREAM(流式套接字,主要用于 TCP 协议)或者 SOCK_DGRAM(数据报套接字,主要用于 UDP 协议)

2、开始监听链接

python
s.listen()

3、进入循环,不断接受客户端的链接请求

python
while True:
    conn, addr = s.accept()

4、接收客户端传来的数据,并且发送给对方发送数据

python
data = s.recv()
s.send('hello world !'.encode())

5、传输完毕后,关闭套接字

python
s.close()

TCP 服务器完整代码

py
import socket

# 创建 socket
tcp_server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 本地信息
address = ('127.0.0.1', 7788)

# 绑定
tcp_server_socket.bind(address)

# 使用 socket 创建的套接字默认的属性是主动的,使用 listen 将其变为被动的,这样就可以接收别人的链接了
tcp_server_socket.listen(128)

# 如果有新的客户端来链接服务器,那么就产生一个新的套接字专门为这个客户端服务
# client_socket 用来为这个客户端服务
# tcp_server_socket 就可以省下来专门等待其他新客户端的链接
client_socket, clientAddr = tcp_server_socket.accept()

# 接收对方发送过来的数据
recv_data = client_socket.recv(1024)  # 接收 1024 个字节
print('接收到的数据为:', recv_data.decode('utf-8'))

# 发送一些数据到客户端
client_socket.send("thank you !".encode('utf-8'))

# 关闭为这个客户端服务的套接字,只要关闭了,就意味着为不能再为这个客户端服务了,如果还需要服务,只能再次重新连接
client_socket.close()

TCP 客户端

所谓的服务器端:就是提供服务的一方,而客户端,就是需要被服务的一方

tcp 客户端构建流程

1、创建套接字并链接至远端地址

python
import socket

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(('127.0.0.1', 7788))

2、链接后发送数据和接收数据

python
s.send('hello world !')
data = s.recv()

3、传输完毕后,关闭套接字

python
s.close()

TCP 客户端完整代码

py
import socket

# 创建 socket
tcp_client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 目的信息
server_ip = input("请输入服务器 ip:")
server_port = int(input("请输入服务器 port:"))

# 链接服务器
tcp_client_socket.connect((server_ip, server_port))

# 提示用户输入数据
send_data = input("请输入要发送的数据:")

tcp_client_socket.send(send_data.encode("utf-8"))

# 接收对方发送过来的数据,最大接收 1024 个字节
recvData = tcp_client_socket.recv(1024)
print('接收到的数据为:', recvData.decode('utf-8'))

# 关闭套接字
tcp_client_socket.close()

运行之后

python
请输入服务器ip: 127.0.0.1
请输入服务器port: 7788
请输入要发送的数据:你好啊
接收到的数据为: 我很好,你呢

循环收发数据

服务接循环收数据

py
# 服务器循环接收数据
import socket

HOST = '127.0.0.1'
PORT = 7788

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((HOST, PORT))
s.listen(5)

print('服务器已经开启在:%s:%s' % (HOST, PORT))
print('等待客户端连接...')

conn, addr = s.accept()
print('客户端已经连接: ', addr)

while True:
    data = conn.recv(1024)
    print(data)

    if data == '拜拜':
        conn.send('over')
        conn.close()
        break

    conn.send("服务器已经收到你的信息")

conn.close()

客户端循环发送数据

py
# 客户端循环发送数据
import socket

HOST = '127.0.0.1'
PORT = 7788

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))

while True:
    cmd = input("请输入需要发送的信息:")
    s.send(cmd)
    data = s.recv(1024)
    print(data)
    if data.decode('utf-8') == 'over':
        break

s.close()

案例:零食销售系统

商品数据

python
goods = {
    '瓜子': 4.5,
    '西瓜': 2,
    '矿泉水': 2.5,
}

请编写客户端与服务器,实现下面的逻辑。

>>> 【服务器】目前商店还有:瓜子(2.5),西瓜(2),矿泉水。请问您需要什么?
>>> 【客户端】红牛
>>> 【服务器】抱歉,您购买的商品目前没有,请选购其他的。
>>> 【客户端】西瓜
>>> 【服务器】购买成功,余额 -2
>>> 【客户端】拜拜
>>> 【客户端】欢迎下次光临
参考答案
py
import socket

goods = {
    '瓜子': 4.5,
    '西瓜': 2,
    '矿泉水': 2.5,
}

tcp_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp_server.bind(('0.0.0.0', 9999))
tcp_server.listen(124)
tcp_client, tcp_client_addr = tcp_server.accept()

tcp_client.send('目前商店还有:瓜子(2.5),西瓜(2),矿泉水(2.5)。请问您需要什么?'.encode('gbk'))

while True:
    recv_data = tcp_client.recv(1024)  # 一次性最大接收 1024 字节的字符串
    good = recv_data.decode('gbk')

    if good == '拜拜':
        tcp_client.send('欢迎下次光临'.encode('gbk'))
        break
    if good in goods.keys():
        price = goods.get(good)
        tcp_client.send(f'购买成功,余额 -{price}'.encode('gbk'))
    else:
        tcp_client.send('抱歉,您购买的商品目前没有,请选购其他的。'.encode('gbk'))

tcp_client.close()
tcp_server.close()

请问如果同时有多个顾客上门,现在的服务器是否能够支持呢?

TCP 总结

TCP 协议,传输控制协议(英语:Transmission Control Protocol,缩写为 TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议,由 IETF 的 RFC 793 定义。

TCP 通信需要经过 创建连接、数据传送、终止连接 三个步骤。

TCP 通信模型中,在通信开始之前,一定要先建立相关的链接,才能发送数据,类似于生活中,"打电话"

TCP 注意点

  1. tcp 服务器一般情况下都需要绑定,否则客户端找不到这个服务器
  2. tcp 客户端一般不绑定,因为是主动链接服务器,所以只要确定好服务器的 ip、port 等信息就好,本地客户端可以随机
  3. tcp 服务器中通过 listen 可以将 socket 创建出来的主动套接字变为被动的,这是做 tcp 服务器时必须要做的
  4. 当客户端需要链接服务器时,就需要使用 connect 进行链接,udp 是不需要链接的而是直接发送,但是 tcp 必须先链接,只有链接成功才能通信
  5. 当一个 tcp 客户端连接服务器时,服务器端会有 1 个新的套接字,这个套接字用来标记这个客户端,单独为这个客户端服务
  6. listen 后的套接字是被动套接字,用来接收新的客户端的链接请求的,而 accept 返回的新套接字是标记这个新客户端的
  7. 关闭 listen 后的套接字意味着被动套接字关闭了,会导致新的客户端不能够链接服务器,但是之前已经链接成功的客户端正常通信。
  8. 关闭 accept 返回的套接字意味着这个客户端已经服务完毕
  9. 当客户端的套接字调用 close 后,服务器端会 recv 解堵塞,并且返回的长度为 0,因此服务器可以通过返回数据的长度来区别客户端是否已经下线

TCP 特点

面向连接

  • 通信双方必须先建立连接
  • 双方间的数据传输都可以通过一个连接进行
  • 完成数据交换后,双方必须断开此连接,以释放系统资源。

这种连接是一对一的,因此 TCP 不适用于广播的应用程序,基于广播的应用程序请使用 UDP 协议。

附录:网络调试助手

socket 协议只要符合规范,就能够与任意的编程语言进行通信。接下来我们来演示 python 如何与网络调试助手进行通信。

提示

网络调试助手下载地址: 点击下载

mac 版网络调试工具: 点击下载

网络调试助手 windows 系统下默认编码为 gbk