接口和接口开发的基本概念

接口和接口开发的基本概念

接口

在软件开发中,“接口”(API)通常指的是不同系统、模块、或组件之间交互的契约或协议。在前后端开发中,接口通常指的是后端服务与前端应用之间数据交换的通道。通过这些接口,前端能够请求后端的数据,后端根据前端的请求返回相应的数据或处理结果。现在每一个大模型(比如DeepSeek、GPT)都有相应的API,开发者不需要理解大模型是怎么实现的,只需要通过这些API向大模型发送请求,就可以获取模型的输出了(例如,生成文本、回答问题、翻译语言等)。比如在 OpenAI 的 GPT 系列模型中,可以通过一个 RESTful API 调用模型来生成文本,一个典型的 API 请求可能是这样的:

  • 请求

    1
    2
    3
    4
    5
    6
    7
    8
    9
    POST https://api.openai.com/v1/completions
    Content-Type: application/json
    Authorization: Bearer YOUR_API_KEY

    {
    "model": "text-davinci-003",
    "prompt": "Tell me a joke about programming",
    "max_tokens": 50
    }

    在这个 JSON 请求中:

    • model: 指定使用的模型,如 GPT-3 或 GPT-4。
    • prompt: 用户输入的提示信息。
    • max_tokens: 指定返回的最大字符数(通常是以 token 数量为单位)。
  • 响应

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    HTTP/1.1 200 OK
    Content-Type: application/json

    {
    "id": "cmpl-5S5bxYqT5e0MiBvYPzFp70p2yk0Wf",
    "object": "text_completion",
    "created": 1611894167,
    "model": "text-davinci-003",
    "choices": [
    {
    "text": "Why do programmers prefer dark mode? Because the light attracts bugs!",
    "index": 0
    }
    ]
    }

    响应会包含模型生成的文本,在这个例子中是一个程序员笑话。那么在这里,具体哪些东西叫作“接口”?实际上这整个流程和规范都算是接口的一部分,比如:

    • 通过哪个 URL 路径和服务器交互:https://api.openai.com/v1/completions
    • 如何向API提交数据:POST方法
    • 需要发送什么样的输入数据:包含Authorization,请求体为JSON格式,必须包含modelpromptmax_tokens等字段。
    • 服务器返回的数据是什么格式:也是 JSON 格式,包含了模型的生成结果,包含idmodel等等字段。

实际上只要是服务器-客户端模式这类的应用,都需要进行客户端与服务端之间的交互和数据交换,也都存在接口的概念。比如 MySQL 是一个关系型数据库管理系统,通信方式遵循客户端-服务器模式。MySQL 服务器是一个持续运行的进程,负责处理数据库操作。它在后台运行,监听来自客户端的请求;客户端程序(如 MySQL Workbench、命令行工具或应用程序)向 MySQL 服务器发起请求。客户端通过发送 SQL 查询、请求数据等来与服务器进行交互。MySQL 客户端和服务器之间使用 MySQL 协议进行通信,MySQL 协议基于 TCP/IP 网络进行传输,它定义了如何将 SQL 查询、数据和响应进行编码和解码。客户端通过 TCP/IP 连接到 MySQL 服务器,发送请求并等待响应。

ANSYS也同样遵循客户端-服务器模式,在大型仿真任务中,ANSYS 服务器处理计算任务,管理并调度计算资源(如多个计算节点、集群等)。仿真计算的实际过程在服务器上运行。用户通过 ANSYS 客户端应用程序(如 ANSYS Workbench)与仿真模型进行交互,创建模型、设置边界条件、提交计算请求等。用户通过 ANSYS Workbench 启动仿真任务;客户端发送任务请求到 ANSYS 服务器,服务器开始进行计算;服务器完成计算后,结果会返回给客户端,用户查看并分析结果。在许多 ANSYS 软件版本中,客户端与服务器之间的通信使用TCP/IP 协议,并通过专门的协议进行数据交换,类似于数据库服务器的通信方式。对于分布式仿真和高性能计算(HPC)环境,ANSYS 可以通过网络将计算任务分发到多个计算节点,并通过客户端收集结果。

