Netzwerk Server-Client Lösung mit Threads (MiniChat)

Hier könnt Ihr gute, von Euch geschriebene Codes posten. Sie müssen auf jeden Fall funktionieren und sollten möglichst effizient, elegant und beispielhaft oder einfach nur cool sein.
Benutzeravatar
mk-soft
Beiträge: 3701
Registriert: 24.11.2004 13:12
Wohnort: Germany

Netzwerk Server-Client Lösung mit Threads (MiniChat)

Beitrag von mk-soft »

Hier mal ein Beispiel wie man mit Netzwerk Server und Client über Threads verarbeiten kann.

Zum Senden und Empfangen werden die String über einen Header mit der Länge des String gesendet und empfangen.
Der Client ist auf Localhost (127.0.0.1) voreingestellt. Man kann aber den Server auch auf eine andere Maschine starten und den Host (IP) im client anpassen.

Für Erweiterung des Beispiel und der Beschreibung bin ich offen. Ist erst mal ein Anfang :wink:

Update v1.01
- Bugfix

Server

Code: Alles auswählen

;-TOP
; Version v1.01

CompilerIf #PB_Compiler_Thread = 0
  CompilerError "Use Compileroption Threadsafe!"
CompilerEndIf

EnableExplicit

Enumeration Windows
  #WinMain
EndEnumeration

Enumeration #PB_Event_FirstCustomValue
  #My_Event_Logging
EndEnumeration

; -----------------------------------------------------------------------------

Structure udtClientData
  ConnectionID.i
  Date.i
  Text.s
EndStructure

Structure udtServerData
  *ThreadID
  *ServerID
  ExitServer.i
  Map Client.udtClientData() 
EndStructure

; -----------------------------------------------------------------------------

Structure udtAny
  StructureUnion
    aVal.a[0]
    bVal.b[0]
    cVal.c[0]
    wVal.w[0]
    uVal.u[0]
    iVal.i[0]
    lVal.l[0]
    qVal.q[0]
    fVal.f[0]
    dVal.d[0]
  EndStructureUnion
EndStructure

Structure udtNetString
  len.l
  pData.udtAny
EndStructure

; -----------------------------------------------------------------------------

; Bei Linux und MacOS kann man Gadgets nicht aus Threads ändern. Daher werden die Texte über PostEvent zur Liste gesendet.

Procedure AllocateString(Text.s)
  Protected *mem.String
  *mem = AllocateStructure(String)
  *mem\s = Text
  ProcedureReturn *mem
EndProcedure

Procedure.s FreeString(*Text.String)
  Protected result.s
  If *Text
    result = *text\s ;PeekS(*Text)
    FreeStructure(*Text)
  EndIf
  ProcedureReturn result
EndProcedure

