mirror of
https://github.com/sshnet/SSH.NET.git
synced 2026-10-22 00:46:19 +08:00
Split up data in multiple messages when it does not fit in remote window size. This prevents timeout when sending data to server with a low window size and which adjusts the window size very conservatively (issue #1706)
This commit is contained in:
@@ -16,11 +16,11 @@
|
||||
* Added callbacks to UploadFile, DownloadFile and ListDirectory in SftpClient (issue #1324)
|
||||
* Allow a given private key file to be used concurrently
|
||||
* Performance improvements:
|
||||
- optimization of payload size for both read and write operations (SftpClient only)
|
||||
- increase window size from 1MB to 2MB
|
||||
- increase buffer size from 16KB to 64KB for SftpClient
|
||||
- take into account the maximum remote packet size of the channel for write operations
|
||||
- increase maximum size of packets that we can receive from 32 KB to 64 KB
|
||||
- optimization of payload size for both read and write operations (SftpClient only)
|
||||
- increase window size from 1MB to 2MB
|
||||
- increase buffer size from 16KB to 64KB for SftpClient
|
||||
- take into account the maximum remote packet size of the channel for write operations
|
||||
- increase maximum size of packets that we can receive from 32 KB to 64 KB
|
||||
* Improve exception message for authentication failures
|
||||
|
||||
Breaking changes:
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
Fixes:
|
||||
|
||||
* Timeout sending data to server with low window size (issue #1706)
|
||||
* No connection possible with the same auth method requested multiple times (issue #1930)
|
||||
* Unobserved exception rethrown by finalizer thread (issue #1298 and #1587)
|
||||
* Client cipher is used to decrypt server messages (issue #1917)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Renci.SshNet.Common;
|
||||
using Renci.SshNet.Messages;
|
||||
@@ -360,7 +360,7 @@ namespace Renci.SshNet.Channels
|
||||
/// <param name="message">The message.</param>
|
||||
protected void SendMessage(Message message)
|
||||
{
|
||||
// Send channel messages only while channel is open
|
||||
// send channel messages only while channel is open
|
||||
if (!this.IsOpen)
|
||||
return;
|
||||
|
||||
@@ -373,99 +373,109 @@ namespace Renci.SshNet.Channels
|
||||
/// <param name="message">The message to send.</param>
|
||||
private void SendMessage(ChannelCloseMessage message)
|
||||
{
|
||||
// Send channel messages only while channel is open
|
||||
// send channel messages only while channel is open
|
||||
if (!this.IsOpen)
|
||||
return;
|
||||
|
||||
this._session.SendMessage(message);
|
||||
|
||||
// When channel close message is sent channel considered to be closed
|
||||
// when channel close message is sent channel considered to be closed
|
||||
this.IsOpen = false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends channel data message to the servers.
|
||||
/// </summary>
|
||||
/// <remarks>This method takes care of managing the window size.</remarks>
|
||||
/// <param name="message">Channel data message.</param>
|
||||
/// <exception cref="InvalidOperationException">The data of <paramref name="message"/> exceeds the maximum packet size of the channel.</exception>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When the data of the message exceeds the maximum packet size or the remote window
|
||||
/// size does not allow the full message to be sent, then this method will send the
|
||||
/// data in multiple chunks and will only wait for the remote window size to be adjusted
|
||||
/// when its zero.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This is done to support SSH servers will a small window size that do not agressively
|
||||
/// increase their window size. We need to take into account that there may be SSH
|
||||
/// servers that only increase their window size when it has reached zero.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
protected void SendMessage(ChannelDataMessage message)
|
||||
{
|
||||
// Send channel messages only while channel is open
|
||||
// send channel messages only while channel is open
|
||||
if (!this.IsOpen)
|
||||
return;
|
||||
|
||||
var messageLength = message.Data.Length;
|
||||
var totalDataLength = (uint) message.Data.Length;
|
||||
var totalDataSent = 0u;
|
||||
|
||||
// RFC4254:
|
||||
// The maximum amount of data allowed is determined by the maximum packet size
|
||||
// for the channel
|
||||
//
|
||||
// there's some ambiguity in the RFC, but most ssh implementations take only the
|
||||
// data into account for determining the size of a packet; the 4 bytes for the
|
||||
// packet length and the 9 bytes of the header are not considered part of the
|
||||
// data
|
||||
if (messageLength > RemotePacketSize)
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture,
|
||||
"The payload of the data message is {0} bytes while the maximum packet size of the channel is {1} bytes.",
|
||||
messageLength, RemotePacketSize));
|
||||
|
||||
do
|
||||
var totalBytesToSend = totalDataLength;
|
||||
while (totalBytesToSend > 0)
|
||||
{
|
||||
lock (this._serverWindowSizeLock)
|
||||
var dataThatCanBeSentInMessage = GetDataLengthThatCanBeSentInMessage(totalBytesToSend);
|
||||
if (dataThatCanBeSentInMessage == totalDataLength)
|
||||
{
|
||||
var serverWindowSize = this.RemoteWindowSize;
|
||||
if (serverWindowSize < messageLength)
|
||||
{
|
||||
// Wait for window to be big enough for this message
|
||||
this._channelServerWindowAdjustWaitHandle.Reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.RemoteWindowSize -= (uint)messageLength;
|
||||
break;
|
||||
}
|
||||
// we can send the message in one chunk
|
||||
this._session.SendMessage(message);
|
||||
}
|
||||
// Wait for window to change
|
||||
this.WaitOnHandle(this._channelServerWindowAdjustWaitHandle);
|
||||
} while (true);
|
||||
|
||||
this._session.SendMessage(message);
|
||||
else
|
||||
{
|
||||
// we need to send the message in multiple chunks
|
||||
var dataToSend = new byte[dataThatCanBeSentInMessage];
|
||||
Array.Copy(message.Data, totalDataSent, dataToSend, 0, dataThatCanBeSentInMessage);
|
||||
this._session.SendMessage(new ChannelDataMessage(message.LocalChannelNumber, dataToSend));
|
||||
}
|
||||
totalDataSent += dataThatCanBeSentInMessage;
|
||||
totalBytesToSend -= dataThatCanBeSentInMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends channel extended data message to the servers.
|
||||
/// </summary>
|
||||
/// <remarks>This method takes care of managing the window size.</remarks>
|
||||
/// <param name="message">Channel data message.</param>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When the data of the message exceeds the maximum packet size or the remote window
|
||||
/// size does not allow the full message to be sent, then this method will send the
|
||||
/// data in multiple chunks and will only wait for the remote window size to be adjusted
|
||||
/// when its zero.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This is done to support SSH servers will a small window size that do not agressively
|
||||
/// increase their window size. We need to take into account that there may be SSH
|
||||
/// servers that only increase their window size when it has reached zero.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
protected void SendMessage(ChannelExtendedDataMessage message)
|
||||
{
|
||||
// Send channel messages only while channel is open
|
||||
// end channel messages only while channel is open
|
||||
if (!this.IsOpen)
|
||||
return;
|
||||
|
||||
var messageLength = message.Data.Length;
|
||||
do
|
||||
{
|
||||
lock (this._serverWindowSizeLock)
|
||||
{
|
||||
var serverWindowSize = this.RemoteWindowSize;
|
||||
if (serverWindowSize < messageLength)
|
||||
{
|
||||
// Wait for window to be big enough for this message
|
||||
this._channelServerWindowAdjustWaitHandle.Reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
this.RemoteWindowSize -= (uint)messageLength;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Wait for window to change
|
||||
this.WaitOnHandle(this._channelServerWindowAdjustWaitHandle);
|
||||
} while (true);
|
||||
var totalDataLength = (uint) message.Data.Length;
|
||||
var totalDataSent = 0u;
|
||||
|
||||
this._session.SendMessage(message);
|
||||
var totalBytesToSend = totalDataLength;
|
||||
while (totalBytesToSend > 0)
|
||||
{
|
||||
var dataThatCanBeSentInMessage = GetDataLengthThatCanBeSentInMessage(totalBytesToSend);
|
||||
if (dataThatCanBeSentInMessage == totalDataLength)
|
||||
{
|
||||
// we can send the message in one chunk
|
||||
this._session.SendMessage(message);
|
||||
}
|
||||
else
|
||||
{
|
||||
// we need to send the message in multiple chunks
|
||||
var dataToSend = new byte[dataThatCanBeSentInMessage];
|
||||
Array.Copy(message.Data, totalDataSent, dataToSend, 0, dataThatCanBeSentInMessage);
|
||||
this._session.SendMessage(new ChannelExtendedDataMessage(message.LocalChannelNumber,
|
||||
message.DataTypeCode, dataToSend));
|
||||
}
|
||||
totalDataSent += dataThatCanBeSentInMessage;
|
||||
totalBytesToSend -= dataThatCanBeSentInMessage;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -479,8 +489,8 @@ namespace Renci.SshNet.Channels
|
||||
|
||||
protected virtual void Close(bool wait)
|
||||
{
|
||||
// Send message to close the channel on the server
|
||||
// Ignore sending close message when client not connected
|
||||
// send message to close the channel on the server
|
||||
// ignore sending close message when client not connected
|
||||
if (!_closeMessageSent && this.IsConnected)
|
||||
{
|
||||
lock (this)
|
||||
@@ -498,7 +508,7 @@ namespace Renci.SshNet.Channels
|
||||
IsOpen = false;
|
||||
}
|
||||
|
||||
// Wait for channel to be closed
|
||||
// wait for channel to be closed
|
||||
if (wait)
|
||||
{
|
||||
WaitOnHandle(this._channelClosedWaitHandle);
|
||||
@@ -637,6 +647,38 @@ namespace Renci.SshNet.Channels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines the length of data that currently can be sent in a single message.
|
||||
/// </summary>
|
||||
/// <param name="messageLength">The length of the message that must be sent.</param>
|
||||
/// <returns>
|
||||
/// The actual data length that currently can be sent.
|
||||
/// </returns>
|
||||
private uint GetDataLengthThatCanBeSentInMessage(uint messageLength)
|
||||
{
|
||||
do
|
||||
{
|
||||
lock (this._serverWindowSizeLock)
|
||||
{
|
||||
var serverWindowSize = RemoteWindowSize;
|
||||
if (serverWindowSize == 0)
|
||||
{
|
||||
// allow us to be signal when remote window size is adjusted
|
||||
this._channelServerWindowAdjustWaitHandle.Reset();
|
||||
}
|
||||
else
|
||||
{
|
||||
var bytesThatCanBeSent = Math.Min(Math.Min(RemotePacketSize, messageLength),
|
||||
serverWindowSize);
|
||||
this.RemoteWindowSize -= bytesThatCanBeSent;
|
||||
return bytesThatCanBeSent;
|
||||
}
|
||||
}
|
||||
// wait for remote window size to change
|
||||
this.WaitOnHandle(this._channelServerWindowAdjustWaitHandle);
|
||||
} while (true);
|
||||
}
|
||||
|
||||
private InvalidOperationException CreateRemoteChannelInfoNotAvailableException()
|
||||
{
|
||||
throw new InvalidOperationException("The channel has not been opened, or the open has not yet been confirmed.");
|
||||
|
||||
@@ -1101,6 +1101,9 @@ namespace Renci.SshNet.Sftp
|
||||
/// <returns>
|
||||
/// The optimal size of the buffer to write data on the channel.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// Currently, we do not take the remote window size into account.
|
||||
/// </remarks>
|
||||
internal uint CalculateOptimalWriteLength(uint bufferSize, byte[] handle)
|
||||
{
|
||||
// 1-4: package length of SSH_FXP_WRITE message
|
||||
|
||||
Reference in New Issue
Block a user