Compare commits

..
17 Commits
Author SHA1 Message Date
meiseayoung a2afd947d0 chore: update v.mod version 2026-01-06 17:03:34 +08:00
meiseayoung cfde1b4306 Merge remote-tracking branch: keep optimized HTTP/1.0 connection handling 2026-01-06 16:58:53 +08:00
meiseayoung 0a4e5d50f3 fix: HTTP/1.0 connection not closing causes ab benchmark to hang
- Add HTTP version detection in parse_http_request_usockets
- Return Connection: close header for HTTP/1.0 requests
- Call shutdown() for graceful connection close after HTTP/1.0 response
- Fix missing is_http11 parameter in WebSocket upgrade handler
- Support HTTP/1.0 with Connection: Keep-Alive header (ab -k mode)
- Optimize: only check Connection header value for keep-alive (like Go net/http)

Root cause: ab uses HTTP/1.0 by default, but server always returned
Connection: keep-alive, causing ab to wait for server to close connection.
2026-01-06 16:36:07 +08:00
meiseayoung ed55ce758e fix: HTTP/1.0 connection not closing causes ab benchmark to hang
- Add HTTP version detection in parse_http_request_usockets
- Return Connection: close header for HTTP/1.0 requests
- Call shutdown() for graceful connection close after HTTP/1.0 response
- Fix missing is_http11 parameter in WebSocket upgrade handler

Root cause: ab uses HTTP/1.0 by default, but server always returned
Connection: keep-alive, causing ab to wait for server to close connection.
2026-01-06 15:58:24 +08:00
meiseayoung 22e8dd4548 chore: update v.mod version 2026-01-06 14:11:40 +08:00
meiseayoung a702746df7 build(usockets): add pre-compiled library for MacOS-arm64 platform 2026-01-06 14:10:02 +08:00
meiseayoung a57ce2e77a chore: update v.mod version 2026-01-06 12:01:45 +08:00
meiseayoung 7a08c3142a build(usockets): add pre-compiled library for MacOS-x86 platform 2026-01-06 12:01:20 +08:00
meiseayoung 362015c1cf docs: update build instructions for uSockets backend
- Clarify that Windows requires gcc (tcc does not support .a static library format)
- Add separate build commands for macOS/Linux and Windows
- Document pre-compiled library locations by platform
- Add w64devkit as recommended MinGW download option
2026-01-05 12:25:06 +08:00
meiseayoung 0969901481 chore: update v.mod version 2026-01-05 11:50:46 +08:00
meiseayoung 6bddfc3731 build(usockets): add pre-compiled library for Linux platform 2026-01-05 11:49:47 +08:00
meiseayoung 26ac804b1d chore: update v.mod version 2026-01-05 11:07:41 +08:00
meiseayoung 56299b5485 refactor(usockets): reorganize lib directory structure for multi-platform support
- Organize lib directory by platform: windows/, macos-arm64/, macos-x64/, linux/
- Update build.sh to support unified build process for Windows/macOS/Linux
- Compile libuv + uSockets and merge into complete static library
- Update usockets.v conditional compilation to point to new directory structure
- Optimize benchmark tests with connection pooling for high concurrency testing
- Clean up unused legacy library files

Performance test results (Windows):
- 1000 concurrent, 1M requests: 35,223 req/s
- 10000 concurrent, 1M requests: 26,884 req/s
2026-01-05 11:06:56 +08:00
meiseayoung d31492fd59 chore: update v.mod version 2026-01-04 18:12:21 +08:00
meiseayoung e19f799e46 chore: upload usokets lib 2026-01-04 18:11:19 +08:00
meiseayoung f7e753969d fix: change the usockets/lib path 2026-01-04 15:44:54 +08:00
meiseayoung e9d14a35f6 fix(streaming): use direct socket fd writes for real-time SSE streaming
Previously, PicoevStreamWriter used picohttpparser.Response.raw() to write
streaming data, which only buffered the data until the entire request handler
completed. This caused SSE responses to be delayed until all sleep() calls
finished (e.g., 2.89s delay for /stream-text endpoint).

Changes:
- Modified PicoevStreamWriter to use socket fd directly from picohttpparser.Response
- Implemented write_to_fd() using C.send() for immediate data delivery
- Updated handle_picoev_streaming_response() to write HTTP headers directly to socket
- Added C.send() function declaration for direct socket writes

This fix enables true real-time streaming where each write() or write_sse()
call immediately sends data to the client, rather than buffering until
the callback completes.

All 74 test files pass with no regressions.
2026-01-04 15:43:01 +08:00
14 changed files with 388 additions and 121 deletions
-1
View File
@@ -2,7 +2,6 @@
*.exe
*.out
*.o
*.a
*.so
*.dll
+26 -9
View File
@@ -50,8 +50,13 @@ cd v-hono
### Standard Build (picoev backend)
```bash
v your_app.v -o app.exe
./app.exe
# macOS / Linux
v -prod -o app your_app.v
./app
# Windows
v -o app.exe your_app.v
.\app.exe
```
### High-Performance Build (uSockets backend)
@@ -59,25 +64,33 @@ v your_app.v -o app.exe
For maximum performance with uSockets backend:
```bash
v -enable-globals -cc gcc -ldflags "-ldbghelp" your_app.v -o app.exe
./app.exe
# macOS / Linux
v -enable-globals -prod -o app your_app.v
./app
# Windows (must use gcc, tcc does not support .a static library format)
v -enable-globals -cc gcc -ldflags "-ldbghelp" -o app.exe your_app.v
.\app.exe
```
**Note**: The `-enable-globals` flag is required for uSockets backend. The `-ldflags "-ldbghelp"` fixes Windows linking issues.
**Important Notes:**
- The `-enable-globals` flag is required for uSockets backend
- On Windows, you **must** use `-cc gcc` because V's default compiler (tcc) does not support MinGW `.a` static library format
- The `-ldflags "-ldbghelp"` is required on Windows for libuv linking
#### Windows Prerequisites
To use uSockets backend on Windows, you need:
1. **GCC (MinGW-w64)** - C compiler
1. **GCC (MinGW-w64)** - C compiler (required, tcc is not supported)
```powershell
# Install via Scoop
# Install via Scoop (recommended)
scoop install mingw
# Or via Chocolatey
choco install mingw
# Or download from: https://www.mingw-w64.org/downloads/
# Or download w64devkit from: https://github.com/skeeto/w64devkit/releases
```
2. **V Compiler** - Version 0.4.x or later
@@ -94,7 +107,11 @@ To use uSockets backend on Windows, you need:
v version # Should show V 0.4.x
```
The uSockets library and libuv are pre-compiled and included in the `usockets/lib/` directory.
The uSockets library and libuv are pre-compiled and included in the `usockets/lib/{platform}/` directory:
- `usockets/lib/windows/` - Windows x64
- `usockets/lib/linux/` - Linux x64
- `usockets/lib/macos-arm64/` - macOS Apple Silicon
- `usockets/lib/macos-x64/` - macOS Intel
### Using uSockets Backend
+33 -5
View File
@@ -448,21 +448,49 @@ func checkKeepAlive(client *http.Client) bool {
func checkThroughput(client *http.Client) float64 {
start := time.Now()
success := 0
requests := 200
concurrency := 1000
totalRequests := 1000000
requestsPerWorker := totalRequests / concurrency
for i := 0; i < requests; i++ {
resp, err := client.Get(baseURL + "/health")
// 创建共享的 Transport,启用连接池复用
transport := &http.Transport{
MaxIdleConns: concurrency,
MaxIdleConnsPerHost: concurrency,
IdleConnTimeout: 30 * time.Second,
DisableKeepAlives: false,
}
results := make(chan int, concurrency)
for i := 0; i < concurrency; i++ {
go func() {
localSuccess := 0
// 每个 goroutine 使用独立 Client,但共享 Transport(连接池)
localClient := &http.Client{
Timeout: 10 * time.Second,
Transport: transport,
}
for j := 0; j < requestsPerWorker; j++ {
resp, err := localClient.Get(baseURL + "/health")
if err != nil {
continue
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
if resp.StatusCode == 200 {
success++
localSuccess++
}
}
results <- localSuccess
}()
}
success := 0
for i := 0; i < concurrency; i++ {
success += <-results
}
elapsed := time.Since(start).Seconds()
fmt.Printf("\n [并发: %d, 总请求: %d, 成功: %d, 耗时: %.2fs]\n ", concurrency, totalRequests, success, elapsed)
return float64(success) / elapsed
}
+41 -17
View File
@@ -583,8 +583,8 @@ fn send_picoev_response(mut res picohttpparser.Response, ctx Context, response h
// handle_picoev_streaming_response - Handle a streaming response using PicoevStreamWriter
// This function:
// 1. Sends the HTTP headers for streaming
// 2. Creates a PicoevStreamWriter
// 1. Sends the HTTP headers for streaming directly to socket fd
// 2. Creates a PicoevStreamWriter with the socket fd
// 3. Executes the streaming callback
// 4. Handles errors and cleanup
fn handle_picoev_streaming_response(mut res picohttpparser.Response, ctx Context, config PicoevConfig) {
@@ -601,14 +601,10 @@ fn handle_picoev_streaming_response(mut res picohttpparser.Response, ctx Context
// Determine status code
status_code := if ctx.status_code != 0 { ctx.status_code } else { 200 }
// Send HTTP response line
if status_code == 200 {
res.http_ok()
} else {
res.raw('HTTP/1.1 ${status_code} ${get_status_text(status_code)}\r\n')
}
// Build HTTP headers string for direct socket write
mut headers_str := 'HTTP/1.1 ${status_code} ${get_status_text(status_code)}\r\n'
// Send headers from context
// Add headers from context
mut has_transfer_encoding := false
mut has_connection := false
@@ -618,7 +614,7 @@ fn handle_picoev_streaming_response(mut res picohttpparser.Response, ctx Context
if key_len == 14 && eq_ignore_case(key, 'content-length') {
continue
}
res.header(key, value)
headers_str += '${key}: ${value}\r\n'
if key_len == 17 && eq_ignore_case(key, 'transfer-encoding') {
has_transfer_encoding = true
} else if key_len == 10 && eq_ignore_case(key, 'connection') {
@@ -628,20 +624,27 @@ fn handle_picoev_streaming_response(mut res picohttpparser.Response, ctx Context
// Ensure Transfer-Encoding: chunked is set
if !has_transfer_encoding {
res.header('Transfer-Encoding', 'chunked')
headers_str += 'Transfer-Encoding: chunked\r\n'
}
// Ensure Connection header is set for streaming
if !has_connection {
res.header('Connection', 'keep-alive')
res.header('Keep-Alive', 'timeout=${config.timeout_secs}')
headers_str += 'Connection: keep-alive\r\n'
headers_str += 'Keep-Alive: timeout=${config.timeout_secs}\r\n'
}
// End headers section (empty body to signal headers are complete)
// Note: We don't call res.body() or res.end() here because we're streaming
res.raw('\r\n')
// End headers section
headers_str += '\r\n'
// Create PicoevStreamWriter for streaming
// Write headers directly to socket fd for immediate delivery
fd := res.fd
write_to_socket_fd(fd, headers_str.bytes()) or {
eprintln('[Picoev Stream Error] Failed to write headers: ${err.msg()}')
cleanup_stream_config(ctx)
return
}
// Create PicoevStreamWriter for streaming (uses fd directly)
mut writer := PicoevStreamWriter.new(res)
// Create StreamContext with the writer
@@ -665,6 +668,27 @@ fn handle_picoev_streaming_response(mut res picohttpparser.Response, ctx Context
cleanup_stream_config(ctx)
}
// write_to_socket_fd - Helper function to write bytes directly to socket fd
// Used for sending HTTP headers before streaming begins
fn write_to_socket_fd(fd int, data []u8) ! {
if data.len == 0 {
return
}
mut total_written := 0
for total_written < data.len {
remaining := data.len - total_written
ptr := unsafe { &u8(data.data) + total_written }
written := C.send(fd, ptr, remaining, 0)
if written < 0 {
return error('Failed to write to socket')
}
if written == 0 {
return error('Connection closed by peer')
}
total_written += written
}
}
// 获取状态码文本
fn get_status_text(code int) string {
return match code {
+45 -13
View File
@@ -8,6 +8,9 @@ import time
import picohttpparser
import usockets
// C function declaration for direct socket writes
fn C.send(sockfd int, buf voidptr, len int, flags int) int
// ============================================================================
// SSE Event Structure
// ============================================================================
@@ -184,23 +187,26 @@ pub fn (mut ctx StreamContext) trigger_abort() {
// PicoevStreamWriter - StreamWriter implementation for picoev server
// Supports chunked transfer encoding for streaming responses
// Uses direct socket fd writes for immediate data delivery (true streaming)
@[heap]
pub struct PicoevStreamWriter {
mut:
res &picohttpparser.Response
fd int // Socket file descriptor for direct writes
connected bool
}
// PicoevStreamWriter.new - Create a new PicoevStreamWriter with the given response
// Extracts the fd from picohttpparser.Response for direct socket writes
pub fn PicoevStreamWriter.new(res &picohttpparser.Response) &PicoevStreamWriter {
return &PicoevStreamWriter{
res: unsafe { res }
fd: res.fd
connected: true
}
}
// write - Write raw bytes to the stream using chunked transfer encoding
// Format: {hex_size}\r\n{data}\r\n
// Writes directly to socket fd for immediate delivery
pub fn (mut w PicoevStreamWriter) write(data []u8) ! {
if !w.connected {
return error('Connection closed')
@@ -210,9 +216,10 @@ pub fn (mut w PicoevStreamWriter) write(data []u8) ! {
}
// Write chunked format: size in hex, CRLF, data, CRLF
chunk_header := format_chunk_size(data.len)
w.res.raw(chunk_header)
w.res.raw(data.bytestr())
w.res.raw('\r\n')
// Write directly to socket fd
w.write_to_fd(chunk_header.bytes())!
w.write_to_fd(data)!
w.write_to_fd('\r\n'.bytes())!
}
// write_string - Write a string to the stream using chunked transfer encoding
@@ -225,15 +232,40 @@ pub fn (mut w PicoevStreamWriter) write_string(data string) ! {
}
// Write chunked format: size in hex, CRLF, data, CRLF
chunk_header := format_chunk_size(data.len)
w.res.raw(chunk_header)
w.res.raw(data)
w.res.raw('\r\n')
// Write directly to socket fd
w.write_to_fd(chunk_header.bytes())!
w.write_to_fd(data.bytes())!
w.write_to_fd('\r\n'.bytes())!
}
// flush - Flush the buffer (picoev handles this automatically)
// write_to_fd - Write bytes directly to the socket file descriptor
// This ensures immediate delivery without buffering
@[inline]
fn (mut w PicoevStreamWriter) write_to_fd(data []u8) ! {
if data.len == 0 {
return
}
mut total_written := 0
for total_written < data.len {
remaining := data.len - total_written
ptr := unsafe { &u8(data.data) + total_written }
written := C.send(w.fd, ptr, remaining, 0)
if written < 0 {
w.connected = false
return error('Failed to write to socket')
}
if written == 0 {
w.connected = false
return error('Connection closed by peer')
}
total_written += written
}
}
// flush - Flush the buffer (direct fd writes are already unbuffered)
pub fn (mut w PicoevStreamWriter) flush() ! {
// picoev automatically flushes data
// No explicit flush needed
// Direct fd writes are unbuffered, no explicit flush needed
// But we can use TCP_NODELAY to disable Nagle's algorithm if needed
}
// close - Close the stream by sending the final chunk
@@ -241,9 +273,9 @@ pub fn (mut w PicoevStreamWriter) flush() ! {
pub fn (mut w PicoevStreamWriter) close() ! {
if w.connected {
// Write the final chunk (zero-length chunk) to signal end of stream
w.res.raw('0\r\n\r\n')
w.res.end()
w.write_to_fd('0\r\n\r\n'.bytes()) or {}
w.connected = false
// Note: Don't close the fd here, picoev manages the connection lifecycle
}
}
+93 -26
View File
@@ -1,13 +1,36 @@
#!/bin/bash
# uSockets 编译脚本
# 用于重新编译 libusockets_full.a
# 用于重新编译 libusockets_full.a (包含 uSockets + libuv)
# 支持: Windows (Git Bash/MSYS2), macOS (Intel/Apple Silicon), Linux
# 输出到: usockets/lib/{platform}/libusockets_full.a
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
USOCKETS_DIR="/tmp/uSockets"
LIBUV_DIR="/tmp/libuv"
MERGE_DIR="/tmp/merge_libs"
echo "=== uSockets 编译脚本 ==="
echo "检测到系统: $OSTYPE"
# 确定输出目录
if [[ "$OSTYPE" == "darwin"* ]]; then
if [[ $(uname -m) == "arm64" ]]; then
OUTPUT_DIR="$SCRIPT_DIR/lib/macos-arm64"
else
OUTPUT_DIR="$SCRIPT_DIR/lib/macos-x64"
fi
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "mingw"* ]]; then
OUTPUT_DIR="$SCRIPT_DIR/lib/windows"
elif [[ "$OSTYPE" == "linux"* ]]; then
OUTPUT_DIR="$SCRIPT_DIR/lib/linux"
else
OUTPUT_DIR="$SCRIPT_DIR/lib/linux"
fi
echo "输出目录: $OUTPUT_DIR"
mkdir -p "$OUTPUT_DIR"
# 检查是否有 uSockets 源码
if [ ! -d "$USOCKETS_DIR" ]; then
@@ -15,41 +38,85 @@ if [ ! -d "$USOCKETS_DIR" ]; then
git clone --depth 1 https://github.com/uNetworking/uSockets.git "$USOCKETS_DIR"
fi
# 检查是否有 libuv 源码
if [ ! -d "$LIBUV_DIR" ]; then
echo "克隆 libuv..."
git clone --depth 1 --branch v1.48.0 https://github.com/libuv/libuv.git "$LIBUV_DIR"
fi
# 应用 backlog 修改
echo "应用 backlog=16384 修改..."
if [ -f "$SCRIPT_DIR/src/bsd.c" ]; then
cp "$SCRIPT_DIR/src/bsd.c" "$USOCKETS_DIR/src/bsd.c"
else
# 如果没有本地修改的源码,直接修改
sed -i.bak 's/listen(listenFd, 512)/listen(listenFd, 16384)/g' "$USOCKETS_DIR/src/bsd.c"
fi
# 编译
echo "编译 uSockets..."
cd "$USOCKETS_DIR"
make clean
# 编译 libuv
compile_libuv() {
echo "编译 libuv..."
cd "$LIBUV_DIR"
rm -rf build 2>/dev/null || true
mkdir -p build
cd build
# 根据系统选择编译参数
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
if [[ $(uname -m) == "arm64" ]]; then
# Apple Silicon
WITH_LIBUV=1 CFLAGS="-I/opt/homebrew/include" make
else
# Intel Mac
WITH_LIBUV=1 CFLAGS="-I/usr/local/include" make
if ! command -v cmake &> /dev/null; then
echo "错误: 需要安装 cmake"
echo " macOS: brew install cmake"
echo " Linux: sudo apt install cmake 或 sudo yum install cmake"
echo " Windows: scoop install cmake 或 choco install cmake"
exit 1
fi
else
# Linux
WITH_LIBUV=1 make
fi
# 复制到 v-hono
echo "复制库文件..."
cp "$USOCKETS_DIR/uSockets.a" "$SCRIPT_DIR/../lib/libusockets_full.a"
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "mingw"* ]]; then
cmake .. -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF -DLIBUV_BUILD_SHARED=OFF
else
cmake .. -DCMAKE_BUILD_TYPE=Release -DBUILD_TESTING=OFF -DLIBUV_BUILD_SHARED=OFF
fi
echo "=== 编译完成 ==="
echo "库文件: $SCRIPT_DIR/../lib/libusockets_full.a"
cmake --build . --config Release
}
# 编译 uSockets
compile_usockets() {
echo "编译 uSockets..."
cd "$USOCKETS_DIR"
rm -f *.o *.a 2>/dev/null || true
gcc -DLIBUS_NO_SSL -DLIBUS_USE_LIBUV -std=c11 -Isrc -I"$LIBUV_DIR/include" -O3 -c src/*.c src/eventing/*.c src/crypto/*.c
ar rcs uSockets.a *.o
rm -f *.o
}
# 合并库文件
merge_libraries() {
echo "合并库文件..."
rm -rf "$MERGE_DIR"
mkdir -p "$MERGE_DIR"
cd "$MERGE_DIR"
ar x "$USOCKETS_DIR/uSockets.a"
ar x "$LIBUV_DIR/build/libuv.a"
ar rcs "$OUTPUT_DIR/libusockets_full.a" *.o
rm -rf "$MERGE_DIR"
}
# 执行编译
compile_libuv
compile_usockets
merge_libraries
# 显示结果
LIB_SIZE=$(ls -lh "$OUTPUT_DIR/libusockets_full.a" | awk '{print $5}')
echo ""
echo "重新编译 v-hono 应用:"
echo " v -enable-globals -prod -o your_app your_app.v"
echo "=== 编译完成 ==="
echo "库文件: $OUTPUT_DIR/libusockets_full.a ($LIB_SIZE)"
echo ""
echo "编译 v-hono 应用:"
if [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" || "$OSTYPE" == "mingw"* ]]; then
echo " v -enable-globals -cc gcc -ldflags \"-ldbghelp\" -o app.exe app.v"
else
echo " v -enable-globals -prod -o app app.v"
fi
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+16 -23
View File
@@ -2,41 +2,36 @@ module usockets
// 条件编译:根据操作系统选择正确的路径和库
// @VMODROOT 指向包含 v.mod 的模块根目录 (v-hono/)
// 预编译库按平台存放在 usockets/lib/{platform}/ 目录
$if windows {
#flag -DLIBUS_USE_LIBUV
#flag -DLIBUS_NO_SSL
#flag -I@VMODROOT/usockets/include
#flag -L@VMODROOT/lib
#flag @VMODROOT/lib/libusockets_full.a
#flag -lws2_32 -liphlpapi -lpsapi -luserenv -lole32
#flag -L@VMODROOT/usockets/lib/windows
#flag @VMODROOT/usockets/lib/windows/libusockets_full.a
#flag -lws2_32 -liphlpapi -lpsapi -luserenv -lole32 -ldbghelp
} $else $if macos {
// macOS: 支持 Apple Silicon (M1/M2/M3) 和 Intel Mac
#flag -DLIBUS_USE_LIBUV
#flag -DLIBUS_NO_SSL
// libuv 路径 - 根据架构选择 Homebrew 路径
$if arm64 {
#flag -I/opt/homebrew/include
#flag -L/opt/homebrew/lib
} $else {
#flag -I/usr/local/include
#flag -L/usr/local/lib
}
// uSockets 库路径
#flag -I@VMODROOT/usockets/include
#flag -L@VMODROOT/lib
#flag @VMODROOT/lib/libusockets_full.a
#flag -luv
$if arm64 {
// Apple Silicon (M1/M2/M3)
#flag -L@VMODROOT/usockets/lib/macos-arm64
#flag @VMODROOT/usockets/lib/macos-arm64/libusockets_full.a
} $else {
// Intel Mac
#flag -L@VMODROOT/usockets/lib/macos-x64
#flag @VMODROOT/usockets/lib/macos-x64/libusockets_full.a
}
} $else {
// Linux 和其他平台
#flag -DLIBUS_USE_LIBUV
#flag -DLIBUS_NO_SSL
#flag -I@VMODROOT/usockets/include
#flag -L@VMODROOT/lib
#flag @VMODROOT/lib/libusockets_full.a
#flag -luv
#flag -L@VMODROOT/usockets/lib/linux
#flag @VMODROOT/usockets/lib/linux/libusockets_full.a
}
#include "libusockets.h"
@@ -105,8 +100,6 @@ pub fn (ctx SocketContext) on_writable(h voidptr) { C.us_socket_context_on_writa
pub fn (ctx SocketContext) on_timeout(h voidptr) { C.us_socket_context_on_timeout(0, ctx, h) }
pub fn (ctx SocketContext) on_end(h voidptr) { C.us_socket_context_on_end(0, ctx, h) }
pub fn (ctx SocketContext) listen(port int) ListenSocket {
// 参数: ssl=0, context, host=nil, port, options=LIBUS_LISTEN_DEFAULT(0), socket_ext_size=0
// 注意: backlog 是在 uSockets 库内部硬编码的,不通过此函数传递
return C.us_socket_context_listen(0, ctx, unsafe { nil }, port, 0, 0)
}
+124 -17
View File
@@ -183,7 +183,7 @@ fn handle_usockets_ws_upgrade(s usockets.Socket, raw_data string, route_match Co
// Check if this is a WebSocket upgrade response
if response.status_code != 101 {
// Not a WebSocket upgrade, send the response as-is
send_usockets_response(s, mutable_ctx, response)
send_usockets_response(s, mutable_ctx, response, true)
return false
}
@@ -227,7 +227,7 @@ fn usockets_on_data(s usockets.Socket, data &char, length int) usockets.Socket {
// 解析 HTTP 请求
raw_data := unsafe { tos(&u8(data), length) }
method, path, query_map, body := parse_http_request_usockets(raw_data)
method, path, query_map, body, is_http11 := parse_http_request_usockets(raw_data)
if method.len == 0 {
s.write_bytes('HTTP/1.1 400 Bad Request\r\nContent-Length: 11\r\n\r\nBad Request')
@@ -259,11 +259,11 @@ fn usockets_on_data(s usockets.Socket, data &char, length int) usockets.Socket {
// 优化1:零中间件快速路径
if !g_usockets_app.has_middlewares {
response := route_match.handler.handle(mut hono_ctx)
send_usockets_response(s, hono_ctx, response)
send_usockets_response(s, hono_ctx, response, is_http11)
} else {
middlewares := get_middlewares_for_path_usockets_optimized(g_usockets_app, path)
response := exec_middlewares_usockets(0, middlewares, mut hono_ctx, route_match.handler)
send_usockets_response(s, hono_ctx, response)
send_usockets_response(s, hono_ctx, response, is_http11)
}
response_sent = true
}
@@ -289,11 +289,11 @@ fn usockets_on_data(s usockets.Socket, data &char, length int) usockets.Socket {
// 优化1:零中间件快速路径
if !g_usockets_app.has_middlewares {
response := route_match.handler.handle(mut hono_ctx)
send_usockets_response(s, hono_ctx, response)
send_usockets_response(s, hono_ctx, response, is_http11)
} else {
middlewares := get_middlewares_for_path_usockets_optimized(g_usockets_app, path)
response := exec_middlewares_usockets(0, middlewares, mut hono_ctx, route_match.handler)
send_usockets_response(s, hono_ctx, response)
send_usockets_response(s, hono_ctx, response, is_http11)
}
response_sent = true
}
@@ -305,9 +305,13 @@ fn usockets_on_data(s usockets.Socket, data &char, length int) usockets.Socket {
if handler := g_usockets_app.not_found_handler {
response := handler(mut hono_ctx)
send_usockets_response(s, hono_ctx, response)
send_usockets_response(s, hono_ctx, response, is_http11)
} else {
s.write_bytes('HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: 9\r\nConnection: keep-alive\r\n\r\nNot Found')
conn_header := if is_http11 { 'keep-alive' } else { 'close' }
s.write_bytes('HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: 9\r\nConnection: ${conn_header}\r\n\r\nNot Found')
if !is_http11 {
s.shutdown()
}
}
}
@@ -334,15 +338,17 @@ fn usockets_on_end(s usockets.Socket) usockets.Socket {
// 解析 HTTP 请求 - 零分配优化版
// 使用指针遍历避免创建临时数组
fn parse_http_request_usockets(raw string) (string, string, map[string]string, string) {
// 返回: (method, path, query_map, body, is_http11)
fn parse_http_request_usockets(raw string) (string, string, map[string]string, string, bool) {
mut method := ''
mut path := ''
mut query_map := map[string]string{}
mut body := ''
mut is_http11 := true
len := raw.len
if len == 0 {
return method, path, query_map, body
return method, path, query_map, body, is_http11
}
// 1. 查找第一行结束位置(\r\n)
@@ -354,7 +360,7 @@ fn parse_http_request_usockets(raw string) (string, string, map[string]string, s
}
}
if line_end == -1 {
return method, path, query_map, body
return method, path, query_map, body, is_http11
}
// 2. 解析 method(到第一个空格)
@@ -363,7 +369,7 @@ fn parse_http_request_usockets(raw string) (string, string, map[string]string, s
method_end++
}
if method_end == 0 || method_end >= line_end {
return method, path, query_map, body
return method, path, query_map, body, is_http11
}
method = raw[..method_end]
@@ -391,7 +397,25 @@ fn parse_http_request_usockets(raw string) (string, string, map[string]string, s
path = raw[path_start..path_end]
}
// 4. 查找 body(\r\n\r\n 之后)
// 4. 检测 HTTP 版本 - "HTTP/1.0" vs "HTTP/1.1"
// 请求行格式: "GET /path HTTP/1.1\r\n"
// HTTP/1.x 版本字符串固定为 8 字符,直接检查第 8 个字符
mut version_start := path_end + 1
for version_start < line_end && raw[version_start] == ` ` {
version_start++
}
// "HTTP/1.0" 或 "HTTP/1.1",检查索引 7 的字符(0-based)
if version_start + 7 < line_end {
is_http11 = raw[version_start + 7] != `0`
}
// 对于 HTTP/1.0,检查 Connection 头是否包含 keep-alive
// 参考 Go net/http: wantsHttp10KeepAlive() 只检查 Connection 头的值
if !is_http11 {
is_http11 = has_connection_keep_alive(raw, line_end, len)
}
// 5. 查找 body(\r\n\r\n 之后)
for i in line_end .. len - 3 {
if raw[i] == `\r` && raw[i + 1] == `\n` && raw[i + 2] == `\r` && raw[i + 3] == `\n` {
if i + 4 < len {
@@ -401,10 +425,82 @@ fn parse_http_request_usockets(raw string) (string, string, map[string]string, s
}
}
return method, path, query_map, body
return method, path, query_map, body, is_http11
}
// 快速解析 query string(单次遍历,避免 split)
// 检查 Connection 头是否包含 keep-alive(参考 Go net/http 实现)
// 只检查 "Connection:" 头的值,避免误判请求体中的内容
@[inline]
fn has_connection_keep_alive(raw string, headers_start int, len int) bool {
mut i := headers_start
// 遍历每一行头部
for i < len - 12 { // 至少需要 "Connection:" + 一些值
// 跳过 \r\n
if raw[i] == `\r` && i + 1 < len && raw[i + 1] == `\n` {
i += 2
// 检查是否到达头部结束(空行)
if i < len - 1 && raw[i] == `\r` && raw[i + 1] == `\n` {
break
}
}
// 检查是否是 "Connection:" 头(不区分大小写)
if (raw[i] == `C` || raw[i] == `c`) && i + 11 < len {
// 检查 "onnection:"
if (raw[i+1] == `o` || raw[i+1] == `O`) &&
(raw[i+2] == `n` || raw[i+2] == `N`) &&
(raw[i+3] == `n` || raw[i+3] == `N`) &&
(raw[i+4] == `e` || raw[i+4] == `E`) &&
(raw[i+5] == `c` || raw[i+5] == `C`) &&
(raw[i+6] == `t` || raw[i+6] == `T`) &&
(raw[i+7] == `i` || raw[i+7] == `I`) &&
(raw[i+8] == `o` || raw[i+8] == `O`) &&
(raw[i+9] == `n` || raw[i+9] == `N`) &&
raw[i+10] == `:` {
// 找到 Connection 头,检查值是否包含 keep-alive
mut value_start := i + 11
// 跳过空格
for value_start < len && raw[value_start] == ` ` {
value_start++
}
// 查找行尾
mut value_end := value_start
for value_end < len && raw[value_end] != `\r` && raw[value_end] != `\n` {
value_end++
}
// 在值中查找 "keep-alive"(不区分大小写)
return contains_keep_alive_token(raw, value_start, value_end)
}
}
i++
}
return false
}
// 检查字符串片段是否包含 keep-alive token
@[inline]
fn contains_keep_alive_token(raw string, start int, end int) bool {
// 查找 "keep-alive" 或 "Keep-Alive"(支持多值如 "keep-alive, upgrade")
mut i := start
for i <= end - 10 {
if raw[i] == `k` || raw[i] == `K` {
if (raw[i+1] == `e` || raw[i+1] == `E`) &&
(raw[i+2] == `e` || raw[i+2] == `E`) &&
(raw[i+3] == `p` || raw[i+3] == `P`) &&
raw[i+4] == `-` &&
(raw[i+5] == `a` || raw[i+5] == `A`) &&
(raw[i+6] == `l` || raw[i+6] == `L`) &&
(raw[i+7] == `i` || raw[i+7] == `I`) &&
(raw[i+8] == `v` || raw[i+8] == `V`) &&
(raw[i+9] == `e` || raw[i+9] == `E`) {
return true
}
}
i++
}
return false
}// 快速解析 query string(单次遍历,避免 split)
@[inline]
fn parse_query_string_fast(raw string, start int, end int) map[string]string {
mut query_map := map[string]string{}
@@ -576,7 +672,7 @@ fn exec_middlewares_usockets(idx int, middlewares []ContextMiddleware, mut ctx C
}
// 发送 uSockets 响应 - 优化版
fn send_usockets_response(s usockets.Socket, ctx Context, response http.Response) {
fn send_usockets_response(s usockets.Socket, ctx Context, response http.Response, is_http11 bool) {
// Check if this is a streaming response
if is_streaming_response(ctx) {
// Handle streaming response
@@ -630,14 +726,25 @@ fn send_usockets_response(s usockets.Socket, ctx Context, response http.Response
resp.write_string(response.body.len.str())
resp.write_string('\r\n')
// Connection
// Connection - HTTP/1.0 用 close,HTTP/1.1 用 keep-alive
if is_http11 {
resp.write_string('Connection: keep-alive\r\n')
} else {
resp.write_string('Connection: close\r\n')
}
// 空行 + body
resp.write_string('\r\n')
resp.write_string(response.body)
s.write_bytes(resp.str())
// HTTP/1.0 请求完成后关闭连接
// 只调用 shutdown() 发送 FIN,让 on_end 回调处理 close()
// 这样可以确保发送缓冲区的数据完全发送
if !is_http11 {
s.shutdown()
}
}
// ============================================================================
+1 -1
View File
@@ -1,7 +1,7 @@
Module {
name: 'hono'
description: 'A high-performance V language web framework inspired by Hono.js, featuring hybrid routing, LRU cache, middleware support, and more.'
version: '0.7.0'
version: '0.7.8'
license: 'MIT'
dependencies: []
}