在hyperf中 设置响应头让后端直接给前端返回 event-stream 流的需求,发现hyperf 中在重写 SwooleConnection 时,设置的属性 $response 为 protected ,不能直接调用此属性。
为了让我能设置此响应头,我 继承了 Hyperf\HttpServer\Server 类 并重写了 onRequest 方法,
具体的做法如下:
首先创建StreamServer 类, 并重写了onRequest 方法,如果只是想某几个路由 重写,需要自己写一下配置
<?php
namespace App\Service\Server;
use Hyperf\Dispatcher\HttpDispatcher;
use Hyperf\ExceptionHandler\ExceptionHandlerDispatcher;
use Hyperf\HttpServer\ResponseEmitter;
use Hyperf\HttpServer\Server;
use Psr\Container\ContainerInterface;
use Swoole\Http\Request;
use Swoole\Http\Response;
class StreamServer extends Server
{
const STREAM_URL = [
'/chat/chat/test'
];
public function __construct(ContainerInterface $container, HttpDispatcher $dispatcher, ExceptionHandlerDispatcher $exceptionHandlerDispatcher, ResponseEmitter $responseEmitter)
{
parent::__construct($container, $dispatcher, $exceptionHandlerDispatcher, $responseEmitter);
}
/**
* @param Request $request
* @param Response $response
*/
public function onRequest($request, $response): void
{
$pathInfo = $request->server['path_info'];
if (in_array($pathInfo, self::STREAM_URL)) {
$response->header('Content-Type', 'text/event-stream');
$response->header('Cache-Control', 'no-cache');
$response->header('Connection', 'keep-alive');
}
parent::onRequest($request, $response);
}
}
然后再将 config/autoload/server.php 中的 http 配置 修改,如下图
这样,我们在控制器里面就可以实现 response 的header 更改为 text/event-stream。
<?php
use Hyperf\HttpServer\Contract\ResponseInterface;
$wrResponse = ApplicationContext::getContainer()->get(ResponseInterface::class);
//此处可以将返回的数据 直接打印字屏幕上
$wrResponse->write("data: " . $res . "\n\n");
也就实现了 chatgpt 的打印流的关键步骤,下面贴出部分代码,使用guzzlehttp 处理chatgpt 返回的流式数据
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Utils;
use Hyperf\Guzzle\ClientFactory;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Hyperf\Utils\ApplicationContext;
use Hyperf\Context\Context;
class HttpRequestService
{
private $clientFactory;
//此处可以模拟类属性,获取协程中的属性
public function __get($key)
{
$className = static::class;
$key = env('APP_NAME').$className . $key;
return Context::get($key);
}
//此处可以模拟类属性,将属性存在协程中
public function __set($key, $value)
{
$className = static::class;
$key = env('APP_NAME').$className . $key;
Context::set($key, $value);
}
public function __construct()
{
$this->clientFactory = make(ClientFactory::class);
}
/**
* 是否使用流式获取
* @param bool $stream
* @return $this
*/
private function setStream($stream = false)
{
$this->stream = $stream;
return $this;
}
/**
* 初始化
* @param string $domainUrl
* @param array $header
* @param int $timeout
*/
private function init($domainUrl = '', $header = [], $timeout = 120)
{
$config = ['timeout' => $timeout];
if (!empty($domainUrl)) {
$config['base_uri'] = $domainUrl;
}
if ($this->stream) {
$config['stream'] = true;
}
if (!empty($header) && is_array($header)) {
$config = array_merge($config, ['headers' => $header]);
}
/** @var Client $client */
$this->client = $this->clientFactory->create($config);
}
public function postStream($domain, $uri, $params, $header = [])
{
$this->setStream(true);
$this->init($domain, $header);
$type = 'json';
$requestParmas = [$type => $params];
$response = $this->client->post($uri, $requestParmas);
$responseData = $response->getBody(); //获取流数据
$parseData = [];
$wrResponse = ApplicationContext::getContainer()->get(ResponseInterface::class);
while (!$responseData->eof()) {
$res = trim(Utils::readLine($responseData)); //一行一行读取
$res = str_replace("data: ", "", $res);
if (empty($res)) {
continue;
}
$wrResponse->write("data: " . $res . "\n\n");
if ($res == '[DONE]') {
break;
}
//休眠时间,用于前端展示
usleep(100000);
$res = json_decode($res, true);
if (!empty($res)) {
$parseData[] = $res['choices'][0]['delta']['content'] ?? ($res['choices'][0]['delta']['role'] ?? '');
}
}
return $parseData;
}
}