跳到主要内容

91 篇博文 含有标签「后端」

后端开发技术文章

查看所有标签

解决SSE中的粘包问题

· 阅读需 2 分钟
素明诚
Full stack development

解决问题的关键在于找到关键问题

  • 确保服务器正确地使用 SSE 协议格式,包括在事件之间使用正确的分隔符。
  • 优化服务器发送事件的逻辑,避免因为过快的发送速度而导致的事件合并。
  • 客户端应当实现健壮的数据处理逻辑,能够处理合并的事件数据,并正确地分割和解析事件。

分隔并延迟

from flask import Flask, Response, stream_with_context
import time
import json

app = Flask(__name__)

def generate_sse_events():
event_id = 1
while True:
# 模拟数据生成
data = {"time": time.time(), "msg": f"Event {event_id}"}
sse_data = f"id: {event_id}\n" \
f"event: message\n" \
f"data: {json.dumps(data)}\n\n"
yield sse_data
event_id += 1
time.sleep(1) # 引入延时以模拟数据处理时间和避免粘包

@app.route('/events')
def sse_request():
return Response(stream_with_context(generate_sse_events()),
content_type='text/event-stream')

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

前端优化

这是我在业务中封装的代码,有需要可以试一下

import { currentEnvironment } from "@/api/axios";

function buildApiUrl(): string {
return `${ currentEnvironment() === '/vnet' ? '/vnet/' : '' }api/chat-process`;
}

async function processStreamedData(reader: ReadableStreamDefaultReader<Uint8Array>, onDataReceived: (data: any) => void) {
let accumulatedText = '';

while (true) {
const { done, value } = await reader.read();
if (done) break;
accumulatedText += value;

while (true) {
const newlineIndex = accumulatedText.indexOf('\n\n');
if (newlineIndex === -1) break;

let dataBlock = accumulatedText.substring(0, newlineIndex).trim();
accumulatedText = accumulatedText.substring(newlineIndex + 2);

if (dataBlock.startsWith('data: ')) {
dataBlock = dataBlock.substring(6);
try {
const json = JSON.parse(dataBlock);
onDataReceived(json);
} catch (error) {
console.error('分析JSON时出错:', error, '来自区块:', dataBlock);
}
}
}
}
}

export async function getGeneratedData(data: any, onDataReceived: (data: any) => void) {
const response = await fetch(buildApiUrl(), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
});

if (!response.body) {
throw new Error("响应体中没有可读流数据。");
}

const reader: any = response.body.pipeThrough(new TextDecoderStream()).getReader();

await processStreamedData(reader, onDataReceived);
}

使用 Fetch API 和 ReadableStream API 来处理流式响应数据

· 阅读需 3 分钟
素明诚
Full stack development

使用 Fetch API 和 ReadableStream API 来处理流式响应数据

在开发时,我们常常需要处理大量的数据,这些数据可能是文件,也可能是网络请求的响应。在这种情况下,使用流(stream)可以有效地处理这些数据,避免一次性加载所有数据,从而减少内存的使用并提高应用的性能。

在这篇文章中,我将介绍如何使用 Fetch API 和 ReadableStream API 来处理流式响应数据。我将会提供具体的代码示例来展示这些概念。

Fetch API 和 ReadableStream

Fetch API 是浏览器提供的一个现代的、强大的 HTTP 请求工具。与旧的 XMLHttpRequest API 相比,Fetch API 提供了更简洁的 API 和更强大的功能,包括流式响应。

当你使用 Fetch API 发出请求时,返回的 Response 对象包含一个 body 属性,这个属性是一个 ReadableStreamReadableStream 表示一个可读的数据流,你可以使用它的 getReader() 方法来获取一个 reader,然后使用这个 reader 的 read() 方法来读取数据。

// 发出请求
fetch('https://example.com/data')
.then(response => {
// 获取 reader
const reader = response.body.getReader();

// 读取数据
return reader.read().then(function process({ done, value }) {
if (done) {
console.log('Stream finished');
return;
}

console.log('Received data chunk', value);

// 读取下一段数据
return reader.read().then(process);
});
})
.catch(console.error);

