Watch, Follow, &
Connect with Us

For forums, blogs and more please visit our
Developer Tools Community.


Welcome, Guest
Guest Settings
Help

Thread: TCP/IP Help Required


This question is answered. Helpful answers available: 2. Correct answers available: 1.


Permlink Replies: 3 - Last Post: Nov 13, 2014 7:45 PM Last Post By: Remy Lebeau (Te...
Glen Adamson

Posts: 7
Registered: 10/30/03
TCP/IP Help Required  
Click to report abuse...   Click to reply to this thread Reply
  Posted: Nov 13, 2014 4:15 PM
Hi All,

My task is to write an Autopilot (AP) module for a sea Navigation (NAV) program.
The NAV system connects with the AP using TCP/IP and the data that is passed (a “Message” ) is of the comma separated AnsiString form and is sent every second.

The AP is ready to bench test and connect to the NAV system.
I have not been involved with TCP connections before so I started with the TTcpClient – TTcpServer demo which worked very well. I modified this code to send a sample message every second using TcpClient.SendLn(MSG, #$D#$A) and ClientSocket.ReceiveLn(#$D#$A) at the server end;

The modified code does not work and I cannot work out why. Clearly a robust TCP must be achieved. I thing the server may be the problem but I have included both my client and server code.

I hope you may be able to help get me on the right TCP track. The OS is Win 7 with Delphi XE2.

Thank you for your interest and help.

Regards
Glen

CLIENT: Form has Start and Stop buttons plus a Log memo.
 
unit d_Client;
interface
uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Web.Win.Sockets, Vcl.StdCtrls,
  Vcl.ExtCtrls;
const
  DEFAULT_HOST = 'localhost';
  DEFAULT_PORT = '2948';
type
  TClient = class(TForm)
    StartBU: TButton;
    LogME: TMemo;
    StopBU: TButton;
    procedure FormCreate         (Sender: TObject);
    procedure StartBUClick       (Sender: TObject);
    procedure StopBUClick        (Sender: TObject);
    procedure TcpClientCreateHnd (Sender: TObject);
    procedure TcpClientDestroyHnd(Sender: TObject);
    procedure TimerTimer         (Sender: TObject);
    procedure FormDestroy        (Sender: TObject);
    procedure TcpClientSend      (Sender: TObject;
                                 Buf: PAnsiChar;
                                 var DataLen: Integer);
  private
    TcpClient: TTcpClient;
    Timer: TTimer;
    SendNum: integer;
    procedure SendMsg            (Msg: AnsiString);
  public
    end;
var
  Client: TClient;
 
implementation
{$R *.dfm}
const
  THE_MSG = ': $ECRMB,A,0.001,L,,001,2810.167,S,15333.666,E,0.195,341.694,7.100,V*36';
 
procedure TClient.FormCreate(Sender: TObject);
begin
  LogME.Lines.Clear;
  LogME.Lines.Add('Host: ' + DEFAULT_HOST);
  LogME.Lines.Add('Port: ' + DEFAULT_PORT);
  {tcp client}
  TcpClient := TTcpClient.Create(self);
  TcpClient.BlockMode       := bmBlocking;
  TcpClient.LocalHost       := DEFAULT_HOST;
  TcpClient.LocalPort       := DEFAULT_PORT;
  TcpClient.OnCreateHandle  := TcpClientCreateHnd;
  TcpClient.OnDestroyHandle := TcpClientDestroyHnd;
  TcpClient.OnSend          := TcpClientSend;
  TcpClient.Active          := true;
  {time}
  Timer := TTimer.Create(self);
  Timer.Enabled  := false;
  Timer.Interval := 1000;
  Timer.OnTimer  := TimerTimer;
  end;
 
procedure TClient.FormDestroy(Sender: TObject);
begin
  Timer.Free;
  TcpClient.Free;
end;
 
procedure TClient.StartBUClick(Sender: TObject);
begin
  Timer.Enabled := true;
end;
 
procedure TClient.StopBUClick(Sender: TObject);
begin
  Timer.Enabled := false;
end;
 
procedure TClient.TimerTimer(Sender: TObject);
begin
  SendMsg(THE_MSG);
end;
 
procedure TClient.SendMsg(Msg: AnsiString);
begin
  TcpClient.Active := true;      //will echo ready
  TcpClient.SendLn(MSG, #$D#$A);
  TcpClient.Active := false;     //will echo sent
end;
 
procedure TClient.TcpClientSend (Sender: TObject;
                                Buf: PAnsiChar;
                                var DataLen: Integer);
begin
  // Buf has CRLF appended. Echo THE_MSG instead
  LogME.Lines.Add(DateTimeToStr(now) + THE_MSG);
end;
 
procedure TClient.TcpClientCreateHnd(Sender: TObject);
begin
  // Occurs when the handle to a socket becomes active
  LogME.Lines.Add(DateTimeToStr(now) + ': ready');
end;
 
procedure TClient.TcpClientDestroyHnd(Sender: TObject);
begin
  //Occurs when a socket is deactivated
  LogME.Lines.Add(DateTimeToStr(now) + ': sent');
  LogME.Lines.Add('');
end;
 
end.


SERVER: Form has Connect an Disconnect buttons plus a Log memo.
unit d_Server;
interface
uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Web.Win.Sockets;
const
  DEFAULT_HOST = 'localhost';
  DEFAULT_PORT = '2948';
type
  TServer = class(TForm)
    ConnectBU: TButton;
    DisconnectBU: TButton;
    LogME: TMemo;
    procedure FormCreate         (Sender: TObject);
    procedure FormDestroy        (Sender: TObject);
    procedure ConnectBUClick     (Sender: TObject);
    procedure DisconnectBUClick  (Sender: TObject);
    procedure TcpServerCreateHnd (Sender: TObject);
    procedure TcpServerDestroyHnd(Sender: TObject);
    procedure TcpServerListening (Sender: TObject);
    procedure TcpServerAccept    (Sender: TObject;
                                 ClientSocket: TCustomIpClient);
  private
    TcpServer: TTcpServer;
  public
    end;
var
  Server: TServer;
 
implementation
{$R *.dfm}
 
procedure TServer.FormCreate(Sender: TObject);
begin
  LogME.Clear;
  LogME.Lines.Add('Host: ' + DEFAULT_HOST);
  LogME.Lines.Add('Port: ' + DEFAULT_PORT);
  {tcp server}
  TcpServer := TTcpServer.Create(Self);
  TcpServer.BlockMode       := bmThreadBlocking;
  TcpServer.LocalHost       := DEFAULT_HOST;
  TcpServer.LocalPort       := DEFAULT_PORT;
  TcpServer.OnAccept        := TCpServerAccept;
  TcpServer.OnCreateHandle  := TcpServerCreateHnd;
  TcpServer.OnDestroyHandle := TcpServerDestroyHnd;
  TcpServer.OnListening     := TcpServerListening;
end;
 
procedure TServer.FormDestroy(Sender: TObject);
begin
  TcpServer.Free;
end;
 
procedure TServer.ConnectBUClick(Sender: TObject);
begin
  //At runtime, use the Open or Close method to open or close the connection
  LogME.Lines.Add(DateTimeToStr(now) + ' About to Connect');
  TcpServer.Open;
end;
 
procedure TServer.TcpServerCreateHnd(Sender: TObject);
begin
  //Occurs when the handle to a socket becomes active
  LogME.Lines.Add(DateTimeToStr(now) + ' Server started ');
end;
 
procedure TServer.TcpServerAccept (Sender: TObject;
                                  ClientSocket: TCustomIpClient);
var
  s: AnsiString;
begin
  //Occurs on server sockets just after the connection to a client socket is accepted.
  //Receives the message from the client
  s := ClientSocket.Receiveln(#$D#$A);
  LogME.Lines.Add(s);
end;
 
procedure TServer.TcpServerListening(Sender: TObject);
begin
  //Occurs when a server socket begins listening for connections
  LogME.Lines.Add(DateTimeToStr(now) + ' About to Start Listning');
end;
 
procedure TServer.DisconnectBUClick(Sender: TObject);
begin
  //At runtime, use the Open or Close method to open or close the connection
  LogME.Lines.Add(DateTimeToStr(now) + ' About to Disconnect');
  TcpServer.Close;
end;
 
procedure TServer.TcpServerDestroyHnd(Sender: TObject);
begin
  //Occurs when a socket is deactivated
  if Self.LogME <> nil then
    LogME.Lines.Add(DateTimeToStr(now) + ' Server stopped ');
end;
 
end.
Remy Lebeau (Te...


Posts: 9,447
Registered: 12/23/01
Re: TCP/IP Help Required  
Click to report abuse...   Click to reply to this thread Reply
  Posted: Nov 13, 2014 5:19 PM   in response to: Glen Adamson in response to: Glen Adamson
Glen wrote:

I have not been involved with TCP connections before so I started with
the TTcpClient – TTcpServer demo which worked very well.

Consider yourself lucky then, because the TCP components of the Web.Win.Sockets
unit are terribly written, dating back to Delphi 6's (unsuccessful) attempt
at supporting cross-platform development in Kylix. I would strongly advise
you to use the VCLs native TClientSocket/TServerSocket components in the
System.Win.ScktComp unit, or Indy's TIdTCPClient/TIdTCPServer components,
or any number of third-party TCP components (ICS, Synapse, etc).

I modified this code to send a sample message every second using
TcpClient.SendLn(MSG, #$D#$A)

SendLn() appends a CRLF by default, so you don't need to specify one yourself:

TcpClient.SendLn(MSG);


However, aside from that, the implementation of SendLn() is problematic,
as it does not guarantee that the entire message is sent. It returns the
number of bytes actually sent, which can be fewer bytes than you attempted
to send.

and ClientSocket.ReceiveLn(#$D#$A) at the server end;

Same thing with ReceiveLn() about CRLF being the default line break:

ClientSocket.ReceiveLn()


However, aside from that, the implementation of SendLn() is problematic,
because internally it reads the socket in fixed chunks and appends those
chunks to the Result, and if a CRLF happens to straddle across two chunks
then ReceiveLn() will never see it and keep reading indefinately until the
socket is closed or errors.

The modified code does not work and I cannot work out why. Clearly
a robust TCP must be achieved.

Then use robust components to begin with ;)

I thing the server may be the problem but I have included both my client
and server code.

On the client side:

You are opening and closing the socket on every message begin sent. That
is terribly inefficient. Open the socket (with error handling) and leave
it open, thus ensuring you have a connection to the AP, then start your timer
and send each message over a single TCP connection. If you happen to lose
the connection to the AP, stop the timer, close the socket, reopen it (with
error handling), and restart the timer.

The OnSend event reports the data you give to SendLn() before it is actually
sent, so it does not reflect the data being transmitted. If SendLn() happens
to send fewer bytes than requested, you are not handling that condition,
but if you were, your log would start looking a little odd with all the partial
sends being done. You should move that logging into your SendMsg() function
instead before you give it to SendLn().

To adequately handle the case of partial sends, you should use SendBuf()
directly instead of SendLn(). Create your message, with a CRLF at the end,
then call SendBuf() in a loop until all bytes have been sent (or an error
occurs).

On the server side:

OnAccept has a quirky implementation. When it is fired, a client has connected.
When OnAccept exits, the client is disconnected. That means that you have
to manage your entire TCP session inside the OnAccept event. At a minimum,
call ReceiveLn() in a loop until the client disconnects. But like I said,
ReceiveLn() has some logic bugs in its implementation, so you are better
off using ReceiveBuf() directly instead, reading all inbound data into your
own buffer that you search for CRLF as needed.

You are using the TTcpServer in thread-blocking mode, which means the OnAccept
event is triggered in a worker thread, not the main UI thread. So you cannot
safely access UI controls, like your Memo log, unless you synchronize with
the main thread, such as with TThread.Synchronize().

Now, with alll of that said, here is your demo re-written to use Indy's components
(which handle all these kinds of details for you):

CLIENT:

 
unit d_Client;
 
interface
 
uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, 
Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, IdTCPClient;
 
const
  DEFAULT_HOST = 'localhost';
  DEFAULT_PORT = 2948;
 
type
  TClient = class(TForm)
    StartBU: TButton;
    LogME: TMemo;
    StopBU: TButton;
    procedure FormCreate (Sender: TObject);
    procedure StartBUClick (Sender: TObject);
    procedure StopBUClick (Sender: TObject);
    procedure TimerTimer (Sender: TObject);
    procedure FormDestroy (Sender: TObject);
  private
    TcpClient: TIdTCPClient;
    Timer: TTimer;
    SendNum: integer;
    procedure LogMsg(Msg: String);
    procedure SendMsg (Msg: String);
  public
  end;
 
var
  Client: TClient;
 
implementation
 
{$R *.dfm}
 
const
  THE_MSG = ': $ECRMB,A,0.001,L,,001,2810.167,S,15333.666,E,0.195,341.694,7.100,V*36';
 
procedure TClient.FormCreate(Sender: TObject);
begin
  LogME.Lines.Clear;
  LogMsg('Host: ' + DEFAULT_HOST);
  LogMsg('Port: ' + IntToStr(DEFAULT_PORT));
  {tcp client}
  TcpClient := TIdTCPClient.Create(self);
  TcpClient.Host := DEFAULT_HOST;
  TcpClient.Port := DEFAULT_PORT;
  {time}
  Timer := TTimer.Create(self);
  Timer.Enabled := false;
  Timer.Interval := 1000;
  Timer.OnTimer := TimerTimer;
end;
 
procedure TClient.FormDestroy(Sender: TObject);
begin
  Timer.Free;
  TcpClient.Free;
end;
 
procedure TClient.LogMsg(Msg: String);
begin
  LogME.Lines.Add(DateTimeToStr(now) + ': ' + Msg);
end;
 
procedure TClient.StartBUClick(Sender: TObject);
begin
  TcpClient.Connect;
  Timer.Enabled := true;
  LogMsg('connected');
end;
 
procedure TClient.StopBUClick(Sender: TObject);
begin
  Timer.Enabled := false;
  TcpClient.Disconnect;
  LogMsg('disconnected');
end;
 
procedure TClient.TimerTimer(Sender: TObject);
begin
  SendMsg(THE_MSG);
end;
 
procedure TClient.SendMsg(Msg: String);
begin
  LogMsg('THE_MSG);
  try
    TcpClient.IOHandler.WriteLn(Msg);
  except
    StopBU.Click;
    raise;
  end;
end;
 
end.

SERVER:

unit d_Server;
 
interface
 
uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, 
Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, IdTCPServer, IdContext;
 
const
  DEFAULT_HOST = 'localhost';
  DEFAULT_PORT = 2948;
 
type
  TServer = class(TForm)
    ConnectBU: TButton;
    DisconnectBU: TButton;
    LogME: TMemo;
    procedure FormCreate (Sender: TObject);
    procedure FormDestroy (Sender: TObject);
    procedure ConnectBUClick (Sender: TObject);
    procedure DisconnectBUClick (Sender: TObject);
    procedure TcpServerConnect (AContext: TIdContext);
    procedure TcpServerExecute (AContext: TIdContext);
    procedure TcpServerDisconnect (AContext: TIdContext);
  private
    TcpServer: TIdTCPServer;
    procedure LogMsg(Msg: String);
  public
  end;
 
var
  Server: TServer;
 
implementation
 
uses
  IdSync;
 
{$R *.dfm}
 
type
  TLogNotify = class(TIdNotify)
  procedure
    fMsg: string
    procedure DoNotify; override;
  public
    constructor Create(Msg: String);
  end;
 
constructor TLogNotify.Create(Msg: String);
begin
  inherited Create;
  fMsg := Msg;
end;
 
procedure TLogNotify.DoNotify;
begin
  if (Server <> nil) and (Server.LogME <> nil) then
    Server.LogME.Lines.Add(DateTimeToStr(now) + ': ' + fMsg);
end;
 
procedure TServer.FormCreate(Sender: TObject);
begin
  LogME.Clear;
  LogMsg('Host: ' + DEFAULT_HOST);
  LogMsg('Port: ' + IntToStr(DEFAULT_PORT));
  {tcp server}
  TcpServer := TIdTCPServer.Create(Self);
  TcpServer.Bindings.Add.SetBinding(DEFAULT_HOST, DEFAULT_PORT);
  TcpServer.OnConnect := TcpServerConnect;
  TcpServer.OnDisconnect := TcpServerDisconnect;
  TcpServer.OnExecute := TcpServerExecute;
end;
 
procedure TServer.FormDestroy(Sender: TObject);
begin
  TcpServer.Free;
end;
 
procedure TServer.LogMsg(Msg: String);
begin
  TLogNotify.Create(Msg).Notify;
end;
 
procedure TServer.ConnectBUClick(Sender: TObject);
begin
  //At runtime, use the Open or Close method to open or close the connection
  LogMsg('About to start Listening');
  TcpServer.Active := true;
  LogMsg('Server started');
end;
 
procedure TServer.TcpServerConnect (AContext: TIdContext);
begin
  LogMsg('client connected');
end;
 
procedure TServer.TcpServerDisconnnect (AContext: TIdContext);
begin
  LogMsg('client disconnected');
end;
 
procedure TServer.TcpServerExecute (AContext: TIdContext);
var
  s: String;
begin
  s := AContext.Connection.IOHandler.ReadLn;
  LogMsg(s);
end;
 
procedure TServer.DisconnectBUClick(Sender: TObject);
begin
  LogMsg('About to stop listening');
  TcpServer.Active := false;
  LogMsg('Server stopped');
end;
 
end.


--
Remy Lebeau (TeamB)
Glen Adamson

Posts: 7
Registered: 10/30/03
Re: TCP/IP Help Required  
Click to report abuse...   Click to reply to this thread Reply
  Posted: Nov 13, 2014 7:22 PM   in response to: Remy Lebeau (Te... in response to: Remy Lebeau (Te...
Thank you Remy for your amazingly quick reply. A better rely could not be had.

Yes I consider myself lucky not to be involved with TCP before now!

Disappointing to hear the history behind TCP, I will stay with Indy TCP, however it is unfortunate that the only documentation I found was pre version 10 and no demos would compiled, no info to fix the bugs.

The code you wrote compiled and worked with the correction of 2 typos and changing localhost to 127.0.0.1. Impressive!

The issue of validation and the preemptive handling of exceptions needs to be research, can you suggest reference material that covers the subject?
Or perhaps application source that is worthy of study?
I suspect it will be thin to non existent.

Remy thank you very much for your help.

Regards
Glen
Remy Lebeau (Te...


Posts: 9,447
Registered: 12/23/01
Re: TCP/IP Help Required  
Click to report abuse...   Click to reply to this thread Reply
  Posted: Nov 13, 2014 7:45 PM   in response to: Glen Adamson in response to: Glen Adamson
Hello Glen,

Thank you Remy for your amazingly quick reply. A better rely could not
be had.

Yes I consider myself lucky not to be involved with TCP before now!

Disappointing to hear the history behind TCP, I will stay with Indy
TCP, however it is unfortunate that the only documentation I found was
pre version 10 and no demos would compiled, no info to fix the bugs.

There is v10 documentation on Indy's website, albeit it is for an older release
of v10. But much of it is still relevant.

The code you wrote compiled and worked with the correction of 2 typos
and changing localhost to 127.0.0.1. Impressive!

Indy recognizes 'localhost', so you should not have had to change that.
Well, client-side anyway, you cannot bind a server to 'localhost'. My fault
for missing that.

--
Remy Lebeau (TeamB)
Legend
Helpful Answer (5 pts)
Correct Answer (10 pts)

Server Response from: ETNAJIVE02