using System.Buffers;
using System.Net.Sockets;
using NewLife.Data;
using NewLife.Log;
using NewLife.Messaging;
namespace NewLife.Net;
/// <summary>增强TCP客户端(å‘é€ï¼šå†™é”串行ã€ç›´å‘æ ¸å¿ƒä¸Žæµå¼/文件/消æ¯å‘é€ï¼‰</summary>
/// <remarks>
/// <para>å‘é€å‡ºå£å…±ç”¨ä¸€æŠŠå†™é”(<see cref="_writeLock"/>ï¼‰ï¼Œä»»ä¸€æ—¶åˆ»åªæœ‰ä¸€ä½å†™è€…在写套接å—;失败在é”外上报并按失败关é—会è¯ã€‚</para>
/// <para>ç›´å‘:<see cref="SessionBase.Send(IPacket)"/> é”å†…åŒæ¥ç›´å†™ï¼ˆ0 åˆ†é… 0 æ‹·è´ï¼Œé“¾å¼åŒ…散列写一次系统调用æäº¤æ•´æ¡é“¾ï¼‰ï¼Œ
/// <see cref="TcpSession.SendAsync(IPacket, CancellationToken)"/> é”内异æ¥ç›´å†™ï¼›æµå¼/文件/消æ¯å‘é€è§å¯¹åº”ç« èŠ‚ï¼Œå‡ºç«™é˜Ÿåˆ—ï¼ˆç¬¬äºŒå‡ºå£ï¼‰ä¸Žå‘逿³µè§ TcpSession.SendQueue。</para>
/// <para>æ•°æ®æµï¼šå…¥å£ → å‘é€éª¨æž¶ SendUnderLock/SendUnderLockAsync(ç‰é” → 写 → 释放 → é”外上报)→ å†™æ ¸å¿ƒï¼ˆSSL æµ / 散列写)→ å†…æ ¸ï¼›
/// å¹³å°å·®å¼‚(Span/数组ã€å¼‚æ¥å†™é™çº§ã€å†…æ ¸é›¶æ‹·è´ï¼‰ç» TcpSession.Platform。</para>
/// </remarks>
public partial class TcpSession
{
#region å‘é€
// å‘é€å‡ºå£ï¼šé»˜è®¤ç›´å‘ï¼Œå¦æœ‰å¯é€‰çš„å‘é€é˜Ÿåˆ—作第二出å£ã€‚两者共用一把写é”ï¼Œä»»ä¸€æ—¶åˆ»åªæœ‰ä¸€ä½å†™è€…在写套接å—。
// 例外:SSL æ¡æ‰‹å†™ä¸ç»å†™é”ï¼ˆå®¢æˆ·ç«¯æ¡æ‰‹å‘生在打开æµç¨‹å†…ï¼Œæ¤æ—¶ä¼šè¯å°šæœªå¯¹ä¸šåŠ¡å¯ç”¨ã€ä¸å¯èƒ½æœ‰å¹¶å‘å‘é€ï¼‰ï¼›
// æœåŠ¡ç«¯æ¡æ‰‹åˆ™ç”± TcpSession.StartSslAsync 抢å 写é”åŽæ‰å‘èµ·ï¼Œæ¡æ‰‹æœŸé—´åˆ°è¾¾çš„å‘é€ä¼šç‰å¾…,é¿å…明文写进TLSæµã€‚
// Send(IPacket/byte[]/Span) é”å†…åŒæ¥ç›´å†™ï¼š0 åˆ†é… 0 æ‹·è´ï¼Œå†™å®Œæ‰è¿”回,失败返回 -1
// SendAsync(IPacket) é”内异æ¥ç›´å†™ï¼š0 åˆ†é… 0 æ‹·è´ï¼Œç‰å¾…å¯å†™æœŸé—´ä¸å 线程
// SendAsync(Stream) æµå¼ï¼šè¯»ä¸€å— → 写完(或挂起)å†è¯»ä¸‹ä¸€å—ï¼Œå†…æ ¸ç¼“å†²å³èƒŒåŽ‹ï¼Œå†…å˜æœ‰ç•Œ
// SendFileAsync(文件) é”å†…è®©å†…æ ¸é›¶æ‹·è´æŽ¨æ–‡ä»¶ï¼ˆSendFile/TransmitFile)
// æ…¢å¯¹ç«¯ç”±å†…æ ¸å‘é€ç¼“冲形æˆå¤©ç„¶èƒŒåŽ‹ï¼›éœ€è¦æŽ’é˜Ÿ/陿µçš„场åˆå¯åœ¨ä¸Šå±‚用 Actor 组åˆï¼Œä¸è¿›å‘逿 ¸å¿ƒã€‚
// 批é‡åœºæ™¯å¯é€‰èµ° TcpSession.SendQueueï¼ˆè§ TcpSession.SendQueue.cs 出站队列与å‘逿³µï¼‰ï¼Œç”±å‘逿³µåœ¨é”内批é‡å†™å‡ºã€‚
// ç›´å‘å…¥å£å…±ç”¨å‘é€éª¨æž¶ SendUnderLock/SendUnderLockAsync:ç‰é” → 写 → 释放 → é”外上报(失败上报/å…³é—必须在é”å¤–ï¼Œé˜²å›žè°ƒå†…å† Send æ»é”)。
/// <summary>写é”。四个å‘é€å…¥å£å…±ç”¨ï¼šä»»ä¸€æ—¶åˆ»åªæœ‰ä¸€ä½å†™è€…在写套接å—,故多线程/å¤šå…¥å£æ··ç”¨ä¸ä¼šäº¤é”™</summary>
private readonly SemaphoreSlim _writeLock = new(1, 1);
/// <summary>获å–ç»å‘æ€»é¢„ç®—çš„æˆªæ¢æ—¶é—´ã€‚返回 0 表示ä¸é™åˆ¶ï¼ˆTimeout 未å¯ç”¨ï¼‰</summary>
private Int64 GetSendDeadline() => Timeout > 0 ? Runtime.TickCount64 + Timeout : 0;
private Int32 _bsize;
/// <summary>按本次å‘é€é‡è°ƒä¼˜å†…æ ¸å‘é€ç¼“冲(_bsize 缓å˜ï¼Œè¯»å– SendBufferSize 耗时很大)</summary>
/// <remarks>
/// <para>è°ƒç”¨æ–¹é¡»å·²æŒæœ‰å†™é”:套接å—傿•°è°ƒæ•´ä¸èƒ½ä¸Žé”内的 Send/Write å¹¶å‘。</para>
/// <para>_bsize åªå¢žä¸å‡ï¼Œä»…åŒæ¥ç›´å†™è·¯å¾„使用;异æ¥ç›´å†™ä¸Žæµå¼åˆ†å—ä¸è°ƒæ•´å†…æ ¸ç¼“å†²ã€‚</para>
/// </remarks>
/// <param name="sock">ç›®æ ‡å¥—æŽ¥å—</param>
/// <param name="count">本次å‘é€å—节数</param>
private void TuneSendBufferSize(Socket sock, Int32 count)
{
if (_bsize == 0) _bsize = sock.SendBufferSize;
if (_bsize < count) sock.SendBufferSize = _bsize = count;
}
/// <summary>å‘é€ä¸€æ®µæ•°æ®ï¼ŒçŸè®¡æ•°è‡ªåЍç»å‘。返回时全部å—èŠ‚å·²äº¤ç»™å†…æ ¸ï¼Œå¤±è´¥æŠ›å¼‚å¸¸</summary>
/// <remarks>åŒæ¥ Send 在接收方窗å£å—陿—¶å¯èƒ½åªå‘出一部分;循环ç»å‘直到å‘完,总耗时超过预算按超时失败。异常由调用方统一转为å‘é€å¤±è´¥å¤„ç†</remarks>
/// <param name="sock">ç›®æ ‡å¥—æŽ¥å—</param>
/// <param name="buffer">æ•°æ®ç¼“冲</param>
/// <param name="offset">èµ·å§‹åç§»</param>
/// <param name="count">å—节数</param>
/// <param name="deadline">ç»å‘æ€»é¢„ç®—æˆªæ¢æ—¶é—´ï¼ˆRuntime.TickCount64 毫秒),0 ä¸é™åˆ¶</param>
/// <returns>å·²å‘é€å—节数,ç‰äºŽ count</returns>
private static Int32 SendAll(Socket sock, Byte[] buffer, Int32 offset, Int32 count, Int64 deadline)
{
var total = 0;
while (total < count)
{
var sent = sock.Send(buffer, offset + total, count - total, SocketFlags.None);
if (sent <= 0) throw new IOException(L.T("Send failed (result={0}), {1}/{2} bytes sent", sent, total, count));
total += sent;
// ç»å‘剿£€æŸ¥æ€»é¢„算:对端æŒç»æ…¢è¯»æ—¶ï¼Œå•次 SendTimeout 约æŸä¸ä½æ•´åŒ…å‘é€çš„æ€»è€—æ—¶
if (deadline > 0 && total < count && Runtime.TickCount64 > deadline)
throw new TimeoutException(L.T("Send timeout, {0}/{1} bytes sent", total, count));
}
return total;
}
/// <summary>å‘é€å¤šæ®µæ•°æ®ï¼ˆscatter-gather),çŸè®¡æ•°åŽæŒ‰æ®µåºè·³è¿‡å·²å‘å—èŠ‚é€æ®µç»å‘</summary>
/// <param name="sock">ç›®æ ‡å¥—æŽ¥å—</param>
/// <param name="segments">æ•°æ®æ®µåˆ—表</param>
/// <param name="total">总å—节数</param>
/// <param name="deadline">ç»å‘æ€»é¢„ç®—æˆªæ¢æ—¶é—´ï¼ˆRuntime.TickCount64 毫秒),0 ä¸é™åˆ¶</param>
/// <returns>å·²å‘é€å—节数,ç‰äºŽ total</returns>
private static Int32 SendAll(Socket sock, IList<ArraySegment<Byte>> segments, Int32 total, Int64 deadline)
{
// å…ˆå°è¯•一次多段å‘é€ï¼ˆå¹³å°å¯åˆå¹¶ç³»ç»Ÿè°ƒç”¨ï¼‰ï¼›æ£å¸¸è·¯å¾„一次å‘完直接返回
var sent = sock.Send(segments);
if (sent <= 0) throw new IOException(L.T("Send failed (result={0}), 0/{1} bytes sent", sent, total));
if (sent >= total) return sent;
// çŸè®¡æ•°ï¼šæŒ‰æ®µåºè·³è¿‡å·²å‘å—èŠ‚ï¼Œé€æ®µç»å‘(å¤ç”¨åŽŸæ®µç¼“å†²ï¼Œé›¶æ‹·è´ï¼‰
var skip = sent;
foreach (var seg in segments)
{
if (skip >= seg.Count)
{
skip -= seg.Count;
continue;
}
sent += SendAll(sock, seg.Array!, seg.Offset + skip, seg.Count - skip, deadline);
skip = 0;
}
return sent;
}
/// <summary>å‘é€éª¨æž¶ï¼ˆåŒæ¥ï¼‰ï¼šç‰å†™é” → 写 → 释放 → é”外上报/å…³é—</summary>
/// <remarks>
/// <para>失败上报与关é—必须放在é”外:OnError/Close 会触å‘ç”¨æˆ·äº‹ä»¶å›žè°ƒï¼Œå›žè°ƒå†…å†æ¬¡ Send 会在åŒä¸€çº¿ç¨‹äºŒæ¬¡è¿›å…¥å†™é”而æ»é”。</para>
/// <para>å†™åŠ¨ä½œç”¨é™æ€ lambda ä¼ å…¥ï¼ˆstate æºå¸¦æ•°æ®åŒ…ç‰è¾“入),é¿å…æ¯æ¬¡å‘é€äº§ç”Ÿé—包分é…ï¼›Span 入傿— 法ç»é—åŒ…ä¼ é€’ï¼Œè§ä¸‹æ–¹é‡è½½ã€‚</para>
/// <para>æµå¼/文件/消æ¯å‘逿œ‰é¢å¤–çš„å¤±è´¥åˆ†ç±»ï¼ˆè°ƒç”¨æ–¹å–æ¶ˆä¸Žæ•°æ®é”™è¯¯ä¸ä¸ŠæŠ¥ä¸å…³ä¼šè¯ï¼‰ï¼Œä¸èµ°æœ¬éª¨æž¶ï¼Œè§å„自方法注释。</para>
/// </remarks>
/// <param name="state">写动作的输入(数æ®åŒ…ç‰ï¼‰</param>
/// <param name="write">é”å†…å†™åŠ¨ä½œï¼ˆé™æ€ lambda)</param>
/// <param name="data">出错的å‘逿•°æ®ï¼Œä¾¿äºŽæ—¥å¿—定ä½</param>
/// <returns>å·²å‘é€å—节数;失败返回 -1</returns>
private Int32 SendUnderLock<TState>(TState state, Func<TState, TcpSession, Int32> write, Object? data)
{
Exception? error = null;
var rs = -1;
_writeLock.Wait();
try
{
rs = write(state, this);
}
catch (Exception ex)
{
error = ex;
}
finally
{
_writeLock.Release();
}
if (error != null)
{
ReportSendError(error, data);
return -1;
}
return rs;
}
/// <summary>å‘é€éª¨æž¶ï¼ˆåŒæ¥ï¼Œå†…å˜æ®µï¼‰ã€‚Span ä¸èƒ½è¢«é—包æ•获,å•独æä¾›é‡è½½ï¼›å…¶ä½™è¯ä¹‰åŒ <see cref="SendUnderLock{TState}"/></summary>
/// <param name="data">æ•°æ®</param>
/// <param name="write">é”å†…å†™åŠ¨ä½œï¼ˆé™æ€ lambda)</param>
/// <returns>å·²å‘é€å—节数;失败返回 -1</returns>
private Int32 SendUnderLock(ReadOnlySpan<Byte> data, MemoryWriter write)
{
Exception? error = null;
var rs = -1;
_writeLock.Wait();
try
{
rs = write(data, this);
}
catch (Exception ex)
{
error = ex;
}
finally
{
_writeLock.Release();
}
if (error != null)
{
ReportSendError(error, null);
return -1;
}
return rs;
}
/// <summary>å†…å˜æ®µå†™åŠ¨ä½œï¼ˆè‡ªå®šä¹‰å§”æ‰˜ï¼šBCL çš„ Func 䏿”¯æŒ ref struct 傿•°ï¼‰</summary>
/// <param name="data">æ•°æ®</param>
/// <param name="session">会è¯</param>
/// <returns>å·²å‘é€å—节数</returns>
private delegate Int32 MemoryWriter(ReadOnlySpan<Byte> data, TcpSession session);
/// <summary>å‘é€éª¨æž¶ï¼ˆå¼‚æ¥ï¼‰ï¼šç‰å†™é” → 写 → 释放 → é”外上报/å…³é—ï¼›ç‰å¾…å¯å†™æœŸé—´ä¸å 线程</summary>
/// <remarks>å–æ¶ˆåœ¨ç‰é”边界生效(WaitAsync);其余è¯ä¹‰åŒ <see cref="SendUnderLock{TState}"/></remarks>
/// <param name="state">写动作的输入(数æ®åŒ…ç‰ï¼‰</param>
/// <param name="write">é”å†…å†™åŠ¨ä½œï¼ˆé™æ€ lambda)</param>
/// <param name="data">出错的å‘逿•°æ®ï¼Œä¾¿äºŽæ—¥å¿—定ä½</param>
/// <param name="cancellationToken">å–æ¶ˆä»¤ç‰Œ</param>
/// <returns>å·²å‘é€å—节数;失败返回 -1</returns>
private async ValueTask<Int32> SendUnderLockAsync<TState>(TState state, Func<TState, TcpSession, ValueTask<Int32>> write, Object? data, CancellationToken cancellationToken)
{
Exception? error = null;
var rs = -1;
await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
rs = await write(state, this).ConfigureAwait(false);
}
catch (Exception ex)
{
error = ex;
}
finally
{
_writeLock.Release();
}
if (error != null)
{
ReportSendError(error, data);
return -1;
}
return rs;
}
/// <summary>ç›´å‘:é”å†…åŒæ¥å†™å‡ºæ•´ä¸ªæ•°æ®åŒ…(直接写数æ®åŒ…缓冲,0 æ‹·è´ 0 分é…),写完æ‰è¿”回</summary>
/// <remarks>写é”ä¿è¯ä»»ä¸€æ—¶åˆ»åªæœ‰ä¸€ä½å†™è€…;失败已上报并按失败关é—会è¯</remarks>
/// <param name="pk">æ•°æ®åŒ…。借用è¯ä¹‰ï¼šè°ƒç”¨æ–¹ä¿ç•™å¥æŸ„,用完自行释放</param>
/// <returns>å·²å‘é€å—节数;失败返回 -1</returns>
private Int32 DirectSend(IPacket pk)
{
if (pk == null) return -1;
if (Log != null && Log.Enable && LogSend) WriteLog("Send [{0}]: {1}", pk.Total, pk.ToHex(LogDataLength));
return SendUnderLock(pk, static (p, s) => s.WritePacket(p), pk);
}
/// <summary>ç›´å‘:é”å†…åŒæ¥å†™å‡ºä¸€æ®µæ•°æ®ï¼ˆç›´æŽ¥å†™è°ƒç”¨æ–¹ç¼“冲,0 æ‹·è´ 0 分é…),写完æ‰è¿”回</summary>
/// <param name="data">æ•°æ®ã€‚调用方需ä¿è¯è¿”回å‰ä¸æ”¹å†™è¯¥ç¼“冲</param>
/// <returns>å·²å‘é€å—节数;失败返回 -1</returns>
private Int32 DirectSend(ReadOnlySpan<Byte> data)
{
if (data.IsEmpty) return 0;
if (Log != null && Log.Enable && LogSend) WriteLog("Send [{0}]: {1}", data.Length, data.ToHex(LogDataLength));
return SendUnderLock(data, static (d, s) => s.WriteMemory(d));
}
/// <summary>ç›´å‘:é”内异æ¥å†™å‡ºæ•´ä¸ªæ•°æ®åŒ…(0 æ‹·è´ 0 分é…),ç‰å¾…å¯å†™æœŸé—´ä¸å 线程</summary>
/// <remarks>失败已上报并按失败关é—会è¯ï¼›åŒæ—¶ä½¿ç”¨å¤šä¸ªå‘é€å…¥å£æ—¶ç”±å†™é”串行化</remarks>
/// <param name="pk">æ•°æ®åŒ…。借用è¯ä¹‰ï¼šè°ƒç”¨æ–¹ä¿ç•™å¥æŸ„,用完自行释放</param>
/// <param name="cancellationToken">å–æ¶ˆä»¤ç‰Œ</param>
/// <returns>å·²å‘é€å—节数;失败返回 -1</returns>
private async ValueTask<Int32> DirectSendAsync(IPacket pk, CancellationToken cancellationToken = default)
{
if (pk == null) return -1;
cancellationToken.ThrowIfCancellationRequested();
if (Log != null && Log.Enable && LogSend) WriteLog("Send [{0}]: {1}", pk.Total, pk.ToHex(LogDataLength));
return await SendUnderLockAsync(pk, static (p, s) => s.WritePacketAsync(p), pk, cancellationToken).ConfigureAwait(false);
}
/// <summary>å†™æ ¸å¿ƒï¼ˆåŒæ¥ï¼‰ï¼šæŠŠæ•´åŒ…(å«é“¾ï¼‰å†™å®Œæ‰è¿”回;çŸè®¡æ•°è‡ªåЍç»å‘,失败抛异常</summary>
/// <param name="pk">æ•°æ®åŒ…</param>
/// <returns>å·²å‘é€å—节数</returns>
private Int32 WritePacket(IPacket pk)
{
var count = pk.Total;
if (count == 0) return 0;
var sock = Client ?? throw new InvalidOperationException(L.T("Session [{0}] is not open.", Name));
TuneSendBufferSize(sock, count);
// SSL æµå†…部处ç†éƒ¨åˆ†å†™ï¼šæ•´æ®µå†™å®Œæ‰è¿”å›žï¼Œæ— éœ€ç»å‘循环
if (_Stream is { } stream)
{
pk.CopyTo(stream);
LastTime = DateTime.Now;
return count;
}
// 一次系统调用写出整包:å•节点直接写数æ®åŒ…自己的缓冲(0 æ‹·è´ 0 分é…);
// 链å¼åŒ…用散列写(scatter-gather)一次æäº¤æ•´æ¡é“¾â€”—é€èŠ‚ç‚¹å¤šæ¬¡å†™ä¼šæŠŠä¸€æ¡é€»è¾‘帧拆æˆå¤šä¸ª TCP 段,
// 对端“整帧åŒçª—到达â€çš„é›¶æ‹·è´å¿«è·¯å¾„éšä¹‹å¤±æ•ˆï¼ˆåŒä¸€æ¬¡è§£è¯»å‡ºå†…å˜è§†å›¾è€Œéžæµå¼ä½“)
SendPacketData(sock, pk, count, GetSendDeadline());
LastTime = DateTime.Now;
return count;
}
/// <summary>åŒæ¥å†™ä¸€å—内å˜ï¼ˆé›¶æ‹·è´ï¼šç›´æŽ¥å†™è°ƒç”¨æ–¹ç¼“冲),çŸè®¡æ•°è‡ªåЍç»å‘</summary>
/// <param name="data">æ•°æ®</param>
/// <returns>å·²å‘é€å—节数</returns>
private Int32 WriteMemory(ReadOnlySpan<Byte> data)
{
var sock = Client ?? throw new InvalidOperationException(L.T("Session [{0}] is not open.", Name));
Int32 rs;
TuneSendBufferSize(sock, data.Length);
if (_Stream is { } stream)
{
// SSL æµå†…部处ç†éƒ¨åˆ†å†™ï¼šæ•´æ®µå†™å®Œæ‰è¿”å›žï¼Œå¤±è´¥ç›´æŽ¥æŠ›å¼‚å¸¸ï¼Œæ— éœ€ç»å‘循环
WriteSsl(stream, data);
rs = data.Length;
}
else
{
// åŒæ¥ Send 在接收方窗å£å—陿—¶å¯èƒ½åªå‘出一部分,ç»å‘循环ä¿è¯æ•´æ®µé€å‡ºï¼›æ€»é¢„ç®—æŒ‰ä¼šè¯ Timeout 计时
rs = SendAllMemory(sock, data, GetSendDeadline());
}
LastTime = DateTime.Now;
return rs;
}
/// <summary>å‘é€å¤±è´¥ä¸ŠæŠ¥ï¼šè®°é”™è¯¯æ—¥å¿—å¹¶å…³é—会è¯</summary>
/// <remarks>调用方必须已释放写é”:OnError/Close 会触å‘ç”¨æˆ·äº‹ä»¶å›žè°ƒï¼Œå›žè°ƒå†…å†æ¬¡ Send 会二次进入写é”而æ»é”</remarks>
/// <param name="error">å‘é€å¼‚常</param>
/// <param name="data">出错的å‘逿•°æ®ï¼Œä¾¿äºŽæ—¥å¿—定ä½</param>
private void ReportSendError(Exception error, Object? data)
{
if (error.IsDisposed()) return;
if (Tracer is { } tracer)
{
using var span = tracer.NewSpan($"net:{Name}:Send");
span?.SetError(error, data);
}
OnError("Send", error);
// å‘é€å¼‚常å¯èƒ½æ˜¯è¿žæŽ¥å‡ºäº†é—®é¢˜ï¼Œéœ€è¦å…³é—
Close("SendError");
}
/// <summary>å‘逿•°æ®åŒ…ï¼ˆåŒæ¥ç›´å‘):写完æ‰è¿”回</summary>
/// <param name="data">æ•°æ®åŒ…。借用è¯ä¹‰ï¼šè°ƒç”¨æ–¹ä¿ç•™å¥æŸ„,用完自行释放</param>
/// <returns>å·²å‘é€å—节数;失败返回 -1</returns>
protected override Int32 OnSend(IPacket data) => DirectSend(data);
/// <summary>å‘逿•°æ®ï¼ˆåŒæ¥ç›´å‘):写完æ‰è¿”回</summary>
/// <param name="data">æ•°æ®</param>
/// <returns>å·²å‘é€å—节数;失败返回 -1</returns>
protected override Int32 OnSend(ArraySegment<Byte> data)
=> data.Array == null || data.Count <= 0 ? 0 : DirectSend(new ReadOnlySpan<Byte>(data.Array, data.Offset, data.Count));
/// <summary>å‘逿•°æ®ï¼ˆåŒæ¥ç›´å‘):写完æ‰è¿”回</summary>
/// <param name="data">æ•°æ®</param>
/// <returns>å·²å‘é€å—节数;失败返回 -1</returns>
protected override Int32 OnSend(ReadOnlySpan<Byte> data) => DirectSend(data);
#endregion
#region æµå¼å‘é€
/// <summary>æµå¼å‘é€çš„读å—大å°</summary>
/// <remarks>64KB:基准实测 16KB 分å—在回环下的åžååªæœ‰ 64KB 分å—的一åŠï¼ˆå°åˆ†å—导致两端 syscall/唤醒次数æˆå€ï¼‰ï¼Œ64KB 与整段å‘逿Œå¹³ä¸”ä»è¿œä½ŽäºŽ LOH 阈值。</remarks>
private const Int32 StreamChunkSize = 64 * 1024;
/// <summary>æµå¼å‘é€ï¼šåˆ†å—è¯»å…¥æ•°æ®æµå¹¶é€å—写出,读一å—写一å—</summary>
/// <remarks>
/// <para>å½¢æ€å¯¹é½ä¸»æµç½‘络框架的 SendAsync(Stream):默认 64KB 分å—,æ¯å—ç”±æ± åŒ–è¯»å—直接写出(0 æ‹·è´ï¼‰ï¼Œå…¨ç¨‹åªåœ¨è¯»å—上驻留,ä¸äº§ç”Ÿæ•´æ®µå†…å˜ã€‚</para>
/// <para><b>背压</b>:æ¯å—写完(或在写é”/å†…æ ¸ä¸ŠæŒ‚èµ·ï¼‰æ‰è¯»ä¸‹ä¸€å—,慢速对端ä¸ä¼šå¯¼è‡´åº”用层积压——内å˜å 用æ’为一å—读缓冲,水ä½ç”±å†…æ ¸å‘é€ç¼“冲充当。</para>
/// <para>与 <see cref="SessionBase.Send(IPacket)"/> 共用åŒä¸€æŠŠå†™é”:并å‘调用ä¸ä¼šäº¤é”™ã€‚但两者是<b>å„è‡ªç‹¬ç«‹åŠ é”</b>,
/// “头 + æµå¼ä½“â€å…ˆå‘头部å†è°ƒç”¨æœ¬æ–¹æ³•åªåœ¨å•å†™è€…åœºæ™¯ä¸‹ä¿æŒä¸€æ¡é€»è¾‘消æ¯ï¼›å¤šå†™è€…并呿—¶å¤´ä½“之间å¯èƒ½è¢«æ’入其它数æ®ï¼Œ
/// éœ€è¦æ•´æ¡æ¶ˆæ¯åŽŸåæ—¶æ”¹ç”¨ <see cref="SendMessageLockedAsync"/>。</para>
/// <para><b>打开时åº</b>:本方法在å–写é”之å‰å…ˆç¡®ä¿ä¼šè¯å·²æ‰“开。并å‘首访åŒä¸€æœªæ‰“å¼€ä¼šè¯æ—¶ï¼Œæ‰“å¼€æµç¨‹ï¼ˆå« SSL æ¡æ‰‹ï¼‰å¯èƒ½è¢«ä¸¤ä¸ªè°ƒç”¨è€…åŒæ—¶è¿›å…¥ï¼›
/// 写é”åªä¸²è¡ŒåŒ–“写套接å—â€ï¼Œä¸è¦†ç›–打开æµç¨‹ï¼Œè°ƒç”¨æ–¹åº”在å‘é€å‰å…ˆå®Œæˆæ‰“开。</para>
/// <para><b>è¶…æ—¶</b>:异æ¥å†™æ ¸å¿ƒæŒ‰â€œå—â€é‡è®¾è®¡æ—¶é¢„算(æ¯å— <see cref="SessionBase.Timeout"/>),故大æµçš„æ€»è€—时上é™çº¦ç‰äºŽ å—æ•° × Timeoutï¼›
/// ä¸ŽåŒæ¥ç›´å†™è·¯å¾„的“整段总预算â€ï¼ˆ<see cref="SessionBase.Send(IPacket)"/> 链å¼åŒ…ç»å‘)å£å¾„ä¸åŒã€‚</para>
/// <para>æµæå‰ç»“æŸï¼ˆä¸è¶³ <paramref name="length"/>)或连接故障时抛出异常,已å‘出的部分ä¸ä¼šå›žé€€ã€‚</para>
/// </remarks>
/// <param name="source">æ•°æ®æµ</param>
/// <param name="length">期望å‘é€çš„å—节数;负数表示读到æµå°¾</param>
/// <param name="cancellationToken">å–æ¶ˆä»¤ç‰Œ</param>
/// <returns>å·²å‘é€çš„æ€»å—节数</returns>
public async ValueTask<Int64> SendAsync(Stream source, Int64 length = -1, CancellationToken cancellationToken = default)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (Disposed) throw new ObjectDisposedException(GetType().Name);
if (!Open()) throw new InvalidOperationException(L.T("Session [{0}] is not open.", GetType().Name));
Exception? error = null;
var total = 0L;
await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
total = await SendStreamCoreAsync(source, length, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// è°ƒç”¨æ–¹ä¸»åŠ¨å–æ¶ˆï¼šä¸æ˜¯å‘逿•…障,ä¸ä¸ŠæŠ¥ã€ä¸å…³ä¼šè¯ï¼ˆfinally 会释放写é”)
throw;
}
catch (InvalidDataException)
{
// æµæå‰ç»“æŸå±žè°ƒç”¨æ–¹æ•°æ®é—®é¢˜ï¼Œä¸æ˜¯å‘逿•…障:ä¸ä¸ŠæŠ¥ã€ä¸å…³ä¼šè¯ï¼Œç›´æŽ¥æŠ›ï¼ˆfinally 会释放写é”)
throw;
}
catch (Exception ex)
{
error = ex;
}
finally
{
_writeLock.Release();
}
// ä¸ŠæŠ¥ä¸Žå…³é—æ”¾é”外:OnError/Close 会触å‘ç”¨æˆ·äº‹ä»¶å›žè°ƒï¼Œå›žè°ƒå†…å†æ¬¡ Send 会在åŒä¸€æŠŠå†™é”上二次ç‰å¾…而æ»é”
if (error != null)
{
ReportSendError(error, null);
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(error).Throw();
}
return total;
}
/// <summary>分å—读æµå¹¶å†™å‡ºã€‚读一å—写一å—ï¼Œå†…å˜æ’为一å—è¯»ç¼“å†²ï¼ˆè°ƒç”¨æ–¹é¡»å·²æŒæœ‰å†™é”)</summary>
/// <remarks>æ¯å—写完(或在写é”/å†…æ ¸ä¸ŠæŒ‚èµ·ï¼‰æ‰è¯»ä¸‹ä¸€å—,慢速对端ä¸ä¼šå¯¼è‡´åº”ç”¨å±‚ç§¯åŽ‹ï¼›æµæå‰ç»“æŸæ—¶æŠ¥é”™ï¼Œå·²å‘出的部分ä¸å›žé€€ã€‚</remarks>
/// <param name="source">æ•°æ®æµ</param>
/// <param name="length">期望å‘é€çš„å—节数;负数表示读到æµå°¾</param>
/// <param name="cancellationToken">å–æ¶ˆä»¤ç‰Œ</param>
/// <returns>å·²å‘é€çš„æ€»å—节数</returns>
private async ValueTask<Int64> SendStreamCoreAsync(Stream source, Int64 length, CancellationToken cancellationToken)
{
var chunk = StreamChunkSize;
var buffer = ArrayPool<Byte>.Shared.Rent(chunk);
try
{
var total = 0L;
while (length < 0 || total < length)
{
var size = chunk;
if (length > 0 && length - total < size) size = (Int32)(length - total);
var count = await source.ReadAsync(buffer, 0, size, cancellationToken).ConfigureAwait(false);
if (count <= 0) break;
// å†™å®Œè¿™ä¸€å—æ‰è¯»ä¸‹ä¸€å—ï¼šå†…å˜æ’为一å—ï¼Œæ…¢å¯¹ç«¯ç”±å†…æ ¸ç¼“å†²å½¢æˆèƒŒåŽ‹
var rs = await WriteMemoryAsync(buffer.AsMemory(0, count)).ConfigureAwait(false);
if (rs < 0) throw new IOException(L.T("Send failed on [{0}], {1} bytes sent before failure.", Name, total));
total += count;
}
// æµæå‰ç»“æŸï¼šæŠ¥é”™ï¼Œé¿å…调用方误以为已完整å‘é€
if (length > 0 && total < length) throw new InvalidDataException(L.T("Stream ended early. Expected {0} bytes but got {1}.", length, total));
return total;
}
finally
{
ArrayPool<Byte>.Shared.Return(buffer);
}
}
#endregion
#region 文件å‘é€
/// <summary>å‘逿–‡ä»¶ï¼šäº¤ç»™å†…æ ¸é›¶æ‹·è´æŽ¨é€ï¼ˆLinux sendfile / Windows TransmitFile),应用层ä¸ç»è¿‡è¯»å—æ¬è¿</summary>
/// <remarks>
/// <para><b>内å˜</b>:文件内容全程ä¸è¿›å…¥åº”用层缓冲,也没有 64KB 读å—往返,大文件å‘é€çš„ CPU 与内å˜å¼€é”€éƒ½ä½ŽäºŽæµå¼åˆ†å—。</para>
/// <para><b>背压</b>:ç‰å¾…å¯å†™æœŸé—´ä¸å çº¿ç¨‹ï¼›æ…¢é€Ÿå¯¹ç«¯ç”±å†…æ ¸å‘é€ç¼“冲形æˆèƒŒåŽ‹ï¼Œåº”ç”¨å±‚æ— ç§¯åŽ‹ã€‚</para>
/// <para>与 <see cref="SessionBase.Send(IPacket)"/> 共用åŒä¸€æŠŠå†™é”:并å‘调用ä¸ä¼šäº¤é”™ã€‚但两者是<b>å„è‡ªç‹¬ç«‹åŠ é”</b>,
/// “å“应头 + 文件体â€å…ˆå‘头部å†è°ƒç”¨æœ¬æ–¹æ³•åªåœ¨å•å†™è€…åœºæ™¯ä¸‹ä¿æŒä¸€æ¡é€»è¾‘消æ¯ï¼›å¤šå†™è€…并呿—¶å¤´ä½“之间å¯èƒ½è¢«æ’入其它数æ®ï¼Œ
/// éœ€è¦æ•´æ¡æ¶ˆæ¯åŽŸåæ—¶æ”¹ç”¨ <see cref="SendMessageLockedAsync"/>。</para>
/// <para><b>打开时åº</b>:本方法在å–写é”之å‰å…ˆç¡®ä¿ä¼šè¯å·²æ‰“开(与 <see cref="SendAsync(Stream, Int64, CancellationToken)"/> åŒæž„)。并å‘首访åŒä¸€æœªæ‰“å¼€ä¼šè¯æ—¶ï¼Œ
/// 打开æµç¨‹ï¼ˆå« SSL æ¡æ‰‹ï¼‰å¯èƒ½è¢«ä¸¤ä¸ªè°ƒç”¨è€…åŒæ—¶è¿›å…¥ï¼›å†™é”åªä¸²è¡ŒåŒ–“写套接å—â€ï¼Œä¸è¦†ç›–打开æµç¨‹ï¼Œè°ƒç”¨æ–¹åº”在å‘é€å‰å…ˆå®Œæˆæ‰“开。</para>
/// <para>SSL 会è¯ï¼ˆ<c>SslStream</c>)ã€Unix 域套接å—ï¼ˆå†…æ ¸é›¶æ‹·è´åœ¨å…¶ä¸æ—¢ä¸ä¼šæŽ¨è¿›ä¹Ÿä¸ä¼šæŠ¥é”™ï¼Œå®žæµ‹æ°¸ä¹…æŒ‚èµ·ï¼‰ä¸Žæ— é›¶æ‹·è´å‘é€ API çš„ç›®æ ‡æ¡†æž¶ï¼Œå‡é™çº§ä¸º <see cref="SendAsync(Stream, Int64, CancellationToken)"/> 的分å—读å–å‘é€ï¼Œè¯ä¹‰ä¸€è‡´ä½†å¤šä¸€æ¬¡è¯»å—æ¬è¿ã€‚</para>
/// <para>䏿”¯æŒå移与长度:需è¦å‘逿–‡ä»¶çš„æŸä¸€æ®µï¼ˆå¦‚ HTTP Range)时,用 <see cref="SendAsync(Stream, Int64, CancellationToken)"/> 定ä½åŽåˆ†å—å‘é€ã€‚</para>
/// </remarks>
/// <param name="filePath">文件路径</param>
/// <param name="cancellationToken">å–æ¶ˆä»¤ç‰Œ</param>
/// <returns>å·²å‘é€å—节数;å‘é€å¤±è´¥è¿”回 -1(已上报并按失败关é—会è¯ï¼‰</returns>
/// <exception cref="ArgumentNullException">路径为空</exception>
/// <exception cref="FileNotFoundException">文件ä¸å˜åœ¨</exception>
/// <exception cref="InvalidOperationException">ä¼šè¯æœªæ‰“å¼€</exception>
public async ValueTask<Int64> SendFileAsync(String filePath, CancellationToken cancellationToken = default)
{
if (filePath.IsNullOrEmpty()) throw new ArgumentNullException(nameof(filePath));
if (Disposed) throw new ObjectDisposedException(GetType().Name);
var fi = filePath.AsFile();
if (!fi.Exists) throw new FileNotFoundException(L.T("File not found: {0}", filePath), filePath);
if (!Open()) throw new InvalidOperationException(L.T("Session [{0}] is not open.", GetType().Name));
var length = fi.Length;
Exception? error = null;
var total = -1L;
await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
// éž SSL 会è¯ä¼˜å…ˆå°è¯•å†…æ ¸é›¶æ‹·è´ï¼ˆNET6+ï¼›SSL/Unix 域/低版本由桥内部排除并返回 -1 åŽé™çº§ï¼‰
var kernel = await TrySendFileKernelAsync(filePath, length, cancellationToken).ConfigureAwait(false);
if (kernel >= 0) return kernel;
// SSL ä¼šè¯æˆ–æ— é›¶æ‹·è´å‘é€ API 的框架:分å—读å–å‘é€ï¼ˆè¯ä¹‰ç›¸åŒï¼Œå¤šä¸€æ¬¡è¯»å—æ¬è¿ï¼‰ã€‚
// 显å¼å¯ç”¨å¼‚æ¥ IO ä¸Žé¡ºåºæ‰«æï¼šé»˜è®¤ OpenRead 得到的 FileStream 未开异æ¥ï¼ŒReadAsync ä¼šåŒæ¥é˜»å¡žçº¿ç¨‹
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, FileOptions.Asynchronous | FileOptions.SequentialScan);
total = await SendStreamCoreAsync(fs, length, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// è°ƒç”¨æ–¹ä¸»åŠ¨å–æ¶ˆï¼šä¸æ˜¯è¿žæŽ¥æ•…障,å‘上抛出(finally 会释放写é”)
throw;
}
catch (InvalidDataException)
{
// æµæå‰ç»“æŸå±žè°ƒç”¨æ–¹æ•°æ®é—®é¢˜ï¼Œä¸æ˜¯å‘逿•…障:ä¸ä¸ŠæŠ¥ã€ä¸å…³ä¼šè¯
throw;
}
catch (Exception ex)
{
error = ex;
}
finally
{
_writeLock.Release();
}
// ä¸ŠæŠ¥ä¸Žå…³é—æ”¾é”外:OnError/Close 会触å‘ç”¨æˆ·äº‹ä»¶å›žè°ƒï¼Œå›žè°ƒå†…å†æ¬¡ Send 会在åŒä¸€æŠŠå†™é”上二次ç‰å¾…而æ»é”
if (error != null)
{
ReportSendError(error, filePath);
return -1;
}
return total;
}
#endregion
#region 消æ¯å‘é€
/// <summary>å‘逿µå¼æ¶ˆæ¯ï¼ˆå议模å¼ï¼‰ï¼šå…ˆå‘åè®®å¤´éƒ¨ï¼ˆå£°æ˜Žä½“é•¿ï¼‰ï¼Œå†æŠŠæ•°æ®æµå†…容读一å—写一å—地é€å‡º</summary>
/// <param name="message">消æ¯ï¼ˆå¤´éƒ¨å—段就ä½ï¼‰</param>
/// <param name="body">消æ¯ä½“æ•°æ®æµ</param>
/// <param name="bodyLength">消æ¯ä½“å—节数;负数时从å¯å®šä½æµæŽ¨å¯¼ï¼ˆ<see cref="Stream.CanSeek"/>)</param>
/// <param name="cancellationToken">å–æ¶ˆé€šçŸ¥</param>
/// <returns>已写出的æµå†…容å—节数</returns>
/// <remarks>
/// <para>头部与æµå†…容在<strong>一次</strong>写锿Œæœ‰æœŸé—´å†™å‡ºï¼ˆæ— äº¤é”™ï¼‰ï¼Œæ•´æ¡æ¶ˆæ¯ä¿æŒä¸€æ¡é€»è¾‘消æ¯è¯ä¹‰ï¼›å¤§æ¶ˆæ¯å…¨ç¨‹åªåœ¨è¯»å—上驻留,ä¸äº§ç”Ÿæ•´æ®µå†…å˜ã€‚</para>
/// <para>æµæå‰ç»“æŸï¼ˆä¸è¶³å£°æ˜Žé•¿åº¦ï¼‰æˆ–连接故障时抛出异常,已å‘出的部分ä¸å›žé€€ã€‚</para>
/// </remarks>
/// <exception cref="InvalidOperationException">未设置åè®®</exception>
/// <exception cref="ArgumentException">体长未知且æµä¸å¯å®šä½</exception>
public virtual async ValueTask<Int64> SendMessageAsync(IMessage message, Stream body, Int64 bodyLength = -1, CancellationToken cancellationToken = default)
{
if (message == null) throw new ArgumentNullException(nameof(message));
if (body == null) throw new ArgumentNullException(nameof(body));
var codec = Protocol ?? throw new InvalidOperationException(L.T("Protocol not set for session [{0}]", Name));
// 长度未知:从å¯å®šä½æµæŽ¨å¯¼ï¼ˆå议头须先声明长度)
if (bodyLength < 0)
{
if (!body.CanSeek) throw new ArgumentException(L.T("Cannot determine the stream length. Provide bodyLength or use a seekable stream."), nameof(bodyLength));
bodyLength = body.Length - body.Position;
}
// 头部先行:与æµå†…容在一次写é”内(头体之间ä¸ä¼šè¢«å…¶å®ƒå†™å…¥è€…æ’入)
var header = codec.BuildHeader(message, bodyLength);
try
{
var rs = await SendMessageLockedAsync(header, body, bodyLength, cancellationToken).ConfigureAwait(false);
// 失败返回 -1:底层已记错误日志并按失败关é—会è¯ï¼Œè¿™é‡Œè½¬æˆå¼‚å¸¸ï¼Œä¿æŒæœ¬æ–¹æ³•â€œå¤±è´¥å³æŠ›â€çš„契约
if (rs < 0) throw new IOException(L.T("Send message failed on [{0}].", Name));
return rs;
}
finally
{
// 写完æ‰è¿”回,æ¤å¤„释放头部构建产物
header.TryDispose();
}
}
/// <summary>é”内å‘é€ã€Œå¤´éƒ¨ + æµå†…å®¹ã€æ•´æ¡æ¶ˆæ¯ï¼šä¸€æ¬¡åŠ é”贯穿,头体之间ä¸ä¼šè¢«å…¶å®ƒå†™å…¥è€…æ’å…¥</summary>
/// <remarks>
/// <para>与分别调用 <see cref="SessionBase.Send(IPacket)"/> 与 <see cref="SendAsync(Stream, Int64, CancellationToken)"/> 的区别:那两æ¡å„è‡ªåŠ é”,
/// å¤šå†™è€…å¹¶å‘æ—¶å¤´ä½“之间å¯èƒ½è¢«æ’入其它数æ®ï¼›æœ¬æ–¹æ³•一次æŒé”è´¯ç©¿æ•´æ¡æ¶ˆæ¯ï¼Œä¿æŒâ€œä¸€æ¡é€»è¾‘消æ¯â€è¯ä¹‰ã€‚</para>
/// <para>å¤´éƒ¨å¥æŸ„借用(调用方释放);失败上报在释放写é”之åŽè¿›è¡Œã€‚</para>
/// </remarks>
/// <param name="header">å议头部数æ®åŒ…。借用è¯ä¹‰ï¼šè°ƒç”¨æ–¹ä¿ç•™å¥æŸ„,用完自行释放</param>
/// <param name="body">消æ¯ä½“æ•°æ®æµ</param>
/// <param name="length">消æ¯ä½“å—节数</param>
/// <param name="cancellationToken">å–æ¶ˆä»¤ç‰Œ</param>
/// <returns>已写出的æµå†…容å—节数;失败返回 -1(已上报)</returns>
internal async ValueTask<Int64> SendMessageLockedAsync(IPacket header, Stream body, Int64 length, CancellationToken cancellationToken = default)
{
if (header == null) throw new ArgumentNullException(nameof(header));
if (body == null) throw new ArgumentNullException(nameof(body));
Exception? error = null;
var total = -1L;
await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (Log != null && Log.Enable && LogSend) WriteLog("Send [{0}]: {1}", header.Total, header.ToHex(LogDataLength));
// 头部先行:与æµå†…容在åŒä¸€æŠŠå†™é”å†…ï¼Œæ•´æ¡æ¶ˆæ¯ä¸ä¼šä¸Žå…¶å®ƒå†™å…¥è€…交错
WritePacket(header);
total = await SendStreamCoreAsync(body, length, cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
{
// è°ƒç”¨æ–¹ä¸»åŠ¨å–æ¶ˆï¼šä¸æ˜¯å‘逿•…障,ä¸ä¸ŠæŠ¥ã€ä¸å…³ä¼šè¯ï¼ˆfinally 会释放写é”)
throw;
}
catch (InvalidDataException)
{
// æµæå‰ç»“æŸå±žè°ƒç”¨æ–¹æ•°æ®é—®é¢˜ï¼Œä¸æ˜¯å‘逿•…障:ä¸ä¸ŠæŠ¥ã€ä¸å…³ä¼šè¯
throw;
}
catch (Exception ex)
{
error = ex;
}
finally
{
_writeLock.Release();
}
// ä¸ŠæŠ¥ä¸Žå…³é—æ”¾é”外:OnError/Close 会触å‘ç”¨æˆ·äº‹ä»¶å›žè°ƒï¼Œå›žè°ƒå†…å†æ¬¡ Send 会在åŒä¸€æŠŠå†™é”上二次ç‰å¾…而æ»é”
if (error != null)
{
ReportSendError(error, null);
return -1;
}
return total;
}
#endregion
#region 异æ¥å‘é€
/// <summary>异æ¥å‘逿•°æ®åŒ…:写完æ‰è¿”回,ç‰å¾…å¯å†™æœŸé—´ä¸å çº¿ç¨‹ï¼ˆä¸ŽåŒæ¥ Send 共用写é”)</summary>
/// <remarks>
/// <para>与 <see cref="SessionBase.Send(IPacket)"/> åŒä¹‰ï¼Œåªæ˜¯å¼‚æ¥æ‰§è¡Œï¼šä¸æŽ’队ã€ä¸æ‹·è´ï¼Œå¤±è´¥è®°å½•错误并关é—会è¯ï¼ˆè¿”回 -1)。</para>
/// <para>借用è¯ä¹‰ï¼šè°ƒç”¨æ–¹ä¿ç•™å¥æŸ„,返回åŽå³å¯é‡Šæ”¾è‡ªå·±çš„引用</para>
/// </remarks>
/// <param name="data">æ•°æ®åŒ…</param>
/// <param name="cancellationToken">å–æ¶ˆä»¤ç‰Œ</param>
/// <returns>å·²å‘é€å—节数;失败返回 -1</returns>
public ValueTask<Int32> SendAsync(IPacket data, CancellationToken cancellationToken = default)
{
if (data == null) throw new ArgumentNullException(nameof(data));
if (Disposed) throw new ObjectDisposedException(GetType().Name);
if (!Open()) throw new InvalidOperationException(L.T("Session [{0}] is not open.", GetType().Name));
return DirectSendAsync(data, cancellationToken);
}
#endregion
}
|