Cassini Under the Hood
The Cassini Web server consists of a number of modules that together support the Web hosting functions. You'll find the main functions that you'll need to implement Cassini in your own application in the Cassini.Server class. The Server class exposes methods that let you start and stop the Web server. Instances of the Server object are created in the caller's App Domain. The Server creates and configures the new App Domain to process HTTP requests with the
System.Web.Hosting.ApplicationHost.CreateApplicationHost() method. The Server class also exposes properties that enable you to specify the physical path for the server files, the port number to be used for HTTP, and the root URL to identify the server. The Server class exposes additional public methods such as
Start(),
Stop(), and
Restart()to support Server application management. By using the
Start() method of the Server class constructor, you can implement a private member named
CreateHost() as shown below:
private void CreateHost() {
_host =
(Host)ApplicationHost.CreateApplicationHost
(typeof(Host), _virtualPath, _physicalPath);
_host.Configure(this, _port, _virtualPath,
_physicalPath, _installPath);
}
The preceding method implements Cassini.Host and its
Configure() method to configure a server process. This method implements a delegate called
_onSocketAccept, which spawns a thread to process a new socket connection using the private member
OnSocketAccept(). This method establishes a connection using Cassini.Connection and processes the request.
Cassini is missing a key featurelogging. As delivered, the server maintains no logs for the incoming requests. But logging is a useful feature for monitoring the use of certain functions in an application, or the times of day that an application experiences the greatest load. This opens up the possibility of extending Cassini to implement this feature, leveraging the availability of the source and an understanding of the internal workings to create log entries when the server accepts new connections and initiates request processing.
Extending Cassini Functionality
To implement a logging feature in Cassini, first determine what type of activity you want to capture. To make it simple, you can start by logging every socket connection that is established and processed. To do that, you can add logging functionality to the OnSocketAccept() function referenced earlier. You'll use the Connection object's
ProcessOneRequest() method, which in turn implements the Request object's
Process() method.
To add functionality to Cassini, create a module containing the new classes and then implement those classes in the Cassini source. Review the sample logging class in
Listing 1. This class exposes a public method called
CreateLogEntry(). This method implements uses a StreamWriter object to create a log file if one does not exist and then uses the StreamWriter.
WriteLine() method to write a string value to the log file. Finally, it closes the StreamWriter. You can find the class in the module named CassiniLogger.cs in the
sample code. To implement this functionality in Cassini, you'll need to modify the Cassini source and modify the build script used to create the Cassini assemblies.
To log an entry for each incoming socket connection as described, change the
OnSocketAccept method of the Host class as shown below:
private void OnSocketAccept(Object acceptedSocket) {
Connection conn = new Connection(this,
(Socket)acceptedSocket);
//***** Modification to Cassini source code *****
//Logging extension to track connections
//Use CassiniLogger class to write connection
//class data to log file
CassiniLogger cl = new CassiniLogger(
"c:\\Cassini\\Logs\\CustomLog.txt");
//Create log entry to track remote IP for
//connection and indicator
//of a local or remote connection
cl.CreateLogEntry(conn.RemoteIP + ", " +
conn.IsLocal);
//***** End modification *****
conn.ProcessOneRequest();
}
As you can see, the code alterations instantiate an instance of the CassiniLogger class using the constructor to specify the location for the log file. The code then calls the
CreateLogEntry() method concatenates the
RemoteIP and
IsLocal properties of the Connection object into a string passed as the input parameter. In other words, the additional code captures the requesting IP address and the value of the
IsLocal property from the instance of the Connection object which the server creates when the
OnSocketAccept() method executes. The code logs an entry every time the Cassini server receives a request to process.
Running Cassini with Extensions
Now that you've modified the code to support the new extensions you need to compile a new version of the application that includes the updates. For more on debugging the changes you've made to the Cassini source, see the section of this article titled
Debugging Cassini. The Cassini source download includes a build script that uses the command line C# compiler (csc.exe) to build the source; but to add the new functionality to the application, you need to build it using the modified
Host.cs class and the new
CassiniLogger.cs module. To do that, copy the
CassiniLogger.cs file to the Cassini directory and modify the build script, a file named
build.bat, as follows:
csc /t:library /r:System.dll /r:System.Web.dll
/debug /out:Cassini.dll
AssemblyInfo.cs ByteParser.cs ByteString.cs
Connection.cs Host.cs
Messages.cs Request.cs Server.cs CassiniLogger.cs
Modifying the build script adds the
CassiniLogger.cs module to the build, and also uses the
/debug switch to output debugging information that may be useful when you need to step through these changes to the code. Executing the modified build script creates the
CassiniWebServer.exe application and builds and registers the Cassini type library in the Global Assembly Cache.