Author: Brian A. Ree
0: What This Tutorial Will Teach You
Along with the sample projects above, this tutorial will teach you how to write a multi-threaded asynchronous socket server
for running network speed tests. Once you've reached the end of the tutorial there will be a section showing you how to configure a
free android network speed test app to test your new server.
1: Java Socket Server: Intro
Ever wanted to create your own lightning fast socket server? Web service calls too slow for your mobile app or network game?
Socket servers are the way to go, lower overhead, faster performance, etc. This little tutorial will show you how to create your
own multi-threaded socket server that handles calls in an asynchronous way. 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 our server and monitoring the state of their connection.
Our first class for this server will be our state class, listed below, we'll be going over the members of this class next.
package com.middlemindgames.SpeedTestDll;
import java.net.Socket;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
*
* @author Victor G. Brusca, Middlemind Games 05-25-2017 9:05 AM EST
*/
public class StateObject {
public Socket workSocket = null;
public int inBufferSize = 0;
public int outBufferSize = 0;
public byte[] inBuffer = null;
public Date timeStamp = null;
public int expecting = 0;
public int received = 0;
public long connTime = 0;
private final DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
public StateObject(int inBuff, int outBuff) {
inBufferSize = inBuff;
outBufferSize = outBuff;
inBuffer = new byte[inBufferSize];
}
public void ResetBuffers() {
inBuffer = new byte[inBufferSize];
}
public String GetTimeStampString() {
return dateFormat.format(timeStamp);
}
}
2: Java Socket Server: State Object
Ok so let's go over the members of our StateObject class. You'll notice that almost all the members are public and there are no get/set methods.
This is generally considered bad practice in the world of Java but it suits our purposes. We want our socket server to be light, and fast, and having to use
access methods seems a bit like overkill for a simple class like StateObject, so we'll opt for the slightly faster member access, as opposed to the method call.
Read over the members and their descriptions below so that you understand what they all do.
- 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 socket example we write directly onto the send channel of the socket
without using an async call. This is due to the nature of this specific server, I'll show you how to add async, out buffer functionality later on.
3: Java Socket Server: Default Variables
For the classes RunReadCallBack, RunAcceptCallBack, RunRegisterServerWebCall, we can just get them out of the way in one fell
swoop. These are all runnable pass through classes that execute a method in the SpeedTest class. They facilitate the asynchronous operations
that the socket server needs to perform. Let's start now on the review of our main socket server class, SpeedTest.
public static final String TEST_URL = "http://www.google.com";
//The number of minutes to wait before considering a connection old.
public static final int DEFAULT_OLD_CONN_MIN = 5;
//The number of log writes to wait before checking the log size for recycling.
public static final int CHECK_LOG_SIZE_TICKS = 25;
//The max log size to allow before recycling the log file.
public static final int CHECK_LOG_SIZE = 5000000;
//The default number of sockets that will be opened up by the server.
public static final int DEFAULT_MAX_SOCKETS = 10;
//The default debug setting.
public static final boolean DEFAULT_DEBUG_ON = false;
//The default public server settings.
public static final boolean DEFAULT_PUBLIC_ON = true;
//Default binary sizes.
public static final float MiB_FLT = 1048576.0f; //1 MiB
public static final double MiB_DBL = 1048576.0; //1 MiB
public static final int MiB_INT = 1048576; //1 MiB
public static final float KiB_FLT = 1024.0f; //1 KiB
public static final double KiB_DBL = 1024.0; //1 KiB
public static final int KiB_INT = 1024; //1 KiB
public static final float MB_FLT = 1000000.0f; //1 MB
public static final double MB_DBL = 1000000.0; //1 MB
public static final int MB_INT = 1000000; //1 MB
public static final float KB_FLT = 1000.0f; //1 KB
public static final double KB_DBL = 1000.0; //1 KB
public static final int KB_INT = 1000; //1 KB
public static final int BITS_PER_BYTE = 8;
- TEST_URL: The URL to use to test network availability.
- 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 values are a bit self explanatory but we'll cover them all in any case just to make sure their
use is clear. Once we're done with class members we can move on to more interesting code.
//Default slow network timeout variables
public static final int DEFAULT_SLOW_NETWORK_TIMEOUT_S = 10; //s
public static final String DEFAULT_SLOW_NETWORK_TIMEOUT_S_STR = "10"; //s
public static final int DEFAULT_SLOW_NETWORK_TIMEOUT_MS = 10000; //ms
public static final String DEFAULT_SLOW_NETWORK_TIMEOUT_MS_STR = "10000"; //ms
//Default port number to start listening on.
public static final int DEFAULT_PORT = 49986;
public static final String DEFAULT_PORT_STR = "49986";
//Default timeout values.
public static final int DEFAULT_TIMEOUT = 15000;
public static final String DEFAULT_TIMEOUT_STR = "15000";
public static final int DEFAULT_CONNECTION_TIMEOUT = 15000;
public static final String DEFAULT_CONNECTION_TIMEOUT_STR = "15000";
//Default buffer sizes.
public static final int DEFAULT_IN_BUFFER = (MiB_INT * 2); //2 MiB
public static String DEFAULT_IN_BUFFER_STR = ((MiB_INT * 2) + ""); //2 MiB
public static final int DEFAULT_OUT_BUFFER = (MiB_INT * 2); //2 MiB
public static String DEFAULT_OUT_BUFFER_STR = ((MiB_INT * 2) + ""); //2 MiB
public static final int DEFAULT_MAX_XFER = (MiB_INT * 100); //100 MiB
public static String DEFAULT_MAX_XFER_STR = ((MiB_INT * 100) + ""); //100 MiB
//Default socket server settings.
private static final int MIN_NUM_THREADS = 5;
private static final int MAX_NUM_THREADS = 1024;
private static final int MIN_KILL_MIN = 3;
private static final int MAX_KILL_MIN = 30;
private static final int MIN_SOCKETS = 5;
private static final int MAX_SOCKETS = 1024;
private static final int MAX_IN_BUFFER_LENGTH = (MiB_INT * 5); //5 MiB
private static final int MIN_IN_BUFFER_LENGTH = (MiB_INT * 1); //1 MiB
private static final int MAX_OUT_BUFFER_LENGTH = (MiB_INT * 5); //5 MiB
private static final int MIN_OUT_BUFFER_LENGTH = (MiB_INT * 1); //1 MiB
private static final int MIN_TIMEOUT = 0;
private static final int MAX_TIMEOUT = (Short.MAX_VALUE * 2);
private static final int MIN_XFER_LIMIT = (MiB_INT * 10); //10 MiB
private static final int MAX_XFER_LIMIT = (MiB_INT * 100); //100 MiB
private static final int MIN_PORT = 0;
private static final int MAX_PORT = (Short.MAX_VALUE * 2);
//Default socket server settings, continued.
public static final 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 static final int DEFAULT_NUM_THREADS = 1;
public static String DEFAULT_LOCAL_IP_ADDRESS = GetLocalIPAddressStat();
public static final 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: Java Socket Server: Variables
A detailed listing of the non-default config, non-binary measurement variables we need is as follows. The code follows directly, a variable by
variable description follows afterwards.
private int inBufferSize = DEFAULT_IN_BUFFER;
private int outBufferSize = DEFAULT_OUT_BUFFER;
private long serverRegistrationStart = -1;
private long serverRegistrationNow = -1;
private String version = "0.8.2.5";
//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;
//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;
//Boolean that indicates the shutdown state of the service.
private boolean 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 = ".";
//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 boolean debugOn = DEFAULT_DEBUG_ON;
private boolean publicOn = DEFAULT_PUBLIC_ON;
//Static local reference.
public static SpeedTest cApp;
//Log writer.
private BufferedWriter debug = null;
private FileWriter fw = 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: Java 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 server. First off let's
take a look at our constructor.
//Default constructure expects a port number and a title.
//The port number can be overwritten by the config file.
public SpeedTest(int Port, String Title) {
cApp = this;
appDir = rootDir + File.separator + appName;
configFilePath = appDir + File.separator + configFileName;
debugFilePath = appDir + File.separator + debugFileName;
System.out.println("");
System.out.println("App Dir: " + appDir);
System.out.println("Config Path: " + configFilePath);
System.out.println("Debug Path: " + debugFilePath);
LoadSettings();
CreateDebugFile();
BeginRegisterServer();
serverThread = new Thread[numThreads];
connSockets = new ArrayList(maxSockets);
}
First thing we do is store a local self reference, this is a deprecated call. Next we setup our config path and debug path values by
combining the root directory and file names. Once that's done we dump the info out to the command line so that our end users know where our server is looking
to find its files. The constructor takes responsibility for loading data driven settings, getting the logging sub system ready, and registering the server
if it's a public server. After these initialization steps the serverThread variable is instantiated, this call is 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.
//Attempts to read the expected config file and load the settings.
//Running the service in cmd line mode shows the expected file paths and config file format.
@SuppressWarnings({"CallToPrintStackTrace", "ConvertToStringSwitch", "UseSpecificCatch"})
private void LoadSettings()
{
try {
System.out.println("");
System.out.println("Loading settings...");
System.out.println("Looking for data directory: " + this.appDir);
File f = new File(this.appDir);
if(f.exists() == false)
{
System.out.println("Creating the app directory, this is where the config file should go, config.txt.");
f.mkdirs();
}
System.out.println("");
System.out.println("Looking for config file: " + this.configFilePath);
System.out.println("Entries in the config file are as follows.");
System.out.println("port=[" + DEFAULT_PORT + ": The desired port number.]");
System.out.println("debug=[" + (DEFAULT_DEBUG_ON ? "1" : "0") + ": Set to 1/true for true, 0/false for false.]");
System.out.println("max_sockets=[" + DEFAULT_MAX_SOCKETS + ": An integer value representing the number of connections, use a small number if your server has few resources.]");
System.out.println("public=[" + (DEFAULT_PUBLIC_ON ? "1" : "0") + ": Set to 1/true for true, 0/false for false. Will register server once every 24 hours.]");
System.out.println("connection_timeout_ms=[" + DEFAULT_TIMEOUT + ": The number of miliseconds to wait for socket timeout.]");
System.out.println("old_socket_kill_min=[5: The number of minutes to wait before killing old sockets.]");
System.out.println("speed_test_registration_url:[" + DEFAULT_SPEED_TEST_REGISTRATION_URL + ": The speed test server registration URL.]");
System.out.println("in_buffer_size=[" + DEFAULT_IN_BUFFER + ": The number of bytes for the input connection buffer.]");
System.out.println("out_buffer_size=[" + DEFAULT_OUT_BUFFER + ": The number of bytes for the output connection buffer.]");
System.out.println("max_xfer_size=[" + DEFAULT_MAX_XFER + ": The max number of bytes for the entire tranfer.]");
//System.out.println("num_threads=[" + DEFAULT_NUM_THREADS + ": The number of threads the server should run with.]");
System.out.println("local_ip_address=[" + DEFAULT_LOCAL_IP_ADDRESS + ": The local IP address to bind to.]");
System.out.println("");
f = new File(this.configFilePath);
if(f.exists() == true)
{
System.out.println("Found a config file.");
System.out.println("Loading config file values...");
BufferedReader sr = new BufferedReader(new FileReader(this.configFilePath));
String line = sr.readLine();
while(line != null)
{
boolean success = false;
int v = 0;
InetAddress ip;
String tmp = "";
String[] pair = line.split("=");
if(pair.length == 2)
{
String key = pair[0];
String val = pair[1];
if("PORT".equals(key.toUpperCase()))
{
success = false;
try {
v = Integer.parseInt(val);
success = true;
}catch (Exception e) {
}
if(success == true)
{
if(port >= MIN_PORT && port <= MAX_PORT)
{
port = v;
System.out.println("Found port: " + this.port);
}
else
{
port = DEFAULT_PORT;
System.out.println("Port outside bounds, using default timeout: " + this.port);
}
}
else
{
port = DEFAULT_PORT;
System.out.println("Could not parse port, using default port: " + this.port);
}
}
else if("DEBUG".equals(key.toUpperCase()))
{
tmp = val.toUpperCase();
if("1".equals(tmp) || "TRUE".equals(tmp))
{
this.debugOn = true;
}
else
{
this.debugOn = false;
}
System.out.println("Found debug: " + this.debugOn);
}
else if ("MAX_SOCKETS".equals(key.toUpperCase()))
{
success = false;
try {
v = Integer.parseInt(val);
success = true;
}catch (Exception e) {
}
if (success == true)
{
if (v >= MIN_SOCKETS && v <= MAX_SOCKETS)
{
maxSockets = v;
System.out.println("Found max sockets: " + this.maxSockets);
}
else
{
maxSockets = DEFAULT_MAX_SOCKETS;
System.out.println("Max Sockets outside bounds, using default max sockets: : " + this.maxSockets);
}
}
else
{
maxSockets = DEFAULT_MAX_SOCKETS;
System.out.println("Could not parse max sockets, using default max sockets: " + this.maxSockets);
}
}
else if ("NUM_THREADS".equals(key.toUpperCase()))
{
/*
success = false;
success = Int32.TryParse(val, out v);
if (success == true)
{
if (v >= MIN_NUM_THREADS && v <= MAX_NUM_THREADS)
{
numThreads = v;
System.out.println("Found num threads: " + this.numThreads);
}
else
{
numThreads = DEFAULT_NUM_THREADS;
System.out.println("Num threads outside bounds, using default num threads: " + this.numThreads);
}
}
else
{
*/
numThreads = DEFAULT_NUM_THREADS;
System.out.println("Could not parse num threads, using default num threads: " + this.numThreads);
//}
}
else if ("PUBLIC".equals(key.toUpperCase()))
{
tmp = val.toUpperCase();
if ("1".equals(tmp) || "TRUE".equals(tmp))
{
this.publicOn = true;
}
else
{
this.publicOn = false;
}
System.out.println("Found public: " + this.publicOn);
}
else if ("CONNECTION_TIMEOUT_MS".equals(key.toUpperCase()))
{
success = false;
try {
v = Integer.parseInt(val);
success = true;
}catch (Exception e) {
}
if (success == true)
{
if (v >= MIN_TIMEOUT && v <= MAX_TIMEOUT)
{
timeout = v;
System.out.println("Found timeout: " + this.timeout);
}
else
{
timeout = DEFAULT_TIMEOUT;
System.out.println("Timeout outside bounds, using default timeout: " + this.timeout);
}
}
else
{
timeout = DEFAULT_TIMEOUT;
System.out.println("Could not parse timeout, using default timeout: " + this.timeout);
}
}
else if ("OLD_SOCKET_KILL_MIN".equals(key.toUpperCase()))
{
success = false;
try {
v = Integer.parseInt(val);
success = true;
}catch (Exception e) {
}
if (success == true)
{
if (v >= MIN_KILL_MIN && v <= MAX_KILL_MIN)
{
oldConnMin = v;
System.out.println("Found old socket kill min: " + this.oldConnMin);
}
else
{
oldConnMin = DEFAULT_OLD_CONN_MIN;
System.out.println("Old conn min outside bounds, using default old conn min: " + this.oldConnMin);
}
}
else
{
oldConnMin = DEFAULT_OLD_CONN_MIN;
System.out.println("Could not parse old socket kill min, using default old socket kill min: " + this.oldConnMin);
}
}
else if ("OUT_BUFFER_SIZE".equals(key.toUpperCase()))
{
success = false;
try {
v = Integer.parseInt(val);
success = true;
}catch (Exception e) {
}
if (success == true)
{
if (v >= MIN_OUT_BUFFER_LENGTH && v <= MAX_OUT_BUFFER_LENGTH)
{
outBufferSize = v;
System.out.println("Found out buffer size: " + this.outBufferSize);
}
else
{
outBufferSize = DEFAULT_OUT_BUFFER;
System.out.println("Out buffer size outside bounds, using default out buffer size: " + this.outBufferSize);
}
}
else
{
outBufferSize = DEFAULT_OUT_BUFFER;
System.out.println("Could not parse out buffer size, using default out buffer size: " + this.outBufferSize);
}
}
else if ("IN_BUFFER_SIZE".equals(key.toUpperCase()))
{
success = false;
try {
v = Integer.parseInt(val);
success = true;
}catch (Exception e) {
}
if (success == true)
{
if (v >= MIN_IN_BUFFER_LENGTH && v <= MAX_IN_BUFFER_LENGTH)
{
inBufferSize = v;
System.out.println("Found in buffer size: " + this.inBufferSize);
}
else
{
inBufferSize = DEFAULT_IN_BUFFER;
System.out.println("In buffer size outside bounds, using default in buffer size: " + this.inBufferSize);
}
}
else
{
inBufferSize = DEFAULT_IN_BUFFER;
System.out.println("Could not parse in buffer size, using default in buffer size: " + this.inBufferSize);
}
}
else if ("MAX_XFER_SIZE".equals(key.toUpperCase()))
{
success = false;
try {
v = Integer.parseInt(val);
success = true;
}catch (Exception e) {
}
if (success == true)
{
if (v >= MIN_XFER_LIMIT && v <= MAX_XFER_LIMIT)
{
maxXferSize = v;
System.out.println("Found max xfer size: " + this.maxXferSize);
}
else
{
maxXferSize = DEFAULT_MAX_XFER;
System.out.println("Max xfer size outside bounds, using default max xfer size: " + this.maxXferSize);
}
}
else
{
maxXferSize = DEFAULT_MAX_XFER;
System.out.println("Could not parse max xfer size, using default max xfer size: " + this.maxXferSize);
}
}
else if("SPEED_TEST_REGISTRATION_URL".equals(key.toUpperCase()))
{
if (val != null && !"".equals(val))
{
speedTestRegUrl = val;
System.out.println("Found speed test registration url: " + this.maxXferSize);
}
else
{
speedTestRegUrl = DEFAULT_SPEED_TEST_REGISTRATION_URL;
System.out.println("Could not parse speed test registration url, using default speed test registration url: " + this.speedTestRegUrl);
}
}
else if ("LOCAL_IP_ADDRESS".equals(key.toUpperCase()))
{
success = false;
try {
Integer.parseInt(val.replace(".", ""));
ip = InetAddress.getByName(val);
success = true;
}catch (Exception e) {
}
if (success == true)
{
localIpAddress = val;
System.out.println("Found local ip address: " + this.localIpAddress);
}
else
{
localIpAddress = DEFAULT_LOCAL_IP_ADDRESS;
System.out.println("Could not parse local ip address, using default local ip address: " + this.localIpAddress);
}
}
}
line = sr.readLine();
}
}
else
{
System.out.println("Could not find a config file.");
System.out.println("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;
}
}catch (Exception e) {
e.printStackTrace();
}
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 information about where the server is looking for files, it also prints
out information on the meaning of the different supported config file entries.
Right before that block of code you'll notice this line,
File f = new File(this.appDir); if(f.exists() == false) { ..., our server will check to see if a config file exists in the target
application directory and if it doesn't one is created. This makes it easier for our users to play with our server software because it sets up
the files it may need, and dumps out information about itself onto the command line.
The details of loading values from the config file is redundant, and a bit tedious. You'll see a lot of our min, max, and default variables used here.
This is because those values are used to gaurantee that the loaded configuration setting 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 is responsible for taking all logging calls and either ignoring them
or writing out to the command line and a debugging file. Oops, I forgot, we'll check the PrintConfig method real quick first.
public void PrintConfig()
{
System.out.println("==========CURRENT CONFIG===========");
System.out.println("Version: " + version);
System.out.println("Found port: " + this.port);
System.out.println("Found debug: " + this.debugOn);
System.out.println("Found max_sockets: " + this.maxSockets);
System.out.println("Found public: " + this.publicOn);
System.out.println("Found connection_timeout_ms: " + this.timeout + " ms");
System.out.println("Found old_socket_kill_min: " + this.oldConnMin + " min");
System.out.println("Found speed_test_registration_url: " + this.speedTestRegUrl);
System.out.println("Found in_buffer_size: " + this.inBufferSize + " bytes");
System.out.println("Found out_buffer_size: " + this.outBufferSize + " bytes");
System.out.println("Found max_xfer_size: " + this.maxXferSize + " bytes");
System.out.println("Found num_threads: " + this.numThreads);
System.out.println("Found local_ip_address: " + this.localIpAddress);
if (GetIsNetworkAvailable() == true) {
//System.out.println("Found local ip pool: ");
GetLocalIPAddress();
}else {
System.out.println("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 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.
@SuppressWarnings("CallToPrintStackTrace")
public void CreateDebugFile(boolean append) {
if (this.debugOn == true) {
try {
File f = new File(this.debugFilePath);
if(f.exists() == false) {
f.createNewFile();
this.debug = new BufferedWriter(new FileWriter(this.debugFilePath));
}else {
this.debug = new BufferedWriter(new FileWriter(this.debugFilePath, append));
}
} catch (Exception e) {
e.printStackTrace();
this.debugOn = false;
this.debug = null;
}
this.wrCount = 0;
}
}
public void CloseDebugFile() {
try {
this.debug.flush();
}catch(Exception e) { }
try {
this.debug.close();
}catch(Exception e) { }
try {
this.fw.flush();
}catch(Exception e) { }
try {
this.fw.close();
}catch(Exception e) { }
}
//Write to the console and the log file if debugging is turned on.
@SuppressWarnings("CallToPrintStackTrace")
private synchronized void wr(String s) {
if (this.debugOn == true) {
System.out.println(System.currentTimeMillis() + ": " + s);
if(s == null) {
s = "";
}
if (this.debug != null) {
try {
this.debug.write(s);
this.debug.newLine();
this.debug.flush();
} catch (Exception e) {
e.printStackTrace();
this.debugOn = false;
this.debug = null;
}
wrCount++;
}
if (wrCount > 0 && wrCount % CHECK_LOG_SIZE_TICKS == 0) {
File fInf = new File(this.debugFilePath);
if (fInf.length() >= CHECK_LOG_SIZE) {
try {
this.debug.flush();
this.debug.close();
} catch (Exception e) {
e.printStackTrace();
this.debugOn = false;
this.debug = null;
}
CreateDebugFile(false);
}
}
}
}
I refer to it as a subsystem because it really is almost a stand alone feature of the methods exposed in this class.
So let's quickly go over them. 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 classes are carefully closed down after a flush. Finally the magical wr method.
All our logging or 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 automatically cycles the log file. Next up let's get the getter methods out of the way.
//Returns the title of the service that has been set.
public String GetTitle() {
return title;
}
//Returns true if logging is turned on.
public boolean 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 boolean 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.
Look over them and recall what the class members were intended for. Next up we'll review how the socket server starts up. Most of the support
methods are now covered, the fun stuff begins!
6: Java 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.
//Stop the service and shut it down.
public void Stop() {
shuttingDown = true;
}
//Start the service listening.
public void Start() {
wr("Starting: " + title);
Thread trd = new Thread(this);
trd.setDaemon(true);
trd.start();
}
@Override
public void run() {
StartListening();
}
//Begin listening for connection requests.
@SuppressWarnings("UseSpecificCatch")
private void StartListening() {
try {
wr("StartListening");
//Establish the local end point for the socket.
InetSocketAddress endPoint = new InetSocketAddress(localIpAddress, port);
//Create TCP/IP socket.
ServerSocket listener = new ServerSocket(port, 0, endPoint.getAddress());
wr("Listening on port: " + port + " Local IP: " + listener.getInetAddress());
while (!shuttingDown)
{
Socket client;
//Wait until a connection is made before continuing.
wr("waiting...");
client = listener.accept();
RunAcceptCallBack r = new RunAcceptCallBack(client, this);
Thread trd = new Thread(r);
trd.start();
}
}catch(Exception e) {
wr(e.getMessage());
PrintStackTrace(e.getStackTrace());
}
}
//Callback method used when a new connection is received.
public void AcceptCallback(Socket handler)
{
StateObject state = null;
try
{
wr("Socket receive buffer size 1: " + handler.getReceiveBufferSize());
//Create the state object.
state = new StateObject(inBufferSize, outBufferSize);
state.connTime = System.currentTimeMillis();
state.workSocket = handler;
handler.setReceiveBufferSize(inBufferSize);
handler.setSendBufferSize(outBufferSize);
handler.setSoTimeout(timeout);
handler.setTcpNoDelay(true);
state.timeStamp = new Date();
wr("Socket receive buffer size 2: " + handler.getReceiveBufferSize());
wr("Found connection request on " + System.currentTimeMillis() + " from " + state.workSocket.getRemoteSocketAddress());
if (publicOn == true)
{
BeginRegisterServer();
}
if (socketCount < maxSockets)
{
socketCount++;
connSockets.add(state);
RunReadCallBack r = new RunReadCallBack(state, this);
Thread trd = new Thread(r);
trd.start();
}
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.getMessage());
PrintStackTrace(e.getStackTrace());
try
{
RemoveSocket(state);
}
catch(Exception ex) { }
}
}
If you noticed our SpeedTest class implements Runnable so it can be run in a thread. All the server start up code is listed above. Let's start reviewing the methods.
The Stop method turns off a boolean flag, shuttingDown, which will exit the main listening loop (which we'll cover soon).
The Start method creates a new thread instance and calls the start method. I know naming this method Start is confusing but the
server start code is simple enough once you get it. The thread class start method will call the run method, this method in turn calls the StartListening method.
Let's look at the StartListening method next. The reason for this long calling chain to start our server is the asynchronous handoff that begins the start listening thread.
wr("StartListening");
//Establish the local end point for the socket.
InetSocketAddress endPoint = new InetSocketAddress(localIpAddress, port);
//Create TCP/IP socket.
ServerSocket listener = new ServerSocket(port, 0, endPoint.getAddress());
wr("Listening on port: " + port + " Local IP: " + listener.getInetAddress());
while (!shuttingDown)
{
Socket client;
//Wait until a connection is made before continuing.
wr("waiting...");
client = listener.accept();
RunAcceptCallBack r = new RunAcceptCallBack(client, this);
Thread trd = new Thread(r);
trd.start();
}
First up we establish an InetSocketAddress 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 see an interesting class instantiation, ServerSocket. This is a special class made for establishing socket
connections with clients and switching to a port number from a pool of available connection ports. This leaves the main listening port free to accept new
connection requests. So we setup our ServerSocket to listen on our IP address and port, this is a blocking call which is why our server is
launched in a thread. The line, client = listener.accept(), is where our client connection is established. You can see that the next call launches
a new thread to handle reading from the new client connection. This ensures that our listening loop can now be free to accept a new client connection.
Simply awesome! We'll be looking at the accept callback method next, this is where the client gets its StateObject that gets passed around to
all read and write callback methods.
7: Java 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 connection, leaving the main listening socket open to process the next connection.
//Callback method used when a new connection is received.
public void AcceptCallback(Socket handler)
{
StateObject state = null;
try
{
wr("Socket receive buffer size 1: " + handler.getReceiveBufferSize());
//Create the state object.
state = new StateObject(inBufferSize, outBufferSize);
state.connTime = System.currentTimeMillis();
state.workSocket = handler;
handler.setReceiveBufferSize(inBufferSize);
handler.setSendBufferSize(outBufferSize);
handler.setSoTimeout(timeout);
handler.setTcpNoDelay(true);
state.timeStamp = new Date();
wr("Socket receive buffer size 2: " + handler.getReceiveBufferSize());
wr("Found connection request on " + System.currentTimeMillis() + " from " + state.workSocket.getRemoteSocketAddress());
if (publicOn == true)
{
BeginRegisterServer();
}
if (socketCount < maxSockets)
{
socketCount++;
connSockets.add(state);
RunReadCallBack r = new RunReadCallBack(state, this);
Thread trd = new Thread(r);
trd.start();
}
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.getMessage());
PrintStackTrace(e.getStackTrace());
try
{
RemoveSocket(state);
}
catch(Exception ex) { }
}
}
The AcceptCallback method is fired asynchronously from the socket server connection point.
A runnable class that wraps this local call is used to create the asynchronous execution.
When a connection accept callback is made we need to create and initialize a new StateObject
that tracks the interaction with this client.
state = new StateObject(inBufferSize, outBufferSize);
state.connTime = System.currentTimeMillis();
state.workSocket = handler;
handler.setReceiveBufferSize(inBufferSize);
handler.setSendBufferSize(outBufferSize);
handler.setSoTimeout(timeout);
handler.setTcpNoDelay(true);
state.timeStamp = new Date();
The AcceptCallback creates a new StateObject instance with the proper buffer sizes.
A timestamp and a connection time are set. The client socket connection is stored in the client state tracking object
so that socket reads and writes can be performed without a lookup into another data structure.
wr("Socket receive buffer size 2: " + handler.getReceiveBufferSize());
wr("Found connection request on " + System.currentTimeMillis() + " from " + state.workSocket.getRemoteSocketAddress());
if (publicOn == true)
{
BeginRegisterServer();
}
if (socketCount < maxSockets)
{
socketCount++;
connSockets.add(state);
RunReadCallBack r = new RunReadCallBack(state, this);
Thread trd = new Thread(r);
trd.start();
}
else
{
wr("Socket count has reached the max. Cannot process request.");
}
There are a few small details left to address and then we'll be done with the accept callback method. You'll notice that if the server is
a public server then a registration of the server is performed, again asynchronously, via a web service call.
After that check the server then makes sure it has room to handle the incoming connection. If so it adds a reference to the
client StateObject into the connSockets array list. The socketCount is incremented and a new thread is spawned
to accept read callback requests. The reason for this is that socket reads are a blocking call, so we maintain the asynchronous nature of
our server by spawning threads to handle the blocking calls like reading from the socket.
8: Java 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 and 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 SpeedTeest 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.get(i);
st.workSocket.close();
connSockets.set(i, null);
}
}catch (Exception e) {
wr("Error in close all connections." + e.getMessage());
}
}
//Closes all old connections.
@SuppressWarnings("UnusedAssignment")
private void CloseAllOldConnections() {
try {
boolean kill = false;
for (int i = 0; i < socketCount; i++) {
StateObject st = (StateObject)connSockets.get(i);
try {
kill = false;
if(st.workSocket.isClosed() == true) {
kill = true;
}
}catch(Exception e) {
kill = true;
}
if (st != null && st.workSocket != null && (System.currentTimeMillis() - st.connTime) > (this.oldConnMin * 60 * 1000) || st.workSocket.isClosed() == true || kill == true) {
try {
wr("##### Removing socket at index: " + i);
RemoveSocket(st);
}catch (Exception e) {
wr("Error closing connection 1.");
wr(e.getMessage());
PrintStackTrace(e.getStackTrace());
}
}
}
}catch (Exception e) {
wr("Error in close all old connections." + e.getMessage());
}
}
private synchronized void CloseSocket(StateObject state, Socket handler) {
wr("Disconnecting");
try {
handler.close();
}catch(Exception ex3) {
wr("Error caught in NetworkReadCallBack server disconnect call - Handler Disconnect");
wr(ex3.getMessage());
PrintStackTrace(ex3.getStackTrace());
}
try {
RemoveSocket(state);
}catch(Exception ex3) {
wr("Error caught in NetworkReadCallBack server disconnect call - RemoveSocket");
wr(ex3.getMessage());
PrintStackTrace(ex3.getStackTrace());
}
try {
CloseAllOldConnections();
}catch(Exception ex3) {
wr("Error caught in NetworkReadCallBack server disconnect call - CloseAllOldConnections.");
wr(ex3.getMessage());
PrintStackTrace(ex3.getStackTrace());
}
this.PrintState();
}
//Removes a socket from the connected socket pool.
@SuppressWarnings("UnusedAssignment")
private synchronized void RemoveSocket(StateObject state) {
try {
wr("Removing socket");
Socket sock = state.workSocket;
if (this.connSockets.contains(state)) {
connSockets.remove(state);
socketCount--;
}
if (sock != null) {
try {
sock.shutdownInput();
sock.shutdownOutput();
}catch(Exception e) { }
try {
sock.shutdownInput();
}catch(Exception e) { }
try {
sock.shutdownOutput();
}catch(Exception e) { }
try {
sock.close();
}catch(Exception e) { }
sock = null;
state.workSocket = null;
state = null;
}
}catch(Exception e) {
wr("Exception caught in the RemoveSocket method.");
wr(e.getMessage());
PrintStackTrace(e.getStackTrace());
}
}
Before we get into the socket reading and writing 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.
//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.get(i);
st.workSocket.close();
connSockets.set(i, null);
}
}catch (Exception e) {
wr("Error in close all connections." + e.getMessage());
}
}
The CloseAllConnections method loops over all the stored connections and attempts to close and remove them from the client list.
Can you see what's wrong with this implementation? Can you see how to make this method better? I'll leave that one up to you.
//Closes all old connections.
@SuppressWarnings("UnusedAssignment")
private void CloseAllOldConnections() {
try {
boolean kill = false;
for (int i = 0; i < socketCount; i++) {
StateObject st = (StateObject)connSockets.get(i);
try {
kill = false;
if(st.workSocket.isClosed() == true) {
kill = true;
}
}catch(Exception e) {
kill = true;
}
if (st != null && st.workSocket != null && (System.currentTimeMillis() - st.connTime) > (this.oldConnMin * 60 * 1000) || st.workSocket.isClosed() == true || kill == true) {
try {
wr("##### Removing socket at index: " + i);
RemoveSocket(st);
}catch (Exception e) {
wr("Error closing connection 1.");
wr(e.getMessage());
PrintStackTrace(e.getStackTrace());
}
}
}
}catch (Exception e) {
wr("Error in close all old connections." + e.getMessage());
}
}
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 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 array list of clients, and we need to decrement our connection count.
private synchronized void CloseSocket(StateObject state, Socket handler) {
wr("Disconnecting");
try {
handler.close();
}catch(Exception ex3) {
wr("Error caught in NetworkReadCallBack server disconnect call - Handler Disconnect");
wr(ex3.getMessage());
PrintStackTrace(ex3.getStackTrace());
}
try {
RemoveSocket(state);
}catch(Exception ex3) {
wr("Error caught in NetworkReadCallBack server disconnect call - RemoveSocket");
wr(ex3.getMessage());
PrintStackTrace(ex3.getStackTrace());
}
try {
CloseAllOldConnections();
}catch(Exception ex3) {
wr("Error caught in NetworkReadCallBack server disconnect call - CloseAllOldConnections.");
wr(ex3.getMessage());
PrintStackTrace(ex3.getStackTrace());
}
this.PrintState();
}
//Removes a socket from the connected socket pool.
@SuppressWarnings("UnusedAssignment")
private synchronized void RemoveSocket(StateObject state) {
try {
wr("Removing socket");
Socket sock = state.workSocket;
if (this.connSockets.contains(state)) {
connSockets.remove(state);
socketCount--;
}
if (sock != null) {
try {
sock.shutdownInput();
sock.shutdownOutput();
}catch(Exception e) { }
try {
sock.shutdownInput();
}catch(Exception e) { }
try {
sock.shutdownOutput();
}catch(Exception e) { }
try {
sock.close();
}catch(Exception e) { }
sock = null;
state.workSocket = null;
state = null;
}
}catch(Exception e) {
wr("Exception caught in the RemoveSocket method.");
wr(e.getMessage());
PrintStackTrace(e.getStackTrace());
}
}
We'll breeze past the next two closing methods. The CloseSocket method safely closes a socket.
Removes that socket from the array list of clients, and then does a quick clean up of any old or dead sockets.
9: Java Socket Server: Network Read Callback
The next section picks up where we left off at the end of the accept callback method review. Our speed test 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 or downloading data and then closing the connection to test how long the
process took. Let's look at some code.
//Handles network read callback requests.
public void NetworkReadCallBack(StateObject state)
{
//Retrieve the state object and the handler socket.
wr("");
wr("");
wr("NeworkReadCallBack called ");
Socket handler = null;
try
{
handler = state.workSocket;
if (handler == null)
{
return;
}
wr("Read call back request from " + state.workSocket.getRemoteSocketAddress());
//Read data from the client socket.
int bytesRead = handler.getInputStream().read(state.inBuffer);
if (bytesRead > 0)
{
wr("Bytes Read: " + bytesRead + " Bytes received: " + state.received + " Bytes Expecting: " + state.expecting + " " + System.currentTimeMillis());
Charset cs = Charset.forName("UTF-8");
byte[] cbytes = new byte[3];
System.arraycopy(state.inBuffer, 0, cbytes, 0, 3);
String code = new String(cbytes, cs);
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.arraycopy(state.inBuffer, 3, b2, 0, 4);
state.expecting = Converter.toInt(b2);
b3 = new byte[4];
System.arraycopy(b2, 0, b3, 0, 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.getInputStream().read(state.inBuffer);
if (dIn == -1)
{
wr("Found -1 in socket read...exiting....");
break;
}
state.received += dIn;
//wr("Found " + dIn + " bytes, total " + state.received + " bytes, expecting " + state.expecting + " bytes");
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;
case "1??":
//DOWNLOAD 1K FILE
byte[] bb2;
byte[] bb3;
bb2 = new byte[KiB_INT];
bb2 = EncodeData(bb2);
bb3 = Converter.toByte(bb2.length);
bb3 = EncodeData(bb3);
state.workSocket.getOutputStream().write(bb3, 0 , bb3.length);
state.workSocket.getOutputStream().write(bb2, 0 , bb2.length);
CloseSocket(state, handler);
break;
case "2??":
//DOWNLOAD 100KB FILE
bb2 = new byte[KiB_INT * 100];
bb2 = EncodeData(bb2);
bb3 = Converter.toByte(bb2.length);
bb3 = EncodeData(bb3);
state.workSocket.getOutputStream().write(bb3, 0 , bb3.length);
state.workSocket.getOutputStream().write(bb2, 0 , bb2.length);
CloseSocket(state, handler);
break;
case "3??":
//SERVER DISCONNECT
CloseSocket(state, handler);
return;
case "4??":
//DOWNLOAD 50KB FILE
bb2 = new byte[KiB_INT * 50];
bb2 = EncodeData(bb2);
bb3 = Converter.toByte(bb2.length);
bb3 = EncodeData(bb3);
state.workSocket.getOutputStream().write(bb3, 0 , bb3.length);
state.workSocket.getOutputStream().write(bb2, 0 , bb2.length);
CloseSocket(state, handler);
break;
case "5??":
//DOWNLOAD 10K FILE
bb2 = new byte[KiB_INT * 10];
bb2 = EncodeData(bb2);
bb3 = Converter.toByte(bb2.length);
bb3 = EncodeData(bb3);
state.workSocket.getOutputStream().write(bb3, 0 , bb3.length);
state.workSocket.getOutputStream().write(bb2, 0 , bb2.length);
CloseSocket(state, handler);
break;
case "6??":
//DOWNLOAD 1MB FILE
bb2 = new byte[MiB_INT];
bb2 = EncodeData(bb2);
bb3 = Converter.toByte(bb2.length);
bb3 = EncodeData(bb3);
state.workSocket.getOutputStream().write(bb3, 0 , bb3.length);
state.workSocket.getOutputStream().write(bb2, 0 , bb2.length);
CloseSocket(state, handler);
break;
case "7??":
//DOWNLOAD 5MB FILE
bb2 = new byte[MiB_INT * 5];
bb2 = EncodeData(bb2);
bb3 = Converter.toByte(bb2.length);
bb3 = EncodeData(bb3);
state.workSocket.getOutputStream().write(bb3, 0 , bb3.length);
state.workSocket.getOutputStream().write(bb2, 0 , bb2.length);
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 = Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.getOutputStream().write(bb3, 0, bb3.length);
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.getOutputStream().write(bb2, 0, bb2.length);
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 = Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.getOutputStream().write(bb3, 0, bb3.length);
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.getOutputStream().write(bb2, 0, bb2.length);
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 = Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.getOutputStream().write(bb3, 0, bb3.length);
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.getOutputStream().write(bb2, 0, bb2.length);
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 = Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.getOutputStream().write(bb3, 0, bb3.length);
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.getOutputStream().write(bb2, 0, bb2.length);
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 = Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.getOutputStream().write(bb3, 0, bb3.length);
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.getOutputStream().write(bb2, 0, bb2.length);
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 = Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.getOutputStream().write(bb3, 0, bb3.length);
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.getOutputStream().write(bb2, 0, bb2.length);
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 = Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.getOutputStream().write(bb3, 0, bb3.length);
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.getOutputStream().write(bb2, 0, bb2.length);
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 = Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.getOutputStream().write(bb3, 0, bb3.length);
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.getOutputStream().write(bb2, 0, bb2.length);
count += bb2.length;
}
CloseSocket(state, handler);
break;
case "16?":
//DOWNLOAD 50MB FILE
fullLen = MiB_INT * 50;
xferLen = outBufferSize;
tlen = (fullLen / xferLen) + 1;
bb2 = new byte[xferLen];
bb2 = EncodeData(bb2);
bb3 = Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.getOutputStream().write(bb3, 0, bb3.length);
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.getOutputStream().write(bb2, 0, bb2.length);
count += bb2.length;
}
CloseSocket(state, handler);
break;
default:
//DEFAULT
wr("Empty code! " + (int)state.inBuffer[0] + " - " + (int)state.inBuffer[1] + " - " + (int)state.inBuffer[2]);
wr("Bytesread: " + bytesRead);
state.workSocket.getOutputStream().write(cbytes, 0, cbytes.length);
CloseSocket(state, handler);
break;
}
}
}
catch (SocketException es)
{
wr("Socket exception caught in NetworkReadCallBack, removing socket.");
wr(es.getMessage());
PrintStackTrace(es.getStackTrace());
wr("Remove socket 1.");
try
{
RemoveSocket(state);
}
catch(Exception ex3)
{
wr("Inner exception caught removing socket.");
wr(ex3.getMessage());
PrintStackTrace(ex3.getStackTrace());
}
}
catch (Exception e)
{
wr("Exception caught in NetworkReadCallBack, removing socket.");
wr(e.getMessage());
PrintStackTrace(e.getStackTrace());
wr("Remove socket 2.");
try
{
RemoveSocket(state);
}
catch (Exception ex3)
{
wr("Inner exception caught removing socket.");
wr(ex3.getMessage());
PrintStackTrace(ex3.getStackTrace());
}
}
}
Finally we reach the important parts of our server code, the read and write callback methods. Well in case you haven't figured it out by
now, we're working on a network speed test server that you can use with a freely available Android client. The NetworkReadCallBack
method is launched in its own thread immediately after the client connection and state are established. In this case the server is expected to
read a 3 letter ASCII code from the read buffer. This code signifies to the server which operation the client would like to perform.
Most of the operations are permutations of the same download function. 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.arraycopy(state.inBuffer, 3, b2, 0, 4);
state.expecting = Converter.toInt(b2);
b3 = new byte[4];
System.arraycopy(b2, 0, b3, 0, 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.getInputStream().read(state.inBuffer);
if (dIn == -1)
{
wr("Found -1 in socket read...exiting....");
break;
}
state.received += dIn;
//wr("Found " + dIn + " bytes, total " + state.received + " bytes, expecting " + state.expecting + " bytes");
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 the client socket and converted to a string the server will run the command associated with that string.
The command for an upload test is 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
to prevent them from going infinite. Next up 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 = Converter.toByte(fullLen);
bb3 = EncodeData(bb3);
state.workSocket.getOutputStream().write(bb3, 0, bb3.length);
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.getOutputStream().write(bb2, 0, bb2.length);
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 a new thread here because there won't be another read 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 manageable 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 ends 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 SpeedTest task that this
code was designed to handle.
10: Java Socket Server: Android Client Testing
You can download the Android speed 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 the NetBeans IDE.
You can see what IP address the server is running on. You'll need an Android device connected to the same network as your computer running your speed test server.
Install the free client. 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!!