对于科学计算软件开发这样的场景,前后端分离是比较适合的开发模式。像ANSYS一样,前端用户界面制定请求,并将特定格式的请求发送到某个接口上,后端接收到这些请求,调度求解器进行模拟计算,并将结果返回给前端。前端开发不需对这些复杂的算法有什么过多的了解。这样,前端和后端可以独立开发、独立演进。即使前端对后端没有任何知识,项目也可以推进下去。对接口和接口开发有一些基本的概念,对于软件开发以及相关软件和应用的使用都很有帮助。上面的讨论涉及到了一些基本概念,比如 HTTP 协议、RESTful API,接口开发也涉及到一些基本的工具,这个博客是对这些基本概念的一些总结。

HTTP

HTTP 请求报文

HTTP(超文本传输协议)是互联网上应用最为广泛的一种网络协议,它可以定义客户端和服务器之间通信的规则。我们在浏览器里访问一个网页,实际上发生的事情是浏览器构造一个 HTTP GET 请求报文并发送给服务器,再将服务器返回的内容在浏览器中渲染呈现给我们。HTTP的请求报文是由请求方法、请求URI、协议版本、可选的请求首部字段和内容实体构成的。

比如下面这个例子展示了一个 HTTP POST 请求的结构,请求方法为 POST ,用于向服务器提交数据;URI 表示请求的资源路径,这里表示客户端请求的是 hackr.jp 网站上的 /form/entry 资源路径;协议版本部分表示客户端与服务器之间使用的 HTTP 协议版本为1.1;请求头部分包含了一些描述请求的原信息,告诉服务器如何处理请求;请求体部分包含了实际发送的数据。请求头部分的 Content-Type 描述了请求体数据的格式,在这里, application/x-www-form-urlencoded 表示 HTML 表单数据。 name=ueno&age=37 表示客户端提交了两个字段,name 字段的值是 ueno,age 字段的值是 37。在实际应用中最常使用的内容实体格式是 application/json ,比如在上面 OpenAI 的大模型接口中也使用了 JSON 作为数据传输格式。

image-20241217103419255

接收到请求的服务器,会将请求内容的处理结果以响应的形式返回。开头是服务器的HTTP版本,接下来的200 OK表示请求的处理结果的状态码(status code)和原因短语(reason-phrase),下一行显示了创建响应的日期时间,是首部字段(header field)内的一个属性。接着以一空行分隔,之后的内容称为资源实体的主体。

image-20241217103526484

发送 HTTP 请求

我们已经知道 HTTP 请求报文和响应的基本格式了,那么如何发送请求并返回结果呢?有很多常用的封装好的工具可以完成这一需求。curl 是一个命令行工具,假如我们要向某个 URL 发送 GET 请求,直接 curl 这个 URL 就可以了:

1
curl https://api.example.com/data

如果要发送 POST 请求并附带数据,可以这样做:

1
2
3
curl -X POST https://api.example.com/submit \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "age": 30}'

其中 -X POST 指定请求方法为 POST,-H "Content-Type: application/json" 设置请求头,表明请求体的格式为 JSON。-d 指定请求体,这里是一个 JSON 字符串。

用各类语言库也可以发送 HTTP 请求,比如使用 Python requests 库发送一个请求:

1
2
3
4
5
6
import requests

url = 'http://www.baidu.com'
proxies = {'http': None, 'https': None}

data = requests.get(url, proxies=proxies)

接口开发

接口设计的不同选择

我们知道如何请求一个接口了,这时我们想设计定义自己的接口。比如对于一个材料数据库的增删查改操作,我们可能开发以下四个接口。用户向每个接口 POST 对应的请求,即可要求后台执行相应的操作。

1
2
3
4
- /create/material:用于创建新材料。
- /delete/material:用于删除现有材料。
- /check/material:用于查询材料的信息。
- /update/material:用于更新材料信息。

比如我们想创建某个材料,可以设计如下的请求体和返回格式:

1
2
3
4
5
{
"name": "铜",
"type": "金属",
"quantity": 300
}
1
2
3
4
5
6
7
8
9
{
"message": "Material '铜' created successfully.",
"material": {
"id": 3,
"name": "铜",
"type": "金属",
"quantity": 300
}
}

接口有很多种设计方式,上面是基于动作的接口设计,每个接口都表示一种操作。假如要创建其他资源,比如创建一个模型,基于这种接口设计思路可能将相应的 URL 设计为 /create/model。 我们也可以设计一些基于资源的接口,例如:

1
2
3
4
- /material/create:用于创建新材料
- /material/delete:用于删除材料
- /material/check:用于查询材料
- /material/update:用于更新材料

这种设计风格去除了动作动词(如 create, delete 等),将资源放在路径前面,将操作通过路径进行指定。

我们也可以设计基于查询参数的接口,比如:

  • /material:通过 GET 获取所有材料,POST 创建新材料,DELETE 删除材料。
  • 使用查询参数来指定操作类型:
    • /material?action=create:创建材料
    • /material?action=delete:删除材料
    • /material?action=update:更新材料

所以从上面的讨论可以看到,接口设计和开发可以有不同的选择。但假如整个项目没有统一的标准,比如材料数据库用基于动作的接口设计,物理数据库用基于参数的接口设计,整个项目就凌乱不清晰了。因此,我们迫切需要一些标准的接口设计规范。

###RESTful API

