Awesome
LazWebsockets
This provides a small Websocket server and client implementation written for the FPC and Lazarus.
It is fully based upon the fcl ssockets
unit and therefore independent from any additional dependencies except from the FCL.
It can thereby easiely built only using fpc without Lazarus or complicated makefiles.
Installation
There is a Lazarus package file (websockets.lpk
) in this repository that can be used for Lazarus projects. An other option is to simply add the src
directory to your unit search path.
Usage & Functionality
For a simple example server see the chatServer.pas
and chatClient.pas
in the examples directory.
Setting up the Server
To create a TWebSocketServer
the constructor mirrors the constructor of ssockets.TInetServer
and can be called with either only a port, or an address, port and optionally a ssockets.TSocketHandler
(e.g. to provide TLS support).
So in order to create a simple WebSocket listener on port 8080 we can simply use:
Server := TWebSocketServer.Create(8080);
The next step is to add Handlers for incoming connections. A Handler is an instance of a class inheriting from
TWebsocketHandler = class
private
FConnections: TThreadedConnectionList;
public
constructor Create;
destructor Destroy; override;
function Accept(const ARequest: TRequestData;
const ResponseHeaders: TStrings): boolean; virtual;
procedure HandleCommunication(ACommunicator: TWebsocketCommunincator); virtual;
procedure PrepareCommunication(ACommunicator: TWebsocketCommunincator); virtual;
procedure DoHandleCommunication(ACommunicator: TWebsocketCommunincator); virtual;
procedure FinalizeCommunication(ACommunicator: TWebsocketCommunincator); virtual;
property Connections: TThreadedConnectionList read FConnections;
end;
overriding at least the the virtual functions Accept
and DoHandleCommunications
. Accept
checks whether the given request shall be accepted, e.g. by checking the Authorization
header transmitted via the HTTP request. It can also set header fields for the response, e.g. cookies.
After the Websocket stream is established, HandleCommunication
will be called, which in turn will simply call PrepareCommunication
DoHandleCommunication
and FinalizeCommunication
with a TWebsocketCommunincator
as argument. The TWebsocketCommunicator
class contains all the functionality nessecary to recieve and transmit data. More about that later.
TWebsocketHandler
automatically manages the TWebsocketCommunicator
objects. All active connections are located in the Connections
property, a thread safe list. PrepareCommunication
will add the objects to that list and FinalizeCommunication
will remove them, close them if this was not already done and free the instances. Therefore it is mandatory to call these functions in any inherited class.
In most cases it is completely sufficient to only override DoHandleCommunication
and/or Accept
(if any acceptance criteria exists). If you override PrepareCommunication
, FinalizeCommunication
or HandleCommunication
be sure you fully understand what you are doing.
The handshake starts of with a simple HTTP request. Therefore a websocket handshake request contains usually an information about the host address as well as the path used.
E.g. when establishing a connection at myhost.com/websocket the host is myhost.com
and the path is /websocket
(note the slash at the beginning, this is important).
Similar to the FCL-Web webmodules, an handler for our WebsocketServer can be registered for a given host and path.
For doing so we provide the method:
procedure TWebSocketServer.RegisterHandler(const AHost: string; const APath: string;
AHandler: TWebsocketHandler; DefaultHost: boolean = False;
DefaultPath: boolean = False);
If DefaultHost
is set to True
, if the hostname of any request does not match any of the hosts, this hostname will be assumed. Similar the DefaultPath sets this handler as the default Handler for unrecognized paths on this host.
For our simple example we simply want to add one handler. As we make it the only handler, we make it the default for any host and path, meaning it doesn't matter what host or path we choose:
Server.FreeHandlers := True;
Server.RegisterHandler('*', '*', TSocketHandler.Create, True, True);
Note that neither host *
nor path *
have any special meaning, they acutally can't be matched to any valid host or path, but we don't care as we set it to the default it will be matched anyway.
The last configuration we can perform is to choose an accepting method via the property TWebSocketServer.AcceptingMethod
which can take either the value samDefault
which performs the Handshake in the same thread as the server is started, samThreaded
, which spawns a new thread for the handshake and samThreadPool
, which executes the handshake in a seperate thread, from a threadpool to save up on time for creating and destroying new threads.
The method chosen does not only impact the performance of the server handling requests, but also which thread the methods of the registered handler will be called. If samDefault
is chosen, and the handler does not spawn new threads for the HandleCommunication
method, no additional connection can be established until HandleCommunication is finished (i.e. the communication is done), making the server single threaded.
For this reason, as well as the fact that Websockets are best utilized using multiple threads, to be able to asynchronous read and write data, the websockets
unit also provides the class TThreadedWebsocketHandler
. This class spawns two new threads, a reciever thread, which in a loop tries to fetch new messages, as well as a handler thread, which calls DoHandleCommunication
. To use this class, simply let your handler inherit from it instead of TWebsocketHandler
.
The last step now is to simply start our server with
Server.Start;
It will then start accepting clients on this thread until Server.Stop
is called.
Setting up the Client
To establish a client connection the class TWebsocketClient
is provided. Its constructor takes three parameters, the Host
and optionally Port
and Path
. If these are not given the default (port 80 and path /) will be used. These can be changed afterwards through their respective properties.
To establish the connection then simply call
function Connect(AHandler: TSocketHandler = nil): TWebsocketCommunincator;
Besides those the class also provides a few properties:
property CustomHeaders: TStrings;
property OnHandshakeSuccess: TWebsocketHandshakeResponseEvent;
property OnHandshakeFailure: TWebsocketHandshakeResponseEvent;
CustomHeaders
can be used to add headers to the HTTP handshake, e.g. an authorization header. The two events are triggered after the handshake, on success or failure respectively. The events get some information about the server response passed, this includes the headers (e.g. for recieving cookies), but also the content and statuscode, in case of a failure (e.g. to check against 404 or 403).
Recieving and Sending Messages
All the communication is done via the TWebsocketCommunincator
class. For recieving messages it contains the following methods:
function RecieveMessage: TWebsocketMessage;
procedure StartRecieveMessageThread;
procedure StopRecieveMessageThread;
function RecieveMessageThreadRunning: Boolean;
function InRecieveMessageThread: Boolean;
function HasMessages: Boolean;
function GetUnprocessedMessages(const MsgList: TWebsocketMessageOwnerList): integer;
function WaitForMessage(MessageTypes: TWebsocketMessageTypes=[wmtString, wmtBinary, wmtPong]
): TWebsocketMessage;
function WaitForStringMessage: TWebsocketStringMessage; inline;
function WaitForBinaryMessage: TWebsocketBinaryMessage; inline;
function WaitForPongMessage: TWebsocketPongMessage; inline;
RecieveMessage
will block until a message was recieved. If the message was a binary or string message, it will be returned. If the message was a Pong message, the message will be added the internal message queue. It will also handle all control frames like pings or close frames transparently. Only use this method if you know what you are doing.
To simply handle all incoming messages, you can use StartRecieveMessageThread
to start a new thread that will continously check for incoming messages and add them to the internal message queue.
Messages from that queue can be retrieved via the method GetUnprocessedMessage
and than be processed. This is implemented thread safe, meaning the message queue is internally locked, so RecieveMessages and GetUnprocessedMessages can be called from different threads. This thread can be stopped via StopRecieveMessageThread
(will take effect after the next message was read) and can be checked if it is running using RecieveMessageThreadRunning
.
The internal message queue is a thread safe list object, that can be emptied via GetUnprocessedMessages
. Whenever a new element is added to the queue, it will trigger the OnRecieveMessage
event. This event is triggered from within the thread that has recieved the message. if you want to interact with other threads, e.g. the GUI thread, potential synchronization is needed. To check if you are currently within the recieve message thread, you can use InRecieveMessageThread
.
If you are expecting a message and want to wait for it, you can use WaitForMessage
, WaitForStringMessage
, WaitForBinaryMessage
or WaitForPongMessage
. Unlike RecieveMessage, these function also return Pong messages, but also work in the asynchronous mode (when a reciever thread is running) as well as in the synchronous mode (i.e. calling recievethread itself). If any other messages arrived during this process, that are not the type of message you are waiting for, it will call the OnRecieveMessage
event from within the current thread.
For writing we have WriteMessage
and the three helper functions WriteRawMessage
, WriteStringMessage
and WriteBinaryMessage
:
function WriteMessage(MessageType: TWebsocketMessageType = wmtString;
MaxFrameLength: int64 = Word.MaxValue): TWebsocketMessageStream;
procedure WriteRawMessage(const AMessage; ALength: SizeInt;
AMessageType: TWebsocketMessageType = wmtString); inline;
procedure WriteStringMessage(const AMessage: String); inline;
procedure WriteBinaryMessage(const AMessage: TBytes); inline;
WriteMessage
creates a TWebsocketMessageStream
for us to send messages to the client. These should be either string (wmtString
), binary (wmtBinary
) or ping (wmtPing
) messages. After a ping, the responding pong will be recieved by RecieveMessages and can be processed by the user as any other message.
WriteRawMessage
will take a buffer and will write that wholy into the stream and close the stream afterwards. WriteStringMessage
and WriteBinaryMessage
will open a stream, write data from a string/array into it and close the stream afterwards. These three helper functions can be used to avoid the boilerplate code of opening the stream and having to free it afterwards, but don't support fragmentation.
While RecieveMessage
is blocking until at least one message is read. But it is implemented thread safe, meaning you can send messages while reading, without problems. This is archived by locking, while in general not nessecary as reading and writing can technically be done in parallel, but to avoid complications we lock stream access
Besides those the TWebsocketCommunincator
class also provides two properties:
property SocketStream: TLockedSocketStream;
property Open: boolean;
SocketStream
grants access to the raw underlying connection and Open
can be used to check whether the stream is still open.
Lastly the communicator provides two events:
property OnRecieveMessage: TNotifyEvent;
property OnClose: TNotifyEvent;
OnRecieveMessage
is triggered when RecieveMessages
adds a new message to the message queue and OnClose
will be triggered when the stream closes, either due to an abrupt disconnect of the underlying TCP stream (detected by a stream reding error while recieving Messages) or after sending the close message. It will be called before the TSocketStream
object will be destroyed, so you still have access to it, e.g. to its RemoteAddress
attribute to identify the client. Both of these events are fired in the context of the thread discovering them, most likely the thread calling RecieveMessages
. Any cross thread accesses need to be secured by the user, either using TThread.Queue
, TThread.Synchronize
, critical sections
or any other method of handling inter-thread communications.
Example
The chatServer
and broadcastServer
example can be built in multiple ways. Either by opening the Lazarus project (examples/chatServer.lpi
and examples/chatClient.lpi
, examples/broadcastServer.lpi
) and building it with the IDE, or by using make
in the examples directory, or by calling the fpc directly via:
$> fpc -Fu../src chatServer.pas
$> fpc -Fu../src chatClient.pas
$> fpc -Fu../src broadcastServer.pas
An alternative JavaScript based client for the server can be found in the html document chatClient.html
and should be usable with any modern browser. The example is a simple chat that lets the user input text messages to send to the other party, and recieve their messages asynchronously. You can try to connect with multiple clients at once to the server as it uses a threaded handler, but isn't built for reading more than a message for one client at a time, so funny things might happen.
The chatClient.pas
example can also send special codes: by typing exit
the connection will be closed gracefully and with ping message
a ping with message
as content can be sent.
The chatServer
simply allows clients to connect and can recive and send messages. The broadcastServer
allows clients to connect and sends all messages sent to it to all currently connected clients.
Thread Pooling
Creating threads is slow, which is why this library implements the ability for thread pooling. This means instead of destroying a thread once it is finished, it will be waiting in the background to be reused when necessary. Thread pooling is enabled by default. In some situations it might be helpful to deactivate thread pooling, especially if you run into problems like deadlocks or memory issues.
To deactivate thread pooling simply call the constructor of TThreadedWebsocketHandler
with the argument pooling
set to False
, i.e. TMySocketHandler.Create(False)
or if your handler has it's own constructor, call inherited Create(False);
from there.
The handshake is by default not threaded, see the TWebsocketServer.AcceptingMethod
property for further information