Search code examples
delphimobileindydelphi-xe8

Using Indy TCPClient/TCPServer to send picture from mobile XE8


I have a simple mobile application written in Delphi XE8 that allows the user to take a picture and then send the picture to a server using Indy TCPClient/TCP Server.

I have scoured the forums and found numerous examples to send the data in a variety of ways. Every method I try results in an access violation or corrupt data on the server side.

My ultimate goal is to send a record containing a unique identifier, description and a picture(bitmap) from the client to the server. But I'm starting out be trying to simply send a record with some text from a windows client to the server. I will then try to implement the solution into my mobile app.

type
  TSendRec = record
//    SONo: string;
    Text: string;
//    Bitmap: TBitMap;
  end

I have tried the following 3 methods as per the code below:

  1. Send Using a Stream

  2. Send using RawToBytes and TIDBytes.

  3. Send a line of text using Writeln and Readln

When I try to send using a stream I get the following access violation:

Project memorystream_server.exe raised the exception class $C0000005 with message 'access violation at 0x00409e46: write of address 0x0065d1bc

The error occurs when I try to access the value of MiRec.Text on the server side.

Memo1.Lines.Add(MiRec.Text);

So I assume the read of the MIRec is failing for some reason:

When I send using RawToBytes, no error message occurs but the value of MIRec.Text is garbage.

When I just send a line of text using WriteLn, the server receives and displays the data correctly and no access violation occurs.

I tried to follow examples that I have found from other posts on this issue. I would greatly appreciate any insight into what I am doing wrong.

Following are my client and server side code snippets:

Client

procedure TfrmMemoryStreamClient.btnSendClick2(Sender: TObject);
var
  Buffer: TIdBytes;
  MIRec: TSendRec;
  msRecInfo: TMemoryStream;
  msRecInfo2: TIdMemoryBufferStream;
begin
  IdTCPClient1.Connect;

  MIRec.Text := 'Hello World';

  if rbSendStream.Checked then
  begin
    msRecInfo := TMemoryStream.Create;
    try
      msRecInfo.Write(MIRec, SizeOf(MIRec));
      IdTCPClient1.IOHandler.Write(msRecInfo, 0, False);
    finally
      msRecInfo.Free;
    end;
{
    msRecInfo2 := TIdMemoryBufferStream.Create(@MIRec, SizeOf(TSendRec));
    try
      IdTCPClient1.IOHandler.Write(msRecInfo2);
    finally
      msRecInfo.Free;
    end;
}
  end
  else
  if rbSendBytes.Checked then
  begin
    Buffer := RawToBytes(MIRec, SizeOf(MIRec));
    IdTCPClient1.IOHandler.Write(Buffer);
  end
  else
  if rbWriteLn.Checked then
  begin
    IdTCPClient1.Socket.WriteLn(Edit1.Text);
  end;

  IdTCPClient1.DisConnect;
end;

Server

procedure TStreamServerForm.IdTCPServer1Execute(AContext: TIdContext);
var sName: String;
  MIRec: TSendRec;
  Buffer: TIdBytes;
  msRecInfo: TMemoryStream;
begin

  if not chkReceiveText.Checked then
  begin
    try
      if chkReadBytes.Checked then
      begin
        AContext.Connection.IOHandler.ReadBytes(Buffer, SizeOf(MIRec));
        BytesToRaw(Buffer, MIRec, SizeOf(MIRec));
        Memo1.Lines.Add(MiRec.Text);
      end
      else
      begin
        msRecInfo := TMemoryStream.Create;

        try
          // does not read the stream size, just the stream data
          AContext.Connection.IOHandler.ReadStream(msRecInfo, SizeOf(MIRec), False);

          msRecInfo.Position := 0;
          msRecInfo.Read(MIRec, SizeOf(MIRec));
          Memo1.Lines.Add(MiRec.Text);
        finally
          msRecInfo.Free;
        end;
{
        AContext.Connection.IOHandler.ReadStream(msRecInfo, -1, False);
        msRecInfo.Position := 0;
        msRecInfo.Read(MIRec, SizeOf(MIRec));
        Memo1.Lines.Add(MiRec.Text);
}
      end;

      Memo1.Lines.Add('read File');
    except
      Memo1.Lines.Add('error in read File');
    end;
  end
  else
  begin
    sName := AContext.Connection.Socket.ReadLn;
    Memo1.Lines.Add(sName);
  end;

  AContext.Connection.Disconnect;
end;

