using System.ComponentModel;
using System.Diagnostics;
using System.Net;
using System.Text;
using NewLife.Data;
using NewLife.Http;
using NewLife.Net;
using Xunit;
namespace XUnitTest.Http;
/// <summary>分å—å“应体在「éžå‘é€é˜Ÿåˆ—出å£ã€ä¸‹çš„奿Ÿ„归还测试</summary>
/// <remarks>
/// <para>分å—å“应体默认走å‘é€é˜Ÿåˆ—出å£ï¼ˆæ‰¿è½½ä¼šè¯æ˜¯æ´»åЍ TcpSession æ—¶ï¼‰ï¼Œå¥æŸ„所有æƒéšå…¥é˜Ÿè½¬ç§»ç»™ç®¡é“。</para>
/// <para>其余情形退化为借用è¯ä¹‰çš„ç›´å‘ï¼šå¥æŸ„ä¸è½¬ç§»ï¼Œå¿…须由本层归还,å¦åˆ™æ¯å—泄æ¼ä¸€ä¸ªæ± 缓冲。UDP 承载的 HttpSession å³èµ°æ¤è·¯ï¼ˆUdpSession 䏿˜¯ TcpSession)。</para>
/// </remarks>
[Collection("Net.D")]
public class HttpChunkedSendTests
{
#region 辅助
/// <summary>ä¸å¯å¯»å€æ•°æ®æµï¼šé•¿åº¦æœªçŸ¥ï¼Œè¿«ä½¿å“应体走分å—ä¼ è¾“</summary>
private sealed class NonSeekableStream : Stream
{
private readonly Byte[] _data;
private Int32 _pos;
/// <summary>创建ä¸å¯å¯»å€æ•°æ®æµ</summary>
/// <param name="data">æ•°æ®å†…容</param>
public NonSeekableStream(Byte[] data) => _data = data;
/// <summary>是å¦å¯è¯»</summary>
public override Boolean CanRead => true;
/// <summary>是å¦å¯å¯»å€</summary>
public override Boolean CanSeek => false;
/// <summary>是å¦å¯å†™</summary>
public override Boolean CanWrite => false;
/// <summary>长度。ä¸å¯å¯»å€ï¼Œå–长度å³ä¸æ”¯æŒ</summary>
public override Int64 Length => throw new NotSupportedException();
/// <summary>当å‰ä½ç½®ã€‚åªè¯»æŽ¨è¿›ï¼Œä¸æ”¯æŒè®¾ç½®</summary>
public override Int64 Position
{
get => _pos;
set => throw new NotSupportedException();
}
/// <summary>æ— ç¼“å†²ï¼Œç©ºå®žçŽ°</summary>
public override void Flush() { }
/// <summary>è¯»å–æ•°æ®</summary>
/// <param name="buffer">ç›®æ ‡ç¼“å†²</param>
/// <param name="offset">èµ·å§‹åç§»</param>
/// <param name="count">最大读å–é‡</param>
/// <returns>实际读å–å—节数</returns>
public override Int32 Read(Byte[] buffer, Int32 offset, Int32 count)
{
var n = Math.Min(count, _data.Length - _pos);
if (n <= 0) return 0;
Array.Copy(_data, _pos, buffer, offset, n);
_pos += n;
return n;
}
/// <summary>ä¸å¯å¯»å€</summary>
public override Int64 Seek(Int64 offset, SeekOrigin origin) => throw new NotSupportedException();
/// <summary>åªè¯»</summary>
public override void SetLength(Int64 value) => throw new NotSupportedException();
/// <summary>åªè¯»</summary>
public override void Write(Byte[] buffer, Int32 offset, Int32 count) => throw new NotSupportedException();
}
/// <summary>回分å—å“应体的处ç†å™¨</summary>
private sealed class ChunkedHandler : HttpSession
{
/// <summary>å“åº”ä½“é•¿åº¦ã€‚å– 64K 读å—的整数å€ä¹‹å¤–,确ä¿åˆ†å‡ºå¤šå—</summary>
public Int32 BodySize { get; set; } = 200 * 1024;
/// <summary>处ç†è¯·æ±‚,返回长度未知的æµå¼å“应体</summary>
/// <param name="request">请求</param>
/// <param name="data">æ•°æ®å¸§</param>
/// <returns>å“应</returns>
protected override HttpResponse ProcessRequest(HttpRequest request, IData data)
=> new() { BodyStream = new NonSeekableStream(new Byte[BodySize]) };
}
/// <summary>æ•获直å‘奿Ÿ„的网络会è¯ã€‚åªè®°å½•ä¸å®žé™…å‘é€ï¼Œä¾¿äºŽæ–è¨€å¥æŸ„是å¦å½’还</summary>
private sealed class CapturingSession : NetSession
{
/// <summary>ç» Send(IPacket) ç›´å‘(借用è¯ä¹‰ï¼‰çš„奿Ÿ„</summary>
public readonly List<IPacket> Packets = [];
/// <summary>é‡å†™å‘é€ï¼Œè®°å½•奿Ÿ„åŽä¸å†ä¸‹å‘</summary>
/// <param name="data">è¦å‘é€çš„æ•°æ®åŒ…</param>
/// <returns>当å‰ä¼šè¯å®žä¾‹</returns>
public override INetSession Send(IPacket data)
{
lock (Packets) Packets.Add(data);
return this;
}
/// <summary>å·²æ•èŽ·å¥æŸ„æ•°</summary>
public Int32 Count { get { lock (Packets) return Packets.Count; } }
/// <summary>å–å·²æ•èŽ·å¥æŸ„的快照</summary>
/// <returns>奿Ÿ„数组</returns>
public IPacket[] Snapshot() { lock (Packets) return [.. Packets]; }
}
/// <summary>以 UDP 承载 Http çš„æœåŠ¡ç«¯ã€‚UDP 承载会è¯ä¸æ˜¯ TcpSession,故分å—å“应体走借用直å‘</summary>
private sealed class UdpHttpServer : HttpServer
{
/// <summary>本æœåŠ¡ç«¯åˆ›å»ºå¹¶æ•获的会è¯</summary>
public volatile CapturingSession? Captured;
/// <summary>创建会è¯ï¼Œæ¢ç”¨å¯æ•èŽ·å¥æŸ„的会è¯ç±»åž‹</summary>
/// <param name="session">底层Socket会è¯</param>
/// <returns>创建的网络会è¯å®žä¾‹</returns>
protected override INetSession CreateSession(ISocketSession session)
{
// Host åªä½œä¸º INetSession æˆå‘˜æš´éœ²ï¼Œé¡»æŒ‰æŽ¥å£èµ‹å€¼
var ns = new CapturingSession { Server = session.Server, Session = session };
((INetSession)ns).Host = this;
Captured = ns;
return ns;
}
/// <summary>创建处ç†å™¨ï¼Œè¿”回分å—å“应处ç†å™¨</summary>
/// <param name="session">网络会è¯</param>
/// <returns>网络处ç†å™¨</returns>
public override INetHandler? CreateHandler(INetSession session) => new ChunkedHandler();
}
#endregion
[Fact]
[DisplayName("HTTP_分å—å“应体_éžå‘é€é˜Ÿåˆ—出å£_æ¯å—奿Ÿ„已归还")]
public async Task ChunkedBody_DirectSend_ReleasesChunkHandles()
{
using var server = new UdpHttpServer { Port = 0, ProtocolType = NetType.Udp };
server.Start();
var request = Encoding.ASCII.GetBytes("GET /chunked HTTP/1.1\r\nHost: 127.0.0.1\r\n\r\n");
using (var udp = new System.Net.Sockets.UdpClient())
{
udp.Send(request, request.Length, new IPEndPoint(IPAddress.Loopback, server.Port));
}
var sw = Stopwatch.StartNew();
// ç‰ UDP æ•°æ®æŠ¥è¢«æœåŠ¡ç«¯å¤„ç†å¹¶å»ºå‡ºä¼šè¯ï¼ˆæŽ¥æ”¶åœ¨æœåŠ¡ç«¯çº¿ç¨‹ä¸Šå¼‚æ¥è¿›è¡Œï¼‰
while (server.Captured == null)
{
Assert.True(sw.ElapsedMilliseconds < 10_000, $"ç‰å¾… UDP 会è¯åˆ›å»ºè¶…时。Port={server.Port}ï¼›Servers={server.Servers.Count}");
await Task.Delay(20);
}
var session = server.Captured!;
// 分å—ç›´å‘æ˜¯åŒæ¥çš„ï¼Œå¥æŸ„按åºè¿›å…¥æ•获列表。
// å…ˆç‰åˆ°å“应至少写出「头 + 首å—ã€ï¼šä»…以"计数ä¸å†å¢žé•¿"判æ–完æˆï¼Œä¼šåœ¨"å“应尚未开始写"æ—¶
// 误判为已完æˆï¼ˆå¹¶è¡Œè´Ÿè½½ä¸‹çº¿ç¨‹è°ƒåº¦å»¶è¿Ÿå¯è¾¾æ•°å毫秒),得到 0 ä¸ªå¥æŸ„而æ–言失败
while (session.Count < 2)
{
Assert.True(sw.ElapsedMilliseconds < 20_000, "ç‰å¾…分å—å“应开始写出超时");
await Task.Delay(20);
}
// å†ç‰æ•´æ¡å“应写完:连ç»ä¸¤æ¬¡é‡‡æ ·ä¸å†å¢žé•¿
Int32 last;
do
{
last = session.Count;
await Task.Delay(20);
Assert.True(sw.ElapsedMilliseconds < 20_000, "ç‰å¾…分å—å“应写出超时");
} while (session.Count != last);
// å“应头 1 个 + 分å—若干个(æ¯å—ç”± BuildChunk å€Ÿå‡ºæ± åŒ–å¥æŸ„)
var owners = session.Snapshot().OfType<OwnerPacket>().ToList();
Assert.True(owners.Count >= 2, $"应æ•获到å“应头与分å—奿Ÿ„ï¼Œå®žé™…åªæœ‰ {owners.Count} 个");
// ç›´å‘为借用è¯ä¹‰ï¼šå¥æŸ„ä¸è½¬ç§»ï¼Œæœ¬å±‚必须归还(引用计数归零å³å·²å½’è¿˜æ± ç¼“å†²ï¼‰
Assert.All(owners, pk => Assert.Equal(0, pk.RefCount));
}
}
|