优化HttpMessage请求行与头部解析,增强兼容性 重构HttpCodec识别HTTP请求逻辑,支持多种HTTP方法; HttpMessage.Read高性能解析请求行,ParseHeaders基于Span逐行解析头部,支持Value含冒号; 新增单元测试,覆盖Method/Uri解析、Host端口、Content-Length大小写等场景,提升健壮性。大石头 authored at 2026-01-11 00:29:31
diff --git a/NewLife.Remoting.Extensions/NewLife.Remoting.Extensions.csproj b/NewLife.Remoting.Extensions/NewLife.Remoting.Extensions.csproj
index 7530a96..986af8b 100644
--- a/NewLife.Remoting.Extensions/NewLife.Remoting.Extensions.csproj
+++ b/NewLife.Remoting.Extensions/NewLife.Remoting.Extensions.csproj
@@ -64,7 +64,7 @@
</None>
</ItemGroup>
<ItemGroup>
- <PackageReference Include="NewLife.XCode" Version="11.23.2026.102" />
+ <PackageReference Include="NewLife.XCode" Version="11.23.2026.109-beta0820" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\NewLife.Remoting\NewLife.Remoting.csproj" />
diff --git a/NewLife.Remoting/Http/HttpCodec.cs b/NewLife.Remoting/Http/HttpCodec.cs
index e5d6957..fd3b394 100644
--- a/NewLife.Remoting/Http/HttpCodec.cs
+++ b/NewLife.Remoting/Http/HttpCodec.cs
@@ -47,16 +47,15 @@ public class HttpCodec : Handler
if (message is not IPacket pk) return base.Read(context, message);
- // 是否Http请求
- var isGet = pk.Length >= 4 && pk[0] == 'G' && pk[1] == 'E' && pk[2] == 'T' && pk[3] == ' ';
- var isPost = pk.Length >= 5 && pk[0] == 'P' && pk[1] == 'O' && pk[2] == 'S' && pk[3] == 'T' && pk[4] == ' ';
-
// 该连接第一包检查是否Http
var ext = context.Owner as IExtend ?? throw new ArgumentOutOfRangeException(nameof(context.Owner));
if (ext["Encoder"] is not HttpEncoder)
{
- // 第一个请求必须是GET/POST,才执行后续操作
- if (!isGet && !isPost) return base.Read(context, message);
+ // 第一个请求必须像HTTP请求行,才执行后续操作
+ var msg0 = new HttpMessage();
+ if (!msg0.Read(pk)) return base.Read(context, message);
+
+ if (msg0.Method.IsNullOrEmpty()) return base.Read(context, message);
ext["Encoder"] = new HttpEncoder { JsonHost = JsonHost };
}
@@ -90,7 +89,7 @@ public class HttpCodec : Handler
if (AllowParseHeader && !msg.ParseHeaders()) throw new XException("Http头部解码失败");
// GET请求一次性过来,暂时不支持头部被拆为多包的场景
- if (isGet)
+ if (msg.Method.EqualIgnoreCase("GET") || msg.ContentLength <= 0)
{
// 匹配输入回调,让上层事件收到分包信息
//context.FireRead(msg);
diff --git a/NewLife.Remoting/Http/HttpMessage.cs b/NewLife.Remoting/Http/HttpMessage.cs
index 2cb7471..5185890 100644
--- a/NewLife.Remoting/Http/HttpMessage.cs
+++ b/NewLife.Remoting/Http/HttpMessage.cs
@@ -1,4 +1,5 @@
using System.Diagnostics.CodeAnalysis;
+using NewLife;
using NewLife.Data;
using NewLife.Messaging;
@@ -66,12 +67,35 @@ public class HttpMessage : IMessage, IDisposable
/// <returns>是否成功</returns>
public virtual Boolean Read(IPacket pk)
{
- var p = pk.GetSpan().IndexOf(NewLine);
+ var span = pk.GetSpan();
+
+ var p = span.IndexOf(NewLine);
if (p < 0) return false;
Header = pk.Slice(0, p, false);
Payload = pk.Slice(p + 4, -1, false);
+ // 高性能解析请求行:METHOD SP URI SP HTTP/x.y
+
+ var lineEnd = span[..p].IndexOf((Byte)'\n');
+ if (lineEnd <= 0) return true;
+
+ var firstLine = span[..lineEnd];
+ if (firstLine.Length > 0 && firstLine[^1] == (Byte)'\r') firstLine = firstLine[..^1];
+
+ var sp1 = firstLine.IndexOf((Byte)' ');
+ if (sp1 <= 0) return true;
+
+ var sp2 = firstLine[(sp1 + 1)..].IndexOf((Byte)' ');
+ if (sp2 <= 0) return true;
+ sp2 += sp1 + 1;
+
+ var methodSpan = firstLine[..sp1];
+ var uriSpan = firstLine.Slice(sp1 + 1, sp2 - sp1 - 1);
+
+ if (methodSpan.Length > 0) Method = methodSpan.ToStr();
+ if (uriSpan.Length > 0) Uri = uriSpan.ToStr();
+
return true;
}
@@ -82,23 +106,58 @@ public class HttpMessage : IMessage, IDisposable
var pk = Header;
if (pk == null || pk.Total == 0) return false;
- // 请求方法 GET / HTTP/1.1
- var dic = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
- var ss = pk.ToStr().Split("\r\n");
+ var span = pk.GetSpan();
+ if (span.IsEmpty) return false;
+
+ // 第一行:请求行 GET / HTTP/1.1
+ var lineEnd = span.IndexOf((Byte)'\n');
+ if (lineEnd < 0) return false;
+
+ var firstLine = span[..lineEnd];
+ span = span[(lineEnd + 1)..];
+ if (firstLine[^1] == (Byte)'\r') firstLine = firstLine[..^1];
+
+ // METHOD SP URI SP HTTP/x.y
+ var sp1 = firstLine.IndexOf((Byte)' ');
+ if (sp1 > 0)
{
- var kv = ss[0].Split(' ');
- if (kv != null && kv.Length >= 3)
+ var rest = firstLine[(sp1 + 1)..];
+ var sp2 = rest.IndexOf((Byte)' ');
+ if (sp2 > 0)
{
- Method = kv[0].Trim();
- Uri = kv[1].Trim();
+ Method = firstLine[..sp1].ToStr();
+ Uri = rest[..sp2].ToStr();
}
}
- for (var i = 1; i < ss.Length; i++)
+
+ // 头部行:Name: Value(Value 可能包含冒号)
+ var dic = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
+ while (!span.IsEmpty)
{
- var kv = ss[i].Split(':');
- if (kv != null && kv.Length >= 2)
- dic[kv[0].Trim()] = kv[1].Trim();
+ lineEnd = span.IndexOf((Byte)'\n');
+
+ ReadOnlySpan<Byte> line;
+ if (lineEnd >= 0)
+ {
+ line = span[..lineEnd];
+ span = span[(lineEnd + 1)..];
+ }
+ else
+ {
+ line = span;
+ span = default;
+ }
+
+ if (!line.IsEmpty && line[^1] == (Byte)'\r') line = line[..^1];
+ if (line.IsEmpty) continue;
+
+ var colon = line.IndexOf((Byte)':');
+ if (colon <= 0) continue;
+
+ var name = line[..colon].Trim().ToStr();
+ dic[name] = line[(colon + 1)..].Trim().ToStr();
}
+
Headers = dic;
// 内容长度
diff --git a/NewLife.Remoting/NewLife.Remoting.csproj b/NewLife.Remoting/NewLife.Remoting.csproj
index 920f7f6..f98d427 100644
--- a/NewLife.Remoting/NewLife.Remoting.csproj
+++ b/NewLife.Remoting/NewLife.Remoting.csproj
@@ -54,7 +54,7 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="NewLife.Core" Version="11.10.2026.108-beta1002" />
+ <PackageReference Include="NewLife.Core" Version="11.10.2026.110-beta1552" />
</ItemGroup>
<ItemGroup>
diff --git a/Samples/IoTZero/IoTZero.csproj b/Samples/IoTZero/IoTZero.csproj
index 89a9d77..70ae3e3 100644
--- a/Samples/IoTZero/IoTZero.csproj
+++ b/Samples/IoTZero/IoTZero.csproj
@@ -23,12 +23,12 @@
</ItemGroup>
<ItemGroup>
- <PackageReference Include="NewLife.Cube.Core" Version="6.8.2026.103-beta0958" />
+ <PackageReference Include="NewLife.Cube.Core" Version="6.8.2026.110-beta0606" />
<PackageReference Include="NewLife.IoT" Version="2.7.2026.102" />
<PackageReference Include="NewLife.MQTT" Version="2.3.2026.102" />
<PackageReference Include="NewLife.Redis" Version="6.4.2026.104" />
- <PackageReference Include="NewLife.Stardust.Extensions" Version="3.6.2026.102" />
- <PackageReference Include="NewLife.XCode" Version="11.23.2026.106-beta0914" />
+ <PackageReference Include="NewLife.Stardust.Extensions" Version="3.6.2026.108-beta1810" />
+ <PackageReference Include="NewLife.XCode" Version="11.23.2026.109-beta0820" />
</ItemGroup>
<ItemGroup>
diff --git a/Samples/Zero.Desktop/Zero.Desktop.csproj b/Samples/Zero.Desktop/Zero.Desktop.csproj
index 1088f30..f7a9fbc 100644
--- a/Samples/Zero.Desktop/Zero.Desktop.csproj
+++ b/Samples/Zero.Desktop/Zero.Desktop.csproj
@@ -26,7 +26,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="NewLife.Stardust" Version="3.6.2026.102" />
+ <PackageReference Include="NewLife.Stardust" Version="3.6.2026.108-beta1810" />
<PackageReference Include="System.Speech" Version="10.0.1" />
</ItemGroup>
diff --git a/Samples/Zero.RpcServer/Zero.RpcServer.csproj b/Samples/Zero.RpcServer/Zero.RpcServer.csproj
index 7402dbc..7255bd0 100644
--- a/Samples/Zero.RpcServer/Zero.RpcServer.csproj
+++ b/Samples/Zero.RpcServer/Zero.RpcServer.csproj
@@ -21,8 +21,8 @@
<ItemGroup>
<PackageReference Include="NewLife.Redis" Version="6.4.2026.104" />
- <PackageReference Include="NewLife.Stardust" Version="3.6.2026.102" />
- <PackageReference Include="NewLife.XCode" Version="11.23.2026.106-beta0914" />
+ <PackageReference Include="NewLife.Stardust" Version="3.6.2026.108-beta1810" />
+ <PackageReference Include="NewLife.XCode" Version="11.23.2026.109-beta0820" />
</ItemGroup>
<ItemGroup>
diff --git a/Samples/ZeroServer/ZeroServer.csproj b/Samples/ZeroServer/ZeroServer.csproj
index 57fbd04..876482c 100644
--- a/Samples/ZeroServer/ZeroServer.csproj
+++ b/Samples/ZeroServer/ZeroServer.csproj
@@ -19,10 +19,10 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="NewLife.Cube.Core" Version="6.8.2026.103-beta0958" />
+ <PackageReference Include="NewLife.Cube.Core" Version="6.8.2026.110-beta0606" />
<PackageReference Include="NewLife.Redis" Version="6.4.2026.104" />
- <PackageReference Include="NewLife.Stardust.Extensions" Version="3.6.2026.102" />
- <PackageReference Include="NewLife.XCode" Version="11.23.2026.106-beta0914" />
+ <PackageReference Include="NewLife.Stardust.Extensions" Version="3.6.2026.108-beta1810" />
+ <PackageReference Include="NewLife.XCode" Version="11.23.2026.109-beta0820" />
</ItemGroup>
<ItemGroup>
diff --git a/Test/Test.csproj b/Test/Test.csproj
index 42ea18e..43e0d42 100644
--- a/Test/Test.csproj
+++ b/Test/Test.csproj
@@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
- <PackageReference Include="NewLife.Core" Version="11.10.2026.108-beta1002" />
+ <PackageReference Include="NewLife.Core" Version="11.10.2026.110-beta1552" />
</ItemGroup>
<ItemGroup>
diff --git a/XUnitTest/HttpMessageTests.cs b/XUnitTest/HttpMessageTests.cs
new file mode 100644
index 0000000..cf5c9d5
--- /dev/null
+++ b/XUnitTest/HttpMessageTests.cs
@@ -0,0 +1,114 @@
+using System;
+using System.Text;
+using System.ComponentModel;
+using NewLife.Data;
+using NewLife.Remoting.Http;
+using Xunit;
+
+namespace XUnitTest;
+
+public class HttpMessageTests
+{
+ [Theory]
+ [InlineData("GET", "/")]
+ [InlineData("POST", "/api")]
+ [InlineData("PUT", "/a/b")]
+ [InlineData("DELETE", "/items/1")]
+ [InlineData("PATCH", "/items/1")]
+ [InlineData("HEAD", "/health")]
+ [InlineData("OPTIONS", "*")]
+ [DisplayName("Read可以解析请求行并填充Method与Uri")]
+ public void ReadCanParseMethodAndUri(String method, String uri)
+ {
+ var text = $"{method} {uri} HTTP/1.1\r\nHost:example\r\n\r\n";
+ var pk = new ArrayPacket(Encoding.ASCII.GetBytes(text));
+
+ var msg = new HttpMessage();
+ var ok = msg.Read(pk);
+
+ Assert.True(ok);
+ Assert.Equal(method, msg.Method);
+ Assert.Equal(uri, msg.Uri);
+ }
+
+ [Fact(DisplayName = "ParseHeaders解析Host时不截断端口")]
+ public void ParseHeaders_ShouldKeepPortInHostHeader()
+ {
+ var text = "GET / HTTP/1.1\r\nHost: 127.0.0.1:8080\r\n\r\n";
+ var pk = new ArrayPacket(Encoding.ASCII.GetBytes(text));
+
+ var msg = new HttpMessage();
+ Assert.True(msg.Read(pk));
+
+ Assert.True(msg.ParseHeaders());
+ Assert.NotNull(msg.Headers);
+ Assert.Equal("127.0.0.1:8080", msg.Headers["Host"]);
+ }
+
+ [Fact(DisplayName = "ParseHeaders解析Content-Length并忽略大小写")]
+ public void ParseHeaders_ShouldParseContentLength_IgnoringCase()
+ {
+ var text = "POST /api HTTP/1.1\r\ncontent-length: 123\r\n\r\n";
+ var pk = new ArrayPacket(Encoding.ASCII.GetBytes(text));
+
+ var msg = new HttpMessage();
+ Assert.True(msg.Read(pk));
+
+ Assert.True(msg.ParseHeaders());
+ Assert.Equal(123, msg.ContentLength);
+ }
+
+ [Fact(DisplayName = "ParseHeaders支持冒号两侧空白并裁剪")]
+ public void ParseHeaders_ShouldTrimWhitespaceAroundNameAndValue()
+ {
+ var text = "GET / HTTP/1.1\r\n Host\t:\t127.0.0.1:8080 \r\n\r\n";
+ var pk = new ArrayPacket(Encoding.ASCII.GetBytes(text));
+
+ var msg = new HttpMessage();
+ Assert.True(msg.Read(pk));
+
+ Assert.True(msg.ParseHeaders());
+ Assert.Equal("127.0.0.1:8080", msg.Headers["Host"]);
+ }
+
+ [Fact(DisplayName = "ParseHeaders支持空值头部")]
+ public void ParseHeaders_ShouldAllowEmptyHeaderValue()
+ {
+ var text = "GET / HTTP/1.1\r\nX-Empty:\r\n\r\n";
+ var pk = new ArrayPacket(Encoding.ASCII.GetBytes(text));
+
+ var msg = new HttpMessage();
+ Assert.True(msg.Read(pk));
+
+ Assert.True(msg.ParseHeaders());
+ Assert.Equal(String.Empty, msg.Headers["X-Empty"]);
+ }
+
+ [Fact(DisplayName = "ParseHeaders遇到不规范请求行不抛异常")]
+ public void ParseHeaders_ShouldNotThrow_OnInvalidRequestLine()
+ {
+ // 缺少空格分隔,ParseHeaders 不应该抛出异常
+ var text = "GET/ HTTP/1.1\r\nHost:example\r\n\r\n";
+ var pk = new ArrayPacket(Encoding.ASCII.GetBytes(text));
+
+ var msg = new HttpMessage();
+ Assert.True(msg.Read(pk));
+
+ var ex = Record.Exception(() => msg.ParseHeaders());
+ Assert.Null(ex);
+ }
+
+ [Fact(DisplayName = "ParseHeaders不要忽略名称为空白的头部行")]
+ public void ParseHeaders_ShouldNotIgnore_WhitespaceOnlyHeaderName()
+ {
+ var text = "GET / HTTP/1.1\r\n : value\r\nHost:example\r\n\r\n";
+ var pk = new ArrayPacket(Encoding.ASCII.GetBytes(text));
+
+ var msg = new HttpMessage();
+ Assert.True(msg.Read(pk));
+
+ Assert.True(msg.ParseHeaders());
+ Assert.True(msg.Headers.ContainsKey("") );
+ Assert.Equal("example", msg.Headers["Host"]);
+ }
+}
diff --git a/XUnitTest/XUnitTest.csproj b/XUnitTest/XUnitTest.csproj
index ad7e021..f6d8fdf 100644
--- a/XUnitTest/XUnitTest.csproj
+++ b/XUnitTest/XUnitTest.csproj
@@ -12,7 +12,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="Moq" Version="4.20.72" />
- <PackageReference Include="NewLife.Core" Version="11.10.2026.108-beta1002" />
+ <PackageReference Include="NewLife.Core" Version="11.10.2026.110-beta1552" />
<PackageReference Include="NewLife.UnitTest" Version="1.1.2026.102" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">