第15章 网络编程

3 阅读12分钟

第15章 网络编程

15.1 网络

使用网络能够把多方电脑等设备链接在一起进行数据传递。网络编程就是让在不同的电脑上的软件能够进行数据传递,即进程之间的通信。

15.1.1 网络编程三要素

  • IP:网络中每台计算机的唯一标识,通过 IP 地址可以找到计算机。
  • 端口:标识进程的逻辑地址,通过端口找到计算机中指定的进程(应用软件)。
  • 协议:定义通信规则。

15.1.2 TCP/IP 协议族

1)通信协议

通信协议是一组用于规定不同设备或计算机之间如何进行数据交换和通信的规则和约定。它定义了通信的各个方面,包括数据的格式、传输的顺序、错误检查机制、如何处理不同情况(如重传丢失的数据包)等。

2)TCP/IP

TCP/IP 协议族,简称 TCP/IP,是一组通信协议,用于互联网的数据传输和网络通信,定义了数据如何在不同的计算机之间传输和路由。TCP/IP 得名于该协议家族的两个核心协议:TCP(传输控制协议)和 IP(网际协议)。

3)分层网络模型

OSI 七层网络模型由国际标准化组织制定,但其实现过于复杂。TCP/IP 模型定义了应用层、传输层、网络层、网络接口层这四层网络结构。在学习和开发中,通常将网络接口层替换为 OSI 七层模型中的数据链路层和物理层来进行理解,这就是五层网络模型

15.2 IP

15.2.1 什么是 IP

IP 地址由一串数字组成,用来标识一台电脑在网络中的位置。当设备连接网络,设备将被分配一个 IP 地址,用作标识。通过 IP 地址设备间可以互相通讯。IP 地址有两个主要功能:标识设备或网络,以及寻址。

  • Windows 下可以在命令提示符中使用 ipconfig 查看网络适配器的 IP。
  • Linux 下可以在终端中使用 ifconfigip addr 查看 IP。

15.2.2 子网掩码

IP 网络可以在 IPv4 和 IPv6 中划分子网。子网掩码确定了 IP 地址如何分为网络部分和主机部分。

例如:IPv4 地址及其子网掩码分别可以是 192.168.10.2255.255.255.0。因为 IP 地址的前 24 位表示网络和子网,所以相同的 IP 地址和子网的 CIDR 表示法为 192.168.10.2/24

主机编号全为 0,表示网络号;主机编号全为 1,表示网络广播。

15.2.3 IPv4 地址的分类

IPv4 地址分为 A、B、C、D、E 五类,其中 A、B、C 类为常用地址类型。

15.2.4 公网与私网

公网 IP 在任何地方都可以访问。而私网 IP 只能在局域网内访问。

国际规定有一部分 IP 地址是用于局域网使用,它们的范围是:

  • 10.0.0.010.255.255.255
  • 172.16.0.0172.31.255.255
  • 192.168.0.0192.168.255.255

其中 127.0.0.1127.255.255.255 用于回路测试,如 127.0.0.1 可以代表本机 IP 地址。

网络地址转换(NAT) 是一种在 IP 数据包通过路由器或防火墙时重写来源或目的 IP 地址或端口的技术。这种技术普遍应用于有多台主机,但只通过一个公有 IP 地址访问互联网的私有网络中。

15.2.5 IPv4 与 IPv6

常见的 IP 地址分为 IPv4 与 IPv6 两大类:

  • IPv4 为 32 位长,通常以四组十进制数字组成,以点分隔,如:172.16.254.1
  • IPv6 为 128 位长,通常以八组十六进制数字组成,以冒号分割,如:2001:db8:0:1234:0:567:8:1

随着互联网的快速成长,IPv4 的 42 亿个地址最终于 2011 年 2 月 3 日用尽。IPv6 的 IP 地址数量最高可达 3.402823669×10³⁸ 个。

15.3 端口

15.3.1 什么是端口

