Wednesday, August 22, 2007

Internet Programming with Pascal

Contents
  1. Compilers and Operating Systems
  2. Getting Started
  3. Using the Domain Name System
  4. Handling Multiple Clients
  5. Binary Data
  6. Obtaining the Software
  7. Final Remarks

1. Compilers and Operating Systems

Many people (such as me) came to Pascal programming through Borland's great Turbo Pascal series for DOS. A lot of them still consider it the best development environment around and want to stay with it as long as possible. Unfortunately, there are a few points that let it seem advisable to say good-bye to Turbo Pascal and set out for new shores.
Most information in this guide doesn't rely on operating system specific features; if it does, I have pointed that out. The example programs have been written under Linux and additionally tested under Windows NT.
All files referred in this document are available from the download link.

2. Getting Started

So, we want to do "Internet programming". In other words, we want to write programs that communicate with other programs (that may or may not be written by us) through the Internet. As you may already know, two transport protocols are normally used on the Internet, namely UDP and TCP. The former is a so-called connection-less protocol that lets you send and receive small packets of data (datagrams) to and from remote computers, while the latter establishes an enduring connection between your computer and a remote host. In this document, we will only use TCP.

2.1. Byte Order

This is a tiresome issue which keeps arising now and then, and which will be a permanent companion in all kinds of network programming. The problem is as simple as this: (The nomenclature is inspired by a certain set of C macros.) Note that the LongInt arguments are in network byte order.

2.2. Error Messages

If you want to write stable, reliable programs, you have to do error checking. You should write an error message to stderr (preferrably one that is helpful for the user), and then either continue operation or, in the case of an unrecoverable error, you should abort. When dealing with the network interface, we are going to use a lot of system functions that will, on error, set a special variable. The system provides us with a "translation service" to turn this number into a human-readable string. I wrote a bunch of procedures that I will use in the examples to deal with such errors: Note Windows programmers: Since strerror is not available under Windows, converting the numbers to human-readable messages is currently impossible. This has probably to be done using Winsock functions. Until I have found a way, there is a special Windows version of the myerror unit that only prints numbers (see the link below).

2.3. Introductory Example

From the application's point of view, the network is accessed through sockets. Any program that wants to send or receive data through the network has to request such a socket from the operating system, which can then be used to open a TCP connection or to handle UDP datagrams. From the network's point of view, a socket is associated with a port.
When sockets were invented for Unix, the authors had the idea to use the normal Unix file descriptors (handles) for this purpose. That way, you can read from and write to the network as if it were a normal text file. However, opening a socket is still a bit more work than just opening a file...
Let's now have a look at a simple example, and describe on a step-by-step basis, what the program does. The task is to write a simple server that waits for incoming connections (contrarily, a client would actively be opening a connection). When a connection is opened, the server reads a line and writes the length of the line back to the client. This (rather tedious occupation) is repeated until the client disconnects.
As all servers, we have to select the port we can be contacted at. Ports below 1024 are reserved for system servers such as the FTP daemon etc. Some systems won't let user processes bind to these ports. Also, many of them are already assigned as well-known port numbers for standardized services such as FTP, HTTP and so on by the Internet Assigned Numbers Authority - certainly nothing we want to mess with. We will therefore use a big number such as $AFFE, which is used throughout the examples.
Ok, let's start off with the initial declarations, and our first action: To request a Socket from the operating system. As you can see, a socket is nothing but a LongInt:
program simserv;

uses
sockets, inetaux, myerror;

const
ListenPort : Word = $AFFE;
MaxConn = 1;

var
lSock, uSock : LongInt;
sAddr : TInetSockAddr;
Len : LongInt;
Line : String;
sin, sout : Text;

begin
lSock := Socket(af_inet, sock_stream, 0);
if lSock = -1 then SockError('Socket: ');
The socket is created using the Socket system call. Let us have a look at the parameters. First of all, you have to know that sockets are not only used for TCP/IP networking, but also for IPX/SPX, AppleTalk and what have you. Since we want to use the Internet, we specify the address family af_inet. The next parameter, sock_stream, indicates that we are going to use a TCP connection. The last parameter specifies a certain protocol. We set it to 0, as there is only once choice, and that is TCP.
A return value of -1 returns an error; we call SockError to abort.
Now that we have a socket, we want to bind it to a certain address/port. For this, we use the variable sAddr of type TInetSockAddr. It has three (relevant) fields; the address family, the address and the port number:
   
