NewLife/NewLife.Remoting

优化HttpMessage请求行与头部解析,增强兼容性

重构HttpCodec识别HTTP请求逻辑,支持多种HTTP方法;
HttpMessage.Read高性能解析请求行,ParseHeaders基于Span逐行解析头部,支持Value含冒号;
新增单元测试,覆盖Method/Uri解析、Host端口、Content-Length大小写等场景,提升健壮性。
大石头 authored at 2026-01-11 00:29:31
c079b9c
Tree
1 Parent(s) a841bf3
Summary: 11 changed files with 204 additions and 32 deletions.
Modified +1 -1
Modified +6 -7
Modified +71 -12
Modified +1 -1
Modified +3 -3
Modified +1 -1
Modified +2 -2
Modified +3 -3
Modified +1 -1
Added +114 -0
Modified +1 -1
Modified +1 -1
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" />
Modified +6 -7
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);
Modified +71 -12
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;
 
         // 内容长度
Modified +1 -1
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>
Modified +3 -3
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>
Modified +1 -1
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>
 
Modified +2 -2
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>
Modified +3 -3
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>
Modified +1 -1
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>
Added +114 -0
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"]);
+    }
+}
Modified +1 -1
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">