Wątek przeniesiony 2018-03-15 22:08 z Newbie przez furious programming.

Niezawodna technologia połączenia dwóch komputerów poprzez sieć

1

Witam,

słuchajcie, piszę aplikację do weryfikatora cen dla sklepu.
Program prosty i przyjemny do napisania.
Jest jednak pewien szkopuł i chciałbym poznać Waszą opinię. Jako że serwer na którym są dane o towarach korzysta z bazy - uwaga: MS Access, to nie chciałbym się z nią łączyć w sposób tradycyjny (bezpośrednio z plikiem czy też przez odbc). Możecie zapytać dlaczego? A no dlatego że połączenie bezpośrednie blokuje bazę przed optymalizacją (a trzeba to robić dość często ze względu na ograniczoną wielkość pliku). Połączenie przez odbc też ma wadę (dotyczy to też połączenia bezpośredniego) a jest nią jakiś sposób odwołania się do pliku na serwerze, czy to przez mapowanie folderu czy po prostu przez wskazanie ścieżki. To nie są technologie 21 wieku.
W tym momencie większość z Was napisze - olej MS Access. Oj mówię Wam, bardzo chętnie ale jeszcze nie teraz, zmiana systemu trochę jeszcze potrwa a weryfikator potrzebny już teraz.

I co z tym zrobić. No oczywiście użyć którejś z technologii klient-serwer. Napiszę sobie prosty program-serwer, który na fizycznym serwerze będzie się łączył z bazą (tylko w momencie gdy przyjdzie pytanie z programu-klient) i odsyłał potrzebne dane.
Znam kilka technologii takich połączeń: Indy, Sockety, webservice, rest, soap i spokojnie mogę którejś z nich użyć ale .. i tu jest pytanie do Was: której?

Nigdy się za bardzo nie zastanawiałem która z nich jest naprawdę niezawodna (w zasadzie nie zauważyłem zbyt dużych problemów w aplikacjach wykorzystujących różne technologie) ale jednak chciałbym ten program napisać, uruchomić i zapomnieć o nim.

Możecie napisać jakieś swoje przemyślenia na ten temat?

Uwaga: w ramach podziękowania, udostępnię program publicznie z kodem źródłowym do pełnego wykorzystania. Program przeznaczony jest do współpracy z bazą KC-Firmy.

Pozdrawiam i z góry dziękuję za poświęcony czas
Robert

0

REST

  • serwer i klienta zrobisz w dowolnym języku
  • możesz wywołać ręcznie (Swagger, Postman) - do testów albo w razie pilnej potrzeby
  • łatwo testowalne
  • łatwo zmienić implementację serwera na dowolnie inną
  • można wersjonować
  • chodzi po HTTP - łatwo monitorować (Live HTTP Headers, proxy), szyfrować (HTTPS)
  • koncepcyjnie jest związane z zasobami bazodanowymi
  • nie wymaga trzymania stanu sesji na serwerze między wywołaniami
0

Jak delphi, to mORMot - za free i szybki.

3

Słowo się rzekło. Minęło pół roku od czasu gdy napisałem bardzo prosty program do sprawdzania cen produktów w sklepie i teraz przyszła pora na udostępnienie kodu źródłowego.

Jak pisałem, szukałem prostej i niezawodnej metody połączenia dwóch aplikacji. Najważniejszym warunkiem była stabilność bo inne czynniki przy tak prostej aplikacji nie są zbyt ważne.

Zastanawiałem się długo nad technologią, robiłem też testy i doszedłem do wniosku popartym kilkumiesięczną pracą aplikacji że użyję kontrolek z pakietu indy czyli TidUDPServer oraz TIdUDPClient.

No dobrze, przejdźmy do rozwiązania. Program oczywiście składa się z dwóch elementów, czyli serwer oraz klient (klienci).

Na początek kod źródłowy programu serwer (całość znajduje się w jednym unicie):