在这个示例中,reader.read() 返回一个 Promise,这个 Promise 的 resolve 值是一个对象,包含两个属性:valuedonevalue 是读取到的数据块,done 是一个布尔值,如果为 true 则表示数据已经读取完毕。

这样,你就可以逐块地处理数据,而不需要一次性加载所有数据。个人觉得有点像迭代器。

处理文本数据

上述示例展示了如何逐块地读取数据,但这些数据是二进制的,如果你想处理文本数据,需要对其进行解码。

以下是一个示例

// 创建一个新的 TextDecoder 实例
const decoder = new TextDecoder('utf-8');

// 发出请求
fetch('https://example.com/text')
.then(response => {
const reader = response.body.getReader();

return reader.read().then(function process({ done, value }) {
if (done) {
console.log('Stream finished');
return;
}

// 解码数据
const text = decoder.decode(value);

console.log('Received text chunk', text);

return reader.read().then(process);
});
})
.catch(console.error);


在这个示例中,我使用 TextDecoder 对象来解码数据。TextDecoder 是一个可以将二进制数据解码为字符串的工具,它的 decode() 方法可以接受一个 ArrayBuffer 或者 TypedArray 并返回一个字符串。

处理 JSON 数据

有时,你可能需要处理的是 JSON 格式的数据。在这种情况下,你需要首先将所有数据读取完成,然后将其解码为字符串,最后解析为 JavaScript 对象。

fetch('https://example.com/data.json')
.then(response => {
const reader = response.body.getReader();
const chunks = [];

return reader.read().then(function process({ done, value }) {
if (done) {
// 将所有数据块连接起来,解码为字符串,然后解析为 JavaScript 对象
const text = decoder.decode(new Uint8Array(chunks.flat()));
const data = JSON.parse(text);

console.log('Received JSON data', data);

return;
}

// 存储数据块
chunks.push(value);

return reader.read().then(process);
});
})
.catch(console.error);

在这个示例中,我使用一个数组来存储所有数据块,然后在数据读取完成后,将所有数据块连接起来,解码为字符串,最后解析为 JavaScript 对象。

Go type 关键字的使用

· 阅读需 2 分钟
素明诚
Full stack development

定义结构体(Struct)

使用场景:当你需要定义一个复合数据类型来组织和存储不同类型的数据时,结构体非常有用。它们通常用于模拟现实世界中的对象和实体

type Employee struct {
ID int
FirstName string
LastName string
Address string
}

定义接口(Interface)

使用场景:接口在为不同的类型提供统一的方法集合方面非常有用。它们广泛用于实现多态性和封装概念,使得你可以编写更灵活和可扩展的代码

type Reader interface {
Read(p []byte) (n int, err error)
}

定义类型别名(Type Alias)

使用场景:类型别名主要用于代码重构,特别是在将一个大型项目从一个 API 迁移到另一个兼容的 API 时,它可以确保向后兼容

type OldLogger = NewLogger

定义自定义类型(Custom Type)

使用场景:自定义类型适用于为特定的数据或概念创建专有的类型,这样可以使代码更加清晰易懂,并且利于类型安全

type UserID int

定义函数类型(Function Type)

使用场景:当你需要将函数作为参数传递给其他函数,或者需要从函数返回函数时,定义函数类型非常有用。这有助于创建高度模块化和可重用的代码库

type HandlerFunc func(w http.ResponseWriter, r *http.Request)

Go 带 和 不带 的结构体的区别

· 阅读需 3 分钟
素明诚
Full stack development

不带*(直接使用结构体实例)

当你直接使用结构体实例时,每次你将结构体赋值给另一个变量或将其作为参数传递给函数时,Go 都会创建该结构体的一个副本。

type Person struct {
Name string
Age int
}

func incrementAge(p Person) {
p.Age += 1 // 只修改了副本的Age
}