Solution

  • TIdTCPServer is a multithreaded component. Its OnConnect, OnDisconnect, and OnExecute events are triggered in the context of a worker thread. As such, you MUST synchronize with the main UI thread when accessing UI controls, like your Memo.

    Also, String is a compiler-managed data type, and TBitmap is an object. Both store their data elsewhere in memory, so you cannot write a record containing such fields as-is. You would be writing only the value of their data pointers, not writing the actual data being pointed at. You need to serialize your record into a transmittable format on the sending side, and then deserialize it on the receiving side. That means handling the record fields individually.

    Try something more like this:

    type
      TSendRec = record
        SONo: string;
        Text: string;
        Bitmap: TBitMap;
      end;
    

    Client

    procedure TfrmMemoryStreamClient.btnSendClick2(Sender: TObject);
    var
      MIRec: TSendRec;
      ms: TMemoryStream;
    begin
      MIRec.SONo := ...;
      MIRec.Text := 'Hello World';
      MIRec.Bitmap := TBitmap.Create;
      ...
      try
        IdTCPClient1.Connect;
        try
          IdTCPClient1.IOHandler.WriteLn(MIRec.SONo);
          IdTCPClient1.IOHandler.WriteLn(MIRec.Text);
          ms := TMemoryStream.Create;
          try
            MIRec.Bitmap.SaveToStream(ms);
            IdTCPClient1.IOHandler.LargeStream := True;
            IdTCPClient1.IOHandler.Write(ms, 0, True);
          finally
            ms.Free;
          end;
        finally
          IdTCPClient1.Disconnect;
        end;
      finally
        MIRec.Bitmap.Free;
      end;
    end;
    

    Server

    procedure TStreamServerForm.IdTCPServer1Execute(AContext: TIdContext);
    var
      MIRec: TSendRec;
      ms: TMemoryStream;
    begin
      MIRec.SONo := AContext.Connection.IOHandler.ReadLn;
      MIRec.Text := AContext.Connection.IOHandler.ReadLn;
      MIRec.Bitmap := TBitmap.Create;
      try
        ms := TMemoryStream.Create;
        try
          AContext.Connection.IOHandler.LargeStream := True;
          AContext.Connection.IOHandler.ReadStream(ms, -1, False);
          ms.Position := 0;
          MIRec.Bitmap.LoadFromStream(ms);
        finally
          ms.Free;
        end;
        TThread.Synchronize(nil,
          procedure
          begin
            Memo1.Lines.Add(MIRec.SONo);
            Memo1.Lines.Add(MIRec.Text);
            // display MIRec.Bitmap as needed...
          end;
        end;
      finally
        MIRec.Bitmap.Free;
      end;
    end;
    

    Alternatively:

    Client

    procedure TfrmMemoryStreamClient.btnSendClick2(Sender: TObject);
    var
      MIRec: TSendRec;
      ms: TMemoryStream;
    
      procedure SendString(const S: String);
      var
        Buf: TIdBytes;
      begin
        Buf := IndyTextEncoding_UTF8.GetBytes(S);
        IdTCPClient1.IOHandler.Write(Int32(Length(Buf)));
        IdTCPClient1.IOHandler.Write(Buf);
      end;
    
    begin
      MIRec.SONo := ...;
      MIRec.Text := 'Hello World';
      MIRec.Bitmap := TBitmap.Create;
      ...
      try
        IdTCPClient1.Connect;
        try
          SendString(MIRec.SONo);
          SendString(MIRec.Text);
          ms := TMemoryStream.Create;
          try
            MIRec.Bitmap.SaveToStream(ms);
            IdTCPClient1.IOHandler.LargeStream := True;
            IdTCPClient1.IOHandler.Write(ms, 0, True);
          finally
            ms.Free;
          end;
        finally
          IdTCPClient1.Disconnect;
        end;
      finally
        MIRec.Bitmap.Free;
      end;
    end;
    

    Server

    procedure TStreamServerForm.IdTCPServer1Execute(AContext: TIdContext);
    var
      MIRec: TSendRec;
      ms: TMemoryStream;
    
      function RecvString: String;
      begin
        Result := AContext.Connection.IOHandler.ReadString(
          AContext.Connection.IOHandler.ReadInt32,
          IndyTextEncoding_UTF8);
      end;
    
    begin
      MIRec.SONo := RecvString;
      MIRec.Text := RecvString;
      MIRec.Bitmap := TBitmap.Create;
      try
        ms := TMemoryStream.Create;
        try
          AContext.Connection.IOHandler.ReadStream(ms, -1, False);
          ms.Position := 0;
          MIRec.Bitmap.LoadFromStream(ms);
        finally
          ms.Free;
        end;
        TThread.Synchronize(nil,
          procedure
          begin
            Memo1.Lines.Add(MIRec.SONo);
            Memo1.Lines.Add(MIRec.Text);
            // display MIRec.Bitmap as needed...
          end;
        end;
      finally
        MIRec.Bitmap.Free;
      end;
    end;