unit Unit1;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, IdComponent, IdUDPBase, IdUDPServer, DB, ADODB, IdSocketHandle, IdGlobal, StdCtrls,
  IdUDPClient, SMCVersInfo, ExtCtrls, IniFiles, IdBaseComponent, ImgList, AppEvnts, RXClock, ShellAPI;

type
  Tmain = class(TForm)
    con1: TADOConnection;
    qrytowary: TADOQuery;
    DStowary: TDataSource;
    Serwer: TIdUDPServer;
    Klient: TIdUDPClient;
    mmo1: TMemo;
    grp1: TGroupBox;
    smvrsnf1: TSMVersionInfo;
    tryicon1: TTrayIcon;
    il1: TImageList;
    ApplicationEvents1: TApplicationEvents;
    rxclck1: TRxClock;
    procedure SerwerUDPRead(AThread: TIdUDPListenerThread; AData: TBytes; ABinding: TIdSocketHandle);
    procedure FormCreate(Sender: TObject);
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
    procedure tryicon1DblClick(Sender: TObject);
    procedure ApplicationEvents1Minimize(Sender: TObject);
    procedure FormHide(Sender: TObject);
    procedure rxclck1Alarm(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  main: Tmain;
  log: TextFile;
  logowanie: Boolean;
  INI: TIniFile;

implementation

{$R *.dfm}

function zapisLog(komunikat: string): Boolean;
begin
  if not DirectoryExists(ExtractFilePath(Application.ExeName) + 'log') then   // sprawdzenie czy folder istnieje
    MkDir(ExtractFilePath(Application.ExeName) + 'log');   // jeśli nie - tworzymy

  AssignFile(log, ExtractFilePath(Application.ExeName) + 'log\log.txt');
  if not FileExists(ExtractFilePath(Application.ExeName) + 'log\log.txt') then // sprawdzenie czy plik istnieje
  begin
    ReWrite(log); // jeżeli nie - stwórz
    Writeln(log, DateTimeToStr(Now) + #09 + komunikat);
  end
  else
  begin
    Append(log); // jeżeli tak - otwórz
    Writeln(log, DateTimeToStr(Now) + #09 + komunikat);
  end;
  CloseFile(log);
  Result := True;
end;

procedure Tmain.ApplicationEvents1Minimize(Sender: TObject);
begin
  Hide();
  WindowState := wsMinimized;
  tryicon1.Visible := True;
  tryicon1.Animate := True;
  tryicon1.ShowBalloonHint;
  if logowanie then
    zapisLog('Zminimalizowano aplikację do zasobnika');
end;

procedure Tmain.FormCloseQuery(Sender: TObject; var CanClose: Boolean);
begin
  case Application.MessageBox('Zakończyć Program?', 'Uwaga!', MB_YESNO + MB_ICONQUESTION + MB_DEFBUTTON2 + MB_TOPMOST) of
    IDYES:
      begin
        if logowanie then
          zapisLog('Zakończono program');

        Serwer.Active := False;
        Klient.Active := False;
        if con1.Connected then
          con1.Connected := False;
      end;
    IDNO:
      begin
        CanClose := False;
      end;
  end;

end;

procedure Tmain.FormCreate(Sender: TObject);
begin
  main.Left := Screen.Width - main.Width - 50;
  main.Top := 50;
  main.Caption := 'PSerwer - wersja: ' + smvrsnf1.FileVersion;

  INI := TINIFile.Create(ExtractFilePath(Application.ExeName) + 'pserwer.ini');
  try
    // inne
    logowanie := INI.ReadBool('inne', 'logowanie', True);

  finally
    INI.Free;
  end;

  con1.ConnectionString := 'FILE NAME=' + ExtractFilePath(Application.ExeName) + 'link.udl';

  if logowanie then
    zapisLog('Uruchomiono program');

  tryicon1.BalloonTitle := 'Przywróć program!';
  tryicon1.BalloonHint := 'Kliknij dwukrotnie aby przywrócić program.';
  tryicon1.BalloonFlags := bfInfo;
  Application.ShowMainForm := False;
end;

procedure Tmain.FormHide(Sender: TObject);
begin
  Application.Minimize;
end;

procedure Tmain.rxclck1Alarm(Sender: TObject);
begin
  Application.Terminate;
  ShellExecute(Handle, 'Open', PChar(Application.ExeName), nil, nil, SW_NORMAL);
end;

procedure Tmain.SerwerUDPRead(AThread: TIdUDPListenerThread; AData: TBytes; ABinding: TIdSocketHandle);
var
  pytanie, ip, kod, nazwa, cena: string;
  port: string;
  S: TStrings;
  i: Integer;
begin
  pytanie := bytestoString(Adata);

  S := TStringList.Create;
  ExtractStrings([#09], [], PChar(pytanie), S);
  ip := S[0];
  port := S[1];
  kod := S[2];
  S.Free;

  mmo1.Lines.Add(DateTimeToStr(Now) + #09 + ip + #09 + port + #09 + kod);

  if logowanie then
    zapisLog('Stanowisko ip: ' + ip + ', zapytało o kod kreskowy: ' + kod);

  if kod <> '' then
  begin
    if not con1.Connected then
      con1.Connected := True;
    with qrytowary, SQL do
    begin
      Close;
      Clear;
      Add('select t.nazwa, t.ce_sb, t.kodpaskowy, k.kodkreskowy from towary t inner join towarykodykreskowe k on t.id = k.idtowaru where t.kodpaskowy=:k1 or k.kodkreskowy=:k2');
      Parameters.ParamByName('k1').Value := kod;
      Parameters.ParamByName('k2').Value := kod;
      Open;
    end;

    Klient.Active := False;
    Klient.Port := StrToInt(port);
    Klient.Host := ip;
    Klient.Active := True;

    if qrytowary.RecordCount > 0 then
    begin
      nazwa := qrytowary.FieldByName('nazwa').AsString;
      cena := qrytowary.FieldByName('ce_sb').AsString;
    end
    else
    begin
      nazwa := '--brak towaru--';
      cena := '0';
    end;

    if con1.Connected then
      con1.Connected := False;

    Klient.Send(nazwa + #09 + cena);
    Klient.Active := False;

    tryicon1.BalloonTitle := 'Nowe pytanie o kod!';
    tryicon1.BalloonHint := 'Kod: ' + kod + #13 + 'Towar: ' + nazwa;
    tryicon1.BalloonFlags := bfInfo;
    tryicon1.Visible := True;
    tryicon1.Animate := True;
    tryicon1.ShowBalloonHint;
  end;

  pytanie := '';
end;

procedure Tmain.tryicon1DblClick(Sender: TObject);
begin
  tryicon1.Visible := False;
  Show();
  WindowState := wsNormal;
  Application.BringToFront();
  if logowanie then
    zapisLog('Kliknięto pokaż program');
end;

end.

Krótki opis, program łączy się z bazą programu KCFirma poprzez ADO. KCFirma używa bazy MS Access (wiem - katastrofa). Łatwo jednak zauważyć że można wymienić link konfiguracyjny na inną bazę i program stanie się bardzo uniwersalny (no wypadałoby jeszcze dostosować selecta w zapytaniu).
Program nie nawiązuje połączenia z bazą do czasu pojawienia się pytania z kodem kreskowym. Powód tego jest taki że ustalenie połączenia z bazą danych powoduje blokadę np. optymalizacji bazy a niestety dość często trzeba ją wykonywać dla poprawy wydajności, są po prostu procedury wymagające dostępu do bazy w trybie exclusive.
Program ma prosty interfejs w którym widać ostatnie pytania o kod kreskowy. Program po uruchomieniu ląduje w zasobniku systemowym i tylko hint informuje że na jakimś czytniku pojawiło się pytanie o kod kreskowy.
W programie jest też prosta funkcja do zapisu logu (w celach kontrolnych) gdyby kogoś to interesowało.

Podstawowe działanie programu znajduje się w procedurze SerwerUDPRead, reszta to tak naprawdę otoczka.

  • Na początku do zmiennej "pytanie" typu string rzutuję dane odczytane przez kontrolkę UDPRead gdy się pojawią na odpowiednim porcie.
  • Z danych tych tworzę listę. Czy to nie jest przerost formy nad treścią? Wydaje mi się że jednak nie ze względu na to że daje to prosty sposób na rozbudowę tego prostego protokołu. Aktualnie w ramce z danymi znajduje się IP komputera który wysłał pytanie, port na jakim będzie nasłuchiwał odpowiedzi oraz kod kreskowy o który klient pyta. Takie rozwiązanie powoduje że dołączenie dodatkowych czytników cen jest wyjątkowo proste i nie wymaga żadnej konfiguracji po stronie serwera. Oczywiście ilość klientów też przestaje być ograniczeniem bo portów wolnych jest mnóstwo.
  • następnie wyświetlam pytanie o kod w kontrolce memo, zapisuję do logu (jeśli tak jest ustawione),
  • teraz pora na połączenie z bazą i zapytanie o kod kreskowy. Na początek sprawdzamy czy kod nie jest pusty (można by to bardziej rozbudować i sprawdzić czy kod spełnia warunki ean8 lub ean13 ale program ma być prosty), następnie select na tabelach (w przypadku kcfirmy kody kreskowe znajdują się w dwóch tabelach stąd join w zapytaniu). Ktoś mógłby zapytać dlaczego są dwa parametry w zapytaniu k1 i k2 a wskazują na ten sam kod kreskowy. Jest chyba jakiś błąd w ADO że nie można użyć dwukrotnie tego samego parametru, w przypadku starego BDE, FireDac itp. tego problemu nie ma. Jednak wybrałem ADO ze względu na proste połączenie linkiem udl i dobrą współpracę z plikami mde.
  • następny etap to ustawienie TIdUDPClient w tryb nieaktywny aby można było podać parametry połączenia, czyli port i adres IP (kontrolka daje się ustawiać gdy jest nieaktywna). Po tym ponownie ustawiam kontrolkę na aktywną i jest gotowa do wysłania komunikatu.
  • następnie sprawdzam czy select na bazach dał jakiś wynik, jeśli tak to do zmiennej "nazwa" i "cena" wpisuję dane z bazy. W przeciwnym wypadku do zmiennej "nazwa" wpisuję -- brak towaru -- a do zmiennej "cena" wartość zero.
  • pozostaje sprawdzić czy jest aktywne połączenie z bazą i jeśli jest aktywne to się odłączyć.
  • przedostatni etap to wysyłka danych poprzez TIdUDPClient (tu nazwany "Klient"). Dane wysyłane w sposób szeregowy: nazwa, tabulator, cena). Oczywiście łatwo rozbudować program o inne dane.
  • ostatni etap to: dezaktywacja "Klienta" i wyświetlenie balonika w zasobniku.

W programie jest jeszcze procedura resetująca aplikację o określonej godzinie. Wrzucam takie coś do programów mających ze swojej natury działać miesiącami bez nadzoru. Procedura taka oparta jest na kontrolce TRxClock z pakietu Jedi. Bardzo proste i dość skuteczne ponieważ korzysta z API systemowego. Można też użyć zwykłego timera albo po prostu w ogóle tego nie używać.

Na koniec pozostają procedury do minimalizacji aplikacji do zasobnika oraz przywróceniu po dwukliku.

Może jeszcze tylko wytłumaczę na koniec dlaczego nie połączyłem aplikacji w inny sposób lub po prostu czytnik cen nie podłączyłem bezpośrednio do bazy. W przypadku bazy sql (nie ważne w jakiej technologii) jest to oczywiście proste i być może nawet lepsze (chociaż konfiguracja byłaby troszkę bardziej zaawansowana no chyba że będzie plik konfiguracyjny który będzie można przekopiować). Należałoby tylko zadbać o to aby utworzyć użytkownika bazy z bardzo małymi prawami (najlepiej tylko do odczytu i to do konkretnej tabeli (tabel)). Pamiętać należy że taki czytnik wiszący w sklepie może zostać np. skradziony i lepiej aby nie było w nim informacji o dostępie do bazy danych.

W przypadku KCFirmy sprawa jest dużo bardziej skomplikowana, wymienię wszystkie wady takiego rozwiązania (nie mam na to wpływu):

  • Jest to baza MS Access. Baza taka w zasadzie jest nie do zabezpieczenia i to jest pierwszy argument na program pośredniczący.
  • Drugi to dostęp do bazy jak do pliku. Czyli aby się do niej podłączyć należy mieć fizyczny dostęp do pliku. Oczywiście istnieje odbc ale ono także musi mieć fizyczny dostęp do pliku (może być w sieci). Udostępnianie folderu z plikiem bazy w czasach gdzie szyfrowanie dysków przez wirusy jest standardem jest nieprawdopodobną niefrasobliwością. Wiem, można udostępnić tylko do odczytu ale ludzie są omylni i zawsze coś się może komuś włączyć.
  • Następna wada to wydłużony start aplikacji przy rosnącym pliku bazy danych. Normalne przy bazach plikowych. Przy wolnym komputerze czytnika (a w zasadzie tablecie), być może wolnej sieci, start może się bardzo wydłużyć, program na szybkim serwerze startuje błyskawicznie.
  • Blokowanie niektórych operacji na bazie przy normalnym podłączeniu. Jak pisałem, dość często trzeba plik bazy optymalizować i jego blokada na czytnikach bardzo by przeszkadzała. Należałoby połączyć się z każdym czytnikiem i wyłączyć program. To kosztuje czas a dodatkowo rozwiązania takie jak vnc nie zawsze działają niezawodnie.

W tym poście opisałem tylko część "serwerową" aplikacji. Jak pisałem, należy traktować ten programik jako bazę do napisania czegoś znacznie lepszego. Przede wszystkim brakuje pułapek na błędy oraz rozbudowania protokołu chociażby o inne dane o towarze czy np. połączenia z jakimś programem lojalnościowym i podawania ilości zgromadzonych punktów na jakiś kartach (aby był na nich kod kreskowy). Można tez rozszerzyć protokół o możliwość konfiguracji czytników. Wszystko przed chętnymi.

Dlaczego to wszytko piszę? Troszkę dla edukacji. Być może się komuś przyda. Najlepiej by było gdyby administrator przeniósł ten post do grupy Newbie bo tam jest chyba jego miejsce?
W następnym poście opiszę część kliencką aplikacji.
Acha, najważniejsze, zezwalam na użycie mojego kodu w celach edukacyjnych, prywatnych jak i firmowych oraz wszelkie zmiany jakie przyjdą komukolwiek do głowy.

Wygląd testowego czytnika:

kilkusekundowy filmik jak to działa

Pozdrawiam
Robert

0

Gratuluję samozaparcia. Najważniejsze żeby aplikacja robiła co potrzebne. Sam kod raczej do niczego się nie przyda osobom trzecim. Powodzenia dalej.

1

Dzięki, kto wie, może się kiedyś przyda. Kuzyn korzysta z KC Firmy, którą zakupił jakieś 15 lat temu i baza jest właśnie na MS Access ... w wersji 97 :)

0

Pierwsze wrażenie - działa szybko. Czasami w sklepie, gdy chcę sprawdzić cenę jakiegoś przedmiotu, to op "piknięciu" tego czytnikiem muszę czekać dobre kilka sekund. A u Ciebie jest to praktycznie od ręki. Wiadomo, że to kwestia wielkości bazy oraz jej obciążenia, ale póki co wrażenie pozytywne :)

Tak, jak pisali przedmówcy - gratuluję doprowadzenia tematu do końca.

Powiedz mi tylko proszę, czemu zrobiłeś komunikację po UDP a nie TCP? Czy są jakieś powodu ku temu, czy po prostu "tak wyszło"? ;)

I ostatnie pytanie - jaki system masz na "tabletach" pokazujących klientowi cenę?

1 użytkowników online, w tym zalogowanych: 0, gości: 1