with sAddr do
begin
Family := af_inet;
Port := htons(ListenPort);
Addr := 0;
end;

if not Bind(lSock, sAddr, sizeof(sAddr)) then SockError('Bind: ');
The family is again af_inet. The port is set to the constant defined earlier, and please note that it is converted to network byte order. The address is set to 0 (invalid address), which makes the operating system automatically fill in our own address.
With the address record ready, we call the Bind function (the third parameter is the size of the address record), which returns False on error. Now we are ready to tell the system to open this socket for incoming connections:
   if not Listen(lSock, MaxConn) then SockError('Listen: ');
MaxConn (also defined in the const part) is a number, also known as backlog, that specifies how many incoming connections should be kept in a queue until we get around to answering them. Anyhow, now that the socket is listening, how do we check for connections and answer them? The following code shows:
   
repeat
Say('Waiting for connections...');
Len := sizeof(sAddr);
uSock := Accept(lSock, sAddr, Len);
if uSock = -1 then SockError('Accept: ');
Say('Accepted connection from ' + AddrToStr(sAddr.Addr));
The Accept call blocks the program until a connection arrives (unless we use it with a non-blocking socket, which I'm not going to cover here). It fills sAddr with the address of the connection's other end. The return value is a new socket for the new connection (the old socket keeps listening), unless an error occurs, in which case it returns -1. The last line produces an output that reports the new connection.
Having successfully opened a connection, we use the procedure Sock2Text to retrieve two text files - one for input, one for output: Then we use it as if it was a normal file to communicate with the remote host:
      Sock2Text(uSock, sin, sout);

Reset(sin);
Rewrite(sout);
Writeln(sout, 'Welcome, stranger!');
while not eof(sin) do
begin
Readln(sin, Line);
if Line = 'close' then break;
Writeln(sout, Length(Line));
end;

Close(sin);
Close(sout);
Shutdown(uSock, 2);
Say('Connection closed.');
until False;
end.
The Shutdown procedure is used to permanently close the connection, with the parameter 2 meaning that no further sending or receiving is allowed. Other choices would be 0: No further receives and 1: No further sends.
To summarize, the course of events is as follows:
  1. Create a new internet socket of type stream.
  2. Bind this socket to our own address, with our custom port.
  3. Call Listen to let the system know we want to accept connections.
  4. Wait for connections with Accept.
  5. After a connection has been accepted, convert the socket to Text files, then open the files.
  6. Exchange data with the client.
  7. Close the files and call Shutdown to close the connection.
To test our server, we can use a standard Telnet client (port $AFFE = 45054). It is a good idea to open two xterms and start the server in the first one and the Telnet client in the other one (in Windows, open two Telnet windows). The telnet output should look like this:
basti@clever:~ > telnet localhost 45054
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Welcome, stranger!
short line
10
this is a very long line
24
close
Connection closed by foreign host.

2.4. The Other End

Ok, now we know in principle what the server has to do. The client is even simpler: Instead of binding to an address on the local host and listening for connection, it just opens one. The function Connect is used for this. It takes as first argument the socket, and as second an address of the type TInetSockAddr that we already know (a third argument is the size of the address record).
The following program implements a client for the server we just wrote:
program simclient;

{ Simple client program }

uses
sockets, inetaux, myerror;

const
RemoteAddress = '127.0.0.1';
RemotePort : Word = $AFFE;

var
Sock : LongInt;
sAddr : TInetSockAddr;
sin, sout : Text;
Line : String;

begin
Sock := Socket(af_inet, sock_stream, 0);
if Sock = -1 then SockError('Socket: ');

with sAddr do
begin
Family := af_inet;
Port := htons(RemotePort);
Addr := StrToAddr(RemoteAddress);
end;

if not Connect(Sock, sAddr, sizeof(sAddr)) then SockError('Connect: ');
Sock2Text(Sock, sin, sout);
Reset(sin);
Rewrite(sout);

Writeln('Connected.');
Readln(sin, Line);
Writeln(Line);
repeat
Write('> ');
Readln(Line);
Writeln(sout, Line);
if Line <> 'close' then
begin
Readln(sin, Line);
Writeln(Line);
end;
until Line = 'close';

Close(sin);
Close(sout);
Shutdown(Sock, 2);
end.
A few remarks: As you can see, the loopback address 127.0.0.1 is hard-coded into the program as a string. We use the function StrToAddr from the inetaux unit to convert it to a LongInt. Another possibility would be to define it as $7F000001 (host byte order!), but this technique quickly becomes impractical. Also, if we use strings, we can easily change the program to accept IP addresses as user input, as we shall see in the next subsection.
The rest of the program is nothing new. Go ahead and try it out, but make sure that the server is already running somewhere.

2.5. Something Useful

It's time that we write a program that actually has a purpose. I mean, determining the length of a string is not really one of the services you want to offer other people over the 'net (and also other people don't offer it). One of the lesser known services on the Internet is "daytime". Some hosts offer a clock service at port 13. The protocol is simple:
  1. You connect to the server.
  2. The server sends a line with the current date and time in some human-readable format.
  3. The server disconnects.
Of course, this is not very accurate, since the message needs some time to travel through the network. For precise time transmission, there is the network time protocol (NTP), as defined in RFC 1305. Let's just say it's very mathy, if that is a word. However, if you awake in a dark, locked room after a long, dreamless sleep, not knowing whether it is day or night, you could simply connect to a daytime server to gain this essential information.
The daytime protocol is defined in RFC 867. I suggest that you read it, it is an easy read compared to your average RFC. RFC documents can be retrieved from the RFC-Editor.
Without further ado, this is the source:
program daytime;

{ Simple client program }

uses
sockets, inetaux, myerror;

const
RemotePort : Word = 13;

var
Sock : LongInt;
sAddr : TInetSockAddr;
sin, sout : Text;
Line : String;

begin
if ParamCount = 0 then GenError('Supply IP address as parameter.');

with sAddr do
begin
Family := af_inet;
Port := htons(RemotePort);
Addr := StrToAddr(ParamStr(1));
if Addr = 0 then GenError('Not a valid IP address.');
end;

Sock := Socket(af_inet, sock_stream, 0);
if Sock = -1 then SockError('Socket: ');

if not Connect(Sock, sAddr, sizeof(sAddr)) then SockError('Connect: ');
Sock2Text(Sock, sin, sout);
Reset(sin);
Rewrite(sout);

while not eof(sin) do
begin
Readln(sin, Line);
Writeln(Line);
end;

Close(sin);
Close(sout);
Shutdown(Sock, 2);
end.
The syntax is "daytime <ip-address>". Many server administrators disable the daytime service. Just try some server at a university that you know. For example, public.uni-hamburg.de works fine:
basti@clever:~/pas > nslookup public.uni-hamburg.de
Server: rzaix240.rrz.uni-hamburg.de
Address: 134.100.33.240

Name: public.uni-hamburg.de
Address: 134.100.32.55

basti@clever:~/pas > daytime 134.100.32.55
Sun Apr 23 16:54:50 2000
basti@clever:~/pas >

2.6. Suggestions

If you want to try your newly gained knowledge, here are some things you could do:
  • Write a server that reads file names from the client and, for each name, sends back the content of that file.
  • The server described above is the worst security hole imaginable. Modify the program so that it will interpret the file names relative to a directory specified either in the code or by the user. Make sure that your program cannot be fooled by file names like '../../../etc/passwd'. Once you start writing network programs, you have to start thinking about security issues!
  • Read RFC 1288 - The Finger User Information Protocol. Write a finger client. Find out why this service is called Finger and tell me about your findings, since I'd like to know.
  • Why not write a daytime server?

3. Using the Domain Name System

As every schoolkid knows, hosts on the Internet are uniquely identified by an IP address, which is - as we have already seen - nothing but a 32 bit number normally written as 4 consecutive bytes. Now if there is something we can learn from the telephone system, it is that numbers are hard to rememeber. Therefore, the domain name system was created. This is not the only use of the DNS - email exchange hosts can also be determined, among other things.

3.1. Interface

Fortunately, we don't have to deal with all this. We can use a simple set of operating system calls to resolve a name to an IP address. For this, you have to tell your operating system the IP address of your provider's name servers, which is something you probably have done when you set up your computer.
  THost = Object
FHostEntry : PHostEnt;
FAlias,FAddr,FError : Longint;
Constructor NameLookup (HostName : String);
Constructor AddressLookup (Const Address : THostAddr);
Destructor Done;
Function Name : String;
Function GetAddress (Select : TSelectType) : String;
Function GetAlias (Select : TSelectType) : String;
Function IPAddress : THostAddr;
Function IPString : String;
Function LastError : Longint;
end;
We are going to call the constructor NameLookup, and then use the fields Name (the official name) and, most importantly, IPAddress, which can be cast to a LongInt without problems and is already in network byte order (for more details please see inet.pp).
program dtname;

{ Simple client program with DNS support }

uses
sockets, inet, inetaux, myerror;

const
RemotePort : Word = 13;

var
Sock : LongInt;
sAddr : TInetSockAddr;
sin, sout : Text;
Line : String;
Host : THost;

begin
if ParamCount = 0 then GenError('Supply hostname as parameter');

with sAddr do
begin
Family := af_inet;
Port := htons(RemotePort);
Addr := StrToAddr(ParamStr(1)); { Maybe it's an IP address }
if Addr = 0 then
begin
with Host do
begin
NameLookup(ParamStr(1));
if LastError <> 0 then GenError('Name lookup failure');
Writeln('Official name: ', Name);
Addr := LongInt(IPAddress);
Writeln('IP address: ', AddrToStr(Addr));
end;
end;
end;

Sock := Socket(af_inet, sock_stream, 0);
if Sock = -1 then SockError('Socket: ');

if not Connect(Sock, sAddr, sizeof(sAddr)) then SockError('Connect: ');
Sock2Text(Sock, sin, sout);
Reset(sin);
Rewrite(sout);

while not eof(sin) do
begin
Readln(sin, Line);
Writeln(Line);
end;

Close(sin);
Close(sout);
Shutdown(Sock, 2);
end.
As an example of its usage, let's see if the folks in the capital will tell us what time it is:
basti@clever:~/pas/collection/inet > dtname news.fu-berlin.de
Official name: news.fu-berlin.de
IP address: 130.133.1.4
Sun Apr 23 19:44:25 2000
basti@clever:~/pas/collection/inet >

3.2. Suggestions

DNS is a powerful system. If you want to find out more about the information you can retrieve through your name servers, you might want to get a hand on the package "dig" which is available for most operating systems. DNS is described in RFCs 1034 and 1035.

4. Handling Multiple Clients

The servers we have written so far can only serve one client at a time. This may be acceptable for very simple services such as daytime, but otherwise it is unacceptable, especially when connections can last longer (NNTP, FTP or Telnet come to mind). There are two basic techniques to write multi-client servers, which we will explore in the following section.

4.1. Multitasking

This is the first approach to the task. If multiple clients can be handled independently, it is possible to start a new process for each client. That means that we proceed like in the simple server presented earlier, but after accepting, issue a Fork command. Fork creates a new process that is an exact copy of the old one, all variable values, file descriptors etc. included. With one small, but subtle difference: The return value of the Fork function is 0 in the child process, and another number in the parent process (more precisely, the process ID of the child). So the basic structure looks like this:
if Fork = 0  then  { we are the child }
begin
handle the new client;
Halt;
end
else proceed with the accepting loop;
However, after executing the Halt, the child process is not really dead. On the other hand, it surely doesn't live anymore, so it is fair to say that it is in a mysterious state between life and death. It is called a zombie. (I am not making jokes, please read the manual page for ps!) The process will stay in this state until it is delivered by its parent through a call of WaitPid.
WaitPid(-1, nil, wnohang);
where -1 means that we are not waiting for a particular process, nil means that we don't want to know more about the child's state and the flag wnohang indicates that if there is no zombie, the function should return immediately. The return value is -1 on failure (no zombie) and the process number of the child otherwise.
program mtserv;

{ A multi-threaded server program. }

uses
sockets, inetaux, myerror, linux;

const
ListenPort : Word = $AFFE;
MaxConn = 1;

var
lSock, uSock : LongInt;
sAddr : TInetSockAddr;
Len : LongInt;
Line : String;
sin, sout : Text;

begin
lSock := Socket(af_inet, sock_stream, 0);
if lSock = -1 then SockError('Socket: ');

with sAddr do
begin
Family := af_inet;
Port := htons(ListenPort);
Addr := 0;
end;

if not Bind(lSock, sAddr, sizeof(sAddr)) then SockError('Bind: ');
if not Listen(lSock, MaxConn) then SockError('Listen: ');

repeat
Say('Waiting for connections...');
Len := sizeof(sAddr);
uSock := Accept(lSock, sAddr, Len);
if uSock = -1 then SockError('Accept: ');
Say('Accepted connection from ' + AddrToStr(sAddr.Addr));

if Fork = 0 then { we are the child }
begin

Sock2Text(uSock, sin, sout);

Reset(sin);
Rewrite(sout);
Writeln(sout, 'Welcome, stranger!');
while not eof(sin) do
begin
Readln(sin, Line);
if Line = 'close' then break;
Writeln(sout, Length(Line));
end;

Close(sin);
Close(sout);
Shutdown(uSock, 2);
Say('Connection closed.');
Halt;
end
else { we are the parent }
repeat until WaitPid(-1, nil, wnohang) < 1;
until False;
end.
After starting mtserv, we get the following result from the ps output (try ps | grep mt):
 3735  ?  S    0:00 mtserv 
Then we connect to the server (e. g. with Telnet, or with our own sophisticated client), and voilá, we have two processes running:
 3735  ?  S    0:00 mtserv 
3753 ? S 0:00 mtserv
A second Telnet session, and we have already three of them running. Now let's close both connections and look at the results in the process list:
 3735  ?  S    0:00 mtserv 
3753 ? Z 0:00 (mtserv <zombie>)
3760 ? Z 0:00 (mtserv <zombie>)
As expected, two zombies are wandering around. We open a new connection, and the parent process will kill off both of them, but on the other hand, a new one will be created:
 3735  ?  S    0:00 mtserv 
3800 ? S 0:00 mtserv
Now this guide is not an introduction to Unix process management, so we stop here and just note that with this type of management, there might always be some processes in zombie state, but at least we free all zombies each time we set a new child into the world.

4.2. Synchronous I/O Multiplexing

This facility is a feature of most Unices, and is not specific to sockets - it works for other types of file descriptors as well. Its heart is the Select system call, which lets the operating system wait for events to happen on one or more files. Of course, regular files are not the most interesting examples, since watching them tends to be rather boring. But as Select works for sockets as well, it will be a great help. (For more information, see the documentation about the Linux unit).
repeat
put all sockets currently connected in the read set;
put the listening socket in the read set;
Select(read set, no other sets, no timeout);
respond to all sockets that are left in the read set;
until false;
As an example, let's assume we want to write a simple chat server. All data that is received from one client is sent to all other clients that are online. To maintain the clients, we are going to use a record ClientRec that keeps all data we have about one client. More precisely, that means the socket descriptor and the two text files. On a more advanced level, you might want to store additional information such as a nickname, and so on. For simplicity, we use an array to hold the client records.
  1. If an event occured on the listening socket, accept the connection. If there is still room for the new client, store it in the clients list, otherwise send an error message and close the connection.
  2. Check all active clients for events. If one occured, test if the connection has been closed (with eof). If so, remove the client from the clients list. If not, read the input and process it.
I believe that a piece of code says more than a thousand words of prose, so here is it:
program mulserv;

{ A server that can handle multiple client connections. }

uses
sockets, inetaux, myerror, linux;

const
ListenPort : Word = $AFFE;
MaxConn = 5;
MaxClients = 5;

type
ClientRec = record
cSock : LongInt;
adstr : String;
sin, sout : Text;
end;

var
lSock, uSock : LongInt;
sAddr : TInetSockAddr;
Len, i, j : LongInt;
Line : String;
Clients : array[1..MaxClients] of ClientRec;
NumClients : LongInt;
MaxFD : LongInt;
ReadSet : FDSet;
sin, sout : Text;

begin
lSock := Socket(af_inet, sock_stream, 0);
if lSock = -1 then SockError('Socket: ');

with sAddr do
begin
Family := af_inet;
Port := htons(ListenPort);
Addr := 0;
end;

if not Bind(lSock, sAddr, sizeof(sAddr)) then SockError('Bind: ');
if not Listen(lSock, MaxConn) then SockError('Listen: ');

Say('Waiting...');

fd_zero(ReadSet);
NumClients := 0;

repeat
MaxFD := 0;
for i := 1 to NumClients do with Clients[i] do
begin
fd_set(cSock, ReadSet);
if cSock > MaxFD then MaxFD := cSock;
end;
fd_set(lSock, ReadSet);
if lSock > MaxFD then MaxFD := lSock;
Inc(MaxFD);

Select(MaxFD, @ReadSet, nil, nil, nil); { No timeout! }

{ New connections? }

if fd_isset(lSock, ReadSet) then
begin
Say('Incoming connection.');
Len := sizeof(sAddr);
uSock := Accept(lSock, sAddr, Len);
if uSock = -1 then SockSay('Accept: ')
else
begin
if NumClients < MaxClients then
begin
Inc(NumClients);
with Clients[NumClients] do
begin
cSock := uSock;
Sock2Text(cSock, sin, sout);
Reset(sin);
Rewrite(sout);
adstr := AddrToStr(sAddr.Addr);
Say('Accepted connection from ' + adstr);
end;
end
else { client limit reached }
begin
Sock2Text(uSock, sin, sout);
Rewrite(sout);
Writeln(sout, 'Sorry, we are fully booked.');
Close(sout);
Shutdown(uSock, 2);
end;
end;
end;

{ And/or an event on an existing connection? }

for i := 1 to NumClients do if fd_isset(Clients[i].cSock, ReadSet) then
begin
if eof(Clients[i].sin) then { Connection has been closed? }
begin
with Clients[i] do
begin
Close(sin);
Close(sout);
Shutdown(cSock, 2);
Say('Disconnected ' + adstr);
end;
for j := i to NumClients - 1 do Clients[j] := Clients[j + 1];
Dec(NumClients);
end
else { No -> Data can be read }
begin
Say('Received message from ' + Clients[i].adstr + '...');
Readln(Clients[i].sin, Line);
Say(Line);
for j := 1 to NumClients do if i <> j then
Writeln(Clients[j].sout, Line);
end;
end;
until False;
end.
(Note: I once expercienced a mysterious CPU hogging by this server after a client had disconnected. I don't know why, and I couldn't reproduce it reliably. If you find anything suspicious, please let me know.)

4.3. Suggestions

The concepts of serving multiple clients should be clear so far. Things to try to get used to client interaction:

5. Binary Data

In the example programs so far, we always assumed that communication through the network is like text file I/O - lines of reasonable length are exchanged with Readln and Writeln (if you are concerned about the line length, consider compiling your programs with AnsiString support). Many protocols work this way, but it also has its drawbacks. If we want to exchange raw, binary data, CR/LF sequences should not be treated in a special way, and buffers should be used to hold the data to be sent or received. For this purpose, we will use the functions Send and Recv. Both have four parameters:
  1. the socket
  2. the buffer
  3. size of the buffer
  4. flags
To illustrate the point, let's assume that we want to conduct bandwidth measurements in a network. We need a server we can tell, "send me so many bytes of random data", and it will begin pumping the bytes into the network. (Actually, a research group at Hamburg University once developed a web server that would accept URLs like http://hostname/50MB for similar purposes.) We also need a client to initiate the transmission and receive the data.
Anyway, this is the server code:
program binserv;

{ A binary data server program. }

uses
sockets, inetaux, myerror;

const
ListenPort : Word = $AFFE;
MaxConn = 1;
BufSize = 1024;

var
Buffy : array[0..BufSize - 1] of Char;

procedure FillBuffer;
var i : LongInt;
begin
for i := 0 to BufSize - 1 do Buffy[i] := Chr(Random(256));
end;

var
lSock, uSock : LongInt;
sAddr : TInetSockAddr;
Len : LongInt;
Amount : LongInt;

begin
Randomize;

lSock := Socket(af_inet, sock_stream, 0);
if lSock = -1 then SockError('Socket: ');

with sAddr do
begin
Family := af_inet;
Port := htons(ListenPort);
Addr := 0;
end;

if not Bind(lSock, sAddr, sizeof(sAddr)) then SockError('Bind: ');
if not Listen(lSock, MaxConn) then SockError('Listen: ');

repeat
Say('Waiting for connections...');
Len := sizeof(sAddr);
uSock := Accept(lSock, sAddr, Len);
if uSock = -1 then SockError('Accept: ');
Say('Accepted connection from ' + AddrToStr(sAddr.Addr));

Len := Recv(uSock, Amount, sizeof(Amount), 0);
if SocketError <> 0 then SockError('Recv: ');
if Len < sizeof(Amount) then GenError('Couldn''t receive a LongInt!');

while Amount > 0 do
begin
FillBuffer;
if Amount < BufSize then Len := Amount else Len := BufSize;
Len := Send(uSock, Buffy, Len, 0);
if Len = -1 then SockError('Send: ');
Dec(Amount, Len);
end;

Shutdown(uSock, 2);
Say('Connection closed.');
until False;
end.
Please note that it is not guaranteed that Send will send all the data we want to have sent, although it is very likely. If not, we don't resend (as the bytes are random anyway), and just take the actual amount of bytes sent (as returned by Send) into consideration.
The client works similar:
program binclient;

{ Simple client for binary data }

uses
sockets, inetaux, myerror;

const
RemoteAddress = '127.0.0.1';
RemotePort : Word = $AFFE;
OutFileName = 'received.bin';
const_Amount = 10005;
BufSize = 2048;

var
Sock : LongInt;
sAddr : TInetSockAddr;
Buffy : array[0..BufSize - 1] of Char;
fout : File of Char;
Amount, Len : LongInt;
Closed : Boolean;
i : LongInt;

begin
Sock := Socket(af_inet, sock_stream, 0);
if Sock = -1 then SockError('Socket: ');

with sAddr do
begin
Family := af_inet;
Port := htons(RemotePort);
Addr := StrToAddr(RemoteAddress);
end;

if not Connect(Sock, sAddr, sizeof(sAddr)) then SockError('Connect: ');
Writeln('Connected.');

Amount := const_Amount;
Len := Send(Sock, Amount, sizeof(Amount), 0);
if Len < sizeof(Amount) then GenError('Couldn''t send a LongInt.');

Assign(fout, OutFileName);
{$I-}
Rewrite(fout);
if IoResult <> 0 then GenError('Couldn''t open file ' + OutFileName);
{$I+}

Closed := False;
while not Closed do
begin
Len := Recv(Sock, Buffy, sizeof(Buffy), 0);
if SocketError <> 0 then
begin
Close(fout);
SockError('Recv: ');
end;
if Len = 0 then Closed := True
else for i := 0 to Len - 1 do Write(fout, Buffy[i]);
end;

Close(fout);
Shutdown(Sock, 2);
end.
Of course, Send/Recv and the Text file functions can be combined. This might be useful in the case of HTTP, which on the one hand works with text-oriented headers, on the other hand is able to send binary files (e.g. pictures). I have not checked the HTTP specification, so I don't know how this is handled. Another thing worth mentioning is the procedure Sock2File which turns the socket descriptor into two untyped files (instead of Text files). This allows for record-oriented file I/O, since you can call Reset and Rewrite with the record size parameter.

Suggestions:

Unstructured data flow has a number of advantages over line-oriented communication. It seems wise to become familiar with it, although there are many situations where it only adds management overhead.
  • In the earlier sections, we developed a file server that could be used to transfer files from one host to the other. However, the transmission technique that we used - line oriented - was suboptimal, as it would produce a tremendous overhead for files with many empty lines, and would scale very badly for files with very long lines.
    It is obvious that it would be a major improvement for the program to change the transmission technique to byte-oriented. On the other hand, aren't there some advantages to line-oriented transmission (hint: think about heterogenous networks with communication between Linux and Windows hosts)?
  • The SMTP protocol is completely line-oriented, regarding the client-to-server commands as well as the transmitted emails themselves. QMTP however, devised by qmail author Dan Bernstein as an SMTP replacement, uses so-called "netstrings", which are not unlike classic Pascal strings, to simplify the protocol and improve throughput.
    Write a simple message exchange service using netstrings. Using AnsiStrings will be a great help. And don't forget the no. 1 optimization rule for AnsiStrings: If you know the final length beforehand, set it with SetLength.

6. Obtaining the Software

Throughout this document, I have quoted quite a lot of program sources, and I have also mentioned several units. All these files are available in a ZIP archive accessible via the link below:
Michaël Van Canneyt's inet package is available from the following address:

7. Final Remarks

As an overview over the subject of sockets programming, this tutorial is far from being complete, and it is still evolving. Topics that might be added in the future include: Connectionless transmission, FPC's overloaded Accept and Connect functions, non-blocking sockets, various fine-tunings (e.g. with the Send and Recv flags) etc. Several problems exist with Windows, and I would like to address them more specifically - but before I can do that, I have to have a closer look at them.

Sebastian Koppehel
Last modified: Thu Apr 27 13:26:57 CEST 2000


Yahoo! oneSearch: Finally, mobile search that gives answers, not web links.

0 Comments: