FTP 8.5: OutOfMemory subiendo ficheros con un cliente .NET
Recuerdo un caso en el que un cliente tenía un OutOfMemory subiendo ficheros de más de 1GB a un FTP usando las clases de .NET. Recordemos que el límite de memoria virtual que tiene un proceso de 64 bit es de 8 TB o 128 TB en Windows 8.1/10/2012 R2 (Memory Limits for Windows and Windows Server Releases)
Técnicamente es posible tener ese OutOfMemory puesto que podríamos crear una aplicación que consuma memoria hasta llegar a ese límite de memoria. Pero, ¿cómo es posible tener ese OutOfMemory si subimos un fichero de 1.4 GB y nos quedan más de 120 TB libres en el proceso?
Usando este código (de la MSDN) , veremos cómo es posible conseguir la excepción subiendo el fichero:
string user = "";
string pass = "";
string file = "";
string ftpUri = "";
try
{
if (args.Length == 0)
{
Console.WriteLine("Enter username: ");
user = Console.ReadLine();
Console.WriteLine("Enter password: ");
pass = Console.ReadLine();
Console.WriteLine("Enter file name: ");
file = Console.ReadLine();
Console.WriteLine("Enter FTP URi: ");
ftpUri = Console.ReadLine();
}
else
{
user = args[0];
pass = args[1];
file = args[2];
ftpUri = args[3];
}
FtpWebRequest request = (FtpWebRequest)WebRequest.Create(ftpUri);
request.Method = WebRequestMethods.Ftp.UploadFile;
request.Credentials = newNetworkCredential(user, pass);
StreamReader sourceStream = newStreamReader(file);
Console.ReadLine();
byte[] fileContents = Encoding.UTF8.GetBytes(sourceStream.ReadToEnd());
sourceStream.Close();
request.ContentLength = fileContents.Length;
Stream requestStream = request.GetRequestStream();
requestStream.Write(fileContents, 0, fileContents.Length);
requestStream.Close();
FtpWebResponse response = (FtpWebResponse)request.GetResponse();
Console.WriteLine("Upload File Complete, status {0}", response.StatusDescription);
response.Close();
}
catch(Exception ex)
{
Console.WriteLine("An exception has occurred: {0}", ex.Message);
}
Como decía, ejecutando este código, tendremos una OutOfMemoryException, concretamente cuando intentemos convertir el Stream del fichero que queremos subir a un array de bytes:
byte[] fileContents = Encoding.UTF8.GetBytes(sourceStream.ReadToEnd());
Si hemos adjuntado un depurador, veremos algo como esto:
# Call Site
00 KERNELBASE!RaiseException
01 clr!DllCanUnloadNowInternal
02 clr!TranslateSecurityAttributes
03 clr!NGenCreateNGenWorker
04 mscorlib_ni!System.Text.StringBuilder.ToString()
05 FTPClient!FTPCLient.Main(System.String[])
Lo que ocurre es que estamos convirtiendo un fichero muy grande en el array de bytes directamente. Lo recomendado sería usar un buffer para evitar este comportamiento: Subir ficheros con buffer
También podemos usar los objetos muy grandes de .NET (como menciono en este otro artículo: OutOfMemoryException al manejar StringBuilder en un proceso de 64 bitshttps://blogs.msdn.com/b/desarrolloweb/archive/2016/02/17/stringbuilder-outofmemory.aspx)
Espero que os sirva.
-- José Ortega Gutiérrez