func main() {
person := Person{Name: "John", Age: 30}
incrementAge(person)
fmt.Println(person.Age) // 输出: 30,因为incrementAge中修改的是person的副本
}

在这个例子中,即使incrementAge函数尝试修改PersonAge,主函数中的person变量也不会受到影响,因为它操作的是person的一个副本。

带*(使用指向结构体的指针)

当你使用指向结构体的指针时,你可以直接修改原始结构体实例中的数据,因为指针存储的是结构体数据的内存地址。

func incrementAge(p *Person) {
p.Age += 1 // 直接修改原始结构体实例的Age
}

func main() {
person := Person{Name: "John", Age: 30}
incrementAge(&person)
fmt.Println(person.Age) // 输出: 31,因为incrementAge中修改的是原始person实例
}

在这个例子中,incrementAge函数接收一个指向Person的指针,这意味着它可以直接修改传递给它的实例。当我们传递&personperson的地址)给incrementAge,函数中的任何修改都会反映在main函数中的person上。

那修改的是哪个实例?

修改的是在函数incrementAge调用时传入的person实例的副本。这个副本只在incrementAge函数的作用域内存在,一旦incrementAge函数执行完毕,这个副本就会被丢弃(如果没有其他引用指向它,它会被垃圾回收器回收)。因此,incrementAge函数内部对p.Age的修改不会影响到main函数中的person实例。

为什么设计成这样?

  1. Go 语言在函数调用时通过值传递的方式避免对原始数据的不必要修改,从而保持数据的安全性和一致性。同时,这种方式也避免了在函数调用时不断修改原始数据可能导致的错误和混乱。
  2. 通过值传递,函数接收的是数据副本,这确保了函数的纯粹性(尤其是对于简单函数而言),函数的行为不会因为外部变量的改变而改变,使得函数更易于理解和预测。
  3. 比如对于大型结构体,复制整个结构体可能会导致额外的内存和性能开销。因此,在处理大型数据或需要直接修改原始数据时,使用指针传递会更高效。所以,遇事不决就使用指针就对了。

常用 go 命令

· 阅读需 1 分钟
素明诚
Full stack development
命令描述
go build编译 Go 程序。
go run编译并运行 Go 程序。
go test运行测试。
go get下载并安装包及其依赖。
go get -u更新包和依赖到最新版本。
go mod init初始化新的模块,创建 go.mod 文件。
go mod tidy添加缺失的模块,移除无用的模块。
go mod download下载 go.mod 文件中指明的所有依赖。
go mod verify验证依赖是否已下载且未修改。
go mod vendor将依赖复制到项目下的 vendor 目录中。
go list列出选定或全部的包。
go fmt格式化 Go 源代码。
go env打印 Go 的环境信息。
go clean移除对象文件和缓存文件。

get -u 注意

    • 如果直接运行 go get -u 而不指定具体的包,它将更新当前模块(项目)所有依赖的包到最新版本。这包括直接依赖和间接依赖。
    • 如果指定了具体的包,如 go get -u some/package,则只会更新该包及其依赖到最新版本。

Python TypeError AsyncConnectionPool init got an unexpected keyword argument socket options

· 阅读需 1 分钟
素明诚
Full stack development

stable diffusion 启动遇到错误

