Поддержка SSE и streaming responses
- Proxy: chunked streaming с Flush() для real-time данных - PHP FastCGI: потоковая обработка ответов через streamFastCGIResponse() - Удалена буферизация - данные отправляются сразу"
This commit is contained in:
@@ -369,32 +369,32 @@ func PHPHandler(w http.ResponseWriter, r *http.Request, host string, originalURI
|
|||||||
packet = createFCGIPacket(FCGI_STDIN, requestID, []byte{})
|
packet = createFCGIPacket(FCGI_STDIN, requestID, []byte{})
|
||||||
conn.Write(packet)
|
conn.Write(packet)
|
||||||
|
|
||||||
// Читаем ответ
|
// Читаем и стримим ответ (с поддержкой SSE и chunked transfer)
|
||||||
response, err := readFastCGIResponse(conn, requestID)
|
err = streamFastCGIResponse(conn, requestID, w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tools.Logs_file(1, "PHP", "❌ Ошибка чтения FastCGI ответа: "+err.Error(), "logs_php.log", false)
|
tools.Logs_file(1, "PHP", "❌ Ошибка чтения FastCGI ответа: "+err.Error(), "logs_php.log", false)
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
// Не вызываем http.Error здесь, т.к. заголовки уже могли быть отправлены
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обрабатываем ответ
|
|
||||||
processPHPResponse(w, response)
|
|
||||||
tools.Logs_file(0, "PHP", fmt.Sprintf("✅ FastCGI обработал: %s (порт %d)", phpPath, port), "logs_php.log", false)
|
tools.Logs_file(0, "PHP", fmt.Sprintf("✅ FastCGI обработал: %s (порт %d)", phpPath, port), "logs_php.log", false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Чтение FastCGI ответа
|
// Streaming чтение FastCGI ответа с поддержкой SSE и chunked transfer
|
||||||
func readFastCGIResponse(conn net.Conn, requestID uint16) ([]byte, error) {
|
func streamFastCGIResponse(conn net.Conn, requestID uint16, w http.ResponseWriter) error {
|
||||||
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
|
conn.SetReadDeadline(time.Now().Add(30 * time.Second))
|
||||||
|
|
||||||
var stdout bytes.Buffer
|
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
|
var headerBuffer bytes.Buffer
|
||||||
|
headersWritten := false
|
||||||
|
flusher, canFlush := w.(http.Flusher)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
// Читаем заголовок FastCGI
|
// Читаем заголовок FastCGI
|
||||||
headerBuf := make([]byte, 8)
|
headerBuf := make([]byte, 8)
|
||||||
_, err := io.ReadFull(conn, headerBuf)
|
_, err := io.ReadFull(conn, headerBuf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
var header FCGIHeader
|
var header FCGIHeader
|
||||||
@@ -406,7 +406,7 @@ func readFastCGIResponse(conn net.Conn, requestID uint16) ([]byte, error) {
|
|||||||
if header.ContentLength > 0 {
|
if header.ContentLength > 0 {
|
||||||
_, err = io.ReadFull(conn, content)
|
_, err = io.ReadFull(conn, content)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,9 +420,55 @@ func readFastCGIResponse(conn net.Conn, requestID uint16) ([]byte, error) {
|
|||||||
switch header.Type {
|
switch header.Type {
|
||||||
case FCGI_STDOUT:
|
case FCGI_STDOUT:
|
||||||
if header.ContentLength > 0 {
|
if header.ContentLength > 0 {
|
||||||
stdout.Write(content)
|
if !headersWritten {
|
||||||
|
// Накапливаем данные до разделителя заголовков
|
||||||
|
headerBuffer.Write(content)
|
||||||
|
|
||||||
|
// Ищем разделитель между заголовками и телом
|
||||||
|
headerStr := headerBuffer.String()
|
||||||
|
sepIndex := strings.Index(headerStr, "\r\n\r\n")
|
||||||
|
if sepIndex == -1 {
|
||||||
|
sepIndex = strings.Index(headerStr, "\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if sepIndex != -1 {
|
||||||
|
// Нашли разделитель - обрабатываем заголовки
|
||||||
|
var sepLen int
|
||||||
|
if strings.Contains(headerStr[:sepIndex+4], "\r\n\r\n") {
|
||||||
|
sepLen = 4
|
||||||
} else {
|
} else {
|
||||||
// Пустой STDOUT означает конец
|
sepLen = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
headersPart := headerStr[:sepIndex]
|
||||||
|
bodyPart := headerStr[sepIndex+sepLen:]
|
||||||
|
|
||||||
|
// Парсим и устанавливаем заголовки
|
||||||
|
processStreamingHeaders(w, headersPart)
|
||||||
|
headersWritten = true
|
||||||
|
|
||||||
|
// Отправляем первую часть тела
|
||||||
|
if len(bodyPart) > 0 {
|
||||||
|
w.Write([]byte(bodyPart))
|
||||||
|
if canFlush {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Заголовки уже отправлены - стримим тело
|
||||||
|
w.Write(content)
|
||||||
|
// Принудительно отправляем данные (критично для SSE)
|
||||||
|
if canFlush {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Пустой STDOUT - конец данных, если остались заголовки без тела
|
||||||
|
if !headersWritten && headerBuffer.Len() > 0 {
|
||||||
|
processStreamingHeaders(w, headerBuffer.String())
|
||||||
|
headersWritten = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case FCGI_STDERR:
|
case FCGI_STDERR:
|
||||||
if header.ContentLength > 0 {
|
if header.ContentLength > 0 {
|
||||||
@@ -433,23 +479,18 @@ func readFastCGIResponse(conn net.Conn, requestID uint16) ([]byte, error) {
|
|||||||
if stderr.Len() > 0 {
|
if stderr.Len() > 0 {
|
||||||
tools.Logs_file(1, "PHP", "FastCGI stderr: "+stderr.String(), "logs_php.log", false)
|
tools.Logs_file(1, "PHP", "FastCGI stderr: "+stderr.String(), "logs_php.log", false)
|
||||||
}
|
}
|
||||||
return stdout.Bytes(), nil
|
// Если заголовки так и не были записаны (пустой ответ)
|
||||||
|
if !headersWritten {
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Обработка PHP ответа (как раньше)
|
// Обработка заголовков для streaming ответа
|
||||||
func processPHPResponse(w http.ResponseWriter, response []byte) {
|
func processStreamingHeaders(w http.ResponseWriter, headersPart string) {
|
||||||
responseStr := string(response)
|
headers := strings.Split(headersPart, "\n")
|
||||||
|
|
||||||
// Разбираем заголовки и тело
|
|
||||||
parts := strings.SplitN(responseStr, "\r\n\r\n", 2)
|
|
||||||
if len(parts) < 2 {
|
|
||||||
parts = strings.SplitN(responseStr, "\n\n", 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(parts) >= 2 {
|
|
||||||
headers := strings.Split(parts[0], "\n")
|
|
||||||
statusCode := 200
|
statusCode := 200
|
||||||
|
|
||||||
for _, header := range headers {
|
for _, header := range headers {
|
||||||
@@ -467,8 +508,7 @@ func processPHPResponse(w http.ResponseWriter, response []byte) {
|
|||||||
} else if strings.HasPrefix(strings.ToLower(header), "location:") {
|
} else if strings.HasPrefix(strings.ToLower(header), "location:") {
|
||||||
location := strings.TrimSpace(strings.SplitN(header, ":", 2)[1])
|
location := strings.TrimSpace(strings.SplitN(header, ":", 2)[1])
|
||||||
w.Header().Set("Location", location)
|
w.Header().Set("Location", location)
|
||||||
w.WriteHeader(http.StatusFound)
|
statusCode = http.StatusFound
|
||||||
return
|
|
||||||
} else if strings.HasPrefix(strings.ToLower(header), "status:") {
|
} else if strings.HasPrefix(strings.ToLower(header), "status:") {
|
||||||
status := strings.TrimSpace(strings.SplitN(header, ":", 2)[1])
|
status := strings.TrimSpace(strings.SplitN(header, ":", 2)[1])
|
||||||
if code, err := strconv.Atoi(strings.Split(status, " ")[0]); err == nil {
|
if code, err := strconv.Atoi(strings.Split(status, " ")[0]); err == nil {
|
||||||
@@ -483,11 +523,6 @@ func processPHPResponse(w http.ResponseWriter, response []byte) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
w.WriteHeader(statusCode)
|
w.WriteHeader(statusCode)
|
||||||
w.Write([]byte(parts[1]))
|
|
||||||
} else {
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
w.Write(response)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// PHP_Stop останавливает все FastCGI процессы
|
// PHP_Stop останавливает все FastCGI процессы
|
||||||
|
|||||||
@@ -154,9 +154,34 @@ func StartHandlerProxy(w http.ResponseWriter, r *http.Request) (valid bool) {
|
|||||||
// Устанавливаем статус код
|
// Устанавливаем статус код
|
||||||
w.WriteHeader(resp.StatusCode)
|
w.WriteHeader(resp.StatusCode)
|
||||||
|
|
||||||
// Копируем тело ответа
|
// Копируем тело ответа с поддержкой streaming (SSE, chunked responses)
|
||||||
if _, err := io.Copy(w, resp.Body); err != nil {
|
// Используем буферизированное копирование с принудительной отправкой данных
|
||||||
log.Printf("Ошибка копирования тела ответа: %v", err)
|
flusher, canFlush := w.(http.Flusher)
|
||||||
|
|
||||||
|
// Буфер для чанков (32KB - оптимальный размер для баланса производительности)
|
||||||
|
buffer := make([]byte, 32*1024)
|
||||||
|
|
||||||
|
for {
|
||||||
|
n, err := resp.Body.Read(buffer)
|
||||||
|
if n > 0 {
|
||||||
|
// Записываем прочитанные данные
|
||||||
|
if _, writeErr := w.Write(buffer[:n]); writeErr != nil {
|
||||||
|
log.Printf("Ошибка записи тела ответа: %v", writeErr)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Принудительно отправляем данные клиенту (критично для SSE)
|
||||||
|
if canFlush {
|
||||||
|
flusher.Flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if err != io.EOF {
|
||||||
|
log.Printf("Ошибка чтения тела ответа: %v", err)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return valid
|
return valid
|
||||||
|
|||||||
Reference in New Issue
Block a user