Author: Brian A. Ree
0:What This Tutorial Will Teach You
Along with the visual studio sample project above, this tutorial will teach you how to write
a multi-threaded astnchronous socket server for running network speed tests.
1: CS Socket Server: Intro
Ever wanted to create your own lightning fast socket server? Web service calls too slow for your mobile
app or networked video game? Socket servers are the way to go, lower overhead, faster performance, etc.
This tutorial will show you how to create your own multi0threaded asynchronous socket server by giving you
a tour of an existing implementation. So let's dive in, shall we. First thing;s first, when clients connect to our
socket server we need a way of tracking their interaction with out server and monitoring the state of their connection.
Our first class for this server will be our StateObject class, listed below we'll be going over
the members of this class next.
using System;
using System.Net.Sockets;
namespace SpeedTestDll
{
public class StateObject
{
public Socket workSocket = null;
public int inBufferSize;
public int outBufferSize;
public byte[] inBuffer;
public DateTime timeStamp;
public int expecting = 0;
public int received = 0;
public long connTime = 0;
public StateObject(int inBuff, int outBuff)
{
inBufferSize = inBuff;
outBufferSize = outBuff;
inBuffer = new byte[inBufferSize];
}
public void ResetBuffers()
{
inBuffer = new byte[inBufferSize];
}
}
}
2: CS Socket Server: State Object
Ok so let's go over the members of our StateObject class. You'll notice that all the members are public
and there are no get/set methods. This is generally considered bed practice from a code encapsulation perspective but
when speed is the main goal it is much more efficient to reference a class member then to push a method call onto the stack.
Read over the member and their descriptions below so that you have an understanding of what they are responsible for.
- workSocket: This is the main socket object used in the server. It contains the connection from the client to the server.
- inBufferSize: This is a variable that controls the size of the socket read buffer.
- outBufferSize: This is a variable that controls the size of the socket write buffer.
- inBuffer: Byte array used to store information read from the socket.
- timeStamp: Timestamp used to track when the client connected to the server.
- expecting: The total number of bytes we are expecting to read off of the socket buffer.
- received: The total number of bytes we have read off of the socket buffer.
- connTime: The socket connection timestamp.
You might have noticed that the outBuffer member doesn't exist. But the outBufferSize member does exist. This
is because the buffer values are used to set the socket buffer sizes as well as local read/write array sizes. In this
particular socket server example we write directly onto the send channel of the socket without using an sync call. This is due
to the nature of this particular server's work load.
3: CS Socket Server: Default Variables
Let's start now on the review of our main socket server class, SpeedTest. We'll start by reviewing the static default variables
that are members of our class.
///
/// The number of minutes to wait before considering a connection old.
///
public const int DEFAULT_OLD_CONN_MIN = 5;
///
/// The number of log writes to wait before checking the log size for recycling.
///
public const int CHECK_LOG_SIZE_TICKS = 25;
///
/// The max log size to allow before recycling the log file.
///
public const int CHECK_LOG_SIZE = 5000000;
///
/// The default number of sockets that will be opened up by the server.
///
public const int DEFAULT_MAX_SOCKETS = 10;
///
/// The default debug setting.
///
public const bool DEFAULT_DEBUG_ON = false;
public const bool DEFAULT_PUBLIC_ON = true;
public const float MiB_FLT = 1048576.0f; //1 MiB
public const double MiB_DBL = 1048576.0; //1 MiB
public const int MiB_INT = 1048576; //1 MiB
public const float KiB_FLT = 1024.0f; //1 KiB
public const double KiB_DBL = 1024.0; //1 KiB
public const int KiB_INT = 1024; //1 KiB
public const float MB_FLT = 1000000.0f; //1 MB
public const double MB_DBL = 1000000.0; //1 MB
public const int MB_INT = 1000000; //1 MB
public const float KB_FLT = 1000.0f; //1 KB
public const double KB_DBL = 1000.0; //1 KB
public const int KB_INT = 1000; //1 KB
public const int BITS_PER_BYTE = 8; //1 byte, 8 bits
- DEFAULT_OLD_CONN_MIN: The number of minutes to wait before considering a connection old.
- CHECK_LOG_SIZE_TICKS: The number of log writes to wait before checking the log size for recycling.
- CHECK_LOG_SIZE: The max log size to allow before recycling the log file.
- DEFAULT_MAX_SOCKETS: The default number of sockets that will be opened up by the server.
- DEFAULT_DEBUG_ON: The default debug setting.
- DEFAULT_PUBLIC_ON: The default public server settings.
- MiB_FLT: Mebibyte size as a float.
- MiB_DBL: Mebibyte size as a double.
- MiB_INT: Mebibyte size as an integer.
- Kib_FLT: Kebibyte size as a float.
- Kib_DBL: Kebibyte size as a double.
- Kib_INT: Kebibyte size as an integer.
- MB_FLT: Megabyte size as a float.
- MB_DBL: Megabyte size as a double.
- MB_INT: Megabyte size as an integer.
- KB_FLT: Megabyte size as a float.
- KB_DBL: Megabyte size as a double.
- KB_INT: Megabyte size as an integer.
- BITS_PER_BYTE: The number of bits in a byte.
These default configuration values are a bit self explanatory but we'll cover them in any case. Once we're done with class
variables then we can move on to more interesting code. The following is a set of local variables used to handle configuration settings.
The previous variables are used in the different speed tests the server supports and play a role in some configuration aspects of
the socket server.
public const int DEFAULT_SLOW_NETWORK_TIMEOUT_S = 10; //s
public const string DEFAULT_SLOW_NETWORK_TIMEOUT_S_STR = "10"; //s
public const int DEFAULT_SLOW_NETWORK_TIMEOUT_MS = 10000; //ms
public const string DEFAULT_SLOW_NETWORK_TIMEOUT_MS_STR = "10000"; //ms
///
/// Default port number to start listening on.
///
public const int DEFAULT_PORT = 49986;
public const string DEFAULT_PORT_STR = "49986";
public const int DEFAULT_TIMEOUT = 15000;
public const string DEFAULT_TIMEOUT_STR = "15000";
public const int DEFAULT_CONNECTION_TIMEOUT = 15000;
public const string DEFAULT_CONNECTION_TIMEOUT_STR = "15000";
public const int DEFAULT_IN_BUFFER = (MiB_INT * 2); //2 MiB
public static string DEFAULT_IN_BUFFER_STR = ((MiB_INT * 2) + ""); //2 MiB
public const int DEFAULT_OUT_BUFFER = (MiB_INT * 2); //2 MiB
public static string DEFAULT_OUT_BUFFER_STR = ((MiB_INT * 2) + ""); //2 MiB
public const int DEFAULT_MAX_XFER = (MiB_INT * 100); //100 MiB
public static string DEFAULT_MAX_XFER_STR = ((MiB_INT * 100) + ""); //100 MiB
private const int MIN_NUM_THREADS = 5;
private const int MAX_NUM_THREADS = 1024;
private const int MIN_KILL_MIN = 3;
private const int MAX_KILL_MIN = 30;
private const int MIN_SOCKETS = 5;
private const int MAX_SOCKETS = 1024;
private const int MAX_IN_BUFFER_LENGTH = (MiB_INT * 5); //5 MiB
private const int MIN_IN_BUFFER_LENGTH = (MiB_INT * 1); //1 MiB
private const int MAX_OUT_BUFFER_LENGTH = (MiB_INT * 5); //5 MiB
private const int MIN_OUT_BUFFER_LENGTH = (MiB_INT * 1); //1 MiB
private const int MIN_TIMEOUT = 0;
private const int MAX_TIMEOUT = (short.MaxValue * 2);
private const int MIN_XFER_LIMIT = (MiB_INT * 10); //10 MiB
private const int MAX_XFER_LIMIT = (MiB_INT * 100); //100 MiB
private const int MIN_PORT = 0;
private const int MAX_PORT = (short.MaxValue * 2);
public const string DEFAULT_SPEED_TEST_REGISTRATION_URL = "http://middlemind.biz:83/vbsvc/Service1.asmx/SetSpeedTestServer2?expireDate={expireDate}&port={port}&inBufferBytes={inBufferBytes}&outBufferBytes={outBufferBytes}&maxXferBytes={maxXferBytes}&timeout={timeout}&code1={code1}&code2={code2}";
public const int DEFAULT_NUM_THREADS = 1;
public static string DEFAULT_LOCAL_IP_ADDRESS = IPAddress.Any.ToString();
public const string DEFAULT_TITLE = "MmgSpeedTest";
- DEFAULT_SLOW_NETWORK_TIMEOUT_S: The default slow network timeout in seconds.
- DEFAULT_SLOW_NETWORK_TIMEOUT_S_STR: A string version of the slow network timeout in seconds.
- DEFAULT_SLOW_NETWORK_TIMEOUT_MS: The default slow network timeout in miliseconds.
- DEFAULT_SLOW_NETWORK_TIMEOUT_MS_STR: A string version of the slow network timeout in miliseconds.
- DEFAULT_PORT: The default socket server port.
- DEFAULT_PORT_STR: A string version of the default socket server port.
- DEFAULT_TIMEOUT: The default socket server timeout.
- DEFAULT_TIMEOUT_STR: A string version of the default socket server timeout.
- DEFAULT_CONNECTION_TIMEOUT: The default socket server connection timeout.
- DEFAULT_CONNECTION_TIMEOUT_STR: A string version of the default socket server timeout.
- DEFAULT_IN_BUFFER: The default in buffer size.
- DEFAULT_IN_BUFFER_STR: A string version of the default in buffer size.
- DEFAULT_OUT_BUFFER: The default out buffer size.
- DEFAULT_OUT_BUFFER_STR: A string version of the default out buffer size.
- DEFAULT_MAX_XFER: The default maximum transfer size this server supports.
- DEFAULT_MAX_XFER_STR: A string version of the default maximum transfer size this server supports.
- MIN_PORT: The minimum allowed port number.
- MAX_PORT: The maximum allowed port number.
- DEFAULT_SPEED_TEST_REGISTRATION_URL: The URL template used to register this socket server with a central database listing.
- DEFAULT_NUM_THREADS: The default number of threads.
- DEFAULT_LOCAL_IP_ADDRESS: The default local IP address this socket server will bind to.
- DEFAULT_TITLE: The default title of this socket server.
4: CS Socket Server: Variables
The following listing provides detailed information on the local, non-default setting, non binary description variables.
As we get closer to the core code you should start to get an idea of what these variables are used for and why they exist.
//LOCAL VARIABLES
private int inBufferSize = DEFAULT_IN_BUFFER;
private int outBufferSize = DEFAULT_OUT_BUFFER;
private long serverRegistrationStart = -1;
private long serverRegistrationNow = -1;
private string version = "1.0.0.1";
///
/// Application name for this windows service.
///
private string appName = DEFAULT_TITLE;
///
/// Application title for this windows service.
///
private string title = DEFAULT_TITLE;
///
/// The port this service will begin listening on.
///
private int port = DEFAULT_PORT;
///
/// Variable that tracks the current socket count.
///
private int socketCount = 0;
///
/// Constant that defines the number of threads to open up.
///
private int numThreads = DEFAULT_NUM_THREADS;
///
/// Constant that defines the connection timeout in ms.
///
private int timeout = DEFAULT_TIMEOUT;
///
/// Hashtable that tracks current connections.
///
//private Hashtable connHt = new Hashtable();
///
/// ArrayList that tracks the connection sockets.
///
private ArrayList connSockets = null;
///
/// Callback event used by the socket connection code.
///
private ManualResetEvent allDone = new ManualResetEvent(false);
///
/// Main thread pool for connection management.
///
private Thread[] serverThread;
///
/// Thread end event callback pool.
///
//private AutoResetEvent[] threadEnd;
///
/// Boolean that indicates the shutdown state of the service.
///
private bool shuttingDown = false;
///
/// The maximum allowed number of sockets.
///
private int maxSockets = DEFAULT_MAX_SOCKETS;
private int oldConnMin = DEFAULT_OLD_CONN_MIN;
///
/// The root directory used for config files and log files.
///
private string rootDir = System.Environment.GetFolderPath(System.Environment.SpecialFolder.ApplicationData);
///
/// A variable that holds the application directory for this service.
///
private string appDir = "";
///
/// The expected file name for the config file.
///
private string configFileName = "config.txt";
///
/// The full path to the config file.
///
private string configFilePath = "";
///
/// The expected file name for the log file.
///
private string debugFileName = "debug.txt";
///
/// The full path to the log file.
///
private string debugFilePath = "";
///
/// Boolean that determines if logging is on or off.
///
private bool debugOn = DEFAULT_DEBUG_ON;
private bool publicOn = DEFAULT_PUBLIC_ON;
///
/// Static local reference.
///
public static SpeedTest cApp;
///
/// Log writer.
///
private StreamWriter debug = null;
///
/// Check log size tick.
///
private int wrCount = 0;
private string speedTestRegUrl = DEFAULT_SPEED_TEST_REGISTRATION_URL;
private int maxXferSize = DEFAULT_MAX_XFER;
private string localIpAddress = DEFAULT_LOCAL_IP_ADDRESS;
private string code1 = "middlemindgames.com-register-speed-test";
private string code2 = "middlemindgames.com-speed-test-2";
- inBufferSize: The in buffer size to use in socket network reads.
- outBufferSize: The out buffer size used in socket network writes.
- serverRegistrationStart: The system time of the last server registration. Used in controlling when a public server registers itself.
- serverRegistrationNow: The current system time of the current server registration time check. Used in controlling when a public server registers itself.
- version: The current version of the socket server software.
- appName: Based on the default title.
- title: Based on the default title.
- port: The port this server will accept sockets on.
- socketCount: The number of client sockets connected to this server.
- numThreads: The number of socket acceptance threads this socket server has open. Deprecated.
- timeout: The socket connection timeout for this server.
- connSockets: An array list of the connected client sockets, stored by StateObject.
- serverThread: An array of socket server connection threads. Deprecated.
- shuttingDown: A boolean flag that indicates if the socket server should be shutting down.
- maxSockets: The maximum number of sockets allowed to connect to the server.
- oldConnMin: The number of minutes the has to pass to consider a socket connection old.
- rootDir: The root directory used for config files and log files.
- appDir: The application root directory from where this socket server has been executed.
- configFileName: The name of the expected config file.
- configFilePath: The full path to the expected config file.
- debugFileName: The name of the expected debug file.
- debugFilePath: The full path to the expected debug file.
- debugOn: A boolean flag indicating if debugging has been turned on.
- publicOn: A boolean flag indicating if this server should register itself as a public server.
- cApp: Local self reference.
- debug: Part of the debug logging sub system.
- fw: A file writer used in the debug logging system.
- wrCount: Log tick tracking variable.
- speedTestRegUrl: The URL for registering the speed test server.
- localIpAddress: The local IP address this server is bound to.
- code1: A web service registration security code.
- code2: A second web service registrtion code.
5: CS Socket Server: Constructor and Prep
Now that we got that nasty bit of business out of the way we can start reviewing the interesting code, that runs our
socket server. First let's take a look at our constructor.
public SpeedTest(int Port, string Title)
{
cApp = this;
appDir = Path.Combine(rootDir, appName);
configFilePath = Path.Combine(appDir, configFileName);
debugFilePath = Path.Combine(appDir, debugFileName);
Console.WriteLine("");
Console.WriteLine("App Dir: " + appDir);
Console.WriteLine("Config Path: " + configFilePath);
Console.WriteLine("Debug Path: " + debugFilePath);
LoadSettings();
CreateDebugFile();
BeginRegisterServer();
serverThread = new Thread[numThreads];
connSockets = new ArrayList(maxSockets);
}
First thing we do is store a static self reference, this is a deprecated call and will be removed
from a future revision. Next we setup our config file paths for our application directory and our
config, debug, file paths. We're going to dump the config paths we setup to standard to standard output
so that users know where the server is attempting to locate config files. The constructor takes responsibility
for loading data driven settings, getting the logging subsystem ready and registering the server if it's a public server.
After these initialization steps the serverThread variable is instantiated, this call is also deprecated in this version of our server.
Similarly the socket connection tracking variables are instantiated. The connSockets instance will be used to keep track of all our client
connections. Let's take a look at the main initialization calls made in the constructor next. We'll look at the LoadSettings method
next so we can soo how our app settings are handled.
private void LoadSettings()
{
Console.WriteLine("");
Console.WriteLine("Loading settings...");
Console.WriteLine("Looking for data directory: " + this.appDir);
if(Directory.Exists(this.appDir) == false)
{
Console.WriteLine("Creating the app directory, this is where the config file should go, config.txt.");
Directory.CreateDirectory(this.appDir);
}
Console.WriteLine("");
Console.WriteLine("Looking for config file: " + this.configFilePath);
Console.WriteLine("Entries in the config file are as follows.");
Console.WriteLine("port=[" + DEFAULT_PORT + ": The desired port number.]");
Console.WriteLine("debug=[" + (DEFAULT_DEBUG_ON ? "1" : "0") + ": Set to 1/true for true, 0/false for false.]");
Console.WriteLine("max_sockets=[" + DEFAULT_MAX_SOCKETS + ": An integer value representing the number of connections, use a small number if your server has few resources.]");
Console.WriteLine("public=[" + (DEFAULT_PUBLIC_ON ? "1" : "0") + ": Set to 1/true for true, 0/false for false. Will register server once every 24 hours.]");
Console.WriteLine("connection_timeout_ms=[" + DEFAULT_TIMEOUT + ": The number of miliseconds to wait for socket timeout.]");
Console.WriteLine("old_socket_kill_min=[5: The number of minutes to wait before killing old sockets.]");
Console.WriteLine("speed_test_registration_url:[" + DEFAULT_SPEED_TEST_REGISTRATION_URL + ": The speed test server registration URL.]");
Console.WriteLine("in_buffer_size=[" + DEFAULT_IN_BUFFER + ": The number of bytes for the input connection buffer.]");
Console.WriteLine("out_buffer_size=[" + DEFAULT_OUT_BUFFER + ": The number of bytes for the output connection buffer.]");
Console.WriteLine("max_xfer_size=[" + DEFAULT_MAX_XFER + ": The max number of bytes for the entire tranfer.]");
//Console.WriteLine("num_threads=[" + DEFAULT_NUM_THREADS + ": The number of threads the server should run with.]");
Console.WriteLine("local_ip_address=[" + DEFAULT_LOCAL_IP_ADDRESS + ": The local IP address to bind to.]");
Console.WriteLine("");
if(File.Exists(this.configFilePath) == true)
{
#region config_exists
Console.WriteLine("Found a config file.");
Console.WriteLine("Loading config file values...");
StreamReader sr = new StreamReader(this.configFilePath);
string line = sr.ReadLine();
while(line != null)
{
bool success = false;
int v = 0;
IPAddress ip;
string tmp = "";
string[] pair = line.Split(new char[] { '=' }, StringSplitOptions.RemoveEmptyEntries);
if(pair.Length == 2)
{
string key = pair[0];
string val = pair[1];
if(key.ToUpper() == "PORT")
{
success = false;
success = Int32.TryParse(val, out v);
if(success == true)
{
if(port >= MIN_PORT && port <= MAX_PORT)
{
port = v;
Console.WriteLine("Found port: " + this.port);
}
else
{
port = DEFAULT_PORT;
Console.WriteLine("Port outside bounds, using default timeout: " + this.port);
}
}
else
{
port = DEFAULT_PORT;
Console.WriteLine("Could not parse port, using default port: " + this.port);
}
}
else if(key.ToUpper() == "DEBUG")
{
tmp = val.ToUpper();
if(tmp == "1" || tmp == "TRUE")
{
this.debugOn = true;
}
else
{
this.debugOn = false;
}
Console.WriteLine("Found debug: " + this.debugOn);
}
else if (key.ToUpper() == "MAX_SOCKETS")
{
success = false;
success = Int32.TryParse(val, out v);
if (success == true)
{
if (v >= MIN_SOCKETS && v <= MAX_SOCKETS)
{
maxSockets = v;
Console.WriteLine("Found max sockets: " + this.maxSockets);
}
else
{
maxSockets = DEFAULT_MAX_SOCKETS;
Console.WriteLine("Max Sockets outside bounds, using default max sockets: : " + this.maxSockets);
}
}
else
{
maxSockets = DEFAULT_MAX_SOCKETS;
Console.WriteLine("Could not parse max sockets, using default max sockets: " + this.maxSockets);
}
}
else if (key.ToUpper() == "NUM_THREADS")
{
/*
success = false;
success = Int32.TryParse(val, out v);
if (success == true)
{
if (v >= MIN_NUM_THREADS && v <= MAX_NUM_THREADS)
{
numThreads = v;
Console.WriteLine("Found num threads: " + this.numThreads);
}
else
{
numThreads = DEFAULT_NUM_THREADS;
Console.WriteLine("Num threads outside bounds, using default num threads: " + this.numThreads);
}
}
else
{
*/
numThreads = DEFAULT_NUM_THREADS;
Console.WriteLine("Could not parse num threads, using default num threads: " + this.numThreads);
//}
}
else if (key.ToUpper() == "PUBLIC")
{
tmp = val.ToUpper();
if (tmp == "1" || tmp == "TRUE")
{
this.publicOn = true;
}
else
{
this.publicOn = false;
}
Console.WriteLine("Found public: " + this.publicOn);
}
else if (key.ToUpper() == "CONNECTION_TIMEOUT_MS")
{
success = false;
success = Int32.TryParse(val, out v);
if (success == true)
{
if (v >= MIN_TIMEOUT && v <= MAX_TIMEOUT)
{
timeout = v;
Console.WriteLine("Found timeout: " + this.timeout);
}
else
{
timeout = DEFAULT_TIMEOUT;
Console.WriteLine("Timeout outside bounds, using default timeout: " + this.timeout);
}
}
else
{
timeout = DEFAULT_TIMEOUT;
Console.WriteLine("Could not parse timeout, using default timeout: " + this.timeout);
}
}
else if (key.ToUpper() == "OLD_SOCKET_KILL_MIN")
{
success = false;
success = Int32.TryParse(val, out v);
if (success == true)
{
if (v >= MIN_KILL_MIN && v <= MAX_KILL_MIN)
{
oldConnMin = v;
Console.WriteLine("Found old socket kill min: " + this.oldConnMin);
}
else
{
oldConnMin = DEFAULT_OLD_CONN_MIN;
Console.WriteLine("Old conn min outside bounds, using default old conn min: " + this.oldConnMin);
}
}
else
{
oldConnMin = DEFAULT_OLD_CONN_MIN;
Console.WriteLine("Could not parse old socket kill min, using default old socket kill min: " + this.oldConnMin);
}
}
else if (key.ToUpper() == "OUT_BUFFER_SIZE")
{
success = false;
success = Int32.TryParse(val, out v);
if (success == true)
{
if (v >= MIN_OUT_BUFFER_LENGTH && v <= MAX_OUT_BUFFER_LENGTH)
{
outBufferSize = v;
Console.WriteLine("Found out buffer size: " + this.outBufferSize);
}
else
{
outBufferSize = DEFAULT_OUT_BUFFER;
Console.WriteLine("Out buffer size outside bounds, using default out buffer size: " + this.outBufferSize);
}
}
else
{
outBufferSize = DEFAULT_OUT_BUFFER;
Console.WriteLine("Could not parse out buffer size, using default out buffer size: " + this.outBufferSize);
}
}
else if (key.ToUpper() == "IN_BUFFER_SIZE")
{
success = false;
success = Int32.TryParse(val, out v);
if (success == true)
{
if (v >= MIN_IN_BUFFER_LENGTH && v <= MAX_IN_BUFFER_LENGTH)
{
inBufferSize = v;
Console.WriteLine("Found in buffer size: " + this.inBufferSize);
}
else
{
inBufferSize = DEFAULT_IN_BUFFER;
Console.WriteLine("In buffer size outside bounds, using default in buffer size: " + this.inBufferSize);
}
}
else
{
inBufferSize = DEFAULT_IN_BUFFER;
Console.WriteLine("Could not parse in buffer size, using default in buffer size: " + this.inBufferSize);
}
}
else if (key.ToUpper() == "MAX_XFER_SIZE")
{
success = false;
success = Int32.TryParse(val, out v);
if (success == true)
{
if (v >= MIN_XFER_LIMIT && v <= MAX_XFER_LIMIT)
{
maxXferSize = v;
Console.WriteLine("Found max xfer size: " + this.maxXferSize);
}
else
{
maxXferSize = DEFAULT_MAX_XFER;
Console.WriteLine("Max xfer size outside bounds, using default max xfer size: " + this.maxXferSize);
}
}
else
{
maxXferSize = DEFAULT_MAX_XFER;
Console.WriteLine("Could not parse max xfer size, using default max xfer size: " + this.maxXferSize);
}
}
else if(key.ToUpper() == "SPEED_TEST_REGISTRATION_URL")
{
if (val != null && val != "")
{
speedTestRegUrl = val;
Console.WriteLine("Found speed test registration url: " + this.maxXferSize);
}
else
{
speedTestRegUrl = DEFAULT_SPEED_TEST_REGISTRATION_URL;
Console.WriteLine("Could not parse speed test registration url, using default speed test registration url: " + this.speedTestRegUrl);
}
}
else if (key.ToUpper() == "LOCAL_IP_ADDRESS")
{
success = false;
success = IPAddress.TryParse(val, out ip);
if (success == true)
{
localIpAddress = val;
Console.WriteLine("Found local ip address: " + this.localIpAddress);
}
else
{
localIpAddress = DEFAULT_LOCAL_IP_ADDRESS;
Console.WriteLine("Could not parse local ip address, using default local ip address: " + this.localIpAddress);
}
}
}
line = sr.ReadLine();
}
#endregion
}
else
{
Console.WriteLine("Could not find a config file.");
Console.WriteLine("Loading default values...");
port = DEFAULT_PORT;
debugOn = DEFAULT_DEBUG_ON;
maxSockets = DEFAULT_MAX_SOCKETS;
publicOn = DEFAULT_PUBLIC_ON;
timeout = DEFAULT_TIMEOUT;
oldConnMin = DEFAULT_OLD_CONN_MIN;
speedTestRegUrl = DEFAULT_SPEED_TEST_REGISTRATION_URL;
outBufferSize = DEFAULT_OUT_BUFFER;
inBufferSize = DEFAULT_IN_BUFFER;
maxXferSize = DEFAULT_MAX_XFER;
numThreads = DEFAULT_NUM_THREADS;
localIpAddress = DEFAULT_LOCAL_IP_ADDRESS;
}
PrintConfig();
}
The LoadSettings method is responsible for loading up data driven server configuration
information and setting all the pertinent local variables to match that information. The
first part of the method prints out the information about where the server is looking for files, it also prints out information
on the meaning of the different supported config file entries. You can see the use of our default, min, max settings variables
in use during the load step.
Right before that block of code you'll notice this line, File f = new FIle(this.appDir); if(f.exists() == false) { ...,
our server code will check to see if a config file exists in the target application directory, the config dir is created
if it doesn't exist. This makes it easier for our users to play with our server software because it sets up the dirs
it needs, and dumps out important information about itself to standard output.
The details of loading values from the config file is redundant, and a bit tedious. You'll see a lot of out min, max, and
default variables used here. This is because those values are used to gaurantee that the loaded configuration settings are valid
and if not resort to default values. A listing of the values loading from the config file follows.
- port: The desired port number.
- debug: Set to 1/true for true, 0/false for false.
- max_sockets: An integer value representing the number of connections, use a small number if your server has few resources.
- public: Set to 1/true for true, 0/false for false. Will register server once every 24 hours.
- connection_timeout_ms: The number of miliseconds to wait for socket timeout.
- old_socket_kill_min: The number of minutes to wait before killing old sockets.
- speed_test_registration_url: The speed test server registration URL.
- in_buffer_size: The number of bytes for the input connection buffer.
- out_buffer_size: The number of bytes for the output connection buffer.
- max_xfer_size: The max number of bytes for the entire tranfer.
- local_ip_address: The local IP address to bind to.
Next up we'll take a look at the debug logging subsystem. This code is responsible
for taking all logging calls and either ignoring them or writing them to standard output
and a debugging file. Oops, I forgot, we'll check the PrintConfig method real quick first.
public void PrintConfig()
{
Console.WriteLine("==========CURRENT CONFIG===========");
Console.WriteLine("Version: " + version);
Console.WriteLine("Found port: " + this.port);
Console.WriteLine("Found debug: " + this.debugOn);
Console.WriteLine("Found max_sockets: " + this.maxSockets);
Console.WriteLine("Found public: " + this.publicOn);
Console.WriteLine("Found connection_timeout_ms: " + this.timeout + " ms");
Console.WriteLine("Found old_socket_kill_min: " + this.oldConnMin + " min");
Console.WriteLine("Found speed_test_registration_url: " + this.speedTestRegUrl);
Console.WriteLine("Found in_buffer_size: " + this.inBufferSize + " bytes");
Console.WriteLine("Found out_buffer_size: " + this.outBufferSize + " bytes");
Console.WriteLine("Found max_xfer_size: " + this.maxXferSize + " bytes");
Console.WriteLine("Found num_threads: " + this.numThreads);
Console.WriteLine("Found local_ip_address: " + this.localIpAddress);
if (System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable() == true)
{
Console.WriteLine("Found local ip pool: ");
this.GetLocalIPAddress();
}
else
{
Console.WriteLine("Network connection not found.");
}
}
The PrintConfig method displays the socket server's current configuration directly to the command line. It also runs
a test to see if the network is available and checks the local IP address for network connectivity. Short and sweet, up next
CreateDebugFile and the debug logging subsystem.
///
/// Opens up a new logging file.
///
private void CreateDebugFile()
{
CreateDebugFile(true);
}
///
/// Opens up a new logging file with the append setting.
///
///
private void CreateDebugFile(bool append)
{
if (this.debugOn == true)
{
this.debug = new StreamWriter(this.debugFilePath, append);
this.debug.AutoFlush = true;
this.wrCount = 0;
}
}
I refer to it as a subsystem because it really is alost a stand alone feature of the methods exposed
by this class. So let's quickly go overt hem. The method CreateDebugFile is overloaded and is designed to support
creating a new debug file or appending to an existing file. The code is pretty straight forward so I'll
let you review it yourself.
Next up is CloseDebugFile. This is also a straight forward method, the file writing
objects are carefully closed down after flushing the stream. Finally the magical wr method.
All our logging and debugging calls that we want controlled by the logging subsystem will go through the wr method.
This method is also straight forward I'll let you review it yourself. Pay special attention to how the method
self heals, any write failures turn off file debugging, and after reaching a certain file size the method will
automatically cycle the log file. Next up let's get the getter methods out of the way. You should start to see the foundations
the socket server is built on now, the settings, logging, and member variables should begin to paint a picture for you.
///
/// Returns the title of the service that has been set.
///
///
public string GetTitle()
{
return title;
}
///
/// Returns true if logging is turned on.
///
///
public bool GetDebugOn()
{
return debugOn;
}
///
/// Returns the full path to the logging file.
///
///
public string GetDebugFilePath()
{
return debugFilePath;
}
///
/// Returns the file name of the logging file.
///
///
public string GetDebugFileName()
{
return debugFileName;
}
///
/// Returns the application name of this service.
///
///
public string GetAppName()
{
return appName;
}
///
/// Returns the port number this service is using.
///
///
public int GetPort()
{
return port;
}
///
/// Returns the socket count of this service.
///
///
public int GetSocketCount()
{
return socketCount;
}
///
/// Returns the full path of the root directory the service is using.
///
///
public string GetRootDir()
{
return rootDir;
}
///
/// Returns the full path of the service config/log directory.
///
///
public string GetAppDir()
{
return appDir;
}
///
/// Returns the name of the config file this application is using.
///
///
public string GetConfigFileName()
{
return configFileName;
}
///
/// Returns the full path of the expected config file.
///
///
public string GetConfigFilePath()
{
return configFilePath;
}
///
/// Returns true if this service is shutting down.
///
///
public bool GetShuttingDown()
{
return shuttingDown;
}
The getter method section is simple, it exposes access to a couple of key values that are important to know about the socket server's settings etc.
Look over them and recall what the underlying class variables were intended for. Wonder why we don't provide many setter methods if at all? I'll let you
ponder that one on your own. Next up we'll review how the socket server starts up. Most of the support methods are now covered, the fun stuff begins!!
6: CS Socket Server: Server Startup
This section covers the socket server startup and shutdown methods. Please review the code below and the detailed tour that follows.
You'll begin to see how connections are handled asynchronously and how we clean up old connections as the tour continues.
///
/// Stop the service and shut it down.
///
public void Stop()
{
shuttingDown = true;
}
///
/// Start the service listening.
///
public void Start()
{
wr("Starting: " + title);
ThreadStart starter = new ThreadStart(StartListening);
serverThread[0] = new Thread(starter);
serverThread[0].IsBackground = true;
serverThread[0].Start();
}
///
/// Begin listening for connection requests.
///
private void StartListening()
{
wr("StartListening");
//Establish the local end point for the socket.
IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, port);
//Create TCP/IP socket.
Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
listener.SendTimeout = timeout;
listener.ReceiveTimeout = timeout;
//Bind the socket to the local end point.
try
{
listener.Bind(endPoint);
listener.Listen((Int32)500);
wr("Listening on port: " + port + " Local IP: " + listener.LocalEndPoint);
while (!shuttingDown)
{
//Sets the event to a non signaled state
allDone.Reset();
//Start asynchronous socket to listen for connections.
listener.BeginAccept(new AsyncCallback(AcceptCallback), listener);
//Wait until a connection is made before continuing.
wr("waiting...");
allDone.WaitOne();
}
}
catch (Exception e)
{
wr("Error caught in StartListening.");
wr(e.Message);
wr(e.StackTrace);
}
}
///
/// Callback method used when a new connection is received.
///
///
private void AcceptCallback(IAsyncResult ar)
{
//Signal the main thread to continue.
allDone.Set();
//Get the socket that handles the client request.
Socket listener = (Socket)ar.AsyncState;
Socket handler = listener.EndAccept(ar);
wr("Socket receive buffer size 1: " + handler.ReceiveBufferSize);
//Create the state object.
StateObject state = new StateObject(inBufferSize, outBufferSize);
state.connTime = System.DateTime.Now.Ticks;
state.workSocket = handler;
handler.ReceiveBufferSize = inBufferSize;
handler.SendBufferSize = outBufferSize;
handler.SendTimeout = timeout;
handler.ReceiveTimeout = timeout;
handler.NoDelay = true;
state.timeStamp = DateTime.Now;
wr("Socket receive buffer size 2: " + handler.ReceiveBufferSize);
wr("Found connection request on " + System.DateTime.Now.Ticks + " from " + state.workSocket.RemoteEndPoint);
if (publicOn == true)
{
BeginRegisterServer();
}
try
{
if (socketCount < maxSockets)
{
Interlocked.Increment(ref socketCount);
Monitor.Enter(connSockets);
connSockets.Add(state);
Monitor.Exit(connSockets);
handler.BeginReceive(state.inBuffer, 0, inBufferSize, SocketFlags.None, new AsyncCallback(NetworkReadCallBack), state);
}
else
{
wr("Socket count has reached the max. Cannot process request.");
}
}
catch (Exception e)
{
wr("Error caught in AcceptCallback, trying to close this connection.");
wr(e.Message);
wr(e.StackTrace);
try
{
RemoveSocket(state);
}
catch { }
}
}
All the server startup code is listed above. Let's start reviewing the mdethods. The Stop method
turns off a boolean flag, shuttingDown, this lets us exit the main listening loop (which we'll cover soon).
The Start method creates a new thread instance of the StartListening method and starts the thread. Let's
look at the StartListening method next. The somewhat complex startup is due to the asynchronous handoff of the Start
method that begins the StartListening process.
wr("StartListening");
//Establish the local end point for the socket.
IPEndPoint endPoint = new IPEndPoint(IPAddress.Any, port);
//Create TCP/IP socket.
Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
listener.SendTimeout = timeout;
listener.ReceiveTimeout = timeout;
//Bind the socket to the local end point.
try
{
listener.Bind(endPoint);
listener.Listen((Int32)500);
wr("Listening on port: " + port + " Local IP: " + listener.LocalEndPoint);
while (!shuttingDown)
{
//Sets the event to a non signaled state
allDone.Reset();
//Start asynchronous socket to listen for connections.
listener.BeginAccept(new AsyncCallback(AcceptCallback), listener);
//Wait until a connection is made before continuing.
wr("waiting...");
allDone.WaitOne();
}
}
catch (Exception e)
{
wr("Error caught in StartListening.");
wr(e.Message);
wr(e.StackTrace);
}
First up we establish an IPEndPoint, internet protocol end point, instance that points to the target local IP address
and port number loaded from our settings file, or set to the default values. Next we instantiate a socket class and set it to
TCP, streaming, and set the socket timeouts to match our settings. We bind our socket to the endpoint we've specified, we call listen
on our socket to put it into a listening state. We then enter our main listening loop controlled by our shuttingDown boolean.
The socket listener is then set to asynchronously accept connectios via the BeginAccept async callback method. This ensures that out listening loop can now
be free to accept a new client connection. Simply awesome! We'll be looking at the AcceptCallback next, this is where the client gets its StateObject
that gets passed around to all read and write callback methods.
7: CS Socket Server: Accept Callback
This section covers the socket server accept callback method. This method is responsible for handing off the connection request on the main listening
socket to a secondary socket for processing the conection, leaving the main listening socket free to process the next connection. It's this subtle litle
piece that often trips people up. We can implement the same arrangement without the use of async callback methods, as shown in the Java version of this tutorial.
The ability of our code to handoff the connection request from the main socket listening to a "handler" socket is what enables our server to handle many connections
very quickly.
///
/// Callback method used when a new connection is received.
///
///
private void AcceptCallback(IAsyncResult ar)
{
//Signal the main thread to continue.
allDone.Set();
//Get the socket that handles the client request.
Socket listener = (Socket)ar.AsyncState;
Socket handler = listener.EndAccept(ar);
wr("Socket receive buffer size 1: " + handler.ReceiveBufferSize);
//Create the state object.
StateObject state = new StateObject(inBufferSize, outBufferSize);
state.connTime = System.DateTime.Now.Ticks;
state.workSocket = handler;
handler.ReceiveBufferSize = inBufferSize;
handler.SendBufferSize = outBufferSize;
handler.SendTimeout = timeout;
handler.ReceiveTimeout = timeout;
handler.NoDelay = true;
state.timeStamp = DateTime.Now;
wr("Socket receive buffer size 2: " + handler.ReceiveBufferSize);
wr("Found connection request on " + System.DateTime.Now.Ticks + " from " + state.workSocket.RemoteEndPoint);
if (publicOn == true)
{
BeginRegisterServer();
}
try
{
if (socketCount < maxSockets)
{
Interlocked.Increment(ref socketCount);
Monitor.Enter(connSockets);
connSockets.Add(state);
Monitor.Exit(connSockets);
handler.BeginReceive(state.inBuffer, 0, inBufferSize, SocketFlags.None, new AsyncCallback(NetworkReadCallBack), state);
}
else
{
wr("Socket count has reached the max. Cannot process request.");
}
}
catch (Exception e)
{
wr("Error caught in AcceptCallback, trying to close this connection.");
wr(e.Message);
wr(e.StackTrace);
try
{
RemoveSocket(state);
}
catch { }
}
}
The AcceptCallback method is fired asynchronously from the socket server connection point via the
BeginAccept method. A new socket called the handler is created by calling EndAccept on the listener socket
and passing in a reference to the IAsyncResult object that is a parameter to this callback method. Next up we create a unique copy of
our StateObject class to track this client's interaction with our socket server. Default buffers and timeouts are set on our handler socket and some time tracking variables
are set on our StateObject.
//Get the socket that handles the client request.
Socket listener = (Socket)ar.AsyncState;
Socket handler = listener.EndAccept(ar);
wr("Socket receive buffer size 1: " + handler.ReceiveBufferSize);
//Create the state object.
StateObject state = new StateObject(inBufferSize, outBufferSize);
state.connTime = System.DateTime.Now.Ticks;
state.workSocket = handler;
handler.ReceiveBufferSize = inBufferSize;
handler.SendBufferSize = outBufferSize;
handler.SendTimeout = timeout;
handler.ReceiveTimeout = timeout;
handler.NoDelay = true;
state.timeStamp = DateTime.Now;
If we have room, i.e. we haven't reached the max connection count yet, we register our state object and start its asynchronous BeginReceive call.
This will handle receiving messages from our client. If not we log that our connections are full and do not process the client.
wr("Socket receive buffer size 2: " + handler.ReceiveBufferSize);
wr("Found connection request on " + System.DateTime.Now.Ticks + " from " + state.workSocket.RemoteEndPoint);
if (publicOn == true)
{
BeginRegisterServer();
}
try
{
if (socketCount < maxSockets)
{
Interlocked.Increment(ref socketCount);
Monitor.Enter(connSockets);
connSockets.Add(state);
Monitor.Exit(connSockets);
handler.BeginReceive(state.inBuffer, 0, inBufferSize, SocketFlags.None, new AsyncCallback(NetworkReadCallBack), state);
}
else
{
wr("Socket count has reached the max. Cannot process request.");
}
}
catch (Exception e)
{
wr("Error caught in AcceptCallback, trying to close this connection.");
wr(e.Message);
wr(e.StackTrace);
try
{
RemoveSocket(state);
}
catch { }
}
There are a few small details left to address and then we'll be done with the AcceptCallback method. You'll
notice that if the server is a public server then a registration of the server is performed asynchronously via the
BeginRegisterServer method and it's async worker RegisterServerWebCall. That wraps up the accept callback
method you should now have some understanding of how to setup a socket server that handles requests asynchronously.
8: CS Socket Server: Closing Connections
Now that we have reviewed all our settings, defaults, and binary measurement variables and reviewed how our socket server accepts connections
asynchronously nad preps itself for an async read from the client we should quickly review how we close these connections. Then we can learn about how
our speed test server does the bulk of it's work during the read data async callback.
///
/// Closes all current connections.
///
private void CloseAllConnections()
{
try
{
//Clean up all connected sockets.
for (int i = 0; i < socketCount; i++)
{
StateObject st = (StateObject)this.connSockets[i];
st.workSocket.Close();
connSockets[i] = null;
}
}
catch (Exception e)
{
wr("Error in close all connections." + e.Message);
}
}
///
/// Closes all old connections.
///
private void CloseAllOldConnections()
{
try
{
bool kill = false;
for (int i = 0; i < socketCount; i++)
{
StateObject st = (StateObject)connSockets[i];
try
{
kill = false;
int n = st.workSocket.Available;
}
catch
{
kill = true;
}
if (st != null && st.workSocket != null && (System.DateTime.Now.Ticks - st.connTime) > (TimeSpan.TicksPerMinute * this.oldConnMin) || st.workSocket.Connected == false || kill == true)
{
try
{
wr("##### Removing socket at index: " + i);
RemoveSocket(st);
}
catch (Exception e)
{
wr("Error closing connection 1.");
wr(e.Message);
wr(e.StackTrace);
}
}
}
}
catch (Exception e)
{
wr("Error in close all old connections." + e.Message);
}
}
private void CloseSocket(StateObject state, Socket handler)
{
wr("Disconnecting");
try
{
handler.Disconnect(true);
}
catch (Exception ex3)
{
wr("Error caught in NetworkReadCallBack server disconnect call - Handler Disconnect");
wr(ex3.Message);
wr(ex3.StackTrace);
}
try
{
RemoveSocket(state);
}
catch (Exception ex3)
{
wr("Error caught in NetworkReadCallBack server disconnect call - RemoveSocket");
wr(ex3.Message);
wr(ex3.StackTrace);
}
try
{
CloseAllOldConnections();
}
catch (Exception ex3)
{
wr("Error caught in NetworkReadCallBack server disconnect call - CloseAllOldConnections.");
wr(ex3.Message);
wr(ex3.StackTrace);
}
this.PrintState();
}
///
/// Removes a socket from the connected socket pool.
///
///
private void RemoveSocket(StateObject state)
{
try
{
wr("Removing socket");
Socket sock = state.workSocket;
Monitor.Enter(this.connSockets);
if (this.connSockets.Contains(state))
{
connSockets.Remove(state);
Interlocked.Decrement(ref this.socketCount);
}
Monitor.Exit(this.connSockets);
if (sock != null)
{
try
{
sock.Shutdown(SocketShutdown.Both);
}
catch { }
try
{
sock.Shutdown(SocketShutdown.Receive);
}
catch { }
try
{
sock.Shutdown(SocketShutdown.Send);
}
catch { }
try
{
sock.Close();
}
catch { }
sock = null;
state.workSocket = null;
state = null;
}
}
catch (Exception e)
{
wr("Exception caught in the RemoveSocket method.");
wr(e.Message);
wr(e.StackTrace);
}
}
Before we get into the socker reading/writing methods I want to go over some of the socket closing methods. These are cleanup methods that are called throughout the server's
client interactions to close and remove client sockets.
private void CloseAllConnections()
{
try
{
//Clean up all connected sockets.
for (int i = 0; i < socketCount; i++)
{
StateObject st = (StateObject)this.connSockets[i];
st.workSocket.Close();
connSockets[i] = null;
}
}
catch (Exception e)
{
wr("Error in close all connections." + e.Message);
}
}
The CloseAllConnections method loops over all the stored connections and attempts to close and remove all server connections.
or any connections
private void CloseAllOldConnections()
{
try
{
bool kill = false;
for (int i = 0; i < socketCount; i++)
{
StateObject st = (StateObject)connSockets[i];
try
{
kill = false;
int n = st.workSocket.Available;
}
catch
{
kill = true;
}
if (st != null && st.workSocket != null && (System.DateTime.Now.Ticks - st.connTime) > (TimeSpan.TicksPerMinute * this.oldConnMin) || st.workSocket.Connected == false || kill == true)
{
try
{
wr("##### Removing socket at index: " + i);
RemoveSocket(st);
}
catch (Exception e)
{
wr("Error closing connection 1.");
wr(e.Message);
wr(e.StackTrace);
}
}
}
}
catch (Exception e)
{
wr("Error in close all old connections." + e.Message);
}
}
The CloseAllOldConnections method loops over all the stored connections and attempts to close and remove any already closed connections or any connections
that have been left hanging and have been open for too long a period of time. This method calls the RemoveSocket method even if a socket is closed because the client StateObject
is from our connection array list, and we need to decrement our connection count.
private void CloseSocket(StateObject state, Socket handler)
{
wr("Disconnecting");
try
{
handler.Disconnect(true);
}
catch (Exception ex3)
{
wr("Error caught in NetworkReadCallBack server disconnect call - Handler Disconnect");
wr(ex3.Message);
wr(ex3.StackTrace);
}
try
{
RemoveSocket(state);
}
catch (Exception ex3)
{
wr("Error caught in NetworkReadCallBack server disconnect call - RemoveSocket");
wr(ex3.Message);
wr(ex3.StackTrace);
}
try
{
CloseAllOldConnections();
}
catch (Exception ex3)
{
wr("Error caught in NetworkReadCallBack server disconnect call - CloseAllOldConnections.");
wr(ex3.Message);
wr(ex3.StackTrace);
}
this.PrintState();
}
///
/// Removes a socket from the connected socket pool.
///
///
private void RemoveSocket(StateObject state)
{
try
{
wr("Removing socket");
Socket sock = state.workSocket;
Monitor.Enter(this.connSockets);
if (this.connSockets.Contains(state))
{
connSockets.Remove(state);
Interlocked.Decrement(ref this.socketCount);
}
Monitor.Exit(this.connSockets);
if (sock != null)
{
try
{
sock.Shutdown(SocketShutdown.Both);
}
catch { }
try
{
sock.Shutdown(SocketShutdown.Receive);
}
catch { }
try
{
sock.Shutdown(SocketShutdown.Send);
}
catch { }
try
{
sock.Close();
}
catch { }
sock = null;
state.workSocket = null;
state = null;
}
}
catch (Exception e)
{
wr("Exception caught in the RemoveSocket method.");
wr(e.Message);
wr(e.StackTrace);
}
}
We'll breeze past the next two cleanup methods. The CloseSocket method safely closes a socket, removes that socket from the list of clients
and then does a quick clean up of any old or dead connections. Pay close attention to the protections used to prevent any exceptions during clean up.
9: CS Socket Server: Network Read
The next section picks up where we left off at the end of the accept callback method review. Our SpeedTest server is such that it simply responds to requests from the client, a
different server setup might require a different design but in our case all actions are client initiated and are followed by a close socket request. This is because the client
is primarily uploading data and then closing the connection to test how long the process took. Let's look at some code.
///
/// Handles network read callback requests.
///
///
private void NetworkReadCallBack(IAsyncResult ar)
{
//Retrieve the state object and the handler socket.
wr("");
wr("");
wr("NeworkReadCallBack called ");
StateObject state = (StateObject)ar.AsyncState;
Socket handler = null;
lock (state)
{
try
{
handler = state.workSocket;
if (handler == null)
{
return;
}
wr("Read call back request from " + state.workSocket.RemoteEndPoint);
//Read data from the client socket.
int bytesRead = handler.EndReceive(ar);
if (bytesRead > 0)
{
wr("Bytes Read: " + bytesRead + " Bytes received: " + state.received + " Bytes Expecting: " + state.expecting + " " + System.DateTime.Now.Ticks);
string code = System.Text.Encoding.UTF8.GetString(state.inBuffer, 0, 3);
byte[] b2 = null;
byte[] b3 = null;
int fullLen = 0;
int xferLen = 0;
int tlen = 0;
int count = 0;
wr("Found Code: " + code);
switch (code)
{
case "0??":
//UPLOAD FILE
wr("UPLOAD FILE: 0??");
wr("Bytesread: " + bytesRead);
b2 = new byte[4];
System.Array.Copy(state.inBuffer, 3, b2, 0, 4);
state.expecting = net.jbcg.Encryption.Converter.toInt(b2);
b3 = new byte[4];
System.Array.Copy(b2, b3, 4);
if (state.expecting <= 0 || state.expecting > (MiB_INT * 100))
{
throw new Exception("Illegal array size: " + state.expecting);
}
state.received = 0;
if (bytesRead >= 7)
{
state.received = (bytesRead - 7);
}
wr("Expecting: " + state.expecting + ", Received: " + state.received);
int tries = 0;
int maxTries = (2048 * 2);
while (state.received < state.expecting)
{
int dIn = state.workSocket.Receive(state.inBuffer);
if (dIn == -1)
{
wr("Found -1 in socket read...exiting....");
break;
}
state.received += dIn;
if (tries > maxTries)
{
wr("Error, maximum tries reached.");
break;
}
tries++;
}
if (state.received < state.expecting)
{
wr("Found " + state.received + ", expecting " + state.expecting + ", error.");
//return;
}
wr("DONE: total " + state.received + " bytes, expecting " + state.expecting + " bytes");
state.expecting = 0;
state.received = 0;
CloseSocket(state, handler);
break;
#region DOWNLOAD_1KB_TO_100KB
case "1??":
//DOWNLOAD 1K FILE
byte[] bb2 = null;
byte[] bb3 = null;
bb2 = new byte[KiB_INT];
bb2 = EncodeData(bb2);
bb3 = net.jbcg.Encryption.Converter.toByte(bb2.Length);
bb3 = EncodeData(bb3);
state.workSocket.Send(bb3, 0, bb3.Length, SocketFlags.None);
state.workSocket.Send(bb2, 0, bb2.Length, SocketFlags.None);
CloseSocket(state, handler);
break;
case "2??":
//DOWNLOAD 100KB FILE
bb2 = new byte[KiB_INT * 100];
bb2 = EncodeData(bb2);
bb3 = net.jbcg.Encryption.Converter.toByte(bb2.Length);
bb3 = EncodeData(bb3);
state.workSocket.Send(bb3, 0, bb3.Length, SocketFlags.None);
state.workSocket.Send(bb2, 0, bb2.Length, SocketFlags.None);
CloseSocket(state, handler);
break;
#endregion
case "3??":
//SERVER DISCONNECT
CloseSocket(state, handler);
return;
#region DOWNLOAD_50KB_TO_1MB
case "4??":
//DOWNLOAD 50KB FILE
bb2 = new byte[KiB_INT * 50];
bb2 = EncodeData(bb2);
bb3 = net.jbcg.Encryption.Converter.toByte(bb2.Length);
bb3 = EncodeData(bb3);
state.workSocket.Send(bb3, 0, bb3.Length, SocketFlags.None);
state.workSocket.Send(bb2, 0, bb2.Length, SocketFlags.None);
CloseSocket(state, handler);
break;
case "5??":
//DOWNLOAD 10K FILE
bb2 = new byte[KiB_INT * 10];
bb2 = EncodeData(bb2);
bb3 = net.jbcg.Encryption.Converter.toByte(bb2.Length);
bb3 = EncodeData(bb3);
state.workSocket.Send(bb3, 0, bb3.Length, SocketFlags.None);
state.workSocket.Send(bb2, 0, bb2.Length, SocketFlags.None);
CloseSocket(state, handler);
break;
case "6??":
//DOWNLOAD 1MB FILE
bb2 = new byte[MiB_INT];
bb2 = EncodeData(bb2);
bb3 = net.jbcg.Encryption.Converter.toByte(bb2.Length);
bb3 = EncodeData(bb3);
state.workSocket.Send(bb3, 0, bb3.Length, SocketFlags.None);
state.workSocket.Send(bb2, 0, bb2.Length, SocketFlags.None);
CloseSocket(state, handler);
break;
#endregion
#region DOWNLOAD_5MB_TO_50MB
case "7??":
//DOWNLOAD 5MB FILE
bb2 = new byte[MiB_INT * 5];
bb2 = EncodeData(bb2);
bb3 = net.jbcg.Encryption.Converter.toByte(bb2.Length);
bb3 = EncodeData(bb3);
state.workSocket.Send(bb3, 0, bb3.Length, SocketFlags.None);
state.workSocket.Send(bb2, 0, bb2.Length, SocketFlags.None);
CloseSocket(state, handler);
break;
case "8??":
//DOWNLOAD 10MB FILE
fullLen = MiB_INT * 10;
xferLen = outBufferSize;
tlen = (fullLen / xferLen) + 1;
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
bb3 = net.jbcg.Encryption.Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.Send(bb3, 0, bb3.Length, SocketFlags.None);
count = 0;
for (int i = 0; i < tlen; i++)
{
if(count + outBufferSize > fullLen)
{
xferLen = (fullLen - count);
}
else
{
xferLen = outBufferSize;
}
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
state.workSocket.Send(bb2, 0, bb2.Length, SocketFlags.None);
count += bb2.Length;
}
CloseSocket(state, handler);
break;
case "9??":
//DOWNLOAD 15MB FILE
fullLen = MiB_INT * 15;
xferLen = outBufferSize;
tlen = (fullLen / xferLen) + 1;
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
bb3 = net.jbcg.Encryption.Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.Send(bb3, 0, bb3.Length, SocketFlags.None);
count = 0;
for (int i = 0; i < tlen; i++)
{
if (count + outBufferSize > fullLen)
{
xferLen = (fullLen - count);
}
else
{
xferLen = outBufferSize;
}
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
state.workSocket.Send(bb2, 0, bb2.Length, SocketFlags.None);
count += bb2.Length;
}
CloseSocket(state, handler);
break;
case "10?":
//DOWNLOAD 20MB FILE
fullLen = MiB_INT * 20;
xferLen = outBufferSize;
tlen = (fullLen / xferLen) + 1;
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
bb3 = net.jbcg.Encryption.Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.Send(bb3, 0, bb3.Length, SocketFlags.None);
count = 0;
for (int i = 0; i < tlen; i++)
{
if (count + outBufferSize > fullLen)
{
xferLen = (fullLen - count);
}
else
{
xferLen = outBufferSize;
}
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
state.workSocket.Send(bb2, 0, bb2.Length, SocketFlags.None);
count += bb2.Length;
}
CloseSocket(state, handler);
break;
case "11?":
//DOWNLOAD 25MB FILE
fullLen = MiB_INT * 25;
xferLen = outBufferSize;
tlen = (fullLen / xferLen) + 1;
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
bb3 = net.jbcg.Encryption.Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.Send(bb3, 0, bb3.Length, SocketFlags.None);
count = 0;
for (int i = 0; i < tlen; i++)
{
if (count + outBufferSize > fullLen)
{
xferLen = (fullLen - count);
}
else
{
xferLen = outBufferSize;
}
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
state.workSocket.Send(bb2, 0, bb2.Length, SocketFlags.None);
count += bb2.Length;
}
CloseSocket(state, handler);
break;
case "12?":
//DOWNLOAD 30MB FILE
fullLen = MiB_INT * 30;
xferLen = outBufferSize;
tlen = (fullLen / xferLen) + 1;
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
bb3 = net.jbcg.Encryption.Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.Send(bb3, 0, bb3.Length, SocketFlags.None);
count = 0;
for (int i = 0; i < tlen; i++)
{
if (count + outBufferSize > fullLen)
{
xferLen = (fullLen - count);
}
else
{
xferLen = outBufferSize;
}
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
state.workSocket.Send(bb2, 0, bb2.Length, SocketFlags.None);
count += bb2.Length;
}
CloseSocket(state, handler);
break;
case "13?":
//DOWNLOAD 35MB FILE
fullLen = MiB_INT * 35;
xferLen = outBufferSize;
tlen = (fullLen / xferLen) + 1;
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
bb3 = net.jbcg.Encryption.Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.Send(bb3, 0, bb3.Length, SocketFlags.None);
count = 0;
for (int i = 0; i < tlen; i++)
{
if (count + outBufferSize > fullLen)
{
xferLen = (fullLen - count);
}
else
{
xferLen = outBufferSize;
}
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
state.workSocket.Send(bb2, 0, bb2.Length, SocketFlags.None);
count += bb2.Length;
}
CloseSocket(state, handler);
break;
case "14?":
//DOWNLOAD 40MB FILE
fullLen = MiB_INT * 40;
xferLen = outBufferSize;
tlen = (fullLen / xferLen) + 1;
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
bb3 = net.jbcg.Encryption.Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.Send(bb3, 0, bb3.Length, SocketFlags.None);
count = 0;
for (int i = 0; i < tlen; i++)
{
if (count + outBufferSize > fullLen)
{
xferLen = (fullLen - count);
}
else
{
xferLen = outBufferSize;
}
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
state.workSocket.Send(bb2, 0, bb2.Length, SocketFlags.None);
count += bb2.Length;
}
CloseSocket(state, handler);
break;
case "15?":
//DOWNLOAD 45MB FILE
fullLen = MiB_INT * 45;
xferLen = outBufferSize;
tlen = (fullLen / xferLen) + 1;
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
bb3 = net.jbcg.Encryption.Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.Send(bb3, 0, bb3.Length, SocketFlags.None);
count = 0;
for (int i = 0; i < tlen; i++)
{
if (count + outBufferSize > fullLen)
{
xferLen = (fullLen - count);
}
else
{
xferLen = outBufferSize;
}
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
state.workSocket.Send(bb2, 0, bb2.Length, SocketFlags.None);
count += bb2.Length;
}
CloseSocket(state, handler);
break;
case "16?":
//DOWNLOAD 50MB FILE
fullLen = MiB_INT * 50;
xferLen = outBufferSize;
tlen = (fullLen / outBufferSize) + 1;
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
bb3 = net.jbcg.Encryption.Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.Send(bb3, 0, bb3.Length, SocketFlags.None);
count = 0;
for (int i = 0; i < tlen; i++)
{
if (count + outBufferSize > fullLen)
{
xferLen = (fullLen - count);
}
else
{
xferLen = outBufferSize;
}
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
state.workSocket.Send(bb2, 0, bb2.Length, SocketFlags.None);
count += bb2.Length;
}
CloseSocket(state, handler);
break;
#endregion
default:
//DEFAULT
wr("Empty code! " + (int)state.inBuffer[0] + " - " + (int)state.inBuffer[1] + " - " + (int)state.inBuffer[2]);
wr("Bytesread: " + bytesRead);
b2 = BytesFromString(code);
state.workSocket.Send(b2, 0, b2.Length, SocketFlags.None);
CloseSocket(state, handler);
break;
}
}
}
catch (System.Net.Sockets.SocketException es)
{
wr("Socket exception caught in NetworkReadCallBack, removing socket.");
wr(es.Message);
wr(es.StackTrace);
wr("Remove socket 1.");
try
{
RemoveSocket(state);
}
catch(Exception ex3)
{
wr("Inner exception caught removing socket.");
wr(ex3.Message);
wr(ex3.StackTrace);
}
}
catch (Exception e)
{
wr("Exception caught in NetworkReadCallBack, removing socket.");
wr(e.Message);
wr(e.StackTrace);
wr("Remove socket 2.");
try
{
RemoveSocket(state);
}
catch (Exception ex3)
{
wr("Inner exception caught removing socket.");
wr(ex3.Message);
wr(ex3.StackTrace);
}
}
}
}
Finally we reach the important parts fo our server code, the read and write callback methods. Well in case you haven't figured it out by now,
we've been working on a network speed test server that you can use with a freely available Android client. The NetworkReadCallBack method
is launched by the async method setup BeginReceive in the AcceptCallback method. In this case the server is expected to read a 3 letter ASCII code from
the read buffer. This code tells the server which operation to perform. Most of the operations are permutations of the same download functionality.
The smaller byte amounts are handled differently than the larger byte downloads. This is done to keep the memory usage per connection down.
case "0??":
//UPLOAD FILE
wr("UPLOAD FILE: 0??");
wr("Bytesread: " + bytesRead);
b2 = new byte[4];
System.Array.Copy(state.inBuffer, 3, b2, 0, 4);
state.expecting = net.jbcg.Encryption.Converter.toInt(b2);
b3 = new byte[4];
System.Array.Copy(b2, b3, 4);
if (state.expecting <= 0 || state.expecting > (MiB_INT * 100))
{
throw new Exception("Illegal array size: " + state.expecting);
}
state.received = 0;
if (bytesRead >= 7)
{
state.received = (bytesRead - 7);
}
wr("Expecting: " + state.expecting + ", Received: " + state.received);
int tries = 0;
int maxTries = (2048 * 2);
while (state.received < state.expecting)
{
int dIn = state.workSocket.Receive(state.inBuffer);
if (dIn == -1)
{
wr("Found -1 in socket read...exiting....");
break;
}
state.received += dIn;
if (tries > maxTries)
{
wr("Error, maximum tries reached.");
break;
}
tries++;
}
if (state.received < state.expecting)
{
wr("Found " + state.received + ", expecting " + state.expecting + ", error.");
//return;
}
wr("DONE: total " + state.received + " bytes, expecting " + state.expecting + " bytes");
state.expecting = 0;
state.received = 0;
CloseSocket(state, handler);
break;
There is going to be a fair amount of byte conversion going on here. The function calls should be straight forward
but in case you need more experience check out the tutorial on byte conversion here.
After the first three bytes are read off of the client socket and converted to a strng the server will
run the command associated with that string. The command for an upload test '1??'. The client tells the server how much data
to expect and the server reads data off of the client's socket until the expected amount is reached or a bug out condition is reached.
It is always good practice to use a bug out condition with while loops insituations like these. The bug out condition will prevent an infinite loop on bad data which
can happen with networking, mobile, and socket servers or via hack attempts. So instead of letting our socket server crash out or worse eat up a ton
or resources on our server we can exit our loop and everything will be fine. Next we'll take a look at some download operations.
case "8??":
//DOWNLOAD 10MB FILE
fullLen = MiB_INT * 10;
xferLen = outBufferSize;
tlen = (fullLen / xferLen) + 1;
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
bb3 = net.jbcg.Encryption.Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.Send(bb3, 0, bb3.Length, SocketFlags.None);
count = 0;
for (int i = 0; i < tlen; i++)
{
if(count + outBufferSize > fullLen)
{
xferLen = (fullLen - count);
}
else
{
xferLen = outBufferSize;
}
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
state.workSocket.Send(bb2, 0, bb2.Length, SocketFlags.None);
count += bb2.Length;
}
CloseSocket(state, handler);
break;
Notice that the download call doesn't use an asynchronous call to NetworkWriteCallBack?
This is because in this particular client/server interaction the operations are very simple.
There really isn't a need to spawn an async call here because there won't be another read/write call during this
client/server interaction. Notice how the expected length is written to the client first, then the for loop sends
the rest of the data in mangeable chunks.
That sort of wraps things up on this tutorial. You can take a moment to review the different codes the server responds to,
and also take a look at how the server handles requests for large file downloads as opposed to smaller file downloads.
Also notice how the interaction end with the client. In general this server has very little back and forth interaction with the client
in the sense that it is either reading or writing a large file to or from the client and then closing the connection asap.
This is due to the speed test task that this server was designed to handle.
10: CS Socket Server: Android Client Testing
You can download the Android soeed test client from this link. It's a free network speed test client that works with the server code you
just reviewed. Now compile and run your server software from the command line or in visual studio IDE. You can see what IP address the server
is running on from the standard output. You'll need an Android device connected to the same network as your computer running your speed test server.
Install the free client app. Go to 'Manage Servers,' then choose 'Add Private.' You'll see a screen like the one below.
Use all the default settings in the form. But specify the IP address your server is running on in the hostname field. Save your server configuration once you are done.
Then run a new speed test against your new speed test server. Congrats! You have completed your own binary socket server and tested it against an Android client! Woot!!