Introduction
In my last article on Code Project, "How to write small and useful programs", I shared my thoughts on designing and implementing easy to use programs. To my surprise, the article was very well received by readers, thank you.
What should we do after writing a useful program? Use it, of course! Today, I am going to introduce a simple server program that is built on top of a set of reusable components. Although the server itself is written in Java, I have included a client utility that allows you to use the server with your .NET programs easily. Here is the list of all the projects (classes) included with this article.
- XYTrace: A reusable component to log errors and debugging messages (Java and .NET versions).
- XYDB: A reusable component to access relational databases (Java only).
- XYThreadPool: A very simple thread pool (Java and .NET versions).
- XYUtil: A utility which contains some common functions (Java and .NET versions).
- XYAppServer: The main server that will be covered in this article (Java only).
- XYAppClient: Client utility class that makes interaction with XYAppServer easy (Java and .NET versions).
- XYAppTest: A demo/test program (Java and .NET versions).
I have already described XYTrace and XYDB in my previous article. XYThreadPool is a class implementing thread pool, after looking at the code, you will be surprised at how simple it really is. XYUtil contains some common functions to be used on both the server side and the client side, such as reading from or writing to a file, converting a string to byte array and vice versa, etc. XYAppServer is the server we are going to focus on in this article. XYAppClient provides simple and clear client interface for accessing the server. XYAppTest is a small demo/test program.
What is XYAppServer - In writing server programs, I found that a lot of tasks can be accomplished by some general components, these tasks include but are not limited to: querying and updating relational databases, moving files between different machines, managing user sessions, sending e-mails, caching application data, etc. XYAppServer is designed to do all these things. It is basically an executable program that can process user requests received from TCP socket connection. It has its own thread pool to handle multiple concurrent requests. You can run multiple instances of XYAppServer for your different applications or you can let multiple applications share a single instance of XYAppServer. It is not hard to add new features to XYAppServer to enable it to handle additional types of work.
The advantages of having a single server to perform the common tasks are pretty obvious: you maintain only one project for these tasks instead of spreading similar code all over the places; your production support team need only to monitor one server for certain problems, etc. However, all your programs will need a way to communicate with this server. That is why I have provided the XYAppClient utility class. With XYAppClient, communicating with XYAppServer is as easy as calling a method in a local library.
All the sample code in this article are in C# except when stated otherwise. It will be easy to write the corresponding code in java because the interface is identical (well, almost, 99% of the time to be exact).
How to use XYAppServer
Starting the server - As with any Java program, first you need to set the correct classpath to avoid the "Class not found" exception. The following command will start an instance of the server at port 4000.
java XYAppServer 4000
On server machines with more than one IP addresses, you can optionally specify which IP address the server will bind to, for example:
java XYAppServer 4000 172.23.2.95
After the server is started, your program calls the methods in the XYAppClient class to access it. With .NET it is easy for you to reference the XYAppClient project or DLL in your programs. Each instance of the client object represents a connection to the server. When creating the client object, you need to specify server IP address or server name as well as the port number. Now let's see what you can do with XYAppServer.
Managing session data - The following code stores/retrieves user name and e-mail strings to/from the server. If the session with given ID does not exist, it will be created with default timeout value 30 (minutes). That is, if nobody accesses data in this session for 30 minutes, it will expire. You can call the SetSessionTimeout method to specify your own timeout value.
XYAppClient oClient =
new XYAppClient("172.23.2.95", 4000);
string sSessionID = "eqegaa45qfovcuraoai0nd55";
if(oClient.SetSessionString(sSessionID,
"UserName", "Xiangyang Liu")==false)
throw new Exception("Failed to store " +
"user name into session");
if(oClient.SetSessionString(sSessionID, "EMail",
"XiangYangL@verizon.net")==false)
throw new Exception("Failed to store " +
"e-mail address into session");
...
XYAppClient oClient =
new XYAppClient("172.23.2.95", 4000);
string sSessionID = "eqegaa45qfovcuraoai0nd55";
string sUserName =
oClient.GetSessionString(sSessionID, "UserName");
string sEMail =
oClient.GetSessionString(sSessionID, "EMail");
...
A session is uniquely identified by a session ID string. You can have unlimited number of data items in each session represented by unique key strings within the session (such as "UserName", "EMail", "SSN", etc.). Session ID and key string are case-sensitive. To access a session data item, you need to specify the correct session ID string and key string. However, the type of session data item can only be string.
Now, what's so special about managing session data this way, compared to ASP.NET built-in session or other tools? The first advantage is, it is platform independent. Secondly, session data can be shared among all applications, not just ASP.NET applications or J2EE applications. Thirdly, although session data is not persisted into any database, we can still shutdown and restart the server instance without losing any session data. I am going to comment on this feature later in the article. In general, this server provides a flexible cross-platform session management tool with potentially better performance than database based session management software.
The disadvantage is that the type of session data item is restricted to string. However, some simple enhancements to the XYAppClient class will make it possible for you to handle any data type that is XML-serializable. For example, if you add the following two methods to XYAppClient, you will be able to store and retrieve .NET DataSet objects in your session.
bool SetSessionDataSet(string sSessionID,
string sKey, DataSet oDataSet)
{
string sXML = oDataSet.GetXml();
return SetSessionString(sSessionID, sKey, sXML);
}
DataSet GetSessionDataSet(string sSessionID,
string sKey)
{
string sXML = GetSessionString(sSessionID, sKey);
if(sXML==null) return null;
DataSet oDataSet = new DataSet();
oDataSet.ReadXml(new StringReader(sXML));
return oDataSet;
}
What if the users store a lot of junk data on the server, will there be a point when this gets out of control and blows up? This is definitely possible if you have a malicious program accessing the server. For normal use, it is not a problem because all the expired sessions will be cleaned up automatically. If you want to remove a session before it expires, call the RemoveSession method.
Please note that this tool is not a replacement of the ASP.NET built-in session or other web related session tools. In fact, it has no knowledge of web page and web server. It can be used to enhance your existing web session management.
Moving files between client/server machines - When XYAppServer starts up, it will create a subfolder XYFile under the current working folder. This folder is used for storing files from client machines. The following code will save the notepad.exe program on the client machine into that server folder as notepad2.exe.
XYAppClient oClient = new XYAppClient("172.23.2.95", 4000);
oClient.WriteFile("notepad2.exe",
XYUtil.ReadFile("c:\\winnt\\system32\\notepad.exe"));
...
The following code will retrieve the file notepad2.exe from the server machine and save it as notepad3.exe in the root folder c:\ of the client machine.
XYAppClient oClient = new XYAppClient("172.23.2.95", 4000);
XYUtil.WriteFile("c:\\notepad3.exe",
oClient.ReadFile("notepad2.exe"));
...
Now, can you see a use for this feature? We can dynamically deploy program files using this server. We assume that only trusted programs are accessing XYAppServer. Otherwise, the server machine will be under all kinds of attacks.
Accessing relational databases - The XYDB class introduced in my previous article, "How to write small and useful programs", provides an easy way to access relational databases via JDBC and ODBC. It is now used in XYAppServer to allow multiple processes running on different servers to share the same pool of database connections. Here are the methods in XYAppClient related to database access.
public bool LoadDriver(string sDriverClass);
public string ExecuteSQL(string sDBURL, string sSQL);
public string ExecuteSQL(string sDBURL, string[] pSQL);
public string ExecuteSQL(string sDBURL,
string sSQL, string sSessionID);
public string ExecuteSQL(string sDBURL,
string[] pSQL, string sSessionID);
public string ExecuteSQLTransaction(string sDBURL,
string[] pSQL);
Obviously, the LoadDriver method will load the specified JDBC driver class. Please remember this is done to the server, not the client. I have put restriction in XYAppServer so that this method can only be called by programs running on the same machine as the server.
The ExecuteSQL method can be called to query or update the database specified by the sDBURL parameter. The return string is in a special XML format (same as the format of a .NET DataSet object) which is described in detail in my previous article. Note that you can execute a single SQL statement or an array of SQL statements together, and your SQL statements can be calls to stored procedures. With the ExecuteSQLTransaction method, it is also possible for you to group an array of SQL statements into a single database transaction.
If you want to cache the data returned from a SQL query and use it later, all you have to do is call the version of the ExceuteSQL method with the extra sSessionID parameter. What happens is, the first time when you call the method; data will be retrieved from the database and also cached in the session identified by the given session ID. The next time when the method is called with the same parameter values, the server will return the cached data instead of accessing the database again. It won't ever need to access the database for the same query until the corresponding session expires (or is deleted).
Sending e-mail - The SendMail method can be called to send e-mails via the SMTP protocol. The e-mails can have multiple recipients and one or more attachment files. The SetSMTPServer method can be called to specify which SMTP server will be used. The e-mail features are not fully developed and tested yet, right now the server cannot retrieve e-mails from a SMTP server.
Note: There are several methods in XYAppClient that are limited to programs running on the same machine as the server. One example is the LoadDriver method mentioned previously. The ShutdownServer method is another one, which forcefully brings down the server. You can provide a true value as parameter when calling this method so that the session data in the server memory will be saved before the server kills itself. Session data is actually saved periodically in a file located in the XYSession subfolder of the current working folder. When the server is started again, it will load all the session data from the previously saved file, that is why the server process can be shutdown and restarted without losing any session data.
Server log files - The XYTrace class is used in XYAppServer to write error messages and debugging information to log files. The log files are located in the XYLog subfolder of the current working folder of the server. If you want to know more about the XYTrace class, please refer to my previous article.
Implementation detail: A thread pool class
I hope you agree with me that XYAppServer is both powerful and easy to use, taking into consideration its small size, of course. It is also simple. As you can see, the server part consists of five straightforward classes. I am not counting XYAppClient because it runs on the client side only. Best of all, four of the five classes (XYTrace, XYDB, XYThreadPool, and XYUtil) can be reused in other programs. Let me describe the XYThreadPool class used in the server.
The thread pool class - Here is the public interface of the class:
public XYThreadPool();
public XYThreadPool(int nMinThreadCount, int nMaxThreadCount);
public void Stop();
public bool SubmitWork(Delegate oDelegate, object[] pData);
public bool SubmitWork(int nType, object[] pData);
virtual public void ProcessWork(int nType, object[] pData);
public int GetCurrentThreadCount();
public XYThreadPool();
public XYThreadPool(int nMinThreadCount, int nMaxThreadCount);
final public void Stop();
final public boolean SubmitWork(Runnable oRunnable);
final public boolean SubmitWork(int nType, Object[] pData);
public void ProcessWork(int nType, object[] pData);
final public int GetCurrentThreadCount();
To use this class in your code, all you have to do is create an instance and call the SubmitWork method. The work item you submitted to the thread pool will be processed by one of the available threads. The difference between C# version and Java version is that, with C# you need to pass a delegate (pointer) of a method to be invoked and an array of objects as parameters for your method, with Java you need to pass an object of a class implementing the Runnable interface. The C# version is more flexible because there is no restriction on the number and the type of the parameters for your method.
Here is how the thread pool works. Initially there is no thread created. When you call the SubmitWork method, one or more new threads will be created to process the work. The total number of threads in the pool will not exceed the nMaxThreadCount parameter value. Once the total number of threads exceeds the value of the nMinThreadCount parameter, it won't drop below it even if there is no more work to be processed.
XYAppServer works by having the main thread go into a while loop, accepting client connection and submitting each client to the internal thread pool. The processing work is done in the run method: this method reads input from the client, does the necessary work, then writes the output to the client, finally it closes the connection to the client.
The following code creates a new thread pool and calls the SubmitWork method passing a delegate of the MyHandler method and an array of parameter values. The MyHandler method will be invoked by the thread pool to do the actual work.
delegate void WorkHandler(string sData1,
string sData2, int nData3);
static void MyHandler(string sData1,
string sData2, int nData3)
{
...
}
static void Main(string[] args)
{
XYThreadPool oPool = new XYThreadPool();
WorkHandler pHandler = new WorkHandler(MyHandler);
object[] pData = new object[3]{"Hello", "world", 101};
oPool.SubmitWork(pHandler, pData);
...
oPool.Stop();
}
Extending XYThreadPool - Another way to use the thread pool class is by deriving a subclass. You need to override the ProcessWork method to do the actual processing and call the second version of the SubmitWork method in your programs that use this derived class. Here is how you override the ProcessWork method:
public void ProcessWork(int nType, object[] pData)
{
switch(nType)
{
case 1:
...
break;
case 2:
...
break;
...
default:
base.ProcessWork(nType, pData);
break;
}
}
It is up to you to define the types of the work your derived class will process and write the code that process them.
Final notes
As you may have noticed, I often take a direct approach in programming and I don't like too many abstractions in my projects. When writing code, I want to make sure that it satisfies the users' requirements first, in other words, make it useful. Then I will consider reusability. What I found out was, taking the direct/straightforward approach does not mean that your project will be hard to maintain or extend. By making your design simple and intuitive, other developers may have a better chance in understanding and hopefully enhancing your code. For example, I don't see any problem in enhancing XYAppServer to do more work in the future, the current version consists of only 550 lines of code.
You may get the impression that I am in favor of hacks or quick-and-dirty solutions. No, it is not true; otherwise I won't care about reusability at all. But I see nothing wrong with a quick-and-dirty solution if my users demand it.
The test program XYAppTest demonstrates how to use the XYAppClient class and the XYThreadPool class to send concurrent requests to the server. The following commands will start the Java version of the test program (the IP address can be replaced by an empty string "" if the server is running on the same machine).
java XYAppTest 172.23.2.95 4000
To start the C# version, do the following (the IP address can be replaced by an empty string "" if the server is running on the same machine).
XYAppTest.exe 172.23.2.95 4000
All the code included in this article are written in Visual Studio .NET 2003 and JDK 1.5.0.
While working on this project, I did a lot of porting between Java and C# code. The process is really smooth, all I did is copy-paste the Java code into C# project or C# code into Java class, and then I tried to eliminate the compiling errors by replacing Java specifics with C# counter-parts or C# specifics with Java counter-parts. When all the compiling errors were eliminated, the program worked. It is that simple!
Thank you for your interest in my code and my articles. I appreciate your support very much.