Introduction to network programming using Lazarus/Free Pascal.
This tutorial is an introduction to Lazarus/Free Pascal TCP/IP server-client programming using the package lNet. It is presupposed:
- that the Lazarus package lnetvisual is installed. If you need help with installation of Lazarus packages, my tutorial Installing supplementary Lazarus/Free Pascal packages might be helpful;
- that you have a basic networking knowledge concerning TCP/IP and client-server communication. You'll have to know the IP address of the computer, where the server is running (and use this address instead of the one used in the tutorial);
- that your computer is part of a local TCP/IP IPv4 network with at least 2 machines (even though the applications work with a single computer).
The sample application build in the tutorial is a simple LAN messaging system, where the clients connect to a server, and via the server can send messages to other clients. It's a sample, not a full real-world application, thus doesn't, for example, include any code to check the validity of the IP address or the port number. The server application has been tested on Windows 10; clients have been tried out on Windows 8.1 and Windows 11 (I did not succeed to install the lnetvisual package on macOS and Linux). Click the following link to download the LANMsg source code.
The tutorial sample is created step by step, creating the server and client applications simultaneously. I think this is the best approach to make the novice understand how to proceed to implement the communication between computers with a TCP/IP client-server application.
Starting the server.
Create a Lazarus application project, called "LANMsgServer", with the form shown on the screenshot below. There is one single network related component to be used: a TLTCPComponent object, that you find in the lNET components menu, that is added to the Lazarus IDE when you install the lnetvisual package. Just add this component to the form, and name it "ltcpServer". All properties will be set in the source code.
The application should do the following: Using the IP address and port, entered by the user (or read from a text file, in order not to have it always be entered manually), the server is started using the "Start" button (the caption of which then changes to "Stop"). The control LED (a TShape component) passes from red to green color when done. The server accepts all client connections (in a real-world scenario, you would probably set a maximum) and sends a list with the IP addresses to all connected clients. If a client sends a message destined to another client (or, in order to run the applications on a single machine, to itself), the server relays the message to this client. During the whole session, the server actions will be logged in the corresponding TMemo.
Concerning IP address and port, the application starts with an unknown IP address and default port 5600. It then looks for the file "server.txt"; if it exists it reads the IP and port from there (this file can be created manually, or using the corresponding item in the "Settings" menu). When the "Start" button is pushed, and the IP and port fields are filled in, the server is started, i.e. starts to listen on the IP and port specified.
Note: You can use another port if you want. However, you should not use the system port numbers 1 – 1024, nor any official standard ports, nor a port used by another server on your computer. For details, please, have a look at the Wikipedia article List of TCP and UDP port numbers.
As explained in this Lazarus/Free Pascal programming tip, placing code in the FormCreate method may result in problems. To be sure to avoid this, I used the trick with the "bStart" variable to read the configuration file in the FormActivate method. With "fLANMsgServer" being the name of my form, here the code of the TfLANMsgServer.FormCreate and TfLANMsgServer.FormActivate methods. Note the Boolean "bServerConnected", that we will set to "True" once the server has been started.
procedure TfLANMsgServer.FormCreate(Sender: TObject);
begin
bStart := True; bServerConnected := False;
end;
procedure TfLANMsgServer.FormActivate(Sender: TObject);
begin
if bStart then begin
if FileExists(ConfigFile) then begin
ServerConfigRead(sServerIP, iServerPort);
end
else begin
sServerIP := ''; iServerPort := DefaultPort;
end;
edServerIP.Text := sServerIP; edServerPort.Text := IntToStr(iServerPort);
bStart := False;
end;
if sServerIP = '' then
edServerIP.SetFocus
else if iServerPort = 0 then
edServerPort.SetFocus
else
btStart.SetFocus;
end;
The method calls the procedure ServerConfigRead to read the configuration file (called "server.txt"). This
file is supposed to contain the following two lines:
ip=192.168.40.1
port=5600
where 192.168.40.1 is the internal IPv4 address of my Windows 10 VMware host-only network adapter, where my LANMsg server is listening to.
Here is the code of my ServerConfigRead procedure (no validity checks done...):
procedure ServerConfigRead(var ServerIP: string; var ServerPort: Word);
var
Line: string;
InFile: Text;
begin
ServerIP := ''; ServerPort := 0;
Assign(InFile, ConfigFile); Reset(InFile);
while not EoF(InFile) do begin
Readln(InFile, Line);
if Line <> '' then begin
if (Length(Line) > 4) and (LeftStr(Line, 3) = 'ip=') then
ServerIP := Trim(Copy(Line, 4, Length(Line)))
else if (Length(Line) > 6) and (LeftStr(Line, 5) = 'port=') then
ServerPort := StrToInt(Trim(Copy(Line, 6, Length(Line))));
end;
end;
Close(InFile);
end;
The next feature to implement is to start the server. This is done by setting the ltcpServer.Host property and calling the function ltcpServer.Listen (remember that "ltcpServer" is the name of my TLTCPComponent object). The function is called when the "Start" button is pushed, so we have to place the code within the TfLANMsgServer.btStartClick method ("fLANMsgServer" being the name of my form, "btStart" the name of my button). This method (more correctly the part of the method, where the button's caption is "Start") has to do the following:
- Read the server IP address and port number from the form.
- Start the server, and set the variable "bServerConnected" to "True".
- Change the color of the control LED to green.
- Write a "Server has been started" message to the server log.
- Set the caption of the button to "Stop".
After the server has been started, we use the same button to stop the server. This is done by calling the procedure ltcpServer.Disconnect, with a Boolean argument set to "True". The part of the TfLANMsgServer.btStartClick method, where the button's caption is "Stop" has to do the following:
- Stop the server, and set the variable "bServerConnected" to "False".
- Change the color of the control LED to red.
- Clear the clients listbox.
- Write a "Server has been stopped" message to the server log.
- Set the caption of the button to "Start".
Here is the code of the TfLANMsgServer.btStartClick method:
procedure TfLANMsgServer.btStartClick(Sender: TObject);
begin
if btStart.Caption = 'Start' then begin
// Starting the server
sServerIP := edServerIP.Text; iServerPort := StrToInt(edServerPort.Text);
if (sServerIP <> '') and (iServerPort <> 0) then begin
ltcpServer.Host := sServerIP;
bServerConnected := ltcpServer.Listen(iServerPort);
if bServerConnected then begin
shServerStatus.Brush.Color := clgreen;
edServerLog.Lines.AddText('LAN Messenger Server started at IP=' + sServerIP + ',
port=' + IntToStr(iServerPort));
btStart.Caption := 'Stop';
end;
end;
end
else begin
// Stopping the server
if bServerConnected then begin
ltcpServer.Disconnect(True);
bServerConnected := False;
shServerStatus.Brush.Color := clRed;
lbClients.Items.Clear;
edServerLog.Lines.AddText('LAN Messenger Server stopped');
btStart.Caption := 'Start';
end;
end;
end;
We should include a method to catch connection errors. This may be done using the onError event handler, so the code (display of an error message) has to be placed in the TfLANMsgServer.ltcpServerError method.
procedure TfLANMsgServer.ltcpServerError(const msg: string; aSocket: TLSocket);
begin
MessageDlg('Network error', 'LANMsgServer returned the error message: ' + msg, mtError, [mbOK], 0);
end;
Note: Not entirely to exclude that I missed something, but I was somewhat surprised of the behavior of the Listen function. Not only, the function return seems always to be "True", but also the onError event seems not to be triggered in cases, where it should. So, my server appeared as connected (no error triggering) when I started it with any random IP address, and it also appeared as connected when I started it on port 80 that, on my laptop, is actually used by the Apache webserver. Conclusion: It is mandatory to set the correct IP address and a valid (not yet used) port number. For the rest, just lets suppose that, under these conditions, calling ltcpServer.Listen effectively starts the server.
One last method to code, before we can build and test the application so far developed: The "Exit" item in the "Server" menu (I called it "mServerExit") should contain a statement to stop the server (if it is actually running). Here is the code of my TfLANMsgServer.mServerExitClick method:
procedure TfLANMsgServer.mServerExitClick(Sender: TObject);
begin
if bServerConnected then
ltcpServer.Disconnect(True);
Close;
end;
The screenshot below shows the application after the "Start" button has been pushed and the server started to listen at port 5600 on the network card with IP = 192.168.40.1.
Connecting the client.
Create a Lazarus application project, called "LANMsgClient", with the form shown on the screenshot below. As for the server, we use an lNET TLTCPComponent object that, here, I named "ltcpClient". Just add it to the form, all properties will be set in the source code.
The application should do the following: Using the IP address and port, entered by the user (or read from the configuration file), the client connects to the server when the "Connect" button is pushed (the caption of the button then changes to "Disconnect"). If the connection succeeds, the control LED passes from red to green color and the "Send" button is enabled, otherwise an error message is displayed. When a new client connects, the server will send a list with the IP addresses to all clients. Doing a selection in this list, the client can send a message, entered in the corresponding edit field to a specific computer. All in- and out-messages concerning the client itself will be displayed in the corresponding TMemo component.
It is obvious that the IP and port entered on the client has to be the one on which the server is listening (not the IP of the computer, where the client runs on).
With "fLANMsgClient" being the name of my form, here the code of the TfLANMsgClient.FormCreate and and TLANMsgClient.FormActivate methods, that are more or less identical to those for the server. Note the Boolean "bClientConnected", that we will set to "True" once the client is actually connected to the server.
procedure TfLANMsgClient.FormCreate(Sender: TObject);
begin
bStart := True; bClientConnected := False; btSend.Enabled := False;
end;
procedure TfLANMsgClient.FormActivate(Sender: TObject);
begin
if bStart then begin
if FileExists(ConfigFile) then begin
ServerConfigRead(sServerIP, iServerPort);
end
else begin
sServerIP := ''; iServerPort := DefaultPort;
end;
edServerIP.Text := sServerIP; edServerPort.Text := IntToStr(iServerPort);
bStart := False;
end;
if sServerIP = '' then
edServerIP.SetFocus
else if iServerPort = 0 then
edServerPort.SetFocus
else
btConnect.SetFocus;
end;
The method calls the procedure ServerConfigRead to read the configuration file (called "server.txt"). This procedure is all identical to the one for the server (cf. above).
The next feature to implement is to connect the client to the server. This is done by calling the function ltcpClient.Connect ("ltcpClient" being the name of my TLTCPComponent object). The function, with two arguments (the server IP and port) is called when the "Connect" button is pushed, so we have to place the code within the TfLANMsgClient.btConnectClick method ("fLANMsgClient" being the name of my form, "btConnect" the name of my button). This is the part of the method, where the button's caption is "Connect". After the client has been successfully connected, we use the same button to disconnect the client. This is done by calling the procedure ltcpClient.Disconnect, with a Boolean argument set to "True" (in fact the same method as for the server). In this part of the TfLANMsgClient.btConnectClick method, we'll have to set the variable "bClientConnected" to "False", to change the color of the control LED to red, to clear the clients list, to disable the "Send" button and, finally, to set the caption of the button to "Disconnect".
Here is the code of the TfLANMsgClient.btConnectClick method:
procedure TfLANMsgClient.btConnectClick(Sender: TObject);
var
ClientConnect: Boolean;
begin
if btConnect.Caption = 'Connect' then begin
// Connecting the client
sServerIP := edServerIP.Text; iServerPort := StrToInt(edServerPort.Text);
if (sServerIP <> '') and (iServerPort <> 0) then
ClientConnect := ltcpClient.Connect(sServerIP, iServerPort);
end
else begin
// Disconnecting the client
if bClientConnected then begin
ltcpClient.Disconnect(True);
bClientConnected := False;
shClientStatus.Brush.Color := clRed;
lbClients.Items.Clear;
btConnect.Caption := 'Connect';
btSend.Enabled := False;
end;
end;
end;
As for ltcpServer.Listen, the return value of the function ltcpClient.Connect seems always to be "True". The big difference here is that we cannot just suppose that the connection succeeded (and it does not for sure, if the server isn't running). That's the reason why we use the local variable "ClientConnect" (and not "bClientConnected"), and why the color of the control LED is not changed to red in this method.
When the client tries to connect to the server, two situations are possible:
- The connection succeeds. In this case the event onConnect is triggered.
- The connection fails. In this case the event onError is triggered.
So, two further methods to be coded. In the method TfLANMsgClient.ltcpClientError, we just display an error message. In the method TfLANMsgClient.ltcpClientConnect, we set "bClientConnected" to "True", we change the LED color to green, we set the button caption to 'Disconnect', and we enable the "Send" button. Code of the two methods:
procedure TfLANMsgClient.ltcpClientConnect(aSocket: TLSocket);
begin
bClientConnected := True;
shClientStatus.Brush.Color := clgreen;
btConnect.Caption := 'Disconnect';
btSend.Enabled := True;
end;
procedure TfLANMsgClient.ltcpClientError(const msg: string; aSocket: TLSocket);
begin
MessageDlg('Network error', 'LANMsgClient returned the error message: ' + msg, mtError, [mbOK], 0);
end;
One method left to be coded, before we can test the connection. The "Exit" item in the "Client" menu (I called it "mClientExit") should contain a statement to disconnect the client (if it is actually connected). Here is the code of my TfLANMsgClient.mClientExitClick method:
procedure TfLANMsgClient.mClientExitClick(Sender: TObject);
begin
if bClientConnected then
ltcpClient.Disconnect(True);
Close;
end;
The screenshots below show a successful connection (screenshot on the left), and a connection failure, because the server was offline (screenshot on the right). Please, note that the screenshots show a client running on my Windows 11 VMware virtual machine (the server running on my "physical" Windows 10 laptop).
Note: On my Windows 10 (the operating system where I run the server), I use ZoneAlarm Free Firewall instead of the Windows Firewall). ZoneAlarm allows inbound connections on the local network by default; this is not the case of Windows Firewall (and may not be the case with the firewall that you are actually using). If the connection fails, review your firewall settings. You'll probably have to create an inbound rule for TCP/IP port 5600. If you need help with this, please, have a look at my Windows Firewall: Allowing intranet computers to access local webserver tutorial.
Connection and disconnection.
As we saw above, when the client connects to the server, the event onConnect is triggered on the client side. On the server side, the connection of a client triggers the event onAccept. As the connected clients have to know who else is online, we'll have to make a list of all clients connected (and updating this list whenever a client connects or disconnects) and send it to all connected clients (we will also display this list in the server's listbox). And, the new connection (as well as disconnection) events have to be written to the log. Here is the code of my TfLANMsgServer.ltcpServerAccept method:
procedure TfLANMsgServer.ltcpServerAccept(aSocket: TLSocket);
begin
edServerLog.Lines.AddText('LAN Messenger client with IP=' + aSocket.PeerAddress + ' connected');
UpdateAndSendClientList;
end;
For each new connection, a new socket is opened, and we have access at it, using the TLSocket argument of the TfLANMsgServer.ltcpServerAccept method. The TLSocket.PeerAddress property is set to the IP address of the client that connects (and we will mention it in the server log). The custom procedure UpdateAndSendClientList doesn't need any arguments. As we will see, the TLTCPComponent object includes an internal list with the sockets of all client connections.
Besides the connection of a client, there are two other situations that we'll have to handle: 1. the disconnection of a client; 2. the disconnection of the server (both manually induced by pushing the corresponding button on the client resp. the server). According to the information that I found, these situations could be managed using the onDisconnect event. On the server side, this event should be triggered when a client disconnects (the procedure's TLSocket argument referring to the socket used by this client), and on the client side this event should be triggered when the server is shutdown. Here is the code of the TfLANMsgServer.ltcpServerDisconnect and TfLANMsgClient.ltcpClientDisconnect methods:
procedure TfLANMsgServer.ltcpServerDisconnect(aSocket: TLSocket);
begin
edServerLog.Lines.AddText('LAN Messenger client with IP=' + aSocket.PeerAddress + ' disconnected');
UpdateAndSendClientList;
end;
procedure TfLANMsgClient.ltcpClientDisconnect(aSocket: TLSocket);
begin
MessageDlg('Network problem', 'LANMsgServer closed the connection', mtWarning, [mbOK], 0);
bClientConnected := False;
shClientStatus.Brush.Color := clRed;
lbClients.Items.Clear;
btConnect.Caption := 'Connect';
btSend.Enabled := False;
end;
Unfortunately, the onDisconnect event seems not to work correctly. When I tested the two procedures above and disconnected one of the two clients connected, the server didn't write any message to the log, what means that the onDisconnect event is not triggered. And when I stopped the server, nothing happened on the client, what means again that the onDisconnect event is not triggered. Of course, with the server no more online, the client wasn't connected anymore. And so, when I closed the client application (the variable bClientConnected still being "True"), I got a shutdown error [10054] as shown on the screenshot below.
The work-around is simple: Let the client, as well as the server, send a message before they close the connection, look at this message on the machine at the other side of the connection and if the message says, for example, "bye", take the actions that have to be taken, i.e. execute the code that, before, was part of the onDisconnect handler routines (these may, and probably should, removed from the code).
Here is the new version of the TfLANMsgServer.mServerExitClick and the TfLANMsgClient.mClientExitClick methods:
procedure TfLANMsgServer.mServerExitClick(Sender: TObject);
begin
if bServerConnected then begin
SendMessage('bye', 'all');
Wait(5);
ltcpServer.Disconnect(True);
end;
Close;
end;
procedure TfLANMsgClient.mClientExitClick(Sender: TObject);
begin
if bClientConnected then begin
ltcpClient.SendMessage('bye');
Wait(5);
ltcpClient.Disconnect(True);
end;
Close;
end;
Before the server application terminates, it sends the message "bye" to all clients, using the custom procedure SendMessage. The client does the same, but here we use the TLTCPComponent procedure ltcpClient.SendMessage, that can be used on a TCP/IP client to send a message to the server to which it has connected before (it can also be used to send a message from the server to a client). The custom procedure Wait is actually not used on my system (procedure without code). I previewed it to make the application pause during, for example 5 seconds, in order to make sure that the message has been sent when the application exits. As it seems, there is no issue here, maybe the system waits by itself until the command has been executed (?). Anyway, if your server or client shuts down without that the machine at the other side of the connection notices it, add some delay code to the Wait procedure.
Now, lets have a look at the custom SendMessage procedure, used by the server to send the "bye" message to the clients, before going offline. The procedure has two arguments: the message to be sent, and a string containing either an IP address (specific client to whom to send the message), or the word "all" (to send the message to all clients connected). Do we really have all we need to perform these send operations? We didn't save any socket information, when a client connected, so how can we access the connected clients without having made a list of them? The answer to this question is that we don't need to make a list ourselves, because it is internally done by the TLTCPComponent object.
All sockets are stored in the array ltcpServer.Socks, the element with index 0 referring to the server itself, the following elements referring to the client sockets. This means that we can, for example, get the IP of the clients by reading the properties ltcpServer.Socks[I].PeerAddress. As the socket connections are in fact stored as a chained list, we can also access them using an iteration approach. The procedure ltcpServer.Reset resets the iterator, i.e. points to the beginning of the list (referring to the server itself). The procedure ltcpServer.IterNext advances the iterator, pointing to the next socket in the list. And finally, ltcpServer.Iterator may be used to access the socket connection actually pointed to. This may appear to be complicated, but when looking at the code, you'll see that it's much easier to understand that you might think.
procedure SendMessage(Mess, Dest: string);
var
I: Integer;
begin
fLANMsgServer.ltcpServer.IterReset; I := 0;
while fLANMsgServer.ltcpServer.IterNext do begin
Inc(I);
if (Dest = 'all') or (Dest = fLANMsgServer.ltcpServer.Socks[I].PeerAddress) then begin
if fLANMsgServer.ltcpServer.Iterator.Connected then
fLANMsgServer.ltcpServer.SendMessage(Mess,
fLANMsgServer.ltcpServer.Iterator);
end;
end;
end;
Note: The code in the procedure mixes the iteration and sockets array approaches to access the client sockets and IPs. So, you can see, how both of them may be used in a Free Pascal program...
The list pointer is set to the beginning of the list (list element with index 0; sever connection) and the counter variable I to 0. The while loop, containing a call to ltcpServer.IterNext advances the pointer to the next list element and executes the code within its block if and as long as there effectively is a list element. If the destination argument of the procedure is "all", a message is sent to the client referred to by the actual list element. The message is also sent if the destination argument contains an IP that is equal to the IP of the actually referred client (here the connection and the corresponding IP are taken from the ltcpServer.Socks array, the counter I having be incremented at the beginning of the while block). The message is sent using the server side form of the ltcpServer.SendMessage procedure, that has two arguments (vs one argument only for the client side form): the message to be sent and the socket where it has to be sent to. This socket is the one actually pointed to, so corresponds to ltcpServer.Iterator.
Important: The chained list of socket connections contains all sockets that have been opened, what means that it can contain references to clients that actually are disconnected. Trying to send a message to a client that isn't anymore online would trigger the onError event and an error 10054 would be displayed. We avoid this by only sending the message if the client is effectively connected. To check the client connection state, we can use the TLSocket.Connected property (in our list iteration we'll have to test if ltcpServer.Iterator.Connected is "True").
Sending the "bye" message being implemented, we have now to see how to do to read this message on the other side of the connection and, if the message text actually is "bye", run the code that we had placed in the (abandoned) methods ltcpServerDisconnect and ltcpClientDisconnect. When the TLTCPComponent object (server, or client) receives a messages the event onReceive is triggered. So, all we have to do is placing the code of the abandoned methods within the methods TfLANMsgServer.ltcpServerReceive resp. TfLANMsgClient.ltcpClientReceive. This method has a TLSocket argument, that allows us not only to retrieve the IP of the sender, but also the message, the first one (as we already saw) using the property PeerAddress, the second one using the procedure GetMessage. Here is the code of the two methods:
procedure TfLANMsgServer.ltcpServerReceive(aSocket: TLSocket);
var
Mess: string;
begin
aSocket.GetMessage(Mess);
if Mess = 'bye' then begin
aSocket.Disconnect(True);
edServerLog.Lines.AddText('LAN Messenger client with IP=' + aSocket.PeerAddress + ' disconnected');
UpdateAndSendClientList;
end;
end;
procedure TfLANMsgClient.ltcpClientReceive(aSocket: TLSocket);
var
Mess: string;
begin
aSocket.GetMessage(Mess);
if Mess = 'bye' then begin
MessageDlg('Network problem', 'LANMsgServer closed the connection', mtWarning, [mbOK], 0);
bClientConnected := False;
shClientStatus.Brush.Color := clRed;
lbClients.Items.Clear;
btConnect.Caption := 'Connect';
btSend.Enabled := False;
end;
end;
Important: As the onDisconnect event doesn't seem to work correctly, on the server side, we use the TLSocket procedure Disconnect (with the argument "force disconnection" = "True") to make sure that the client, that has sent the "bye" message, has its TLSocket.Disconnected property set to "True". This will allow us to test if the client is effectively online, and only sending messages if it is (cf. procedure SendMessage above).
Letting the UpdateAndSendClientList for later, we have all together now to test connection and disconnection of server and clients. The screenshot below shows the server log after the following events: start of the server, connection of a Windows 10 client from 192.168.40.1, connection of a Windows 11 client from 192.168.40.80, connection of a Windows 8.1 client from 192.168.40.101, disconnection of the Windows 8.1 client, disconnection of the Windows 10 client, reconnection of the Windows 8.1 client, disconnection of the Windows 11 client.
The following two screenshots show the warning message displayed on the clients when the server goes offline (user pressing the "Stop" button, or quitting the application): on the left, the Windows 10 client, on the right, the Windows 8.1 client. The control LED will change to red color after the OK button in the dialog box is pressed...
Updating the client list.
As we saw above, the server knows all clients (as sockets stored in the internal list of the TLTCPComponent object), whereas the client only knows the server. In order to communicate with the other clients, the clients have to know their IP addresses and sending them a message is only useful if they are actually online. That's why our server application, each time a client connects or disconnects has to send some information about the clients effectively connected at this moment to all clients. The most obvious way to do this would probably be to send the client's IP (together or not with an indication, if the client has connected or disconnected). Another possibility is to send the list with all connected clients. Perhaps not the best way to do (as this implies a higher bandwidth), but no big deal in our sample application (and even not in a real world application on a small LAN), and also giving the opportunity to show how to proceed to "decrypt" a message containing several data segments (several IPs in our case).
The implementation of the communication between a custom server and its clients is up to the programmer; it's ourselves who define the language that the server and the clients use to communicate. This language has to include commands and the associated data. On an FTP server, for example, we have commands to change the directory, to create, rename, or delete a file and data consisting of the file or directory name. Our application needs at least three commands:
- bye, to indicate the disconnection (of the server as well as the client).
- list, to send the list with the client IPs from the server to the clients.
- msg, to send the message to be relayed to another client to the server, resp. to relay the message from a client to another client.
Of course, you can define your own language, using "lst", "update", or whatever instead of "list". You can also use two different commands instead of "msg", depending upon the message is sent to or by the server. The only thing that matters is that server and client speak the same language and so understand each other.
A second point to consider is the usage of special characters to separate the different parts of the message string. A separator between the command and the data and, in the case where the data is made of several parts (as in the case of our IP list), a character to separate these parts. As a command (normally) is one single word, the most obvious separator between the command and the data is a space. For the data, another character will be needed if the data parts contain spaces themselves. This is not our case. The only command that includes several data parts is "list" and the data is a series of IP addresses (that are of the form xxx.xxx.xxx.xxx, so never containing a space). No problem so, to use a space as separator of the IPs, too. Long explanations, but this text is intended for network newbies, remember?
On the server side, we have already added the call to a custom procedure UpdateAndSendClientList to the methods ltcpServerAccept (connection of a client) and ltcpServerReceive (reception of a "bye" message from a client = disconnection of this client). Beside sending a new client list to all clients, this procedure should also replace the content of the clients listbox. With the knowledge that we have acquired in the preceding paragraphs, the implementation of these tasks is not complicated: Not lots more than the iteration of the internal sockets list and using our custom procedure SendMessage to send the IP addresses to all clients. Here is the code ("lbClients" being the name of my listbox):
procedure UpdateAndSendClientList;
var
MsgText, IP: string;
begin
fLANMsgServer.lbClients.Items.Clear;
MsgText := 'list';
fLANMsgServer.ltcpServer.IterReset;
while fLANMsgServer.ltcpServer.IterNext do begin
if fLANMsgServer.ltcpServer.Iterator.Connected then begin
IP := fLANMsgServer.ltcpServer.Iterator.PeerAddress;
fLANMsgServer.lbClients.Items.AddText(IP);
MsgText += ' ' + IP;
end;
end;
SendMessage(MsgText, 'all');
end;
On the client side, we have to extend the TfLANMsgClient.ltcpClientReceive method, adding the code for the case the message from the server contains the "list" command: Splitting the message string in order to extract the different IP addresses and add them to the listbox (that I named "lbClients"). Here is the new version of TfLANMsgClient.ltcpClientReceive:
procedure TfLANMsgClient.ltcpClientReceive(aSocket: TLSocket);
var
I: Integer;
Mess: string;
Clients: TArray;
begin
aSocket.GetMessage(Mess);
if Mess = 'bye' then begin
// Command "bye": Server is going offline
MessageDlg('Network problem', 'LANMsgServer closed the connection', mtWarning, [mbOK], 0);
bClientConnected := False;
shClientStatus.Brush.Color := clRed;
lbClients.Items.Clear;
btConnect.Caption := 'Connect';
btSend.Enabled := False;
end
else if LeftStr(Mess, 4) = 'list' then begin
// Command "list": Server has sent clients list
Delete(Mess, 1, 4);
Split(Mess, Clients, ' ');
lbClients.Clear;
for I := 0 to Length(Clients) - 1 do
lbClients.Items.AddText(Clients[I]);
end;
end;
To extract the IP addresses, the method calls the custom procedure Split, that "transforms" a string into an array of strings:
procedure Split(Str: string; out Arr: TArray; Sep: Char);
var
P: Integer;
begin
SetLength(Arr, 0);
while Str <> '' do begin
SetLength(Arr, Length(Arr) + 1);
P := Pos(Sep, Str);
if P = 0 then begin
Arr[Length(Arr) - 1] := Str;
Str := '';
end
else begin
Arr[Length(Arr) - 1] := LeftStr(Str, P - 1);
Delete(Str, 1, P);
end;
end;
end;
Note: TArray is a custom type defined by: TArray = array of string;. This is necessary, because using the procedure SetLength on a variable of the standard type array of string is not permitted and would produce an error during compilation.
Time to test the application! The screenshots below show the server log after the successive connection of clients from 192.168.40.1 (Windows 10), 192.168.40.101 (Windows 8.1), and 192.168.40.80 (Windows 11) (screenshot on the left), and the corresponding client list shown on the Windows 11 machine (screenshot on the right).
Now, disconnecting the Windows 11 and the Windows 8.1 clients, then reconnecting the Windows 8.1 client. The screenshot on the left shows the corresponding server log, the screenshot on the right shows the new client list (2 connections) on the Windows 10 client.
And finally, stopping the server. The screenshot on the left shows the server that has gone offline, the screenshot on the right shows the corresponding warning displayed on the Windows 8.1 client.
Sending messages.
Note: I wrote this tutorial during the development of the sample applications. This means, for example, that some screenshots look not exactly the same as those taken in an earlier stage. In particular, at the beginning, I had set all border icons to "False", with the aim to disable exiting the application by just closing the window (because this will not inform the other side of the connection about the fact that the machine is now offline). However, removing the "System" border icon, also removes the "Minimize" icon (even if it's set to "True"). So, I decided later to change this, in order to give the user the possibility to minimize the window. Another change done at this stage, was adding the statements edMessages.Lines.Clear; and edMessage.Text := ''; to the disconnect part of the TfLANMsgClient.btConnectClick method, in order to clear all messages at client disconnection.
On the client side, sending a message consists in selecting the client (IP) to whom the message should be sent to in the clients listbox, entering the message to be sent in the corresponding edit field and pushing the "Send" button (that I called "btSend"). This should send a "msg" command to the server (as described at the beginning of the previous section). The message itself, together with the destination IP should also be listed in the message TMemo. Here is the code of my TfLANMsgClient.btSendClick method:
procedure TfLANMsgClient.btSendClick(Sender: TObject);
var
S: string;
begin
sMessage := edMessage.Text;
if (sDestination <> '') and (sMessage <> '') then begin
// Display message (plus destination IP)
if sDestination <> sOldDestination then begin
S := ' To ' + sDestination + ':';
edMessages.Append(S);
sOldDestination := sDestination;
sOldSource := '';
end;
edMessages.Append(sMessage);
// Send the message to the server
S := 'msg ' + sDestination + ' ' + sMessage;
ltcpClient.SendMessage(S);
end;
end;
The message actually sent consists of three parts: the command "msg", the IP of the destination client, and the message itself, the different parts being separated by a space. Maybe you wonder, where the value of "sDestination" comes from. The user selects the destination client in the listbox, and the variable is set whenever the listbox selection changes, so the code has to be in the TfLANMsgClient.lbClientsSelectionChange method:
procedure TfLANMsgClient.lbClientsSelectionChange(Sender: TObject; User: boolean);
begin
sDestination := lbClients.GetSelectedText ;
end;
The variable sDestination, as well as the two variables sOldDestination and sOldSource (that are used in the message TMemo to not display the destination and source IP if it is the same as for the send resp. receive before), is initialized each time the client connects. Here is the new version of the TfLANMsgClient.ltcpClientConnect method.
procedure TfLANMsgClient.ltcpClientConnect(aSocket: TLSocket);
begin
bClientConnected := True;
shClientStatus.Brush.Color := clgreen;
btConnect.Caption := 'Disconnect';
btSend.Enabled := True;
sDestination := ''; sOldDestination := ''; sOldSource := '';
end;
On the server side, we have to extend the TfLANMsgServer.ltcpServerReceive method, adding the code concerning the reception of a message, containing a "msg" command. The server has to relay the message to the client whose IP address has been passed together with the message text; this new "msg" command has also to contain the message source, i.e. the IP of the client who sent the message (IP that we find as a TLSocket.PeerAddress property). The server should also add an entry concerning the relay to its log. Here is the new version of the TfLANMsgServer.ltcpServerReceive method:
procedure TfLANMsgServer.ltcpServerReceive(aSocket: TLSocket);
var
P: Integer;
begin
sSource := aSocket.PeerAddress; aSocket.GetMessage(sMessage);
if sMessage = 'bye' then begin
// Command "bye": Client is going offline
aSocket.Disconnect(True);
edServerLog.Lines.AddText('LAN Messenger client with IP=' + sSource + ' disconnected');
UpdateAndSendClientList;
end
else if LeftStr(sMessage, 3) = 'msg' then begin
// Command "msg": Client sent a message to be relayed to another client
Delete(sMessage, 1, 4);
P := Pos(' ', sMessage);
sDestination := Copy(sMessage, 1, P - 1);
Delete(sMessage, 1, P);
sMessage := 'msg ' + sSource + ' ' + sMessage;
SendMessage(sMessage, sDestination);
edServerLog.Lines.AddText('Relayed message from ' + sSource + ' to ' + sDestination);
end;
end;
When a client receives a message containing the command "msg", the message text (together with its source, i.e. the IP of the sender) has to be displayed in the messages TMemo. Here the new version of the TfLANMsgClient.ltcpClientReceive method:
procedure TfLANMsgClient.ltcpClientReceive(aSocket: TLSocket);
var
P, I: Integer;
S: string;
Clients: TArray;
begin
aSocket.GetMessage(sMessage);
if sMessage = 'bye' then begin
// Command "bye": Server is going offline
MessageDlg('Network problem', 'LANMsgServer closed the connection', mtWarning, [mbOK], 0);
bClientConnected := False;
shClientStatus.Brush.Color := clRed;
lbClients.Items.Clear;
edMessages.Lines.Clear; edMessage.Text := '';
btConnect.Caption := 'Connect';
btSend.Enabled := False;
end
else if LeftStr(sMessage, 4) = 'list' then begin
// Command "list": Server has sent clients list
Delete(sMessage, 1, 4);
Split(sMessage, Clients, ' ');
lbClients.Clear;
for I := 0 to Length(Clients) - 1 do
lbClients.Items.AddText(Clients[I]);
end
else if LeftStr(sMessage, 3) = 'msg' then begin
// Command "msg": Server has relayed message from another client
Delete(sMessage, 1, 4);
P := Pos(' ', sMessage);
sSource := Copy(sMessage, 1, P - 1);
if sSource <> sOldSource then begin
S := ' From ' + sSource + ':';
edMessages.Append(S);
sOldSource := sSource; sOldDestination := '';
end;
Delete(sMessage, 1, P);
edMessages.Append(sMessage);
end;
end;
All coding done to have a fully functional client-server application; time to test the programs by sending some messages from one client to the others. The two screenshots below show the server with the 3 clients from the examples above: 192.168.40.1 (Windows 10; the server runs on this machine, too), 192.168.40.80 (Windows 11), and 192.168.40.101 (Windows 8.1). The screenshot on the left shows the messages sent and received on the Windows 11 client. The screenshot on the right shows the log on the server (containing also references to messages exchanged between the other two clients).
The following screenshots have been taken after the Windows 11 client had disconnected. The screenshots show the message TMemo on the Windows 8.1 client (screenshot on the left) and the Windows 10 client (screenshot on the right).
Remains to write the code of the TfLANMsgServer.mSettingsConfigClick and TfLANMsgClient.mSettingsConfigClick methods (user click on the menu item "Settings > Server configuration"), that allows to input an IP address and a port, and the procedure ServerConfigWrite (identical for server and client), that writes these values to a text file. This is standard Lazarus/Free Pascal programming, and I will not include the code here (have a look at the LANMsg source code instead).
If you want to use my LANMsgServer and LANMsgClient samples as templates to test/increase your Free Pascal programming skills by trying to solve some coding exercises, here some suggestions:
- Server side:
- Extend the server log, adding a timestamp.
- Save the server log to a file, either automatically, or by pushing a "Save" button.
- Keep a copy (file) with all message content on the server.
- Refuse the connection (during a given time period) to clients, who use some predefined words/expressions in the messages they send.
- Client side:
- Connect to the server using a username, that will replace the IP address in the clients listbox and the messages TMemo (this requires to adapt the code of the server application, too, of course).
- Give the client the possibility to send a message to all other clients (either using a "Send to all" button, or to take advantage of the MultiSelect feature of the TListBox objects).
- As an alternative, preview a "two (or three) modes" messenger application: In "public mode", messages are sent to all clients, in "private mode", messages are sent to one single client, and a third possibility ("strictly private mode"), where the communication concerns two given clients (to whom other clients cannot sent any messages).
I agree that this tutorial is really long. But, I wanted to be as explicit as possible and to proceed step by step in order to allow people, who never did some network programming before, to easily follow the implementation of a simple client-server application. I don't know, how far I succeeded. Any comments, suggestions, criticisms are appreciated (just send me an email).
If you find this text helpful, please, support me and this website by signing my guestbook.