Отправка файлов по сети имеет определенные особенности по сравнению с отправкой текстовых сообщений. Размер файла в большинстве случаев значительно больше текста и отправку (прием) вследствие этого необходимо осуществлять порциями. Более того при обмене файлами по сети необходимо кроме содержимого файла передавать информацию об его имени, типе (расширении) и возможно еще какие-либо дополнительные данные. Сетевой поток байтов в этом случае будет насыщен различной информацией, содержание которой можно узнать только после полной расшифровки.
Разрабатывая сетевое приложение для отправки файлов по сети, сталкиваешься с трудностями надежного приема точного количества байтов при заранее неизвестном составе получаемого сетевого пакета. Используя при обмене сетевыми сообщениями принцип работы протокола TCP, разработанный умными людьми для надежной доставки информации, можно успешно обмениваться файлами и текстовыми сообщениями. Суть способа проста: вся необходимая информация содержится в самом пересылаемом пакете байтов в заранее известном месте.
Подробнее: создается первый (главный заголовок) с информацией о последующих объектах сетевого потока. Главный заголовок имеет фиксированный размер для однозначной его интерпретации. После заголовка следует информационная структура с подробной информацией о присланных сетевых данных. Размер ее мы и указываем в главном заголовке. При успешной расшифровке (десериализации) информационной структуры мы получим точные данные, ведь такая структура может нести в себе текстовые, числовые и двоичные данные.
// Метод отправки файлов и других данных по сети
public void SendData()
{
// Состав отсылаемого универсального сообщения
// 1. Заголовок о следующим объектом класса подробной информации дальнейших байтов
// 2. Объект класса подробной информации о следующих байтах
// 3. Байты непосредственно готовых к записи в файл или для чего-то иного.
SendInfo si = new SendInfo();
si.message = "текст сообщения";
...
FileInfo fi = new FileInfo(SendFileName);
si.filesize = (int)fi.Length;
si.filename = fi.Name;
BinaryFormatter bf = new BinaryFormatter();
MemoryStream ms = new MemoryStream();
bf.Serialize(ms, si);
ms.Position = 0;
byte[] infobuffer = new byte[ms.Length];
int r = ms.Read(infobuffer, 0, infobuffer.Length);
ms.Close();
// байты главного заголовка
byte[] header = GetHeader(infobuffer.Length);
// Общий массив байтов
byte[] total = new byte[header.Length + infobuffer.Length + si.filesize];
Buffer.BlockCopy(header, 0, total, 0, header.Length);
Buffer.BlockCopy(infobuffer, 0, total, header.Length, infobuffer.Length);
// Добавим содержимое файла в общий массив сетевых данных
FileStream fs = new FileStream(SendFileName, FileMode.Open, FileAccess.Read);
fs.Read(total, header.Length + infobuffer.Length, si.filesize);
fs.Close();
// Отправим данные подключенным клиентам
NetworkStream ns = _tcpClient.tcpClient.GetStream();
// Так как данный метод вызывается в отдельном потоке
// рациональней использовать синхронный метод отправки
ns.Write(total, 0, total.Length);
...
// Подтверждение успешной отправки
Parent.ShowReceiveMessage("Данные успешно отправлены!");
}
// Асинхронный метод приема и расшифровки сетевых данных
public void ReadCallback(IAsyncResult ar)
{
TcpClientData myTcpClient = (TcpClientData)ar.AsyncState;
try
{
NetworkStream ns = myTcpClient.tcpClient.GetStream();
int r = ns.EndRead(ar);
if (r > 0)
{
// Из главного заголовка получим размер массива байтов информационного объекта
string header = Encoding.Default.GetString(myTcpClient.buffer);
int leninfo = int.Parse(header);
// Получим и десериализуем объект с подробной информацией
// о содержании получаемого сетевого пакета
MemoryStream ms = new MemoryStream(leninfo);
byte[] temp = new byte[leninfo];
r = ns.Read(temp, 0, temp.Length);
ms.Write(temp, 0, r);
BinaryFormatter bf = new BinaryFormatter();
ms.Position = 0;
SendInfo sc = (SendInfo)bf.Deserialize(ms);
ms.Close();
...
// Создадим файл на основе полученной информации и
// массива байтов следующих за объектом информации
FileStream fs = new FileStream(sc.filename, FileMode.Create,
FileAccess.ReadWrite, FileShare.ReadWrite, sc.filesize);
do
{
temp = new byte[global.MAXBUFFER];
r = ns.Read(temp, 0, temp.Length);
// Записываем строго столько байтов сколько прочтено методом Read()
fs.Write(temp, 0, r);
// Как только получены все байты файла, останавливаем цикл,
// иначе он заблокируется в ожидании новых сетевых данных
if (fs.Length == sc.filesize)
{
fs.Close();
break;
}
}
while (r > 0);
...
if (Receive != null)
Receive(this, new ReceiveEventArgs(sc));
...
}
else
{
DeleteClient(myTcpClient);
// Событие клиент отключился
if (Disconnected != null)
Disconnected.BeginInvoke(this, "Клиент отключился!", null, null);
}
}
catch (Exception e)
{
DeleteClient(myTcpClient);
// Событие клиент отключился
if (Disconnected != null)
Disconnected.BeginInvoke(this, "Клиент отключился аварийно!", null, null);
SoundError();
}
}
Исходник построен на классах TcpListener и TcpClient упрощающих процесс создания сетевых приложений. Для сериализации в двоичный формат и обратной десериализации используется класс BinaryFormatter. Ограничений по размеру на пересылаемый файла в исходнике не установлен, максимальный размер зависит от производительности компьютера и объема оперативной памяти.
Среда программирования MS Visual Studio.NET 2010 и выше, открытая среда программирования для C# SharpDevelop 4.2 и выше, .NET Framework 3.0 и выше.