UTF-8 String über Netzwerk empfangen

Für allgemeine Fragen zur Programmierung mit PureBasic.
Benutzeravatar
NicTheQuick
Ein Admin
Beiträge: 8807
Registriert: 29.08.2004 20:20
Computerausstattung: Ryzen 7 5800X, 64 GB DDR4-3200
Ubuntu 24.04.2 LTS
GeForce RTX 3080 Ti
Wohnort: Saarbrücken

UTF-8 String über Netzwerk empfangen

Beitrag von NicTheQuick »

Hi Leute,

ich überlege gerade wie ich effizient und sicher einen UTF-8-String empfangen könnte, ohne dass ein einzelnes Zeichen auf zwei Pakete aufgeteilt wird.

Eine andere Anwendung, die nicht in Purebasic geschrieben wurde, schickt mir UTF-8 kodierte Strings. Jetzt kann es ja passieren, dass ein einzelnes Zeichen aus mehr als einem Byte besteht und somit in zwei Paketen ankommt. Oder 'ReceiveNetworkData()' teilt einen solchen String in zwei Häppchen auf. Das heißt ja, ich sollte nicht versuchen jedes Paket einzeln in einen Unicode-String zu konvertieren und dann zusammenzusetzen, sondern nur so weit konvertieren bis ein halbes UTF-8-Zeichen auftritt.

Ich habe mir jetzt erst mal so beholfen, dass ich mir eine eigene PeekUTF8()-Funktion geschrieben habe, die mir den lesbaren Teil eines halben UTF-8-Strings und die noch fehlenden Bytes zurück gibt. Im Code unten erstelle ich einen UTF-8-String und schneide immer wieder ein Byte hinten ab um damit meine Funktion zu füttern. Das funktioniert soweit gut, aber könnte vermutlich effizienter lösbar sein. Habt ihr darüber schon einmal nachgedacht?

Code: Alles auswählen

EnableExplicit