RESTful (Representational State Transfer,表现层状态转化,https://www.ruanyifeng.com/blog/2011/09/restful.html) API是一套流行的、标准的 API 设计规范。RESTful API 强调的是“资源”而非“操作”,并且通过标准化的 HTTP 方法(如 GET、POST、PUT、DELETE)来描述对资源的操作。在 RESTful API 中,每个接口路径都表示一种资源,每种 HTTP 方法表示对该资源的不同操作。所谓的“资源”,就是一个“实体”,比如一段文本、一张图片、或一种服务。在 RESTful 设计中,接口路径应该简洁且表达资源的含义,比如对于我们的材料数据库,

  • /materials:表示“所有材料”这一资源。
  • /materials/{id}:表示特定 ID 的材料资源。

RESTful API 通过使用 GETPOSTPUTDELETE 来分别对应“查询”(获取数据)、“创建”(插入数据)、“更新”(修改数据)和“删除”(删除数据)等操作。RESTful API 的设计遵循标准的 HTTP 方法和清晰的资源路径,使得 API 更加简洁、可读,并且符合资源导向的设计原则。比如对于我们的材料数据库的增删查改,基于 RESTful API 可以设计如下的接口:

查询所有材料(GET)

1
GET /materials

查询特定材料(GET)

1
GET /materials/1

创建新材料(POST)

1
2
3
4
5
6
POST /materials
{
"name": "铜",
"type": "金属",
"quantity": 300
}

更新材料(PUT)

1
2
3
4
PUT /materials/1
{
"quantity": 350
}

删除材料(DELETE)

1
DELETE /materials/1

RESTful API 的核心就是,每个 URL 都代表一个明确的资源,例如 /materials/materials/{id}。这些资源路径表达了数据的含义,而不需要依赖于动词来描述行为。

Flask 接口开发框架

接口定义了前后端交互的标准和规则,但实际的开发和实现则需要借助开发框架来处理请求、管理数据和生成响应。Flask 是一个流行的、轻量级的 Python Web 框架,它简单易用、灵活性强、且具有高度可扩展的特点。Flask 使用 路由(routes)来将 URL 与特定的处理函数关联,允许我们根据 HTTP 请求的类型(如 GET、POST、PUT、DELETE)编写相应的逻辑。此外,Flask 不强制使用复杂的结构,开发者可以根据需求自由选择各种扩展模块,如数据库连接、身份验证等。下面是一个最简单的 Flask 案例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# app.py
from flask import Flask

# 创建一个 Flask 应用实例
app = Flask(__name__)

# 定义路由和视图函数
@app.route('/')
def hello_world():
return 'Hello, World!'

# 启动应用,默认在 http://127.0.0.1:5000/ 上运行
if __name__ == '__main__':
app.run(debug=True)
  • Flask(__name__) 这一行代码创建了一个 Flask 应用实例。Flask 类是 Flask 框架的核心,__name__ 传递给 Flask 类是当前 Python 脚本的名称。Flask 会使用这个名称来确定应用的配置、静态文件位置等。
  • @app.route('/') 是 Flask 中的一个装饰器,用于定义 URL 路径与处理函数的映射。在这个例子中,'/' 是根路径,当用户访问 http://127.0.0.1:5000/ 时,Flask 会调用 hello_world() 函数。
  • app.run(debug=True):这行代码启动 Flask 应用,debug=True 表示启用调试模式。在调试模式下,当修改代码后,Flask 会自动重新加载应用,并显示详细的错误信息,方便开发。

Flask 非常简洁,一个“Hello, World”应用只需要非常少量的代码。此外 Flask 提供了灵活的扩展机制,开发者可以根据需要添加数据库支持、模板渲染、身份验证等功能。对于上面材料的增删查改的RESTful API,用 Flask 框架也可以很容易地开发:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
from flask import Flask, request, jsonify

app = Flask(__name__)

# 模拟一个简单的数据库(字典)
materials_db = {
1: {"name": "钢铁", "type": "金属", "quantity": 500},
2: {"name": "塑料", "type": "聚合物", "quantity": 1000},
}

# 获取所有材料信息(GET)
@app.route('/materials', methods=['GET'])
def get_all_materials():
return jsonify(materials_db)

# 获取单个材料信息(GET)
@app.route('/materials/<int:id>', methods=['GET'])
def get_material(id):
material = materials_db.get(id)
if material:
return jsonify(material)
else:
return jsonify({"message": "Material not found!"})

# 创建新材料(POST)
@app.route('/materials', methods=['POST'])
def create_material():
new_material = request.get_json()
new_id = max(materials_db.keys()) + 1 # 自动生成一个新ID
materials_db[new_id] = new_material
return jsonify({"message": "Material created successfully!", "material": new_material})

# 更新材料信息(PUT)
@app.route('/materials/<int:id>', methods=['PUT'])
def update_material(id):
material = materials_db.get(id)
if material:
updated_data = request.get_json()
material.update(updated_data)
return jsonify({"message": "Material updated successfully!", "material": material})
else:
return jsonify({"message": "Material not found!"})

# 删除材料(DELETE)
@app.route('/materials/<int:id>', methods=['DELETE'])
def delete_material(id):
if id in materials_db:
del materials_db[id]
return jsonify({"message": "Material deleted successfully!"})
else:
return jsonify({"message": "Material not found!"})

if __name__ == '__main__':
app.run(debug=True)

在这里,路由的定义更加丰富了一点。'/materials/<int:id>' 是一个包含动态部分的 URL 路径。具体来说:

  • /materials:这是固定的部分,表示访问的是 materials 资源。
  • <int:id>:这部分是动态的,<int:id> 表示路径中可以包含一个名为 id 的参数,并且这个 id 必须是一个整数。Flask 会在请求到来时,将 URL 中的值提取出来,并将它传递给视图函数。

例如,访问路径 /materials/3 时,Flask 会将 id 的值提取为 3,并将其传递给视图函数。同时,通过指定 methods=['DELETE'], Flask 这个路由只响应 DELETE 请求,也就是说,只有客户端发送 DELETE 请求到该 URL 时,才会触发该路由的视图函数。我们可以为同一个 URL 的不同请求开发不同的视图函数。

在启动了服务之后,就可以通过 curl http://127.0.0.1:5000/materials 等方式测试一下接口了。

###标准返回和异常处理

上面基本完成了整个接口的基本开发,但是还存在两个问题:1. 接口的返回没有明确的规范;2. 没有错误和异常处理。针对第一个问题,我们可以定义一个标准的返回格式,比如要求所有的接口都以两级形式返回:

  • code: 错误代码,比如 0 表示成功。
  • message: 提示信息,描述请求的处理结果。比如:“材料信息获取成功”。
  • result: 包含操作结果的数据,具体的数据在 result 内部嵌套。比如具体材料的JSON信息。

对于错误和异常处理,我们可以针对每一种错误编制一个错误码,方便前端明确错误类型:

1
2
3
4
5
6
7
8
# 错误码定义
ERROR_CODES = {
2001: "请求参数缺失",
2002: "请求参数格式错误",
3001: "记录不存在",
3002: "记录已存在",
5001: "系统内部错误",
}

我们可以定义一个统一的 API 异常基类:

1
2
3
4
5
6
7
8
# 通用 API 异常基类
class APIException(Exception):
def __init__(self, code, message=None):
self.code = code
self.message = message or ERROR_CODES.get(code, "未知错误")

def to_dict(self):
return {"code": self.code, "message": self.message, "result": None}

APIException 继承了 Exception 类,表示一种自定义的异常。之后,就可以把每个视图函数包在一个 try except 里,根据不同的错误抛出 APIException,最后统一由 except 处理。比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 获取单个材料信息(GET)
@app.route('/materials/<int:id>', methods=['GET'])
def get_material(id):
try:
material = materials_db.get(id)
if not material:
raise APIException(3001)

return jsonify({
'code': 0,
'message': '材料信息获取成功',
'result': {
"id": id,
"name": material["name"],
"type": material["type"],
"quantity": material["quantity"]
}
})

except APIException as api_error:
return jsonify(api_error.to_dict())
except Exception as e:
error = APIException(5001, f"系统内部错误: {str(e)}")
return jsonify(error.to_dict())

接口文档

在上面,接口的基本功能已经有了,但为了前后端顺利交互,还需要制订一份详细的接口文档。实际上,正常情况下应该先制定接口文档后,再进行实际开发,保证前后端可以同时推进项目。接口文档应该描述API 的基本信息、接口的参数、返回格式等,我们可以用markdown或者word进行接口文档的编写。比如对于GET /materials/<int:id> 这个接口,可以编写如下的接口文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# API 文档

## 获取单个材料信息

**接口路径**: `/materials/<int:id>`

**请求方式**: `GET`

### 请求参数

| 参数 | 类型 | 是否必需 | 说明 |
|-------|--------|----------|------------|
| `id` | `int` | 是 | 材料的唯一 ID |

### 请求示例

```bash
GET /materials/1
```

### 返回示例
#### 成功

```json
{
"code": 0,
"message": "材料信息获取成功",
"result": {
"id": 1,
"name": "钢铁",
"type": "金属",
"quantity": 500
}
}
```
#### 失败

```json
{
"code": 3001,
"message": "记录不存在",
"result": null
}
```
### 错误代码

| 错误码 | 错误信息 |
|-------|------------|
| 3001 | 记录不存在 |
| 5001 | 系统错误 |

就像我们前面提到的接口可以有很多种设计一样,接口文档也没有什么统一的标准和要求。为了处理由于没有标准而带来的不规范,OpenAPI 定义了一种开放的标准,用于描述和规范化 RESTful API。它使用 JSON 或 YAML 格式来描述 API 的各个方面,包括路径、请求方法、请求参数、返回类型等。OpenAPI 旨在为 API 提供标准化、自动化、可互操作的描述,使得开发者能够轻松地编写、理解、测试和维护 API。

Swagger 是一套基于 OpenAPI 标准,用于描述、生成、测试和文档化 RESTful API 的工具集。Swagger 提供了一整套工具和服务,帮助开发者更高效地定义、测试和维护 API。Swagger 工具集包括多个组件,常见的包括:

  • Swagger Editor 是一个开源的基于浏览器的工具,用于编辑和生成 OpenAPI 规范文档。你可以在 Swagger Editor 中用 YAML 或 JSON 格式编写 OpenAPI 文档,并通过图形化界面实时查看生成的 API 文档。
  • Swagger UI 提供了一个交互式的界面,可以展示 API 文档并允许开发者直接在浏览器中测试 API。
  • Swagger Codegen 是一个工具,它可以根据 OpenAPI 定义的文档自动生成客户端代码、服务器端代码以及 API 文档。支持多种编程语言和框架...

在 Flask 中集成 Swagger 文档生成,通常使用 Flasgger 这个扩展,直接使用 pip 即可安装。Flasgger 让 Flask 应用可以使用 Swagger 注解来自动生成和展示 API 文档。下面是一个简单的示例,展示了如何为 Flask 路由生成 Swagger 文档。需要注意的是这里的 docstring 要按照 YAML 格式来编写,因此其对缩进敏感。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
from flask import Flask, request, jsonify
from flasgger import Swagger

app = Flask(__name__)
swagger = Swagger(app)

# 获取单个材料信息
@app.route('/materials/<int:id>', methods=['GET'])
def get_material(id):
"""
获取单个材料信息
---
parameters:
- name: id
in: path
type: integer
required: true
description: 材料的唯一 ID
responses:
200:
description: 材料信息获取成功或错误信息
schema:
type: object
properties:
code:
type: integer
enum: [0, 3001, 5001]
description: |
**请求错误**
- `3001` 记录不存在

**系统错误**
- `5001` 服务器内部错误
example: 0
message:
type: string
description: 返回消息,成功时返回操作结果,失败时为错误信息
example: "材料信息获取成功"
result:
type: object
description: 返回的材料数据或错误信息
example: {"id": 1, "name": "钢铁", "type": "金属", "quantity": 500}
"""
try:
material = materials_db.get(id)
if not material:
raise APIException(3001)

return jsonify({
'code': 0,
'message': '材料信息获取成功',
'result': {
"id": id,
"name": material["name"],
"type": material["type"],
"quantity": material["quantity"]
}
})

except APIException as api_error:
return jsonify(api_error.to_dict())
except Exception as e:
error = APIException(5001, f"系统内部错误: {str(e)}")
return jsonify(error.to_dict())

运行脚本后,Flasgger 会根据上述注释自动生成 Swagger UI 文档,可以在浏览器中访问 http://127.0.0.1:5000/apidocs/ 来查看生成的文档,并在该界面进行交互式测试。

Postman 接口测试

接口开发好之后,接下来需要进行接口测试,确保它能够按预期工作,处理请求并返回正确或合理的结果。事实上,正常情况下我们应当先准备好测试用例,再开始接口的开发工作,这样可以边测试边开发,中确保每个功能都符合设计要求,及早发现问题。Postman 是一个广泛使用的接口测试和开发工具,它能够帮助快速验证接口的功能。Postman 不仅支持基本的 HTTP 请求,还具备自动化测试的功能,能够进行接口的集成、功能和性能测试。打开 Postman 后,可以选择 New Request 来创建一个新的请求。选择 HTTP 请求类型(如 GET、POST、PUT、DELETE),输入接口的 URL 地址。可以在请求体中添加数据,设置请求头和参数。输入请求信息后,点击 Send 按钮发送请求,Postman 会返回接口的响应,包括状态码、响应时间、响应体等信息。可以查看响应的内容,并根据返回的数据判断接口是否符合预期。Postman 还允许在每个请求中编写测试脚本,以确保接口的返回结果符合预期。比如,可以检查 HTTP 状态码、响应时间、响应数据格式等。以下是一个简单的测试脚本示例:

1
2
3
4
5
6
7
8
9
10
11
12
pm.test("状态码是 200", function () {
pm.response.to.have.status(200);
});

pm.test("响应时间小于 200ms", function () {
pm.response.to.have.responseTime.below(200);
});

pm.test("返回的 JSON 数据包含 'id' 字段", function () {
var jsonData = pm.response.json();
pm.expect(jsonData).to.have.property("id");
});