这里的端口指的是逻辑端口,即 TCP/IP 协议中的端口。端口用于进程(应用软件)在同一设备或不同设备之间通信。每个端口有一个对应的端口号。端口号有 65536 个。

可以使用 netstat -ano 查看端口信息。

15.3.2 端口号的分配

1)公认端口

01023,它们紧密绑定于一些服务。端口号 0 是被保留的,不可使用。11023 系统保留,只能由 root 用户使用。

2)动态端口

102465535,之所以称为动态端口,是因为它一般不固定分配某种服务,而是动态分配。

3)常见端口

端口服务
0/TCP,UDP保留端口,不使用
7/TCP,UDPEcho(回显)协议
21/TCP,UDPFTP 文件传输协议
22/TCP,UDPSSH 安全远程登录协议
23/TCP,UDPTelnet 终端仿真协议
25/TCP,UDPSMTP 简单邮件传输协议
53/TCP,UDPDNS 域名服务系统
80/TCP,UDPHTTP 超文本传输协议
110/TCPPOP3 邮局协议第3版
443/TCPHTTPS 安全超文本传输协议
3306/TCP,UDPMySQL 数据库系统
3389/TCP远程桌面协议(RDP)

15.4 socket 套接字

15.4.1 什么是 socket

socket(套接字)是同一或不同电脑的进程(任务、应用软件)间通信的一个工具,进程之间想要进行网络通信需要基于 socket。只要与网络相关的应用程序或者软件都使用到了 socket。

15.4.2 socket 的使用

Python 中提供了 socket 模块用于创建套接字。

import socket

# AF_INET 用于 Internet 进程间通信;SOCK_STREAM 流式套接字,TCP
tcp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)

# AF_INET 用于 Internet 进程间通信;SOCK_DGRAM 数据报套接字,UDP
udp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)

15.5 UDP

15.5.1 什么是 UDP

用户数据报协议(UDP:User Datagram Protocol)是一个简单的面向数据报的通信协议。UDP 只提供数据的不可靠传递,它一旦把应用程序发给网络层的数据发送出去,就不保留数据备份。

UDP 避免了协议栈中执行错误检查和纠正处理的开销,适用于对时间有较高要求的应用程序。流媒体、在线游戏流量通常使用 UDP 传输。

15.5.2 UDP 编程

UDP 服务端:

"""udp 服务端"""
import socket

# 创建udp套接字
udp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
# 绑定ip和端口
udp_socket.bind(("127.0.0.1", 8080))

while True:
    # 接收数据
    recv_data, client_addr = udp_socket.recvfrom(1024)
    client_ip = client_addr[0]
    client_port = client_addr[1]
    print(f"{client_ip}:{client_port}>> {recv_data.decode('utf-8')}")
    # 发送数据
    udp_socket.sendto("你好".encode("utf-8"), client_addr)

# 关闭套接字
udp_socket.close()

UDP 客户端:

"""udp 客户端"""
import socket

# 创建udp套接字
udp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)

while True:
    try:
        # 发送数据
        server_ip = "127.0.0.1"
        server_port = 8080
        udp_socket.sendto(
            input(f"{server_ip}:{server_port}<< ").encode("utf-8"),
            (server_ip, server_port)
        )
        # 接收数据
        recv_data, client_addr = udp_socket.recvfrom(1024)
        client_ip = client_addr[0]
        client_port = client_addr[1]
        print(f"{client_ip}:{client_port}>> {recv_data.decode('utf-8')}")
    except KeyboardInterrupt:
        break

# 关闭套接字
udp_socket.close()

15.6 TCP

15.6.1 什么是 TCP