Procedure.s PeekUTF8(*memory.Ascii, size.i, *remainder.Integer)
	Protected result.s, pos.i = 0, characters.i, *c.Ascii = *memory
	Protected cLen.i
	
	While pos < size
		;Debug RSet(Bin(*c\a), 8, "0") + " " + *c\a
		
		If *c\a & %11110000 = %11110000
			cLen = 4
		ElseIf *c\a & %11100000 = %11100000
			cLen = 3
		ElseIf *c\a & %11000000 = %11000000
			cLen = 2
		ElseIf *c\a & %10000000 = %10000000
			Debug "Kein gültiges UTF-8-Zeichen gefunden!"
			ProcedureReturn ""
		Else
			cLen = 1
		EndIf
		
		If (pos + cLen > size)
			Break
		EndIf
		
		characters + 1
		pos + cLen
		If (*c\a = 0)
			Break
		EndIf
		*c + cLen
	Wend
	
	If (*remainder)
		*remainder\i = size - pos
	EndIf
	
	ProcedureReturn PeekS(*memory, characters, #PB_UTF8)
EndProcedure


Define *utf8 = AllocateMemory(1024)

Define size.i = PokeS(*utf8, "Schweiß", -1, #PB_UTF8) + 1

Debug "Bytes: " + size
Define i.i, hex.s = ""
For i = 0 To size - 1
	hex + RSet(Hex(PeekA(*utf8 + i), #PB_Ascii), 2, "0") + " "
Next
Debug "Hex: " + hex
Debug ""

Define remainder.i

For i = 0 To 3
	Debug "Mit " + i + " fehlenden Bytes am Ende:"
	Debug PeekUTF8(*utf8, size - i, @remainder)
	Debug "Restbytes: " + remainder
	Debug ""
Next
Benutzeravatar
NicTheQuick
Ein Admin
Beiträge: 8807
Registriert: 29.08.2004 20:20
Computerausstattung: Ryzen 7 5800X, 64 GB DDR4-3200
Ubuntu 24.04.2 LTS
GeForce RTX 3080 Ti
Wohnort: Saarbrücken

Re: UTF-8 String über Netzwerk empfangen

Beitrag von NicTheQuick »

Ich habe meine Funktion noch etwas verbessert, damit sie sich selbst darum kümmert halbe UTF-8-Zeichen wieder zusammenzusetzen. Die Funktion benötigt jetzt zwei zusätzliche Parameter, die das halbe Zeichen vom letzten Aufruf und dessen Länge beinhalten. Das funktioniert dann im Grunde wie ein Carry-Bit bei einem Volladdierer. :D

Beispiel ist anbei.

Code: Alles auswählen

EnableExplicit

Procedure.s PeekUTF8(*memory.Ascii, size.i, *remainder.Long = 0)
	Protected result.s, pos.i = 0, characters.i, *c.Ascii = *memory
	Protected cLen.i, diff.i, *remainderLength.Ascii
	
	If (*remainder)
		*remainderLength = *remainder + 3
	EndIf
	
	If (*remainder And *remainderLength\a > 0)
		cLen = 0
		*c = *remainder
		While *c\a & (1 << (7 - cLen)) : cLen + 1 : Wend
		If (cLen = 1)
			Debug "Kein gültiges UTF-8-Zeichen gefunden!"
			ProcedureReturn ""
		EndIf
		If (cLen = 0) : cLen = 1 : EndIf
		diff = cLen - *remainderLength\a
		If (diff > size)
			CopyMemory(*memory, *remainder + *remainderLength\a, size)
			*remainderLength\a = diff - size
			ProcedureReturn ""
		EndIf
		CopyMemory(*memory, *remainder + *remainderLength\a, diff)
		
		result = PeekS(*remainder, 1, #PB_UTF8)
		*c = *memory + diff
		pos + diff
	EndIf
	
	While pos < size
		;Debug RSet(Bin(*c\a), 8, "0") + " " + *c\a
		
		cLen = 0
		While *c\a & (1 << (7 - cLen)) : cLen + 1 : Wend
		If (cLen = 1)
			Debug "Kein gültiges UTF-8-Zeichen gefunden!"
			ProcedureReturn ""
		EndIf
		If (cLen = 0) : cLen = 1 : EndIf
		
		If (pos + cLen > size)
			Break
		EndIf
		
		characters + 1
		pos + cLen
		If (*c\a = 0)
			Break
		EndIf
		*c + cLen
	Wend
	
	If (*remainder)
		*remainder\l = 0
		*remainderLength\a = size - pos
		CopyMemory(*c, *remainder, *remainderLength\a)
	EndIf
	
	ProcedureReturn result + PeekS(*memory, characters, #PB_UTF8)
EndProcedure


Define *utf8 = AllocateMemory(1024)

Define size.i = PokeS(*utf8, "äö", -1, #PB_UTF8) + 1

Debug "Bytes: " + size
Define i.i, hex.s = ""
For i = 0 To size - 1
	hex + RSet(Hex(PeekA(*utf8 + i), #PB_Ascii), 2, "0") + " "
Next
Debug "Hex: " + hex
Debug ""

Define remainder.l

For i = 0 To 3
	Debug "Mit " + i + " fehlenden Bytes am Ende:"
	Debug PeekUTF8(*utf8, size - i, @remainder)
	Debug "Restbytes: " + PeekA(@remainder + 3)
	remainder = 0
	Debug ""
Next

Debug "AUFRUF NACHEINANDER"
For i = 0 To 3
	Debug "Zuerst das " + Str(i + 1) + ". Byte des Strings:"
	Debug "'" + PeekUTF8(*utf8 + i, 1, @remainder) + "' (Restbytes: " + Str(PeekA(@remainder + 3)) + ")"
	Debug ""
Next
Edit: Mit einem Parameter ist es doch hübscher! :mrgreen:
GPI
Beiträge: 1511
Registriert: 29.08.2004 13:18
Kontaktdaten:

Re: UTF-8 String über Netzwerk empfangen

Beitrag von GPI »

Ich würde einfach als erstes ein Byte mit der Länge des Strings in Byte (.a) übertragen (wenn die Strings kleiner als 255Bytes sind, ansonsten halt .U). Dann weist du sicher, ob Zeichen fehlen und wieviele. Geht hier wohl nicht...

Aber müsste der Test nicht ein bischen anders sein? Ich würde zumindest den Test auf die folgende Null einführen.

Code: Alles auswählen

If *c\a & %11111000 = %11110000
         cLen = 4
      ElseIf *c\a & %11110000 = %11100000
         cLen = 3
      ElseIf *c\a & %11100000 = %11000000
         cLen = 2
      ElseIf *c\a & %10000000 = %10000000
         debug "nicht erlaubt"
      endif
Dein Code würde sonst ein fehlerhaftes %11111000 als 4er erkennen. Es wäre aber ein 5er und damit nicht erlaubt.

Meine Idee wäre es, mehrbytes erstmal mit static zwischenzuspeichern, bis es vollständig ist, ungefähr so:

Code: Alles auswählen

EnableExplicit

;CompilerIf Not #PB_Compiler_Unicode
;  CompilerError "nix da, unicode ist pflicht!"
;CompilerEndIf

Structure savechars
  c.a[6]
EndStructure


Procedure.s PeekUTF8(*pos.ascii,size)
  Static save.savechars
  Static count.i
  Static needed.i
  Protected *endpos
  Protected ret.s
  Protected *buf.character
  Protected *bufstart
  Debug "count:"+count+" needed:"+needed
  
  
  ret=Space(size+1);maximale anzahl
  *buf=@ret
  
  *endpos=*pos+size
  
  While *pos<*endpos And *pos\a>0
    If needed>count
      save\c[count]=*pos\a
      count+1
      If needed=count
        *buf+PokeS(*buf,PeekS(@save,needed,#PB_UTF8))
        needed=0
        count=0
      EndIf
    Else
      If *pos\a & %10000000 = %10000000 ;mehrzeichen endeckt!
        ;welches denn?
        If *pos\a & %11111000 = %11110000
          needed=4
          ;Debug "4 zeichen"
        ElseIf *pos\a & %11110000 = %11100000
          needed= 3
          ;Debug "3 Zeichen"
        ElseIf *pos\a & %11100000 = %11000000
          needed= 2
          ;Debug "2 Zeichen"
        ElseIf *pos\a & %10000000 = %10000000
          needed=0 ; gibts nicht
          Debug "Fehler UTF8"
        EndIf
        If needed 
          ;wenn kein fehler da ist, gleich mal erstes speichern
          save\c[count]=*pos\a
          count+1
        EndIf
      Else
        *buf\c=*pos\a
        *buf+SizeOf(character)
      EndIf
    EndIf  
    *pos+1
  Wend
  *buf\c=0
  ProcedureReturn ret
EndProcedure

Define *mem, size
Define i
Define ret.s,control.s

*mem=AllocateMemory(1024)    

control="halloäasdföefüewrß23ß4ß342€s€a€2"
size=PokeS(*mem,control,-1,#PB_UTF8)+1
Debug "geschrieben:"+size
For i=0 To size Step 3
  ret+ peekutf8(*mem+i,3)
  Debug Str(i)+":"+ret
Next
Debug control
Debug ret
Man müsste halt peekutf8 noch etwas beschleunigen, momentan wird halt der String Zeichen für Zeichen zusammengesetzt, das dauert. Kann man aber anders lösen :)

Die Frage ist auch, läuft dein Programm in Unicode oder Ascii - vermutlich unicode oder?

edit: Kopieren etwas beschleunigt.
CodeArchiv Rebirth: Deutsches Forum Github Hilfe ist immer gern gesehen!
Benutzeravatar
NicTheQuick
Ein Admin
Beiträge: 8807
Registriert: 29.08.2004 20:20
Computerausstattung: Ryzen 7 5800X, 64 GB DDR4-3200
Ubuntu 24.04.2 LTS
GeForce RTX 3080 Ti
Wohnort: Saarbrücken

Re: UTF-8 String über Netzwerk empfangen

Beitrag von NicTheQuick »

GPI hat geschrieben:Aber müsste der Test nicht ein bischen anders sein? Ich würde zumindest den Test auf die folgende Null einführen.

Code: Alles auswählen

If *c\a & %11111000 = %11110000
         cLen = 4
      ElseIf *c\a & %11110000 = %11100000
         cLen = 3
      ElseIf *c\a & %11100000 = %11000000
         cLen = 2
      ElseIf *c\a & %10000000 = %10000000
         debug "nicht erlaubt"
      endif
Dein Code würde sonst ein fehlerhaftes %11111000 als 4er erkennen. Es wäre aber ein 5er und damit nicht erlaubt.
Danke für den Tipp. Das war wirklich noch falsch. Es sind noch ein paar andere Dinge falsch, aber ich bin mir nicht sicher, ob ich wirklich alles überprüfen soll oder den Rest einfach von PeekS() abfangen lassen soll.
Ich hab noch einen kleinen Test mit eingebaut:

Code: Alles auswählen

EnableExplicit

Procedure.s PeekUTF8(*memory.Ascii, size.i, *remainder.Long = 0)
	Protected result.s, pos.i = 0, characters.i, *c.Ascii = *memory
	Protected cLen.i, diff.i, *remainderLength.Ascii
	
	If (*remainder)
		*remainderLength = *remainder + 3
	EndIf
	
	If (*remainder And *remainderLength\a > 0)
		cLen = 0
		*c = *remainder
		While *c\a & (1 << (7 - cLen)) : cLen + 1 : Wend
		If (cLen = 1 Or cLen > 4)
			Debug "Kein gültiges UTF-8-Zeichen gefunden!"
			ProcedureReturn ""
		EndIf
		If (cLen = 0) : cLen = 1 : EndIf
		diff = cLen - *remainderLength\a
		If (diff > size)
			CopyMemory(*memory, *remainder + *remainderLength\a, size)
			*remainderLength\a = diff - size
			ProcedureReturn ""
		EndIf
		CopyMemory(*memory, *remainder + *remainderLength\a, diff)
		
		result = PeekS(*remainder, 1, #PB_UTF8)
		*c = *memory + diff
		pos + diff
	EndIf
	
	While pos < size
		;Debug RSet(Bin(*c\a), 8, "0") + " " + *c\a
		
		cLen = 0
		While *c\a & (1 << (7 - cLen)) : cLen + 1 : Wend
		If (cLen = 1 Or cLen > 4)
			Debug "Kein gültiges UTF-8-Zeichen gefunden!"
			ProcedureReturn ""
		EndIf
		If (cLen = 0) : cLen = 1 : EndIf
		
		If (pos + cLen > size)
			Break
		EndIf
		
		characters + 1
		pos + cLen
		If (*c\a = 0)
			Break
		EndIf
		*c + cLen
	Wend
	
	If (*remainder)
		*remainder\l = 0
		*remainderLength\a = size - pos
		CopyMemory(*c, *remainder, *remainderLength\a)
	EndIf
	
	ProcedureReturn result + PeekS(*memory, characters, #PB_UTF8)
EndProcedure

DataSection
	utf8_chars:
		Data.a 1, %01111111, 0, 0, 0
		Data.a 0, %10000000, 0, 0, 0
		Data.a 1, %11000000, %10111111, 0, 0
		Data.a 0, %11000000, %11000000, 0, 0
		Data.a 0, %11000000, %00111111, 0, 0
		Data.a 1, %11100000, %10101010, %10000000, 0
		Data.a 1, %11110111, %10111111, %10111111, %10111111
		Data.a 0, %11110111, %10111111, %10111111, %01000000
		Data.a 0, %11111011, 0, 0, 0
		Data.a 0, 0, 0, 0, 0
EndDataSection

Define correct.a, char.l
Restore utf8_chars
Repeat
	Read.a correct
	Read.l char
	If (char = 0) : Break : EndIf
	If correct
		Debug "Das folgende UTF-8 Zeichen sollte funktionieren (Hex: " + RSet(Hex(char, #PB_Long), 8, "0") + ")"
	Else
		Debug "Das folgende UTF-8 Zeichen darf NICHT funktionieren (Hex: " + RSet(Hex(char, #PB_Long), 8, "0") + ")"
	EndIf
	Debug PeekUTF8(@char, 4, 0)
ForEver


Define *utf8 = AllocateMemory(1024)

Define size.i = PokeS(*utf8, "äö", -1, #PB_UTF8) + 1

Debug "Bytes: " + size
Define i.i, hex.s = ""
For i = 0 To size - 1
	hex + RSet(Hex(PeekA(*utf8 + i), #PB_Ascii), 2, "0") + " "
Next
Debug "Hex: " + hex
Debug ""

Define remainder.l

For i = 0 To 3
	Debug "Mit " + i + " fehlenden Bytes am Ende:"
	Debug PeekUTF8(*utf8, size - i, @remainder)
	Debug "Restbytes: " + PeekA(@remainder + 3)
	remainder = 0
	Debug ""
Next

Debug "AUFRUF NACHEINANDER"
For i = 0 To 3
	Debug "Zuerst das " + Str(i + 1) + ". Byte des Strings:"
	Debug "'" + PeekUTF8(*utf8 + i, 1, @remainder) + "' (Restbytes: " + Str(PeekA(@remainder + 3)) + ")"
	Debug ""
Next
Benutzeravatar
Thorium
Beiträge: 1722
Registriert: 12.06.2005 11:15
Wohnort: Germany
Kontaktdaten:

Re: UTF-8 String über Netzwerk empfangen

Beitrag von Thorium »

Also ich puffere immer.
Das heisst ich dekodiere die Daten erst, wenn sie komplett übertragen wurden. Solange wandern sie in einen Puffer.
Ist natürlich immer die Frage woran man erkennt das alle Daten übertragen wurden. Ich nutze dafür immer eine Längenangabe, welche als erstes gesendet wird. Einige Anwendungen nutzen Abschlußzeichen. Z.B. Nullterminierung bei Strings.
Zu mir kommen behinderte Delphine um mit mir zu schwimmen.

Wir fordern mehr Aufmerksamkeit für umfallende Reissäcke! Bild
Antworten