Simple Web Server - Felix John COLIBRI. |
- abstract : a simple Web Server and Web Browser using WinSock APIs
- key words : Web Server, Web Browser, Windows sockets, HTTP
- software used : Windows XP, Delphi 6
- hardware used : Pentium 1.400Mhz, 256 M memory, 140 G hard disc
- scope : Delphi 1 to 8 for Windows, Kylix
- level : Delphi developer
- plan :
1 - Introduction
We will present here a simple web server and the corresponding simple web browser. The brower will request an Web page, the Server will send it back, and the Browser will display the text of this page (as an ASCII text). We will use
the basic Windows Socket library, and a small class hierarchy. Several servers and browsers are available, even in source form (see the reference list below). So the present Server and Browser
were mainly built in order to validate our CLASS organisation, before using this CLASS library for for custom applications
2 - The Socket Library 2.1 - Requirements
As presented in the socket programming paper, any Socket library using the asynchronous mode contains
- a client socket which uses Socket, Connect, Send, Recv and CloseSocket calls, with a Windows message handler receiving fd_connect, fd_write, fd_read and fd_close notifications
- a server socket with Socket, Bind, Listen and CloseSocket and a Windows message handler receiving fd_accept, fd_write, fd_read and fd_close
notifications. The fd_accept notification is used to create separate sockets for each incoming client, and each of those socket performs the actual communication with the Client.
Our goal was to separate as much as possible the Client part from the Server part. Here is the result. 2.2 - The overall picture Our library contains the following CLASSes:
- a c_base_socket containing all attributes and methods uses by all three kinds of sockets (client, server, server_client)
- a c_base_socket_with_buffer is used to manage the received or sent data
which may arrive or be assembled in several packets
- a c_client_socket, used by client applications (a Browser, for instance)
- a c_server_socket which basically waits for Client connections and creates
the c_server_client_sockets. The c_server_client_sockets are managed by a c_server_client_socket_list
- a c_server_client_socket which handles the communication with a single Client
The UML class diagram is the following:
2.3 - c_base_socket
Everything that is not specific to server handling or client handling has been placed in this CLASS, which is defined by:
c_base_socket= class(c_basic_object)
m_wsa_data: twsaData;
m_base_socket_handle: tSocket;
m_window_handle: hWnd;
m_is_open: Boolean;
m_on_after_socket_error: t_po_base_socket_event;
m_on_after_do_close_socket: t_po_base_socket_event;
Constructor create_base_socket(p_name: String);
procedure raise_exception(p_socket_error: t_socket_error;
p_wsa_error_code: Integer; p_error_text: String);
procedure wsa_startup;
procedure allocate_window;
procedure WndProc(var Message: TMessage);
procedure create_win_socket;
function f_do_send_string(p_string: String): Integer; Virtual;
function f_do_send_buffer(p_pt_buffer: Pointer;
p_send_count: Integer): Integer; Virtual;
function f_do_read_data(p_pt: Pointer; p_get_max: Integer): Integer;
procedure do_close_win_socket; Virtual;
end; // c_base_socket | And:
- to handle the Windows notification we need a handle. We chose to create a new (invisible) Window, using AllocateHwnd. Since both c_client_socket
and c_server_socket will use the window, we placed the window management in the base socket. The window handle is placed there as well
- the calls used to read and write data have also been placed at this base level
- m_on_after_do_close_socket is an event pointer, used to notify that CloseSocket has been called
2.4 - c_base_socket_with_buffer When data arrives in packet, or when we want to assemble the data before
sending it, we need some buffering. This is handled by: c_base_socket_with_buffer=
class(c_base_socket)
m_c_reception_buffer: c_byte_buffer;
m_c_emission_buffer: c_byte_buffer;
Constructor create_base_socket_with_buffer(p_name: String);
procedure send_buffered_data; Virtual;
procedure receive_buffered_data; Virtual;
Destructor Destroy; Override;
end; // c_base_socket_with_buffer |
2.4.1 - c_client_socket
Basically this CLASS is used to connect to a Server, send over some request, and wait for the answer. Since data is not transfered in a single chunk, we must buffer the data, and this is why the class is derived from the
c_base_socket_with_buffer: c_client_socket=
class(c_base_socket_with_buffer)
m_lookup_handle: THandle;
m_pt_get_host_data: Pointer;
m_on_after_socket_host_lookup: t_po_client_socket_event;
m_on_after_socket_connected: t_po_client_socket_event;
m_on_after_socket_wrote_data: t_po_client_socket_event;
m_on_after_socket_received_data: t_po_client_socket_event;
m_on_after_remote_server_client_socket_closed: t_po_client_socket_event;
Constructor create_client_socket(p_name: String);
procedure wsa_select; Virtual;
procedure handle_wm_lookup_host(var pv_wm_lookup_host:
t_wm_lookup_host); Message wm_lookup_host;
procedure do_lookup_win_socket_server(p_server_name: String);
procedure do_connect_win_socket(p_server_ip: String; p_port: Integer);
procedure handle_wm_async_select(var pv_wm_async_select:
t_wm_async_select); Message wm_asynch_select;
procedure do_close_win_socket; Override;
end; // c_client_socket | And:
- when we only know the symbolic name of the Server (www.borland.com) we have to search the IP address (80.15.235.71). We perform this search in an asynchronous way, using a message notification telling us about the search
result. This is managed by the "lookup" attributes and methods
- the selection of client messages (fd_connect, fd_write, fd_read, fd_close) is specified with wsa_select. handle_wm_async_select is the message handler
- to connect to a Server, we call do_connect_win_socket. When the connection is established, we receive an fd_connect notification
- we then send data using f_do_send_buffer. And when the Server sends data
back, the fd_read notification stores this data in the reception buffer
2.5 - c_server_socket, c_server_client_socket The c_server_socket is defined by:
c_server_socket=
Class(c_base_socket)
m_c_parent_ref: c_basic_object;
m_on_after_accept: t_po_accept_socket_event;
m_c_server_client_socket_list: c_server_client_socket_list;
m_c_class_of_server_client_socket: c_class_of_server_client_socket;
Constructor create_server_socket(p_name: String;
p_c_class_of_server_client_socket: c_class_of_server_client_socket);
procedure wsa_select; Virtual;
procedure do_bind_win_socket(p_port: Integer);
procedure do_listen_to_client(p_listen_queue_size: Integer);
procedure handle_wm_async_select(var pv_wm_async_select:
t_wm_async_select); Message wm_asynch_select;
procedure do_close_win_socket; Override;
Destructor Destroy; Override;
end; // c_server_socket | Note that:
- wsa_select and handle_wm_async_select work in the same way as for the c_client_socket (but we want the fd_accept notification, and have no use for fd_connect)
- do_bind_win_socket and do_listen_to_client start the client watch
- when a Client connects, the fd_accept notification will be used to create
a c_server_client_socket and add it to the m_c_server_client_socket_list.
- note that the server itself never sends or receives any data. But all the
c_server_client_sockets will use the c_server_socket message handler. So the handling of fd_write, fd_read and fd_close which are sent to one of the c_server_client_socket, have to be monitored in the
c_server_socket.handle_wm_async_select message handler.
The c_server_client_socket is defined by:
c_server_client_socket= class(c_base_socket_with_buffer)
// -- back link to the c_server_socket
m_c_server_socket_ref: c_server_socket;
Constructor create_server_client_socket(p_name: String;
p_c_server_socket_ref: c_server_socket); Virtual;
// -- Empty handlers. Will be Overriden by descendents
procedure handle_can_write_data; Virtual;
procedure handle_received_data; Virtual;
procedure handle_remote_client_closed; Virtual;
end; // c_server_client_socket |
and this object is contained in a tStringList encapsulation using the socket handle as a key:
c_server_client_socket_list= Class(c_base_socket)
m_c_server_client_socket_list: tStringList;
constructor create_server_client_socket_list(p_name: String);
function f_c_server_client_socket(p_index: Integer): c_server_client_socket;
procedure add_server_client_socket(p_socket_handle_string: String;
p_c_server_client_socket: c_server_client_socket);
procedure delete_server_client_socket(p_socket_handle_string: String);
function f_c_find_server_client_socket_index(
p_socket_handle_string: String): Integer;
function f_c_find_server_client_socket(
p_socket_handle_string: String): c_server_client_socket;
Destructor Destroy; Override;
end; // c_server_client_socket_list |
Now the interesting part: - a Client connects:
- c_server_socket receives an fd_accept notification
- the fd_accept handler
- calls Winsock.Accept to get the handle of the new Socket record
- creates a new c_server_client_socket object
- this object is appended to the c_server_client_list
procedure handle_fd_accept_notification(p_socket_handle: tSocket);
var l_address_socket_in: tSockAddrIn;
l_address_size: Integer;
l_server_client_socket_handle: tSocket;
l_c_server_client_socket: c_server_client_socket; begin
l_address_size:= sizeof(l_address_socket_in);
l_server_client_socket_handle:= Accept(p_socket_handle, @l_address_socket_in, @l_address_size);
// -- create this client socket, using a Class reference and a Virtual constructor
l_c_server_client_socket:=
m_c_class_of_server_client_socket.create_server_client_socket('serv_cli', Self);
with l_c_server_client_socket do begin
m_base_socket_handle:= l_server_client_socket_handle;
end; // with l_c_server_client_socket
// -- add the socket to the list
m_c_server_client_socket_list.add_server_client_socket(
IntToStr(l_server_client_socket_handle), l_c_server_client_socket);
// -- notify the user
if Assigned(m_on_after_accept)
then m_on_after_accept(Self, l_c_server_client_socket);
end; // handle_fd_accept_notification | - since the WinSock sending buffer of the newly c_server_client_socket is
empty, the server Windows message handler receives an fd_write notification. In our case, we do not send anything back to the client, since we have not yet analyzed what was requested
- the server Windows message handler then receives an fd_read notification:
- the parameter is the server_client handle which is used to locate the c_server_client socket
- once this c_server_client_socket is found in the list, the data is read into its reception buffer.
- any notification is sent to the user
Just a couple of notes:
- the c_server_socket does not need any buffer: it receives the connection requests from the Clients, and generates the fd_accept notification. All data transfer will be performed between the c_server_client_socket and the
remote c_client_socket
- the c_server_client_sockets do require buffering, since the Client's request is size is not known: the c_server_client_socket is unable to tell
beforehand how many bytes are supposed to arrive. So the c_server_client_socket grabs whatever arrives, stores them in a buffer, and analyze the content after each reception. Most of the TCP/IP protocol
programming reside in the analysis of this buffer, with detection of double return/line feed, empty periods on single lines etc
- the Delphi tServerSocket handles all received bytes in this class (the
server client socket delegates reception and emission events to the server socket class). When we implement any protocol, most of the protocol specific handling will be placed in the server client socket. And we must keep the
received bytes and analyzed parameters on a per server client basis. So this organization forces us to duplicate the server client socket objets and their container list in each protocol application.
Therefore we chose to keep the reception and emission in a specific c_server_client_socket class, which can easily be derived. This will now be presented.
2.6 - New protocol implementation
When we implement any protocol (HTTP, POP2, or custom protocols), we try to keep the protocol independent part in the general socket classes, and put the protocol specific parts in derived components. In our case
- for the Client, we simply create a c_client_socket descendent to handle the Client part of the chosen protocol
- for the Server:
The UML class diagram shows the derived class at the bottom, below the purple line.
We will illustrate this specialization with the HTPP protocol, implementing a simple server and browser.
3 - The HTTP Server and the HTTP Client 3.1 - The Browser Our Browser
- selects (in a tFileListBox) a page name
- builds the HTTP request header including the requested page
- sends the request header to the Server
- waits the HTTP answer, and stores it in the c_client_socket buffer
- displays the answer in a tMemo (ASCII display: no outlay font selection, image display etc)
Since the task is so easy, we did not derive a specific c_http_client_socket class, and handled everything in the Browser From.
Here is a snapshot of the browser:
3.2 - The Server The Server receives the page requests and sends the pages to each Client.
We created a c_web_server class which contains the c_server_socket and the page path:
c_web_server= class(c_basic_object)
m_c_server_socket: c_server_socket;
m_site_path: String;
Constructor create_web_server(p_name, p_site_path: String);
procedure start_web_server(p_port: Integer);
procedure close_web_server;
Destructor Destroy; Override;
end; // c_web_server |
And each Client is handled by the c_http_server_client_socket CLASS:
c_http_server_client_socket= Class(c_server_client_socket)
m_http_answer_error: t_http_answer_error;
m_page_name: String;
Constructor create_server_client_socket(p_name: String;
p_c_server_socket_ref: c_server_socket); Override;
procedure handle_received_data; Override;
function f_mime_type(p_page_name: String): String;
function f_answer_content_type(p_MIME_type: String): String;
function f_answer_content_length(p_body_length: Integer): String;
function f_server_full_file_name(p_file_name: string): String;
procedure send_page_not_found(p_page_name: String);
procedure read_file_and_send_page(p_page_name: String);
end; // c_http_server_client_socket | And the header is analyzed in this method:
procedure c_http_server_client_socket.handle_received_data;
var l_read_index: Integer; begin
// -- fetch the bytes receive_buffered_data;
with m_c_reception_buffer do begin
// -- presently only analyze GET and the name
l_read_index:= f_string_position(0, 'get');
if l_read_index< 0
then m_http_answer_error:= e_no_GET
else begin
// -- skip GET
m_read_index:= l_read_index+ 3;
// -- skip blanks
skip_blanks(m_read_index);
m_page_name:= f_extract_non_blank(m_read_index);
if (m_page_name= '/') or (m_page_name= '')
then m_page_name:= k_default_HTML_page;
read_file_and_send_page(m_page_name);
end; end; // with m_c_reception_buffer
end; // handle_received_data |
Here is a snapshot of the server:
4 - Improvements Most socket library use an "horizontal" layering:
- all basic socket routines (Socket, Bind, Listen, Accept, Connect, CloseSocket) in a single class. The primitives are used for both server sockets and client sockets
- upper classes are dedicated to server or client handling
We chose stress the separation between client and server: - a basic socket class handles whatever is common to both client and server: Send, Recv
- a c_client socket contains only client tasks
- a c_server_socket contains
- the server socket with addition / removal of server client sockets
- each server client socket handles the incoming clients
This vertical organization eases the derivation of classes for specific TCP IP protocol applications.
Please note that: - this "vertical" socket library is not supposed to be a better socket library
implementation. But with the the horizontal layer we found more difficult to separate the server part from the client part.
- in addition, we did not try to minimize the communication between the
layers. In a well built library, the user project is supposed to just call Run and OnFinished and everything in between is performed behind closed doors. On the contrary, we allowed every event to percolate up to the user.
So we can watch when the connection was established, when data was received and so on. This would not normally be displayed on a user Form.
- we did not try to implement all socket possibilities: UDP, blocking mode etc.
- nevertheless, we are using this library for all our socket project (POP mail reader, NEWS group reader, FTP client for uploading our .HTML pages, or download the IIS logs from our hosting service, reverse DNS and WHOIS to
analyze those logs, web spidering, CVS downloads, peer to peer server and clients, etc)
5 - Download the Sources Here are the source code files:
Those .ZIP files contain:
- the main program (.DPR, .DOF, .RES), the main form (.PAS, .DFM), and any other auxiliary form
- any .TXT for parameters
- all units (.PAS) for units
Those .ZIP
- are self-contained: you will not need any other product (unless expressly mentioned).
- can be used from any folder (the pathes are RELATIVE)
- will not modify your PC in any way beyond the path where you placed the .ZIP
(no registry changes, no path creation etc).
To use the .ZIP: - create or select any folder of your choice
- unzip the downloaded file
- using Delphi, compile and execute
To remove the .ZIP simply delete the folder.
As usual: - please tell us at fcolibri@felix-colibri.com if you found some errors, mistakes, bugs, broken
links or had some problem downloading the file. Resulting corrections will be helpful for other readers
- we welcome any comment, criticism, enhancement, other sources or reference
suggestion. Just send an e-mail to fcolibri@felix-colibri.com.
- or more simply, enter your (anonymous or with your e-mail if you want an
answer) comments below and clic the "send" button
- and if you liked this article, talk about this site to your fellow
developpers, add a link to your links page ou mention our articles in your blog or newsgroup posts when relevant. That's the way we operate: the more traffic and Google references we get, the more articles we will write.
6 - Other Servers and Browsers Here are some other web server you might be interested in (check our links page ) :
- the Delphi library SvrHTTP server unit
- the Indy suite IdHTTPServer component
- the ICS component suite tHTTPServer class
- the TurboPower aPRO library (now on SourceForge)
Among other sources:
- TinyWeb from RIT Labs which is a full fledged CGI server
- Server 7 from Matt TAYLOR, which is an ISAPI server
- Ben Ziegler added a server to allow checking his HTML generation tool (same testing purpose as Delphi's SvrHTTP)
- kbMem is a commercial version of the previous
On the Browser front: - Delphi includes a tWebBrowser which encapsulates Microsoft's SHDOCVW.DLL Shell Doc Object and Control Library (nice and easy rendering, but no
source)
- pBear has a rendering engine Shareware (source$)
- TurboPower APRO (SourceForge) has a complete rendering engine (free source)
7 - The author
Felix John COLIBRI works at the Pascal Institute. Starting with Pascal in 1979, he then became involved with Object Oriented Programming, Delphi, Sql, Tcp/Ip, Html, UML. Currently, he is mainly
active in the area of custom software development (new projects, maintenance, audits, BDE migration, Delphi
Xe_n migrations, refactoring), Delphi Consulting and Delph
training. His web site features tutorials, technical papers about programming with full downloadable source code, and the description and calendar of forthcoming Delphi, FireBird, Tcp/IP, Web Services, OOP / UML, Design Patterns, Unit Testing training sessions. |