传输控制协议(TCP:Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。TCP 协议的运行可划分为三个阶段:连接建立、数据传送和连接终止。

很多重要的机制保证了 TCP 的可靠性和强壮性,包括:

  • 使用序号,对收到的 TCP 报文段进行排序以及检测重复的数据。
  • 使用校验和检测报文段的错误,即无错传输。
  • 使用确认和计时器来检测和纠正丢包或延时。
  • 流控制。
  • 拥塞控制。
  • 丢失包的重传。

15.6.2 TCP 编程

TCP 服务端:

"""tcp 服务端"""
import socket

# 创建tcp套接字
tcp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 绑定ip和端口
tcp_socket.bind(("127.0.0.1", 8080))
# 设置监听
tcp_socket.listen(2)
# 等待客户端连接
client_socket, client_addr = tcp_socket.accept()

while True:
    # 接收数据
    recv_data = client_socket.recv(1024)
    print(f"{client_addr[0]}:{client_addr[1]}>> {recv_data.decode('utf-8')}")
    # 发送数据
    client_socket.send("你好".encode("utf-8"))

# 关闭套接字
tcp_socket.close()

TCP 客户端:

"""tcp 客户端"""
import socket

# 创建tcp套接字
tcp_socket = socket.socket(family=socket.AF_INET, type=socket.SOCK_STREAM)
# 连接服务器
server_ip = "127.0.0.1"
server_port = 8080
tcp_socket.connect((server_ip, server_port))

while True:
    try:
        # 发送数据
        tcp_socket.send(
            input(f"{server_ip}:{server_port}<< ").encode("utf-8")
        )
        # 接收数据
        recv_data = tcp_socket.recv(1024)
        print(f"{server_ip}:{server_port}>> {recv_data.decode('utf-8')}")
    except KeyboardInterrupt:
        break

# 关闭套接字
tcp_socket.close()

15.7 HTTP

15.7.1 什么是 HTTP

HTTP(超文本传输协议)是一种用于分布式、协作式和超媒体信息系统的应用层协议。是万维网的数据通信的基础。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。

HTTP 上的一个典型工作流程是客户端计算机向服务器发出请求,然后服务器发送响应消息。

15.7.2 HTTP 消息结构

1)客户端请求消息

客户端发送一个 HTTP 请求到服务器的请求消息包括以下格式:请求行、请求头、空行和请求体四个部分组成。

  • 请求行:请求方法(如 GET、POST)、请求 URI、协议版本。格式示例:GET /index.html HTTP/1.1
  • 请求头:包含了客户端环境信息、请求体的大小等。常见的请求头包括 HostUser-AgentAccept 等。
  • 空行:请求头和请求体之间的分隔符。
  • 请求体:在某些类型的 HTTP 请求(如 POST 和 PUT)中,请求体包含要发送给服务器的数据。

2)服务端响应消息

HTTP 响应由四个部分组成:状态行、消息报头、空行和响应正文

  • 状态行:HTTP 版本、状态码、状态信息。格式示例:HTTP/1.1 200 OK
  • 响应头:包含了服务器环境信息、响应体的大小等。常见的响应头包括 Content-TypeContent-Length 等。
  • 空行:响应头和响应体之间的分隔符。
  • 响应体:包含服务器返回的数据,如请求的网页内容、图片、JSON 数据等。

15.7.3 HTTP 请求方法

HTTP/1.1 协议中共定义了八种方法:

  1. GET:向指定的资源发出"显示"请求。应该只用在读取资料。
  2. HEAD:与 GET 方法一样,但服务器将不传回资源的本文部分。
  3. POST:向指定资源提交数据,请求服务器进行处理。
  4. PUT:向指定资源位置上传其最新内容。
  5. DELETE:请求服务器删除 Request-URI 所标识的资源。
  6. TRACE:回显服务器收到的请求,主要用于测试或诊断。
  7. OPTIONS:使服务器传回该资源所支持的所有 HTTP 请求方法。
  8. CONNECT:预留给能够将连接改为隧道方式的代理服务器。

15.7.4 HTTP 状态码

HTTP 状态码是服务器对客户端请求的响应,状态码分为五类:

