Use ArraySegment for channel data (#1650)

The library currently allocates 4 bytes (and some) for every 1 byte of file
downloaded(*). It could be 0. This takes it to 3.

(*)
1. Array allocated for read of encrypted packet from socket
2. Array for decrypted packet
3. Array for channel data (removed in this change)
4. Array for sftp data packet
This commit is contained in:
Rob Hague
2025-06-06 08:52:23 +02:00
committed by GitHub
parent 1f1a5fe2ac
commit fd05d76ad6
18 changed files with 79 additions and 60 deletions
+6 -6
View File
@@ -378,9 +378,9 @@ namespace Renci.SshNet.Channels
/// Called when channel data is received.
/// </summary>
/// <param name="data">The data.</param>
protected virtual void OnData(byte[] data)
protected virtual void OnData(ArraySegment<byte> data)
{
AdjustDataWindow(data);
AdjustDataWindow(data.Count);
DataReceived?.Invoke(this, new ChannelDataEventArgs(LocalChannelNumber, data));
}
@@ -392,7 +392,7 @@ namespace Renci.SshNet.Channels
/// <param name="dataTypeCode">The data type code.</param>
protected virtual void OnExtendedData(byte[] data, uint dataTypeCode)
{
AdjustDataWindow(data);
AdjustDataWindow(data.Length);
ExtendedDataReceived?.Invoke(this, new ChannelExtendedDataEventArgs(LocalChannelNumber, data, dataTypeCode));
}
@@ -651,7 +651,7 @@ namespace Renci.SshNet.Channels
{
try
{
OnData(e.Message.Data);
OnData(new ArraySegment<byte>(e.Message.Data, e.Message.Offset, e.Message.Size));
}
catch (Exception ex)
{
@@ -768,9 +768,9 @@ namespace Renci.SshNet.Channels
}
}
private void AdjustDataWindow(byte[] messageData)
private void AdjustDataWindow(int count)
{
LocalWindowSize -= (uint)messageData.Length;
LocalWindowSize -= (uint)count;
// Adjust window if window size is too low
if (LocalWindowSize < LocalPacketSize)
@@ -194,7 +194,7 @@ namespace Renci.SshNet.Channels
/// Called when channel data is received.
/// </summary>
/// <param name="data">The data.</param>
protected override void OnData(byte[] data)
protected override void OnData(ArraySegment<byte> data)
{
base.OnData(data);
@@ -204,7 +204,7 @@ namespace Renci.SshNet.Channels
{
if (_socket.IsConnected())
{
SocketAbstraction.Send(_socket, data, 0, data.Length);
SocketAbstraction.Send(_socket, data.Array, data.Offset, data.Count);
}
}
}
@@ -201,14 +201,14 @@ namespace Renci.SshNet.Channels
/// Called when channel data is received.
/// </summary>
/// <param name="data">The data.</param>
protected override void OnData(byte[] data)
protected override void OnData(ArraySegment<byte> data)
{
base.OnData(data);
var socket = _socket;
if (socket.IsConnected())
{
SocketAbstraction.Send(socket, data, 0, data.Length);
SocketAbstraction.Send(socket, data.Array, data.Offset, data.Count);
}
}
}
@@ -13,17 +13,22 @@ namespace Renci.SshNet.Common
/// <param name="channelNumber">Channel number.</param>
/// <param name="data">Channel data.</param>
/// <exception cref="ArgumentNullException"><paramref name="data"/> is <see langword="null"/>.</exception>
public ChannelDataEventArgs(uint channelNumber, byte[] data)
public ChannelDataEventArgs(uint channelNumber, ArraySegment<byte> data)
: base(channelNumber)
{
ThrowHelper.ThrowIfNull(data);
ThrowHelper.ThrowIfNull(data.Array);
Data = data;
}
internal ChannelDataEventArgs(uint channelNumber, byte[] data)
: this(channelNumber, new ArraySegment<byte>(data))
{
}
/// <summary>
/// Gets channel data.
/// </summary>
public byte[] Data { get; }
public ArraySegment<byte> Data { get; }
}
}
@@ -1,4 +1,6 @@
namespace Renci.SshNet.Common
using System;
namespace Renci.SshNet.Common
{
/// <summary>
/// Provides data for <see cref="Channels.Channel.ExtendedDataReceived"/> events.
@@ -12,7 +14,7 @@
/// <param name="data">Channel data.</param>
/// <param name="dataTypeCode">Channel data type code.</param>
public ChannelExtendedDataEventArgs(uint channelNumber, byte[] data, uint dataTypeCode)
: base(channelNumber, data)
: base(channelNumber, new ArraySegment<byte>(data))
{
DataTypeCode = dataTypeCode;
}
+15 -1
View File
@@ -232,7 +232,7 @@ namespace Renci.SshNet.Common
}
/// <summary>
/// Reads next data type as byte array from internal buffer.
/// Reads a length-prefixed byte array from the internal buffer.
/// </summary>
/// <returns>
/// The bytes read.
@@ -242,6 +242,20 @@ namespace Renci.SshNet.Common
return _stream.ReadBinary();
}
/// <summary>
/// Reads a length-prefixed byte array from the internal buffer,
/// returned as a view over the buffer.
/// </summary>
/// <remarks>
/// When using this method, consider whether the underlying buffer is shared
/// or reused, and whether the returned <see cref="ArraySegment{T}"/> may
/// exist beyond the lifetime for which it is valid to be used.
/// </remarks>
private protected ArraySegment<byte> ReadBinarySegment()
{
return _stream.ReadBinarySegment();
}
/// <summary>
/// Reads next name-list data type from internal buffer.
/// </summary>
@@ -120,9 +120,11 @@ namespace Renci.SshNet.Messages.Connection
{
base.LoadData();
Data = ReadBinary();
Offset = 0;
Size = Data.Length;
var data = ReadBinarySegment();
Data = data.Array;
Offset = data.Offset;
Size = data.Count;
}
/// <summary>
+2 -2
View File
@@ -121,9 +121,9 @@ namespace Renci.SshNet.NetConf
WaitOnHandle(_serverCapabilitiesConfirmed, OperationTimeout);
}
protected override void OnDataReceived(byte[] data)
protected override void OnDataReceived(ArraySegment<byte> data)
{
var chunk = Encoding.UTF8.GetString(data);
var chunk = Encoding.UTF8.GetString(data.Array, data.Offset, data.Count);
if (ServerCapabilities is null)
{
+6 -6
View File
@@ -257,7 +257,7 @@ namespace Renci.SshNet
using (var input = ServiceFactory.CreatePipeStream())
using (var channel = Session.CreateChannelSession())
{
channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
channel.DataReceived += (sender, e) => input.Write(e.Data.Array!, e.Data.Offset, e.Data.Count);
channel.Closed += (sender, e) => input.Dispose();
channel.Open();
@@ -300,7 +300,7 @@ namespace Renci.SshNet
using (var input = ServiceFactory.CreatePipeStream())
using (var channel = Session.CreateChannelSession())
{
channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
channel.DataReceived += (sender, e) => input.Write(e.Data.Array!, e.Data.Offset, e.Data.Count);
channel.Closed += (sender, e) => input.Dispose();
channel.Open();
@@ -346,7 +346,7 @@ namespace Renci.SshNet
using (var input = ServiceFactory.CreatePipeStream())
using (var channel = Session.CreateChannelSession())
{
channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
channel.DataReceived += (sender, e) => input.Write(e.Data.Array!, e.Data.Offset, e.Data.Count);
channel.Closed += (sender, e) => input.Dispose();
channel.Open();
@@ -389,7 +389,7 @@ namespace Renci.SshNet
using (var input = ServiceFactory.CreatePipeStream())
using (var channel = Session.CreateChannelSession())
{
channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
channel.DataReceived += (sender, e) => input.Write(e.Data.Array!, e.Data.Offset, e.Data.Count);
channel.Closed += (sender, e) => input.Dispose();
channel.Open();
@@ -429,7 +429,7 @@ namespace Renci.SshNet
using (var input = ServiceFactory.CreatePipeStream())
using (var channel = Session.CreateChannelSession())
{
channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
channel.DataReceived += (sender, e) => input.Write(e.Data.Array!, e.Data.Offset, e.Data.Count);
channel.Closed += (sender, e) => input.Dispose();
channel.Open();
@@ -469,7 +469,7 @@ namespace Renci.SshNet
using (var input = ServiceFactory.CreatePipeStream())
using (var channel = Session.CreateChannelSession())
{
channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length);
channel.DataReceived += (sender, e) => input.Write(e.Data.Array!, e.Data.Offset, e.Data.Count);
channel.Closed += (sender, e) => input.Dispose();
channel.Open();
+17 -19
View File
@@ -302,38 +302,36 @@ namespace Renci.SshNet.Sftp
WorkingDirectory = RequestRealPath(".")[0].Key;
}
protected override void OnDataReceived(byte[] data)
protected override void OnDataReceived(ArraySegment<byte> data)
{
ArraySegment<byte> d = new(data);
// If the buffer is empty then skip a copy and read packets
// directly out of the given data.
if (_buffer.ActiveLength == 0)
{
while (d.Count >= 4)
while (data.Count >= 4)
{
var packetLength = BinaryPrimitives.ReadInt32BigEndian(d);
var packetLength = BinaryPrimitives.ReadInt32BigEndian(data);
if (d.Count - 4 < packetLength)
if (data.Count - 4 < packetLength)
{
break;
}
if (!TryLoadSftpMessage(d.Slice(4, packetLength)))
if (!TryLoadSftpMessage(data.Slice(4, packetLength)))
{
// An error occured.
return;
}
d = d.Slice(4 + packetLength);
data = data.Slice(4 + packetLength);
}
if (d.Count > 0)
if (data.Count > 0)
{
// Now buffer the remainder.
_buffer.EnsureAvailableSpace(d.Count);
d.AsSpan().CopyTo(_buffer.AvailableSpan);
_buffer.Commit(d.Count);
_buffer.EnsureAvailableSpace(data.Count);
data.AsSpan().CopyTo(_buffer.AvailableSpan);
_buffer.Commit(data.Count);
}
return;
@@ -341,20 +339,20 @@ namespace Renci.SshNet.Sftp
// The buffer already had some data. Append the new data and
// proceed with reading out packets.
_buffer.EnsureAvailableSpace(d.Count);
d.AsSpan().CopyTo(_buffer.AvailableSpan);
_buffer.Commit(d.Count);
_buffer.EnsureAvailableSpace(data.Count);
data.AsSpan().CopyTo(_buffer.AvailableSpan);
_buffer.Commit(data.Count);
while (_buffer.ActiveLength >= 4)
{
d = new ArraySegment<byte>(
data = new ArraySegment<byte>(
_buffer.DangerousGetUnderlyingBuffer(),
_buffer.ActiveStartOffset,
_buffer.ActiveLength);
var packetLength = BinaryPrimitives.ReadInt32BigEndian(d);
var packetLength = BinaryPrimitives.ReadInt32BigEndian(data);
if (d.Count - 4 < packetLength)
if (data.Count - 4 < packetLength)
{
break;
}
@@ -362,7 +360,7 @@ namespace Renci.SshNet.Sftp
// Note: the packet data in the buffer is safe to read from
// only for the duration of this load. If it needs to be stored,
// callees should make their own copy.
_ = TryLoadSftpMessage(d.Slice(4, packetLength));
_ = TryLoadSftpMessage(data.Slice(4, packetLength));
_buffer.Discard(4 + packetLength);
}
+2 -2
View File
@@ -241,12 +241,12 @@ namespace Renci.SshNet
private void Channel_ExtendedDataReceived(object sender, ChannelExtendedDataEventArgs e)
{
_extendedOutputStream?.Write(e.Data, 0, e.Data.Length);
_extendedOutputStream?.Write(e.Data.Array, e.Data.Offset, e.Data.Count);
}
private void Channel_DataReceived(object sender, ChannelDataEventArgs e)
{
_outputStream?.Write(e.Data, 0, e.Data.Length);
_outputStream?.Write(e.Data.Array, e.Data.Offset, e.Data.Count);
}
private void Channel_Closed(object sender, ChannelEventArgs e)
+3 -3
View File
@@ -965,16 +965,16 @@ namespace Renci.SshNet
{
lock (_sync)
{
_readBuffer.EnsureAvailableSpace(e.Data.Length);
_readBuffer.EnsureAvailableSpace(e.Data.Count);
e.Data.AsSpan().CopyTo(_readBuffer.AvailableSpan);
_readBuffer.Commit(e.Data.Length);
_readBuffer.Commit(e.Data.Count);
Monitor.PulseAll(_sync);
}
DataReceived?.Invoke(this, new ShellDataEventArgs(e.Data));
DataReceived?.Invoke(this, new ShellDataEventArgs(e.Data.ToArray()));
}
}
}
+2 -2
View File
@@ -583,7 +583,7 @@ namespace Renci.SshNet
private void Channel_ExtendedDataReceived(object? sender, ChannelExtendedDataEventArgs e)
{
ExtendedOutputStream.Write(e.Data, 0, e.Data.Length);
ExtendedOutputStream.Write(e.Data.Array!, e.Data.Offset, e.Data.Count);
if (e.DataTypeCode == 1)
{
@@ -593,7 +593,7 @@ namespace Renci.SshNet
private void Channel_DataReceived(object? sender, ChannelDataEventArgs e)
{
OutputStream.Write(e.Data, 0, e.Data.Length);
OutputStream.Write(e.Data.Array!, e.Data.Offset, e.Data.Count);
}
/// <summary>
+1 -1
View File
@@ -173,7 +173,7 @@ namespace Renci.SshNet
/// Called when data is received.
/// </summary>
/// <param name="data">The data.</param>
protected abstract void OnDataReceived(byte[] data);
protected abstract void OnDataReceived(ArraySegment<byte> data);
/// <summary>
/// Raises the error.
@@ -68,7 +68,7 @@ namespace Renci.SshNet.Tests.Classes.Channels
}
}
protected override void OnData(byte[] data)
protected override void OnData(ArraySegment<byte> data)
{
base.OnData(data);
@@ -72,7 +72,7 @@ namespace Renci.SshNet.Tests.Classes.Channels
}
}
protected override void OnData(byte[] data)
protected override void OnData(ArraySegment<byte> data)
{
base.OnData(data);
@@ -145,9 +145,7 @@ namespace Renci.SshNet.Tests.Classes.Messages.Connection
target.Load(bytes, 1, bytes.Length - 1); // skip message type
Assert.IsTrue(target.Data.SequenceEqual(data.Take(offset, size)));
Assert.AreEqual(0, target.Offset);
Assert.AreEqual(size, target.Size);
CollectionAssert.AreEqual(data.Take(offset, size), target.Data.Take(target.Offset, target.Size));
}
}
}
@@ -37,7 +37,7 @@ namespace Renci.SshNet.Tests.Classes
}
}
protected override void OnDataReceived(byte[] data)
protected override void OnDataReceived(ArraySegment<byte> data)
{
OnDataReceivedInvocations.Add(new ChannelDataEventArgs(0, data));