; Logging aus Threads
Procedure thLogging(Text.s)
  PostEvent(#My_Event_Logging, 0, 0, 0, AllocateString(Text))
EndProcedure

; Logging aus Mainscope
Procedure Logging(Text.s)
  Protected rows
  AddGadgetItem(0, -1, Text)
  rows = CountGadgetItems(0)
  SetGadgetState(0, rows - 1)
  SetGadgetState(0, -1)
EndProcedure

; -----------------------------------------------------------------------------

; Um ein String über Netzwerk zu versenden wird am Anfang die Länge in Bytes von den String eingetragen.
; Somit weiss man wie viele Bytes aus dem Empfangsbuffer gelesen werden muss um den gesamten String zu erhalten

Procedure NetSendString(ConnectionID, Text.s)
  Protected *Buffer.udtNetString, len, cnt
  len = StringByteLength(Text)
  If len > 65536 - SizeOf(Long)
    ProcedureReturn 0; Daten zu lang
  EndIf
  *Buffer = AllocateMemory(len + SizeOf(Long))
  *Buffer\len = Len
  CopyMemory(@Text, *Buffer\pData, len)
  len + SizeOf(Long)
  cnt = SendNetworkData(ConnectionID, *Buffer, len)
  FreeMemory(*Buffer)
  ProcedureReturn cnt
EndProcedure

; -----------------------------------------------------------------------------

Procedure.s NetReceiveString(ConnectionID, *Error.Integer = 0)
  Protected len, cnt, size, timeout, error, *Buffer, NetStringLen, *NetString, result.s
  ; Stringlänge aus Empfangsbuffer lesen
  len = ReceiveNetworkData(ConnectionID, @NetStringLen, 4)
  If NetStringLen
    *NetString = AllocateMemory(NetStringLen + SizeOf(Character))
  Else
    ProcedureReturn ""
  EndIf
  *Buffer = AllocateMemory(65536)
  size = 65536
  If size > NetStringLen
    size = NetStringLen
  EndIf
  ; String aus Empfangsbuffer lesen bis dieser vollständig ist
  Repeat
    len = ReceiveNetworkData(ConnectionID, *Buffer, size)
    If len > 0
      CopyMemory(*Buffer, *NetString + cnt, len)
      cnt + len
      If size > (NetStringLen - cnt)
        size = (NetStringLen - cnt)
      EndIf
      timeout = 0
    Else
      Delay(10)
      timeout + 10 ; Millisecond
      If timeout >= 5000
        error = #True ; Communication error
        Break
      EndIf
    EndIf
  Until cnt >= NetStringLen
  result = PeekS(*NetString)
  If *Error
    *Error\i = error
  EndIf
  FreeMemory(*Buffer)
  FreeMemory(*NetString)
  ProcedureReturn result
EndProcedure

; -----------------------------------------------------------------------------

; Dies ist der Server-Dienst der die Daten im Hintergrund verarbeitet

Procedure ThreadServer(*ServerData.udtServerData)
  Protected Event, ConnectionID, keyConnectionID.s, count, Text.s
  With *ServerData
    Repeat
      Event = NetworkServerEvent(\ServerID)
      If Event
        ConnectionID = EventClient()
        keyConnectionID = Hex(ConnectionID)
      EndIf
      Select Event
        Case #PB_NetworkEvent_Connect
          ; Daten für neuen Client anlegen
          If FindMapElement(\Client(), keyConnectionID)
            DeleteMapElement(\Client(), keyConnectionID) ; Sollte nicht passieren
          EndIf
          AddMapElement(\Client(), keyConnectionID)
          \Client()\ConnectionID = ConnectionID
          \Client()\Date = Date()
          thLogging("Network: Client connected: ID " + keyConnectionID)
          
        Case #PB_NetworkEvent_Data
          ; String aus Empfangbuffer lesen
          Text = NetReceiveString(ConnectionID)
          If FindMapElement(\Client(), keyConnectionID)
            \Client()\Date = Date()
            \Client()\Text = Text
          EndIf
          ; An alle anderen Clients senden
          ForEach \Client()
            If \Client()\ConnectionID <> ConnectionID
              NetSendString(\Client()\ConnectionID, "Empfang: " + Text)
            EndIf
          Next
          ; Bestätigung senden
          count = MapSize(\Client()) - 1
          NetSendString(ConnectionID, "Senden ("+ count + "): " + Text)
         
        Case #PB_NetworkEvent_Disconnect
          ; Daten von Client entfernen
          thLogging("Network: Client disconnected: ID " + keyConnectionID)
          If FindMapElement(\Client(), keyConnectionID)
            DeleteMapElement(\Client(), keyConnectionID)
          EndIf
         
        Default
          Delay(10)
         
      EndSelect
    Until \ExitServer
   
    ; Server Shutdown Text an alle Clients senden
    ForEach \Client()
      NetSendString(\Client()\ConnectionID, "Server Shutdown")
    Next
   
    ; Server beenden, Daten bereinigen und Thread verlassen
    CloseNetworkServer(\ServerID)
    \ThreadID = 0
    \ServerID = 0
    \ExitServer = 0
    ClearMap(\Client())
  EndWith
EndProcedure

; -----------------------------------------------------------------------------

; Hier wird der Server angelegt und beim erfolg der Thread gestartet der die Server-Dienste ausführt

Procedure InitServer(*ServerData.udtServerData, Port, BindedIP.s = "")
  Protected ServerID
 
  With *ServerData
    ServerID = CreateNetworkServer(#PB_Any, Port, #PB_Network_TCP, BindedIP)
    If ServerID
      \ServerID = ServerID
      \ThreadID = CreateThread(@ThreadServer(), *ServerData)
      Logging("Network: Init Server: ID " + Hex(ServerID))
    Else
      Logging("Network: Error Init Network Server")
    EndIf
    ProcedureReturn ServerID
  EndWith
EndProcedure

; -----------------------------------------------------------------------------

; Hier wird das Beenden des Servers angestossen
; Sollte diese nicht erfolgreich sein, wird der Server und der Thread zwangsweise geschlossen

Procedure CloseServer(*ServerData.udtServerData)
  Protected timeout
 
  With *ServerData
    If \ServerID = 0
      ProcedureReturn 0
    EndIf
    Logging("Network: Close Network Server: ID " + \ServerID)
    \ExitServer = 1
    Repeat
      If \ExitServer = 0
        Break
      Else
        timeout + 100
        If timeout > 10000
          CloseNetworkServer(\ServerID)
          KillThread(\ThreadID)
          \ThreadID = 0
          \ServerID = 0
          \ExitServer = 0
          ClearMap(\Client())
          Logging("Network: Error - Kill Network Server: ID " + \ServerID)
          Break
        EndIf
      EndIf
      Delay(100)
    ForEver
  EndWith
EndProcedure

; -----------------------------------------------------------------------------

Global ExitApplication
Global ServerData.udtServerData

Procedure Main()
  Protected Event, rows
 
  If OpenWindow(#WinMain, #PB_Ignore, #PB_Ignore, 600, 400, "MiniChat-Server",#PB_Window_SystemMenu)
    CreateStatusBar(0, WindowID(#WinMain))
    AddStatusBarField(100)
    AddStatusBarField(#PB_Ignore)
    ListViewGadget(0, 0, 0, WindowWidth(0), WindowHeight(0) - StatusBarHeight(0))
   
    ; Init Server
    InitServer(ServerData, 6037)
   
    ; LOOP
    Repeat
      Event = WaitWindowEvent()
      Select Event
        Case #PB_Event_CloseWindow
          Select EventWindow()
            Case #WinMain
              CloseServer(ServerData)
              ExitApplication = #True
          EndSelect
        Case #My_Event_Logging
          Logging(FreeString(EventData()))
         
      EndSelect
     
    Until ExitApplication And ServerData\ExitServer = 0
  EndIf
 
EndProcedure

InitNetwork()
Main()
Zuletzt geändert von mk-soft am 05.03.2018 10:29, insgesamt 1-mal geändert.
Alles ist möglich, fragt sich nur wie...
Projekte ThreadToGUI / EventDesigner V3 / OOP-BaseClass-Modul
Downloads auf MyWebspace / OneDrive
Benutzeravatar
mk-soft
Beiträge: 3701
Registriert: 24.11.2004 13:12
Wohnort: Germany

Re: Netzwerk Server-Client Lösung mit Threads (MiniChat)

Beitrag von mk-soft »

Client

Code: Alles auswählen

;-TOP
; Version v1.01

CompilerIf #PB_Compiler_Thread = 0
  CompilerError "Use Compileroption Threadsafe!"
CompilerEndIf

EnableExplicit

Enumeration Windows
  #WinMain
EndEnumeration

Enumeration MenuItems
  #MenuItem_Send
  #MenuItem_Connect
  #MenuItem_Disconnect
EndEnumeration

Enumeration #PB_Event_FirstCustomValue
  #My_Event_Logging
EndEnumeration

; -----------------------------------------------------------------------------

Structure udtClientData
  *ThreadID
  *ConnectionID
  ExitClient.i
EndStructure

; -----------------------------------------------------------------------------

Structure udtAny
  StructureUnion
    aVal.a[0]
    bVal.b[0]
    cVal.c[0]
    wVal.w[0]
    uVal.u[0]
    iVal.i[0]
    lVal.l[0]
    qVal.q[0]
    fVal.f[0]
    dVal.d[0]
  EndStructureUnion
EndStructure

Structure udtNetString
  len.l
  pData.udtAny
EndStructure

; -----------------------------------------------------------------------------

; Bei Linux und MacOS kann man Gadgets nicht aus Threads ändern. Daher werden die Texte über PostEvent zur Liste gesendet.

Procedure AllocateString(Text.s)
  Protected *mem.String
  *mem = AllocateStructure(String)
  *mem\s = Text
  ProcedureReturn *mem
EndProcedure

Procedure.s FreeString(*Text.String)
  Protected result.s
  If *Text
    result = *text\s
    FreeStructure(*Text)
  EndIf
  ProcedureReturn result
EndProcedure

; Logging aus Threads
Procedure thLogging(Text.s)
  PostEvent(#My_Event_Logging, 0, 0, 0, AllocateString(Text))
EndProcedure

; Logging aus Mainscope
Procedure Logging(Text.s)
  Protected rows
  AddGadgetItem(0, -1, Text)
  rows = CountGadgetItems(0)
  SetGadgetState(0, rows - 1)
  SetGadgetState(0, -1)
EndProcedure

; -----------------------------------------------------------------------------

; Um ein String über Netzwerk zu versenden wird am Anfang die Länge in Bytes von den String eingetragen.
; Somit weiss man wie viele Bytes aus dem Empfangsbuffer gelesen werden muss um den gesamten String zu erhalten

Procedure NetSendString(ConnectionID, Text.s)
  Protected *Buffer.udtNetString, len, cnt
  len = StringByteLength(Text)
  If len > 65536 - SizeOf(Long)
    ProcedureReturn 0; Daten zu lang
  EndIf
  *Buffer = AllocateMemory(len + SizeOf(Long))
  *Buffer\len = Len
  CopyMemory(@Text, *Buffer\pData, len)
  len + SizeOf(Long)
  cnt = SendNetworkData(ConnectionID, *Buffer, len)
  FreeMemory(*Buffer)
  ProcedureReturn cnt
EndProcedure

; -----------------------------------------------------------------------------

Procedure.s NetReceiveString(ConnectionID, *Error.Integer = 0)
  Protected len, cnt, size, timeout, error, *Buffer, NetStringLen, *NetString, result.s
  ; Stringlänge aus Empfangsbuffer lesen
  len = ReceiveNetworkData(ConnectionID, @NetStringLen, 4)
  If NetStringLen
    *NetString = AllocateMemory(NetStringLen + SizeOf(Character))
  Else
    ProcedureReturn ""
  EndIf
  *Buffer = AllocateMemory(65536)
  size = 65536
  If size > NetStringLen
    size = NetStringLen
  EndIf
  ; String aus Empfangsbuffer lesen bis dieser vollständig ist
  Repeat
    len = ReceiveNetworkData(ConnectionID, *Buffer, size)
    If len > 0
      CopyMemory(*Buffer, *NetString + cnt, len)
      cnt + len
      If size > (NetStringLen - cnt)
        size = (NetStringLen - cnt)
      EndIf
      timeout = 0
    Else
      Delay(10)
      timeout + 10 ; Millisecond
      If timeout >= 5000
        error = #True ; Communication error
        Break
      EndIf
    EndIf
  Until cnt >= NetStringLen
  result = PeekS(*NetString)
  If *Error
    *Error\i = error
  EndIf
  FreeMemory(*Buffer)
  FreeMemory(*NetString)
  ProcedureReturn result
EndProcedure

; -----------------------------------------------------------------------------

; Dies ist der Client-Dienst der die Daten im Hintergrund verarbeitet

Procedure ThreadClient(*ClientData.udtClientData)
  Protected Event, count, Text.s
  With *ClientData
    Repeat
      Event = NetworkClientEvent(\ConnectionID)
      Select Event
        Case #PB_NetworkEvent_Data
          ; String aus Empfangbuffer lesen
          Text = NetReceiveString(\ConnectionID)
          thLogging(Text)
         
        Case #PB_NetworkEvent_Disconnect
          ; Server hat die Verbindung beendet
          \ExitClient = 1
         
        Default
          Delay(10)
         
      EndSelect
    Until \ExitClient
   
    ; Exit Thread
    CloseNetworkConnection(\ConnectionID)
    \ThreadID = 0
    \ConnectionID = 0
    \ExitClient = 0
  EndWith
EndProcedure

; -----------------------------------------------------------------------------

; Hier wird die Verbindung zum Server angelegt und beim erfolg der Thread gestartet der die Client-Dienste ausführt

Procedure InitClient(*ClientData.udtClientData, IP.s, Port, Timeout = 0)
  Protected ConnectionID
 
  With *ClientData
    If \ConnectionID
      ProcedureReturn \ConnectionID
    EndIf
    ConnectionID = OpenNetworkConnection(IP, Port, #PB_Network_TCP, Timeout)
    If ConnectionID
      \ConnectionID = ConnectionID
      \ThreadID = CreateThread(@ThreadClient(), *ClientData)
      Logging("Network: Init Client: ID " + Hex(ConnectionID))
    Else
      Logging("Network: Error Init Connection")
    EndIf
    ProcedureReturn ConnectionID
  EndWith
EndProcedure

; -----------------------------------------------------------------------------

; Hier wird das Beenden der Verbindung zu Server angestossen
; Sollte diese nicht erfolgreich sein, wird die Verbindung und der Thread zwangsweise geschlossen

Procedure CloseClient(*ClientData.udtClientData)
  Protected timeout
 
  With *ClientData
    If \ConnectionID = 0
      ProcedureReturn 0
    EndIf
    Logging("Network: Close Network Connection: ID " + \ConnectionID)
    \ExitClient = 1
    Repeat
      If \ExitClient = 0
        Break
      Else
        timeout + 100
        If timeout > 10000
          CloseNetworkConnection(\ConnectionID)
          KillThread(\ThreadID)
          \ThreadID = 0
          \ConnectionID = 0
          \ExitClient = 0
          Logging("Network: Error - Kill Network Connection: ID " + \ConnectionID)
          Break
        EndIf
      EndIf
      Delay(100)
    ForEver
  EndWith
EndProcedure

; -----------------------------------------------------------------------------

Global ExitApplication
Global ClientData.udtClientData
Global Host.s = "127.0.0.1"
Global Port = 6037

Procedure Main()
  Protected Event, rows, text.s
 
  If OpenWindow(#WinMain, #PB_Ignore, #PB_Ignore, 600, 400, "MiniChat-Client",#PB_Window_SystemMenu)
    CreateStatusBar(0, WindowID(#WinMain))
    AddStatusBarField(100)
    AddStatusBarField(#PB_Ignore)
    CreateMenu(0, WindowID(#WinMain))
    MenuTitle("Datei")
    MenuItem(#MenuItem_Connect, "Connect")
    MenuItem(#MenuItem_Disconnect, "Disconnect")
   
    ListViewGadget(0, 0, 0, WindowWidth(0), WindowHeight(0) - StatusBarHeight(0) - 35)
    StringGadget(1, 5, GadgetHeight(0) + 5, WindowWidth(0) - 10, 25, "")
    AddKeyboardShortcut(#WinMain, #PB_Shortcut_Return, #MenuItem_Send)
   
    ; LOOP
    Repeat
      Event = WaitWindowEvent()
      Select Event
        Case #PB_Event_Menu
          Select EventMenu()
            Case #MenuItem_Connect
              InitClient(ClientData, Host, Port)
             
            Case #MenuItem_Disconnect
              CloseClient(ClientData)
             
            Case #MenuItem_Send
              If GetActiveGadget() = 1 And ClientData\ConnectionID
                text = GetGadgetText(1)
                If text > ""
                  NetSendString(ClientData\ConnectionID, text)
                  SetGadgetText(1, "")
                EndIf
              EndIf
             
          EndSelect
         
        Case #PB_Event_CloseWindow
          Select EventWindow()
            Case #WinMain
              CloseClient(ClientData)
              ExitApplication = #True
          EndSelect
        Case #My_Event_Logging
          Logging(FreeString(EventData()))
         
      EndSelect
     
    Until ExitApplication And ClientData\ExitClient = 0
  EndIf
 
EndProcedure


InitNetwork()
Main()
Alles ist möglich, fragt sich nur wie...
Projekte ThreadToGUI / EventDesigner V3 / OOP-BaseClass-Modul
Downloads auf MyWebspace / OneDrive
Antworten