ChatGPT 使用 Fetch来实现SSE请求

3,624 阅读3分钟

什么是SSE

SSE(Server-Sent Events)是一Web API,允许服务器将事件数据推送到客户端,以实现服务器向客户端的单向通信。 它建立在HTTP协议之上,并通过HTTP连接的持打开来实现长轮询。 这意味着,一旦连接建立,客户端和服务器之间就可以实时交互,服务器可以随时向客户端提供新数据,不需要客户端首先请求数据。
下面是我用fetch写的一个SSE请求的Demo,服务端用的Nest

服务端代码:

  @Post('completion')
  @Sse('completion')
  async completion(
    @Body() obj: any,
    @Headers() hearder: any,
  ): Promise<Observable<MessageEvent> | string> {
    // 验证会员等级和回答次数
    const token = this.chatService.extractTokenFromHeader(
      hearder.authorization,
    );
    const { id } = await this.jwtService.verifyAsync(token, {
      secret: jwtConstants.secret,
    });
    const user = await this.chatService.getBuyChatTimes(id);
    if (user && user.chat_times < user.buy_chat_times) {
      if (obj.messages.length > 0) {
        obj.messages.forEach((item: any) => {
          delete item.time;
        });
      }
      const response = this.httpService.post(CHAT_URL, obj, {
        responseType: 'stream', // 设置响应类型为流
        headers: {
          'Content-Type': 'application/json',
        },
      });
      const chatStream$ = new Observable<string>((observer) => {
        response.subscribe((response) => {
          response.data.on('data', (chunk: string) => {
            const message = chunk.toString();
            observer.next(message); // 将从第三方接口返回的数据流实时传递给前端
            response.data.on('end', () => {
              observer.complete(); // 在数据流结束时发送 complete 通知
            });
          });
        });
      });
      return chatStream$.pipe(map((data) => ({ data: { data } })));
    }
    const res = new Observable<string>((observer) => {
      observer.next('{"content":"今日查询次数已达上限!"}');
      observer.complete();
    });
    return res.pipe(map((data) => ({ data: { data } })));
  }

客户端代码:

 const obj = {
      "messages": [
        {
          "role": "system",
          "content": "欢迎回来!您想聊些什么?",
          "time": 1690275348057
        },
        {
          "role": "user",
          "content": "写一篇关于父亲的作文",
          "time": 1690337646761
        },
      ],
      "temperature": 1.2,
      "max_tokens": 200
    }
    // const res = await request('/user/info')
    const baseUrl = 'http://localhost:7003/api/chat/completion';
    const eventFetch = new FetchEventSource()
    eventFetch.stopFetchEvent()
    eventFetch.startFetchEvent(baseUrl, obj, res => {
      const regex = /\\"content\\":\\"(.*?)\\"/g;
      let match;
      while ((match = regex.exec(res)) !== null) {
        replyMsg += match[1]
          .replace(/\\n/g, "\n")
          .replace(/(\\u[0-9a-fA-F]{4})/g, function (match, p1) {
            return String.fromCharCode(parseInt(p1.substring(2), 16));
          })
          .replace(/\\/g, "");
      }
      // console.log(replyMsg, 'replyMsg');
    }, () => {
      console.log('end', replyMsg);
    }, error => {
      console.log(error, 'error');
    })
   
class FetchEventSource {
    constructor() {
      this.abortController = new AbortController() || null;
    }
    startFetchEvent(url, body, onMessage, onEnd, onError, headers = {}) {
      const fetchOptions = {
        method: 'POST',
        body: JSON.stringify(body),
        headers: Object.assign({}, {
          'Content-Type': 'application/json',
          Authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXNzd29yZCI6IjEyMzQ1NiIsInBob25lIjoiMTg4MTkzMTA0ODgiLCJpZCI6MTgsImlhdCI6MTY5MDM0MDUwNiwiZXhwIjoxNjkwNDI2OTA2fQ.iol2hbgdGnmbRtsxDHn-NbXH1fgUapZpX4Wg6FDDsw0'
        }),
        signal: this.abortController.signal,
      };
      fetch(url, fetchOptions).then((response) => {
        const reader = response.body.getReader();
        reader.read().then(function processResult(result) {
          if (result.done) {
            onEnd()
            return;
          }
          const decoder = new TextDecoder();
          const receivedString = decoder.decode(result.value, { stream: true });
          oneMssage(receivedString)
          return reader.read().then(processResult);
        });
        return response;
      }).catch(() => {
        this.eventController.abort();
        onError({ code: 201, message: '服务器异常' })
      });
    }
    stopFetchEvent() {
      if (this.eventController) {
        this.eventController.abort();
        this.eventController = null;
      }
    }
  }

在上面的代码中,我封装FetchEventSource类,这个类是一个支持流式请求的工具类,用Fetch + AbortController实现的Server-SentEvents(SSE)的流式请求,用于简化实时数据的推送和更新。对外提供了startFetchEvent和stopFetchEvent两个方法,用于启动和停止一个SSE的流式请求。该类支持自定义请求头和请求体,最终的效果就如下面这样了

image.png 这边用到了AbortController 来取消网络请求,其他参数:

  • url: 请求的URL地址
  • body: 请求体数据对象,将会被转成 JSON 格式
  • onMessage: 服务端有数据返回时的回调函数
    onEnd: 请求结束时的回调函数
  • onError:请求错误时的回调函数
  • headers:请求头信息,可选参数,主要是传token
    接下来发送 POST 请求并监听响应数据的处理过程是:
  1. 使用 fetch 方法发送请求,将参数 fetchOptions 传递给它;
  2. 请求发送成功后,获取响应对象 response
  3. 从应对象 response 中获取 body 的读取器 reader
  4. 通过 reader.read() 方法获取响应的一部分数据,并对的result执行processResult处理。processResult 逐步将获取的数据传递到回调函数 onMessage 中;
  5. 如果读取完毕,执行 onEnd,请求结束。
    总体来说,这个类的作用是发送HTTP POST请求并实时监听服务端返回的内容,这一般是用于实现实时消息推送的功能。