分类描述常见状态码
1xx信息状态码,表示接收的请求正在处理100(继续)、101(切换协议)
2xx成功状态码,表示请求正常处理完毕200(请求成功)、202(已接受)
3xx重定向状态码,需要后续操作才能完成请求301(永久移动)、302(临时移动)、304(未修改)
4xx客户端错误状态码,请求包含语法错误或无法完成400(语法错误)、403(拒绝执行)、404(未找到)、405(方法被禁止)
5xx服务器错误状态码,服务器处理请求时发生错误500(内部错误)、501(不支持)、502(网关错误)

15.8 案例:发送 HTTP 请求以及获取响应数据

import requests

# 一言网的 API 地址
url = 'https://v1.hitokoto.cn/'

# 请求参数
params = {
    'c': 'a',  # 可以根据需要修改类型,a 代表动画,b 代表漫画等
    'encode': 'json'
}

try:
    print(f"正在发送 GET 请求到: {url},参数: {params}")
    response = requests.get(url, params=params)
    status_code = response.status_code
    if status_code == 200:
        print(f"请求成功!状态码: {status_code}")
        data = response.json()
        hitokoto = data['hitokoto']
        from_who = data['from_who'] if data['from_who'] else '未知'
        print(f"随机名言: {hitokoto} - {from_who}")
    elif status_code == 404:
        print(f"请求的资源未找到!状态码: {status_code}")
    elif status_code == 500:
        print(f"服务器内部错误!状态码: {status_code}")
    else:
        print(f"发生未知错误,状态码: {status_code}")
except requests.RequestException as e:
    print(f"请求过程中出现错误: {e}")

15.9 案例:通过 Starlette 构建 web 接口

Starlette 是一个轻量级的 Python 异步 Web 框架,专为构建高性能的异步应用程序而设计。

  • Uvicorn:它是一个基于 Python 的 ASGI(Asynchronous Server Gateway Interface)服务器,为 Python 异步 Web 应用提供了高性能的运行环境。
  • Starlette:是一个轻量级的 Python 异步 Web 框架,它遵循 ASGI 标准,提供了路由、中间件、请求和响应处理等核心功能。
  • 协作方式:Uvicorn 为 Starlette 应用提供了运行的基础环境。

1)安装依赖包

pip install starlette uvicorn requests

2)代码实现

from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
import requests
import uvicorn

# 一言网的 API 地址
HITOKOTO_URL = 'https://v1.hitokoto.cn/'

# 定义异步函数来获取随机名言
async def get_hitokoto():
    try:
        params = {
            'c': 'a',
            'encode': 'json'
        }
        response = requests.get(HITOKOTO_URL, params=params)
        status_code = response.status_code
        if status_code == 200:
            data = response.json()
            hitokoto = data['hitokoto']
            from_who = data['from_who'] if data['from_who'] else '未知'
            return {'hitokoto': hitokoto, 'from_who': from_who}
        else:
            return {'error': f'请求一言网 API 失败,状态码: {status_code}'}
    except requests.RequestException as e:
        return {'error': f'请求过程中出现错误: {str(e)}'}

# 定义处理根路径请求的异步函数
async def homepage(request):
    result = await get_hitokoto()
    return JSONResponse(result)

# 创建 Starlette 应用实例
app = Starlette(debug=True, routes=[
    Route('/', homepage),
])

if __name__ == "__main__":
    # 使用 uvicorn 运行应用
    uvicorn.run(app, host='0.0.0.0', port=8000)

3)代码说明

  • get_hitokoto 函数:负责发送 HTTP 请求到一言网的 API,获取随机名言。
  • homepage 函数:作为 Web 服务的根路径处理函数,调用 get_hitokoto 获取随机名言,并将结果封装成 JSON 响应返回给客户端。
  • Starlette 应用:创建 Starlette 应用实例,并定义路由规则,将根路径 / 映射到 homepage 处理函数。使用 uvicorn 作为 ASGI 服务器运行应用。