(venv) PS D:\Desktop\Private\stable-diffusion-webui-windows> .\webui-user.bat
venv "D:\Desktop\Private\stable-diffusion-webui-windows\venv\Scripts\Python.exe"
Python 3.10.6 (tags/v3.10.6:9c7b4bd, Aug 1 2022, 21:53:49) [MSC v.1932 64 bit (AMD64)]
Version: v1.6.0-14-g27096813
Commit hash: 2709681331de6031ee9dc80abe218d336ad4c6ee
Installing requirements
Launching Web UI with arguments: --api --xformers --opt-sdp-no-mem-attention --enable-insecure-extension-access --listen
Traceback (most recent call last):
File "D:\Desktop\Private\stable-diffusion-webui-windows\launch.py", line 48, in <module>
main()
File "D:\Desktop\Private\stable-diffusion-webui-windows\launch.py", line 44, in main
start()
File "D:\Desktop\Private\stable-diffusion-webui-windows\modules\launch_utils.py", line 432, in start
import webui
File "D:\Desktop\Private\stable-diffusion-webui-windows\webui.py", line 13, in <module>
initialize.imports()
File "D:\Desktop\Private\stable-diffusion-webui-windows\modules\initialize.py", line 21, in imports
import gradio # noqa: F401
File "D:\Desktop\Private\stable-diffusion-webui-windows\venv\lib\site-packages\gradio\__init__.py", line 3, in <module>
import gradio.components as components
File "D:\Desktop\Private\stable-diffusion-webui-windows\venv\lib\site-packages\gradio\components\__init__.py", line 1, in <module>
from gradio.components.annotated_image import AnnotatedImage
File "D:\Desktop\Private\stable-diffusion-webui-windows\venv\lib\site-packages\gradio\components\annotated_image.py", line 12, in <module>
from gradio import utils
File "D:\Desktop\Private\stable-diffusion-webui-windows\venv\lib\site-packages\gradio\utils.py", line 353, in <module>
class AsyncRequest:
File "D:\Desktop\Private\stable-diffusion-webui-windows\venv\lib\site-packages\gradio\utils.py", line 372, in AsyncRequest
client = httpx.AsyncClient()
File "D:\Desktop\Private\stable-diffusion-webui-windows\venv\lib\site-packages\httpx\_client.py", line 1397, in __init__
self._transport = self._init_transport(
File "D:\Desktop\Private\stable-diffusion-webui-windows\venv\lib\site-packages\httpx\_client.py", line 1445, in _init_transport
return AsyncHTTPTransport(
File "D:\Desktop\Private\stable-diffusion-webui-windows\venv\lib\site-packages\httpx\_transports\default.py", line 275, in __init__
self._pool = httpcore.AsyncConnectionPool(
TypeError: AsyncConnectionPool.__init__() got an unexpected keyword argument 'socket_options'

TypeError: AsyncConnectionPool.__init__() got an unexpected keyword argument 'socket_options' 错误,从多个讨论和解决方案中,一个常见的建议是将 httpx 库降级到 0.24.1 或更早的版本。这个错误通常是由于 httpx 新版本(大于 0.24.1)中引入了一个不兼容的变更所导致的。根据 GitHub 上的用户反馈和讨论,降级 httpx 可以解决这个问题

https://github.com/AUTOMATIC1111/stable-diffusion-webui/issues/13840### 降级版本

pip install httpx==0.24.1

重新启动

Python 没 requirementstxt 如何安装包

· 阅读需 1 分钟
素明诚
Full stack development

先安装 pipreqs 生成项目需要的 requirements.txt 文件

pip install pipreqs

生成文件

 pipreqs .

安装

pip install -r requirements.txt

注意这里,如果已经存在了 requirements.txt 你想覆盖的话直接 -f

pipreqs . --force

Python 虚拟环境使用

· 阅读需 2 分钟
素明诚
Full stack development

创建虚拟环境

打开命令行工具

  • Windows 打开命令提示符或 PowerShell。
  • Unix/macOS 打开终端。

创建虚拟环境

在项目目录内,运行以下命令创建虚拟环境

python -m venv venv

这里 venv 是虚拟环境目录的名字,可以按需更改。该命令会在当前目录下创建一个名为 venv 的文件夹,其中包含 Python 解释器和 pip。

激活虚拟环境

Windows

.\venv\Scripts\activate

windows bash

source venv/Scripts/activate

Unix/macOS

source venv/bin/activate

安装依赖

安装项目所需的依赖, 记得这里要进入虚拟环境进行依赖按照,否则还是安装在全局

pip install <package_name>

使用虚拟环境

虚拟环境激活期间,任何 Python 和 pip 命令都将使用虚拟环境中的版本。这意味着你可以运行应用程序、脚本测试或添加更多依赖,就像在全局环境中操作一样。

注意事项

  • 保持虚拟环境目录与项目代码一致通常,将虚拟环境目录放在项目根目录下,但不要将其加入到版本控制系统(如 git)中。可通过 .gitignore 文件排除虚拟环境目录。
  • 依赖管理为了确保其他开发者或部署环境能够准确复现你的环境,经常使用 pip freeze > requirements.txt 命令将当前虚拟环境中安装的所有包及其版本导出到 requirements.txt 文件中。其他人可以通过 pip install -r requirements.txt 命令在他们的虚拟环境中安装相同的依赖。
  • 退出虚拟环境当完成工作,需要退出虚拟环境时,使用 deactivate 命令。这将恢复到系统的全局 Python 环境。

ORMObject-Relational Mapping对象关系映射

· 阅读需 2 分钟
素明诚
Full stack development

ORM,简单地说,就像是数据库和你的代码之间的翻译器。它让你能用你熟悉的 JavaScript 对象来处理数据,而不用去写那些有时候让人头疼的 SQL 语句。想象一下,你有个用户表,通常你得写 SQL 去添加、查找、更新或删除用户,对吧?但是有了 ORM,这一切就像是在处理一个普通的 JavaScript 对象。

比如说,你用 Sequelize(这是 Node.js 里一个很流行的 ORM 工具),你可以定义一个“User”模型,这个模型就代表了数据库里的用户表。然后,当你要创建一个新用户时,你只需要像操作 JavaScript 对象一样去做,Sequelize 会帮你把这些操作转换成相应的 SQL 语句。

这样做的好处是显而易见的:首先,你不需要花时间去写和调试 SQL 语句,这让你能更专注于编写业务逻辑;其次,因为你是在用 JavaScript 对象,所以代码的可读性也变得更好;最后,如果将来需要换数据库,你的代码改动会少很多,因为 ORM 会帮你处理数据库之间的差异。

用户表模型

const User = sequelize.define('User', {
username: DataTypes.STRING,
birthday: DataTypes.DATE
});

添加一条新纪录

User.create({
username: 'zhangsan',
birthday: new Date(1980, 6, 20)
});

不过,也有一些缺点。比如,对于一些复杂的查询,ORM 可能不够灵活,有时候生成的 SQL 语句也不是最优的。但总的来说,ORM 是个很有用的工具,尤其是在项目规模较大时,它能大大提升开发效率。

Koa 的 ctx 是什么如何使用

· 阅读需 2 分钟
素明诚
Full stack development

ctx 在 Koa 中是上下文(Context)的缩写,它是一个封装了 Node.js 的 requestresponse 对象的对象,用于在中间件之间传递信息和状态。下面是一个简单的例子来说明这个过程

const Koa = require('koa');
const app = new Koa();

// 第一个中间件
app.use(async (ctx, next) => {
// 设置一个用户信息对象到 ctx.state,这个是中间件间共享的状态对象
ctx.state.user = { name: '张三', age: 30 };
await next(); // 调用下一个中间件
});

// 第二个中间件
app.use(async (ctx, next) => {
// 读取上一个中间件设置的用户信息
const user = ctx.state.user;
// 做一些操作,比如权限检查
if (user.name === '张三') {
await next(); // 用户名匹配,继续下一个中间件
} else {
ctx.status = 403; // 用户名不匹配,返回403 Forbidden
}
});

// 第三个中间件
app.use(async (ctx) => {
// 再次读取用户信息,用于生成响应
const user = ctx.state.user;
// 设置响应体为用户信息
ctx.body = `欢迎 ${user.name},您的年龄是 ${user.age}岁`;
});

app.listen(3000);

第一个中间件在 ctx.state 对象上设置了 user 对象。

第二个中间件检查了 ctx.state.user,并根据用户信息决定是否继续执行下一步。

第三个中间件使用了 ctx.state.user 来构建响应。