Last active
May 1, 2023 06:30
-
-
Save define-private-public/e75db7dfd50fe5415786e7141f8398b9 to your computer and use it in GitHub Desktop.
UDP based file transfer in C#
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Filename: Block.cs | |
// Author: Benjamin N. Summerton <define-private-public> | |
// License: Unlicense (http://unlicense.org/) | |
using System; | |
using System.Text; | |
using System.Linq; | |
namespace UdpFileTransfer | |
{ | |
// These are the chunks of data that will be sent across the network | |
public class Block | |
{ | |
public UInt32 Number { get; set; } | |
public byte[] Data { get; set; } = new byte[0]; | |
#region Constructors | |
// Creates a new block of data w/ the supplied number | |
public Block(UInt32 number=0) | |
{ | |
Number = number; | |
} | |
// Creates a Block from a byte array | |
public Block (byte[] bytes) | |
{ | |
// First four bytes are the number | |
Number = BitConverter.ToUInt32(bytes, 0); | |
// Data starts at byte 4 | |
Data = bytes.Skip(4).ToArray(); | |
} | |
#endregion // Constructors | |
public override string ToString() | |
{ | |
// Take some of the first few bits of data and turn that into a string | |
String dataStr; | |
if (Data.Length > 8) | |
dataStr = Encoding.ASCII.GetString(Data, 0, 8) + "..."; | |
else | |
dataStr = Encoding.ASCII.GetString(Data, 0, Data.Length); | |
return string.Format( | |
"[Block:\n" + | |
" Number={0},\n" + | |
" Size={1},\n" + | |
" Data=`{2}`]", | |
Number, Data.Length, dataStr); | |
} | |
// Returns the data in the block as a byte array | |
public byte[] GetBytes() | |
{ | |
// Convert meta-data | |
byte[] numberBytes = BitConverter.GetBytes(Number); | |
// Join all the data into one bigger array | |
byte[] bytes = new byte[numberBytes.Length + Data.Length]; | |
numberBytes.CopyTo(bytes, 0); | |
Data.CopyTo(bytes, 4); | |
return bytes; | |
} | |
} | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Filename: NetworkMessage.cs | |
// Author: Benjamin N. Summerton <define-private-public> | |
// License: Unlicense (http://unlicense.org/) | |
using System.Net; | |
namespace UdpFileTransfer | |
{ | |
// This is a Simple datastructure that is used in packet queues | |
public class NetworkMessage | |
{ | |
public IPEndPoint Sender { get; set; } | |
public Packet Packet { get; set; } | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Filename: Packet.cs | |
// Author: Benjamin N. Summerton <define-private-public> | |
// License: Unlicense (http://unlicense.org/) | |
using System; | |
using System.Text; | |
using System.Linq; | |
namespace UdpFileTransfer | |
{ | |
public class Packet | |
{ | |
#region Messge Types (Static) | |
public static UInt32 Ack = BitConverter.ToUInt32(Encoding.ASCII.GetBytes("ACK "), 0); | |
public static UInt32 Bye = BitConverter.ToUInt32(Encoding.ASCII.GetBytes("BYE "), 0); | |
public static UInt32 RequestFile = BitConverter.ToUInt32(Encoding.ASCII.GetBytes("REQF"), 0); | |
public static UInt32 RequestBlock = BitConverter.ToUInt32(Encoding.ASCII.GetBytes("REQB"), 0); | |
public static UInt32 Info = BitConverter.ToUInt32(Encoding.ASCII.GetBytes("INFO"), 0); | |
public static UInt32 Send = BitConverter.ToUInt32(Encoding.ASCII.GetBytes("SEND"), 0); | |
#endregion | |
// The Fields for the packet | |
public UInt32 PacketType { get; set; } | |
public byte[] Payload { get; set; } = new byte[0]; | |
#region Handy Properties | |
public bool IsAck { get { return PacketType == Ack; } } | |
public bool IsBye { get { return PacketType == Bye; } } | |
public bool IsRequestFile { get { return PacketType == RequestFile; } } | |
public bool IsRequestBlock { get { return PacketType == RequestBlock; } } | |
public bool IsInfo { get { return PacketType == Info; } } | |
public bool IsSend { get { return PacketType == Send; } } | |
public bool IsUnknown { get { return !(IsAck || IsBye || IsRequestFile || IsRequestBlock || IsInfo || IsSend); } } | |
public string MessageTypeString { get { return Encoding.UTF8.GetString(BitConverter.GetBytes(PacketType)); } } | |
#endregion | |
#region Constructors | |
public Packet(UInt32 packetType) | |
{ | |
// Set the message type | |
PacketType = packetType; | |
} | |
// Creates a Packet from a byte array | |
public Packet(byte[] bytes) | |
{ | |
PacketType = BitConverter.ToUInt32(bytes, 0); // Will grab the first four bytes (which are the type) | |
// Payload starts at byte 4 | |
Payload = new byte[bytes.Length - 4]; | |
bytes.Skip(4).ToArray().CopyTo(Payload, 0); | |
} | |
#endregion // Constructors | |
public override string ToString() | |
{ | |
// Take some of the first few bits of data and turn that into a string | |
String payloadStr; | |
int payloadSize = Payload.Length; | |
if (payloadSize > 8) | |
payloadStr = Encoding.ASCII.GetString(Payload, 0, 8) + "..."; | |
else | |
payloadStr = Encoding.ASCII.GetString(Payload, 0, payloadSize); | |
// type string | |
String typeStr = "UKNOWN"; | |
if (!IsUnknown) | |
typeStr = MessageTypeString; | |
return string.Format( | |
"[Packet:\n" + | |
" Type={0},\n" + | |
" PayloadSize={1},\n" + | |
" Payload=`{2}`]", | |
typeStr, payloadSize, payloadStr); | |
} | |
// Gets the Packet as a byte array | |
public byte[] GetBytes() | |
{ | |
// Join the byte arrays | |
byte[] bytes = new byte[4 + Payload.Length]; | |
BitConverter.GetBytes(PacketType).CopyTo(bytes, 0); | |
Payload.CopyTo(bytes, 4); | |
return bytes; | |
} | |
} | |
#region Definite Packets | |
// ACK | |
public class AckPacket : Packet | |
{ | |
public string Message | |
{ | |
get { return Encoding.UTF8.GetString(Payload); } | |
set { Payload = Encoding.UTF8.GetBytes(value); } | |
} | |
public AckPacket(Packet p=null) : | |
base(Ack) | |
{ | |
if (p != null) | |
Payload = p.Payload; | |
} | |
} | |
// REQF | |
public class RequestFilePacket : Packet | |
{ | |
public string Filename | |
{ | |
get { return Encoding.UTF8.GetString(Payload); } | |
set { Payload = Encoding.UTF8.GetBytes(value); } | |
} | |
public RequestFilePacket(Packet p=null) : | |
base(RequestFile) | |
{ | |
if (p != null) | |
Payload = p.Payload; | |
} | |
} | |
// REQB | |
public class RequestBlockPacket : Packet | |
{ | |
public UInt32 Number | |
{ | |
get { return BitConverter.ToUInt32(Payload, 0); } | |
set { Payload = BitConverter.GetBytes(value); } | |
} | |
public RequestBlockPacket(Packet p = null) | |
: base(RequestBlock) | |
{ | |
if (p != null) | |
Payload = p.Payload; | |
} | |
} | |
// INFO | |
public class InfoPacket : Packet | |
{ | |
// Should be an MD5 checksum | |
public byte[] Checksum | |
{ | |
get { return Payload.Take(16).ToArray(); } | |
set { value.CopyTo(Payload, 0); } | |
} | |
public UInt32 FileSize | |
{ | |
get { return BitConverter.ToUInt32(Payload.Skip(16).Take(4).ToArray(), 0); } | |
set { BitConverter.GetBytes(value).CopyTo(Payload, 16); } | |
} | |
public UInt32 MaxBlockSize | |
{ | |
get { return BitConverter.ToUInt32(Payload.Skip(16 + 4).Take(4).ToArray(), 0); } | |
set { BitConverter.GetBytes(value).CopyTo(Payload, 16 + 4); } | |
} | |
public UInt32 BlockCount | |
{ | |
get { return BitConverter.ToUInt32(Payload.Skip(16 + 4 + 4).Take(4).ToArray(), 0); } | |
set { BitConverter.GetBytes(value).CopyTo(Payload, 16 + 4 + 4); } | |
} | |
public InfoPacket(Packet p = null) | |
: base(Info) | |
{ | |
if (p != null) | |
Payload = p.Payload; | |
else | |
Payload = new byte[16 + 4 + 4 + 4]; | |
} | |
} | |
// SEND | |
public class SendPacket : Packet | |
{ | |
public Block Block { | |
get { return new Block(Payload); } | |
set { Payload = value.GetBytes(); } | |
} | |
public SendPacket(Packet p=null) | |
: base(Send) | |
{ | |
if (p != null) | |
Payload = p.Payload; | |
} | |
} | |
#endregion // Definite Packets | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Filename: UdpFileReceiver.cs | |
// Author: Benjamin N. Summerton <define-private-public> | |
// License: Unlicense (http://unlicense.org/) | |
using System; | |
using System.Diagnostics; | |
using System.IO; | |
using System.IO.Compression; | |
using System.Text; | |
using System.Linq; | |
using System.Net; | |
using System.Net.Sockets; | |
using System.Threading; | |
using System.Collections.Generic; | |
using System.Security.Cryptography; | |
namespace UdpFileTransfer | |
{ | |
class UdpFileReceiver | |
{ | |
#region Statics | |
public static readonly int MD5ChecksumByteSize = 16; | |
#endregion // Statics | |
enum ReceiverState { | |
NotRunning, | |
RequestingFile, | |
WaitingForRequestFileACK, | |
WaitingForInfo, | |
PreparingForTransfer, | |
Transfering, | |
TransferSuccessful, | |
} | |
// Connection data | |
private UdpClient _client; | |
public readonly int Port; | |
public readonly string Hostname; | |
private bool _shutdownRequested = false; | |
private bool _running = false; | |
// Receive Data | |
private Dictionary<UInt32, Block> _blocksReceived = new Dictionary<UInt32, Block>(); | |
private Queue<UInt32> _blockRequestQueue = new Queue<UInt32>(); | |
private Queue<NetworkMessage> _packetQueue = new Queue<NetworkMessage>(); | |
// Other data | |
private MD5 _hasher; | |
// Constructor, sets up connection to <hostname> on <port> | |
public UdpFileReceiver(string hostname, int port) | |
{ | |
Port = port; | |
Hostname = hostname; | |
// Sets a default client to send/receive packets with | |
_client = new UdpClient(Hostname, Port); // will resolve DNS for us | |
_hasher = MD5.Create(); | |
} | |
// Tries to perform a graceful shutdown | |
public void Shutdown() | |
{ | |
_shutdownRequested = true; | |
} | |
// Tries to grab a file and download it to our local machine | |
public void GetFile(string filename) | |
{ | |
// Init the get file state | |
Console.WriteLine("Requesting file: {0}", filename); | |
ReceiverState state = ReceiverState.RequestingFile; | |
byte[] checksum = null; | |
UInt32 fileSize = 0; | |
UInt32 numBlocks = 0; | |
UInt32 totalRequestedBlocks = 0; | |
Stopwatch transferTimer = new Stopwatch(); | |
// Small function to reset the transfer state | |
Action ResetTransferState = new Action(() => | |
{ | |
state = ReceiverState.RequestingFile; | |
checksum = null; | |
fileSize = 0; | |
numBlocks = 0; | |
totalRequestedBlocks = 0; | |
_blockRequestQueue.Clear(); | |
_blocksReceived.Clear(); | |
transferTimer.Reset(); | |
}); | |
// Main loop | |
_running = true; | |
bool senderQuit = false; | |
bool wasRunning = _running; | |
while (_running) | |
{ | |
// Check for some new packets (if there are some) | |
_checkForNetworkMessages(); | |
NetworkMessage nm = (_packetQueue.Count > 0) ? _packetQueue.Dequeue() : null; | |
// In case the sender is shutdown, quit | |
bool isBye = (nm == null) ? false : nm.Packet.IsBye; | |
if (isBye) | |
senderQuit = true; | |
// The state | |
switch (state) | |
{ | |
case ReceiverState.RequestingFile: | |
// Create the REQF | |
RequestFilePacket REQF = new RequestFilePacket(); | |
REQF.Filename = filename; | |
// Send it | |
byte[] buffer = REQF.GetBytes(); | |
_client.Send(buffer, buffer.Length); | |
// Move the state to waiting for ACK | |
state = ReceiverState.WaitingForRequestFileACK; | |
break; | |
case ReceiverState.WaitingForRequestFileACK: | |
// If it is an ACK and the payload is the filename, we're good | |
bool isAck = (nm == null) ? false : (nm.Packet.IsAck); | |
if (isAck) | |
{ | |
AckPacket ACK = new AckPacket(nm.Packet); | |
// Make sure they respond with the filename | |
if (ACK.Message == filename) | |
{ | |
// They got it, shift the state | |
state = ReceiverState.WaitingForInfo; | |
Console.WriteLine("They have the file, waiting for INFO..."); | |
} | |
else | |
ResetTransferState(); // Not what we wanted, reset | |
} | |
break; | |
case ReceiverState.WaitingForInfo: | |
// Verify it's file info | |
bool isInfo = (nm == null) ? false : (nm.Packet.IsInfo); | |
if (isInfo) | |
{ | |
// Pull data | |
InfoPacket INFO = new InfoPacket(nm.Packet); | |
fileSize = INFO.FileSize; | |
checksum = INFO.Checksum; | |
numBlocks = INFO.BlockCount; | |
// Allocate some client side resources | |
Console.WriteLine("Received an INFO packet:"); | |
Console.WriteLine(" Max block size: {0}", INFO.MaxBlockSize); | |
Console.WriteLine(" Num blocks: {0}", INFO.BlockCount); | |
// Send an ACK for the INFO | |
AckPacket ACK = new AckPacket(); | |
ACK.Message = "INFO"; | |
buffer = ACK.GetBytes(); | |
_client.Send(buffer, buffer.Length); | |
// Shift the state to ready | |
state = ReceiverState.PreparingForTransfer; | |
} | |
break; | |
case ReceiverState.PreparingForTransfer: | |
// Prepare the request queue | |
for (UInt32 id = 1; id <= numBlocks; id++) | |
_blockRequestQueue.Enqueue(id); | |
totalRequestedBlocks += numBlocks; | |
// Shift the state | |
Console.WriteLine("Starting Transfer..."); | |
transferTimer.Start(); | |
state = ReceiverState.Transfering; | |
break; | |
case ReceiverState.Transfering: | |
// Send a block request | |
if (_blockRequestQueue.Count > 0) | |
{ | |
// Setup a request for a Block | |
UInt32 id = _blockRequestQueue.Dequeue(); | |
RequestBlockPacket REQB = new RequestBlockPacket(); | |
REQB.Number = id; | |
// Send the Packet | |
buffer = REQB.GetBytes(); | |
_client.Send(buffer, buffer.Length); | |
// Some handy info | |
Console.WriteLine("Sent request for Block #{0}", id); | |
} | |
// Check if we have any blocks ourselves in the queue | |
bool isSend = (nm == null) ? false : (nm.Packet.IsSend); | |
if (isSend) | |
{ | |
// Get the data (and save it | |
SendPacket SEND = new SendPacket(nm.Packet); | |
Block block = SEND.Block; | |
_blocksReceived.Add(block.Number, block); | |
// Print some info | |
Console.WriteLine("Received Block #{0} [{1} bytes]", block.Number, block.Data.Length); | |
} | |
// Requeue any requests that we haven't received | |
if ((_blockRequestQueue.Count == 0) && (_blocksReceived.Count != numBlocks)) | |
{ | |
for (UInt32 id = 1; id <= numBlocks; id++) | |
{ | |
if (!_blocksReceived.ContainsKey(id) && !_blockRequestQueue.Contains(id)) | |
{ | |
_blockRequestQueue.Enqueue(id); | |
totalRequestedBlocks++; | |
} | |
} | |
} | |
// Did we get all the block we need? Move to the "transfer successful state." | |
if (_blocksReceived.Count == numBlocks) | |
state = ReceiverState.TransferSuccessful; | |
break; | |
case ReceiverState.TransferSuccessful: | |
transferTimer.Stop(); | |
// Things were good, send a BYE message | |
Packet BYE = new Packet(Packet.Bye); | |
buffer = BYE.GetBytes(); | |
_client.Send(buffer, buffer.Length); | |
Console.WriteLine("Transfer successful; it took {0:0.000}s with a success ratio of {1:0.000}.", | |
transferTimer.Elapsed.TotalSeconds, (double)numBlocks / (double)totalRequestedBlocks); | |
Console.WriteLine("Decompressing the Blocks..."); | |
// Reconstruct the data | |
if (_saveBlocksToFile(filename, checksum, fileSize)) | |
Console.WriteLine("Saved file as {0}.", filename); | |
else | |
Console.WriteLine("There was some trouble in saving the Blocks to {0}.", filename); | |
// And we're done here | |
_running = false; | |
break; | |
} | |
// Sleep | |
Thread.Sleep(1); | |
// Check for shutdown | |
_running &= !_shutdownRequested; | |
_running &= !senderQuit; | |
} | |
// Send a BYE message if the user wanted to cancel | |
if (_shutdownRequested && wasRunning) | |
{ | |
Console.WriteLine("User canceled transfer."); | |
Packet BYE = new Packet(Packet.Bye); | |
byte[] buffer = BYE.GetBytes(); | |
_client.Send(buffer, buffer.Length); | |
} | |
// If the server told us to shutdown | |
if (senderQuit && wasRunning) | |
Console.WriteLine("The sender quit on us, canceling the transfer."); | |
ResetTransferState(); // This also cleans up collections | |
_shutdownRequested = false; // In case we shut down one download, but want to start a new one | |
} | |
public void Close() | |
{ | |
_client.Close(); | |
} | |
// Trys to fill the queue of packets | |
private void _checkForNetworkMessages() | |
{ | |
if (!_running) | |
return; | |
// Check that there is something available (and at least four bytes for type) | |
int bytesAvailable = _client.Available; | |
if (bytesAvailable >= 4) | |
{ | |
// This will read ONE datagram (even if multiple have been received) | |
IPEndPoint ep = new IPEndPoint(IPAddress.Any, 0); | |
byte[] buffer = _client.Receive(ref ep); | |
Packet p = new Packet(buffer); | |
// Create the message structure and queue it up for processing | |
NetworkMessage nm = new NetworkMessage(); | |
nm.Sender = ep; | |
nm.Packet = p; | |
_packetQueue.Enqueue(nm); | |
} | |
} | |
// Trys to uncompress the blocks and save them to a file | |
private bool _saveBlocksToFile(string filename, byte[] networkChecksum, UInt32 fileSize) | |
{ | |
bool good = false; | |
try | |
{ | |
// Allocate some memory | |
int compressedByteSize = 0; | |
foreach (Block block in _blocksReceived.Values) | |
compressedByteSize += block.Data.Length; | |
byte[] compressedBytes = new byte[compressedByteSize]; | |
// Reconstruct into one big block | |
int cursor = 0; | |
for (UInt32 id = 1; id <= _blocksReceived.Keys.Count; id++) | |
{ | |
Block block = _blocksReceived[id]; | |
block.Data.CopyTo(compressedBytes, cursor); | |
cursor += Convert.ToInt32(block.Data.Length); | |
} | |
// Now save it | |
using (MemoryStream uncompressedStream = new MemoryStream()) | |
using (MemoryStream compressedStream = new MemoryStream(compressedBytes)) | |
using (DeflateStream deflateStream = new DeflateStream(compressedStream, CompressionMode.Decompress)) | |
{ | |
deflateStream.CopyTo(uncompressedStream); | |
// Verify checksums | |
uncompressedStream.Position = 0; | |
byte[] checksum = _hasher.ComputeHash(uncompressedStream); | |
if (!Enumerable.SequenceEqual(networkChecksum, checksum)) | |
throw new Exception("Checksum of uncompressed blocks doesn't match that of INFO packet."); | |
// Write it to the file | |
uncompressedStream.Position = 0; | |
using (FileStream fileStream = new FileStream(filename, FileMode.Create)) | |
uncompressedStream.CopyTo(fileStream); | |
} | |
good = true; | |
} | |
catch (Exception e) | |
{ | |
// Crap... | |
Console.WriteLine("Could not save the blocks to \"{0}\", reason:", filename); | |
Console.WriteLine(e.Message); | |
} | |
return good; | |
} | |
#region Program Execution | |
public static UdpFileReceiver fileReceiver; | |
public static void InterruptHandler(object sender, ConsoleCancelEventArgs args) | |
{ | |
args.Cancel = true; | |
fileReceiver?.Shutdown(); | |
} | |
public static void Main(string[] args) | |
{ | |
// setup the receiver | |
string hostname = "localhost";//args[0].Trim(); | |
int port = 6000;//int.Parse(args[1].Trim()); | |
string filename = "short_message.txt";//args[2].Trim(); | |
fileReceiver = new UdpFileReceiver(hostname, port); | |
// Add the Ctrl-C handler | |
Console.CancelKeyPress += InterruptHandler; | |
// Get a file | |
fileReceiver.GetFile(filename); | |
fileReceiver.Close(); | |
} | |
#endregion // Program Execution | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Filename: UdpFileSender.cs | |
// Author: Benjamin N. Summerton <define-private-public> | |
// License: Unlicense (http://unlicense.org/) | |
using System; | |
using System.Diagnostics; | |
using System.IO; | |
using System.IO.Compression; | |
using System.Text; | |
using System.Linq; | |
using System.Net; | |
using System.Net.Sockets; | |
using System.Threading; | |
using System.Collections.Generic; | |
using System.Security.Cryptography; | |
namespace UdpFileTransfer | |
{ | |
public class UdpFileSender | |
{ | |
#region Statics | |
public static readonly UInt32 MaxBlockSize = 8 * 1024; // 8KB | |
#endregion // Statics | |
enum SenderState | |
{ | |
NotRunning, | |
WaitingForFileRequest, | |
PreparingFileForTransfer, | |
SendingFileInfo, | |
WaitingForInfoACK, | |
Transfering | |
} | |
// Connection data | |
private UdpClient _client; | |
public readonly int Port; | |
public bool Running { get; private set; } = false; | |
// Transfer data | |
public readonly string FilesDirectory; | |
private HashSet<string> _transferableFiles; | |
private Dictionary<UInt32, Block> _blocks = new Dictionary<UInt32, Block>(); | |
private Queue<NetworkMessage> _packetQueue = new Queue<NetworkMessage>(); | |
// Other stuff | |
private MD5 _hasher; | |
// Constructor, creates a UdpClient on <port> | |
public UdpFileSender(string filesDirectory, int port) | |
{ | |
FilesDirectory = filesDirectory; | |
Port = port; | |
_client = new UdpClient(Port, AddressFamily.InterNetwork); // Bind in IPv4 | |
_hasher = MD5.Create(); | |
} | |
// Prepares the Sender for file transfers | |
public void Init() | |
{ | |
// Scan files (only the top directory) | |
List<string> files = new List<string>(Directory.EnumerateFiles(FilesDirectory)); | |
_transferableFiles = new HashSet<string>(files.Select(s => s.Substring(FilesDirectory.Length + 1))); | |
// Make sure we have at least one to send | |
if (_transferableFiles.Count != 0) | |
{ | |
// Modify the state | |
Running = true; | |
// Print info | |
Console.WriteLine("I'll transfer these files:"); | |
foreach (string s in _transferableFiles) | |
Console.WriteLine(" {0}", s); | |
} | |
else | |
Console.WriteLine("I don't have any files to transfer."); | |
} | |
// signals for a (graceful) shutdown) | |
public void Shutdown() | |
{ | |
Running = false; | |
} | |
// Main loop of the sender | |
public void Run() | |
{ | |
// Transfer state | |
SenderState state = SenderState.WaitingForFileRequest; | |
string requestedFile = ""; | |
IPEndPoint receiver = null; | |
// This is a handly little function to reset the transfer state | |
Action ResetTransferState = new Action(() => | |
{ | |
state = SenderState.WaitingForFileRequest; | |
requestedFile = ""; | |
receiver = null; | |
_blocks.Clear(); | |
}); | |
while (Running) | |
{ | |
// Check for some new messages | |
_checkForNetworkMessages(); | |
NetworkMessage nm = (_packetQueue.Count > 0) ? _packetQueue.Dequeue() : null; | |
// Check to see if we have a BYE | |
bool isBye = (nm == null) ? false : nm.Packet.IsBye; | |
if (isBye) | |
{ | |
// Set back to the original state | |
ResetTransferState(); | |
Console.WriteLine("Received a BYE message, waiting for next client."); | |
} | |
// Do an action depending on the current state | |
switch (state) | |
{ | |
case SenderState.WaitingForFileRequest: | |
// Check to see that we got a file request | |
// If there was a packet, and it's a request file, send and ACK and switch the state | |
bool isRequestFile = (nm == null) ? false : nm.Packet.IsRequestFile; | |
if (isRequestFile) | |
{ | |
// Prepare the ACK | |
RequestFilePacket REQF = new RequestFilePacket(nm.Packet); | |
AckPacket ACK = new AckPacket(); | |
requestedFile = REQF.Filename; | |
// Print info | |
Console.WriteLine("{0} has requested file file \"{1}\".", nm.Sender, requestedFile); | |
// Check that we have the file | |
if (_transferableFiles.Contains(requestedFile)) | |
{ | |
// Mark that we have the file, save the sender as our current receiver | |
receiver = nm.Sender; | |
ACK.Message = requestedFile; | |
state = SenderState.PreparingFileForTransfer; | |
Console.WriteLine(" We have it."); | |
} | |
else | |
ResetTransferState(); | |
// Send the message | |
byte[] buffer = ACK.GetBytes(); | |
_client.Send(buffer, buffer.Length, nm.Sender); | |
} | |
break; | |
case SenderState.PreparingFileForTransfer: | |
// Using the requested file, prepare it in memory | |
byte[] checksum; | |
UInt32 fileSize; | |
if (_prepareFile(requestedFile, out checksum, out fileSize)) | |
{ | |
// It's good, send an info Packet | |
InfoPacket INFO = new InfoPacket(); | |
INFO.Checksum = checksum; | |
INFO.FileSize = fileSize; | |
INFO.MaxBlockSize = MaxBlockSize; | |
INFO.BlockCount = Convert.ToUInt32(_blocks.Count); | |
// Send it | |
byte[] buffer = INFO.GetBytes(); | |
_client.Send(buffer, buffer.Length, receiver); | |
// Move the state | |
Console.WriteLine("Sending INFO, waiting for ACK..."); | |
state = SenderState.WaitingForInfoACK; | |
} | |
else | |
ResetTransferState(); // File not good, reset the state | |
break; | |
case SenderState.WaitingForInfoACK: | |
// If it is an ACK and the payload is the filename, we're good | |
bool isAck = (nm == null) ? false : (nm.Packet.IsAck); | |
if (isAck) | |
{ | |
AckPacket ACK = new AckPacket(nm.Packet); | |
if (ACK.Message == "INFO") | |
{ | |
Console.WriteLine("Starting Transfer..."); | |
state = SenderState.Transfering; | |
} | |
} | |
break; | |
case SenderState.Transfering: | |
// If there is a block request, send it | |
bool isRequestBlock = (nm == null) ? false : nm.Packet.IsRequestBlock; | |
if (isRequestBlock) | |
{ | |
// Pull out data | |
RequestBlockPacket REQB = new RequestBlockPacket(nm.Packet); | |
Console.WriteLine("Got request for Block #{0}", REQB.Number); | |
// Create the response packet | |
Block block = _blocks[REQB.Number]; | |
SendPacket SEND = new SendPacket(); | |
SEND.Block = block; | |
// Send it | |
byte[] buffer = SEND.GetBytes(); | |
_client.Send(buffer, buffer.Length, nm.Sender); | |
Console.WriteLine("Sent Block #{0} [{1} bytes]", block.Number, block.Data.Length); | |
} | |
break; | |
} | |
Thread.Sleep(1); | |
} | |
// If there was a receiver set, that means we need to notify it to shutdown | |
if (receiver != null) | |
{ | |
Packet BYE = new Packet(Packet.Bye); | |
byte[] buffer = BYE.GetBytes(); | |
_client.Send(buffer, buffer.Length, receiver); | |
} | |
state = SenderState.NotRunning; | |
} | |
// Shutsdown the underlying UDP client | |
public void Close() | |
{ | |
_client.Close(); | |
} | |
// Trys to fill the queue of packets | |
private void _checkForNetworkMessages() | |
{ | |
if (!Running) | |
return; | |
// Check that there is something available (and at least four bytes for type) | |
int bytesAvailable = _client.Available; | |
if (bytesAvailable >= 4) | |
{ | |
// This will read ONE datagram (even if multiple have been received) | |
IPEndPoint ep = new IPEndPoint(IPAddress.Any, 0); | |
byte[] buffer = _client.Receive(ref ep); | |
// Create the message structure and queue it up for processing | |
NetworkMessage nm = new NetworkMessage(); | |
nm.Sender = ep; | |
nm.Packet = new Packet(buffer); | |
_packetQueue.Enqueue(nm); | |
} | |
} | |
// Loads the file into the blocks, returns true if the requested file is ready | |
private bool _prepareFile(string requestedFile, out byte[] checksum, out UInt32 fileSize) | |
{ | |
Console.WriteLine("Preparing the file to send..."); | |
bool good = false; | |
fileSize = 0; | |
try | |
{ | |
// Read it in & compute a checksum of the original file | |
byte[] fileBytes = File.ReadAllBytes(Path.Combine(FilesDirectory, requestedFile)); | |
checksum = _hasher.ComputeHash(fileBytes); | |
fileSize = Convert.ToUInt32(fileBytes.Length); | |
Console.WriteLine("{0} is {1} bytes large.", requestedFile, fileSize); | |
// Compress it | |
Stopwatch timer = new Stopwatch(); | |
using (MemoryStream compressedStream = new MemoryStream()) | |
{ | |
// Perform the actual compression | |
DeflateStream deflateStream = new DeflateStream(compressedStream, CompressionMode.Compress, true); | |
timer.Start(); | |
deflateStream.Write(fileBytes, 0, fileBytes.Length); | |
deflateStream.Close(); | |
timer.Stop(); | |
// Put it into blocks | |
compressedStream.Position = 0; | |
long compressedSize = compressedStream.Length; | |
UInt32 id = 1; | |
while (compressedStream.Position < compressedSize) | |
{ | |
// Grab a chunk | |
long numBytesLeft = compressedSize - compressedStream.Position; | |
long allocationSize = (numBytesLeft > MaxBlockSize) ? MaxBlockSize : numBytesLeft; | |
byte[] data = new byte[allocationSize]; | |
compressedStream.Read(data, 0, data.Length); | |
// Create a new block | |
Block b = new Block(id++); | |
b.Data = data; | |
_blocks.Add(b.Number, b); | |
} | |
// Print some info and say we're good | |
Console.WriteLine("{0} compressed is {1} bytes large in {2:0.000}s.", requestedFile, compressedSize, timer.Elapsed.TotalSeconds); | |
Console.WriteLine("Sending the file in {0} blocks, using a max block size of {1} bytes.", _blocks.Count, MaxBlockSize); | |
good = true; | |
} | |
} | |
catch (Exception e) | |
{ | |
// Crap... | |
Console.WriteLine("Could not prepare the file for transfer, reason:"); | |
Console.WriteLine(e.Message); | |
// Reset a few things | |
_blocks.Clear(); | |
checksum = null; | |
} | |
return good; | |
} | |
#region Program Execution | |
public static UdpFileSender fileSender; | |
public static void InterruptHandler(object sender, ConsoleCancelEventArgs args) | |
{ | |
args.Cancel = true; | |
fileSender?.Shutdown(); | |
} | |
public static void Main(string[] args) | |
{ | |
// Setup the sender | |
string filesDirectory = "Files";//args[0].Trim(); | |
int port = 6000;//int.Parse(args[1].Trim()); | |
fileSender = new UdpFileSender(filesDirectory, port); | |
// Add the Ctrl-C handler | |
Console.CancelKeyPress += InterruptHandler; | |
// Run it | |
fileSender.Init(); | |
fileSender.Run(); | |
fileSender.Close(); | |
} | |
#endregion // Program Execution | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment