Marvin Esse: Websocket Verbindung schlägt fehl nach Seite neuladen (F5)

Hallo,

ich hab leider ein Problem mit Websocket-Verbindungen, das ich nicht gelöst bekomme…

Der Client baut eigentlich ganz gewöhnlich und erfolgreich eine Socket-Verbindung auf. Danach funktioniert die Kommunikation tadellos, bis der Benutzer seine Seite neu lädt (z.B. F5). Theoretisch bedeutet das doch, dass die Seite kurz verlassen und dann wieder neu aufgebaut wird. Also wird die Verbindung getrennt und auch wieder neu aufgebaut. Aber scheinbar bekommt der Server das nicht richtig mit, denn der client kann zwar noch Nachrichten versenden, aber es kommen keine Nachrichten mehr beim ihm an. Auf dem Server sehe ich, dass beim Wiederverbinden dieselbe Ressource-ID verwendet wird, was wohl schon nicht richtig ist und der Server meldet ein paar Sekunden später, dass der Client disconnected wäre.

Ich habe sogar schon sicherheitshalber ein websocket.close() eingebaut, bevor die Seite beendet wird:

	window.onbeforeunload = function() {
		websocket.onclose = function () {}; // disable onclose handler first
		websocket.close()
	};

Aber das scheint den Server nicht zu interessieren.

Client-Script:

	var wsUri = "ws://10.10.10.123:9000/scripts/server.php"; 	
	websocket = new WebSocket(wsUri); 
	
	websocket.onopen = function(ev) { // connection is open 
		$('#message_box').append("<div class=\"system_msg\">Connected!</div>"); //notify user
		var mymessage = "user"; // first hello to give system the name
		//prepare json data
		var msg = {
			message: mymessage,
			name: myname,
			type: mytype
		};
		//convert and send data to server
		websocket.send(JSON.stringify(msg));
	}

	//#### Message received from server?
	websocket.onmessage = function(ev) {
		var msg = JSON.parse(ev.data);	//PHP sends Json data
		var utype = msg.type;			//message type
		var umsg = msg.message;			//message text
		var uname = msg.name;			//user name

		$('#message_box').append("<div><span class=\"user_name\">"+uname+"</span>: <span class=\"user_message\">"+umsg+" ("+utype+")</span></div>");
		
		var objDiv = document.getElementById("message_box");
		objDiv.scrollTop = objDiv.scrollHeight;
	};
	
	websocket.onerror	= function(ev){$('#message_box').append("<div class=\"system_error\">Error Occurred - "+ev.data+"</div>");}; 
	websocket.onclose 	= function(ev){$('#message_box').append("<div class=\"system_msg\">Connection Closed</div>");}; 

Server-Script:

//Create TCP/IP sream socket
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
//reuseable port
socket_set_option($socket, SOL_SOCKET, SO_REUSEADDR, 1);

//bind socket to specified host
socket_bind($socket, 0, $port);

//listen to port
socket_listen($socket);

//create & add listning socket to the list
$clients = array($socket);
$clientsid = array();

//start endless loop, so that our script doesn't stop
while (true) {
	//manage multipal connections
	$changed = $clients;
	//returns the socket resources in $changed array
	socket_select($changed, $null, $null, 0, 10);
	
	//check for new socket
	if (in_array($socket, $changed)) {
		$socket_new = socket_accept($socket); //accpet new socket
		$clients[] = $socket_new; //add socket to client array
		
		$header = socket_read($socket_new, 1024); //read data sent by the socket
		perform_handshaking($header, $socket_new, $host, $port); //perform websocket handshake
		
		socket_getpeername($socket_new, $ip); //get ip address of connected socket
		$response = mask(json_encode(array('type'=>'system', 'message'=>$ip.' connected'))); //prepare json data
		send_message($response,""); //notify all users about new connection
		
		//make room for new socket
		$found_socket = array_search($socket, $changed);
		unset($changed[$found_socket]);
	}
	
	//loop through all connected sockets
	foreach ($changed as $changed_socket) {	
		
		//check for any incomming data
		while(socket_recv($changed_socket, $buf, 1024, 0) >= 1)
		{
			$received_text = unmask($buf);					//unmask data
			$tst_msg = json_decode($received_text);			//json decode 
			$user_name = $tst_msg->name;					//sender name
			$user_message = $tst_msg->message;				//message text
			$user_type = $tst_msg->type;					//message type

			echo "$user_name sent: $user_message (type: $user_type)\r\n";

			$erg = array_multi_search($user_name, $clientsid);
			if (empty($erg[0][0])) {						// neues Device angemeldet
				array_push($clientsid, array($user_name,$changed_socket));
				$response = mask(json_encode(array('type'=>'system', 'name'=>'system', 'message'=>$user_name.' added.')));
				send_message($response,"");				// notify all  users about new connection
				foreach($clientsid as $subKey => $subArray){
					echo "-->".$subArray[0].":".$subArray[1]."\r\n";
					if (empty($subArray[0])) {
					   unset($clientsid[$subKey]);
					}
				}
			}
			$x = explode(":",$user_message);
			if ($x[0] <> $user_message) {					// user sends to specific target
				$target = $x[0];
				array_shift($x);							// erstes Element löschen
				$user_message = implode($x);
				$response_text = mask(json_encode(array('type'=>'whisper', 'name'=>$user_name." (to ".$target.")", 'message'=>$user_message)));
				send_message($response_text,$user_name);	//send data to inform sender that message will be sent
			} else {
				$target = "";
			}
			
			//prepare data to be sent to client
			$response_text = mask(json_encode(array('type'=>$user_type, 'name'=>$user_name, 'message'=>$user_message)));
			send_message($response_text,$target); //send data to target device
			break 2; //exits this loop
		}
		
		$buf = @socket_read($changed_socket, 1024, PHP_NORMAL_READ);
		if ($buf === false) { // check disconnected client
			// remove client for $clients array
			$found_socket = array_search($changed_socket, $clients);
			socket_getpeername($changed_socket, $ip);
			unset($clients[$found_socket]);

			// remove client from $clientsid multi-array
			foreach($clientsid as $subKey => $subArray){
				echo "-->".$subArray[0].":".$subArray[1]."\r\n";
				if (($subArray[1] == $changed_socket) OR (empty($subArray[0]))) {
				   unset($clientsid[$subKey]);
				}
			}
			
			//notify all service-pc users about disconnected connection
			$response = mask(json_encode(array('type'=>'system', 'message'=>$ip.' disconnected')));
			send_message($response,"");
			echo $ip." disconnected\r\n";
		}
	}
}
// close the listening socket
socket_close($socket);

function send_message($msg,$target) {
	global $clients;
	global $clientsid;
	if (!empty($target)) {
		$erg = array_multi_search($target, $clientsid);
	}
	if ((!empty($target)) AND (!empty($erg[0][0]))) {
		echo "sende zu ".$erg[0][0]." mit resource: ".$erg[0][1]."\r\n";
		$usertarget = $erg[0][1];
		@socket_write($usertarget,$msg,strlen($msg));				// send only to selected user
	} else {
		foreach($clientsid as $nr => $client) {
			if (substr($client[0],0,9) == "user") {				// only inform all users
				echo "sende zu ".$client[0]." mit resource: ".$client[1]."\r\n";
				$changed_socket = $client[1];
				@socket_write($changed_socket,$msg,strlen($msg));
			}
		}
	}
	return true;
}


//Unmask incoming framed message
function unmask($text) {
	$length = ord($text[1]) & 127;
	if($length == 126) {
		$masks = substr($text, 4, 4);
		$data = substr($text, 8);
	}
	elseif($length == 127) {
		$masks = substr($text, 10, 4);
		$data = substr($text, 14);
	}
	else {
		$masks = substr($text, 2, 4);
		$data = substr($text, 6);
	}
	$text = "";
	for ($i = 0; $i < strlen($data); ++$i) {
		$text .= $data[$i] ^ $masks[$i%4];
	}
	return $text;
}

//Encode message for transfer to client.
function mask($text) {
	$b1 = 0x80 | (0x1 & 0x0f);
	$length = strlen($text);
	
	if($length <= 125)
		$header = pack('CC', $b1, $length);
	elseif($length > 125 && $length < 65536)
		$header = pack('CCn', $b1, 126, $length);
	elseif($length >= 65536)
		$header = pack('CCNN', $b1, 127, $length);
	return $header.$text;
}

//handshake new client.
function perform_handshaking($receved_header,$client_conn, $host, $port) {
	$headers = array();
	$lines = preg_split("/\r\n/", $receved_header);
	foreach($lines as $line)
	{
		$line = chop($line);
		if(preg_match('/\A(\S+): (.*)\z/', $line, $matches))
		{
			$headers[$matches[1]] = $matches[2];
		}
	}

	$secKey = $headers['Sec-WebSocket-Key'];
	$secAccept = base64_encode(pack('H*', sha1($secKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')));
	//hand shaking header
	$upgrade  = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" .
	"Upgrade: websocket\r\n" .
	"Connection: Upgrade\r\n" .
	"WebSocket-Origin: $host\r\n" .
	"WebSocket-Location: ws://$host:$port/demo/shout.php\r\n".
	"Sec-WebSocket-Accept:$secAccept\r\n\r\n";
	socket_write($client_conn,$upgrade,strlen($upgrade));
}

function array_multi_search($mSearch, $aArray, $sKey = "") {
    $aResult = array();
   
    foreach( (array) $aArray as $aValues)     {
        if($sKey === "" && in_array($mSearch, $aValues)) $aResult[] = $aValues;
        else
        if(isset($aValues[$sKey]) && $aValues[$sKey] == $mSearch) $aResult[] = $aValues;
    }
   
    return $aResult;
}

LG Marvin

  1. Hello,

    nur eben kurz geschossen:
    Welchen Webserver benutzt Du zum Testen?
    Welche Versionen haben die übrigen Beteiligten?
    Was sagen das Access-Log und das Error-Log des Webservers?
    Wie wurde der Websocket-Server gestartet?

    Wie wäre es, für den Websocket-Server auch ein vernünftiges Error-Log einzurichten?

    Hast Du es schon mal mit einem anderen Port versucht? Auf meinem lokalen Webserver hatte ich mit Port 9000 auch Probleme, Port 9300 hat dann funktioniert. Woran das liegt, konnte ich auch noch nicht fetstellen.

    Liebe Grüße
    Tom S.

    --
    Es gibt nichts Gutes, außer man tut es
    Andersdenkende waren noch nie beliebt, aber meistens diejenigen, die die Freiheit vorangebracht haben.
    1. Hallo Tom,

      Welchen Webserver benutzt Du zum Testen?
      Welche Versionen haben die übrigen Beteiligten?
      Was sagen das Access-Log und das Error-Log des Webservers?
      Wie wurde der Websocket-Server gestartet?

      Als Webserver setze ich einen Apache ein. Die Clients sind alle Windows 10 mit aktuellster Firefox-Version. Access.log und Error.log zeigen keine Einträge. Der Websocket-Server wurde auf dem Apache-Server in der DOS-Box mit php -q server.php gestartet.

      Wie wäre es, für den Websocket-Server auch ein vernünftiges Error-Log einzurichten?

      Ich gebe zumindest an den markanten Stellen über echo einige Informationen aus. Syntax-Fehler würden beim Aufruf gemeldet und logische Fehler erhoffte ich mir durch die Ausgabe zu finden. Für mich sieht es im Moment so aus, als wenn der Server nicht mitbekommt, dass der Client disconnected.

      Hast Du es schon mal mit einem anderen Port versucht? Auf meinem lokalen Webserver hatte ich mit Port 9000 auch Probleme, Port 9300 hat dann funktioniert. Woran das liegt, konnte ich auch noch nicht fetstellen.

      Nein, habe ich noch nicht versucht, aber das kann ich ja schnell probieren.

      LG Marvin

      1. Hello,

        Der Websocket-Server wurde auf dem Apache-Server in der DOS-Box mit php -q server.php gestartet.

        Ist es ein diskret installierter Apache odre ein XAMPP?

        Wie wäre es, für den Websocket-Server auch ein vernünftiges Error-Log einzurichten?

        Ich gebe zumindest an den markanten Stellen über echo einige Informationen aus.

        Und wo landen die? Auf der Standardausgabe und damit im Client? Oder hast Du Sorge getragen, einen Fehlerkanal auf dem Server bereitzustellen, in den die Meldungen gehen? ...

        Hast Du es schon mal mit einem anderen Port versucht? Auf meinem lokalen Webserver hatte ich mit Port 9000 auch Probleme, Port 9300 hat dann funktioniert. Woran das liegt, konnte ich auch noch nicht fetstellen.

        Nein, habe ich noch nicht versucht, aber das kann ich ja schnell probieren.

        Ich bin gespannt.

        Liebe Grüße
        Tom S.

        --
        Es gibt nichts Gutes, außer man tut es
        Andersdenkende waren noch nie beliebt, aber meistens diejenigen, die die Freiheit vorangebracht haben.
        1. Ist es ein diskret installierter Apache odre ein XAMPP?

          ist ein XAMPP.

          Wie wäre es, für den Websocket-Server auch ein vernünftiges Error-Log einzurichten?

          Ich gebe zumindest an den markanten Stellen über echo einige Informationen aus.

          Und wo landen die? Auf der Standardausgabe und damit im Client? Oder hast Du Sorge getragen, einen Fehlerkanal auf dem Server bereitzustellen, in den die Meldungen gehen? ...

          Die Ausgaben landen in der DOS-Box, in die server.php ausgeführt wird.

          Hast Du es schon mal mit einem anderen Port versucht? Auf meinem lokalen Webserver hatte ich mit Port 9000 auch Probleme, Port 9300 hat dann funktioniert. Woran das liegt, konnte ich auch noch nicht fetstellen.

          Nein, habe ich noch nicht versucht, aber das kann ich ja schnell probieren.

          Ich bin gespannt.

          Ich verspreche mir davon nicht soviel, aber versuchen werde ich es.

          LG Marvin

          1. Kurzes Update: Auch mit Port 9300 ist das Verhalten unverändert.

            Über die Ausgabe auf der Konsole habe ich jetzt folgendes Szenario:

            Sobald sich der 1. Client anmeldet, werden mir die beiden Resource #4 und Resource #5 ausgegeben. Resource #4 wird der Server selber sein, Resource #5 ist der 1. Client. Sobald sich der 2. Client anmeldet, kommt die Resouce #6 dazu.

            Wenn ich jetzt auf dem 2. Client F5 drücke, dann wird erstmal die Resource #7 hinzugefügt, die Resource #6 aber nicht gelöscht.

            Nach ca. 10 Sekunden wird erst die Resource #6 gelöscht, aber wohl damit auch automatisch die Verbinmdung zu Resource #7 geschlossen.

            Eigentlich hätte also die Resource #6 direkt gelöscht werden sollen, zusammen mit dem Erstellen der Resource #7.

            LG Marvin

  2. var wsUri = "ws://10.10.10.123:9000/scripts/server.php";

    Du schreibst andererseits dass server.php auf der KdoZeile gestartet wurde. Dann sollte das auch am Port 9000 lauschen, bitte prüfe das mal mit netstat -an.

    Sofern das der Fall ist, sähe der Uri so aus: ws://10.10.10.123:9000 ganz einfach also weil es den Client gar nicht interessiert wie Dein Script auf dem Server heißt und in welchem Verzeichnis es gestartet wurde.

    Bring Dein Serverscript dazu, dass es entweder loggt oder Ausgaben auf der Konsole erzeugt, dann kannst Du die Anwendung auch erfolgreich entwickeln. MfG

    1. var wsUri = "ws://10.10.10.123:9000/scripts/server.php";

      Du schreibst andererseits dass server.php auf der KdoZeile gestartet wurde. Dann sollte das auch am Port 9000 lauschen, bitte prüfe das mal mit netstat -an.

      Sofern das der Fall ist, sähe der Uri so aus: ws://10.10.10.123:9000 ganz einfach also weil es den Client gar nicht interessiert wie Dein Script auf dem Server heißt und in welchem Verzeichnis es gestartet wurde.

      Der Server lauscht auf Port 9000. Dass ich im Client auch noch das Server-Script angebe, interpretiere ich so, als dass im Server-Script "reusable port" auf "ja" gesetzt wird, also theoretisch mehrere Script auf demselben Port lauschen könnten und durch die Angabe des Scriptnamens geklärt wird, an welches Script die Message gesendet werden soll.

      Bring Dein Serverscript dazu, dass es entweder loggt oder Ausgaben auf der Konsole erzeugt, dann kannst Du die Anwendung auch erfolgreich entwickeln.

      Mein Server-Script erzeugt (loggt) Ausgaben auf der Konsole (Kommando-Box). Leider haben mir meine bisherigen Debug-Ausgaben nicht weitergeholfen. Sobald ich an einem Client F5 gedrückt habe, muss ich immer erstmal 10 Sekunden warten, bevor ich überhaupt wieder eine Verbindung aufgebaut bekomme. Dennoch wird dann die Verbindung auch immer nach 10 Sekunden wieder getrennt. Ohne Neuladen der Seite läuft die Verbindung über Tage hinweg fehlerfrei.

      LG Marvin

      1. Nochmal:

        var wsUri = "ws://10.10.10.123:9000/scripts/server.php";

        Dieser URI kann nicht stimmen. Da hast Du irgendwo einen Konflikt. z.B. wenn 2 Prozesse versuchen auf demselben Port zu lauschen, oder es wird per http versucht, weitere Instanzen des Serverscripts zu starten.

        Lass Deinen Eigenen Server mal beiseite und arbeite Dich zunächst ein mit

        var ws = new WebSocket("wss://echo.websocket.org");

        womit Du alles ganz ausführlich und in Ruhe testen kannst, Schritt für Schritt. Am Ende wird es auch mit dem eigenen Server laufen Du wirst sehen. MfG

        1. Ok, da ich sowieso nur 1 Websocket-Script laufen haben werde, habe ich die URI angepasst und den Pfad entfernt. Hilft aber noch nicht beim Problem. Ich denke, das Problem ist, dass das Server-Script erst nach 10 Sekunden merkt, dass der Client nicht mehr da ist, der Reload aber natürlich viel schneller ist. Dann wird nach 10 Sekunden die Verbindung getrennt und damit auch die neue. Ich weiß nur nicht, warum das Server-Script das nicht mitbekommt bzw. was ich tun muss, damit die Verbindung beim F5 direkt getrennt wird.

          LG Marvin

          1. Ich denke, das Problem ist, dass das Server-Script erst nach 10 Sekunden merkt, dass der Client nicht mehr da ist,

            Dann guck doch einfach mal, wie die anderen das machen, z.B. hier, für connect und disconnect gibt es die Möglichkeit, Handler sowohl client wie auch serverseitig zu definiern. Die Events schlagen sofort zu, 10 Sekunden Verzögerung ist ungewöhnlich.

            Nutze den Echo-Server, diesen ersten Schritt zu tun ist in Deinem Fall die beste Empfehlung zur Einarbeitung in das Thema. MfG

            PS: Wie verhält sich denn Dein Client mit dem echo-Server?

            1. Dann guck doch einfach mal, wie die anderen das machen, z.B. hier, für connect und disconnect gibt es die Möglichkeit, Handler sowohl client wie auch serverseitig zu definiern. Die Events schlagen sofort zu, 10 Sekunden Verzögerung ist ungewöhnlich.

              Client-seitig sehe ich keinen Unterschied. Ich gehe sogar eher noch auf Nummer sicher und führe extra bei einem onbeforeunload-event nochmal ein websocket.close() durch.

              Nutze den Echo-Server, diesen ersten Schritt zu tun ist in Deinem Fall die beste Empfehlung zur Einarbeitung in das Thema. MfG

              PS: Wie verhält sich denn Dein Client mit dem echo-Server?

              Mit dem echo-Server funktioniert der Reload und automatische Reconnect korrekt. Damit wäre zwar bewiesen, dass es am Server-Script liegt, aber das war mir eigentlich auch vorher schon klar. Die 10 Sekunden könnten eventuell von socket_select($changed, $null, $null, 0, 10); kommen?

              Man müsste jetzt an das Server-Script vom echo-Server sehen können.

              LG Marvin

  3. Hi,

    ich hab jetzt nicht alles gelesen, aber das Problem mit dem 10sec delay scheint ein Timeout zu sein.

    Wenn man sich deinen socket_select-Aufruf anguckt, könnte es sein, dass PHP den Socket mit der geschlossenen Verbindung im dritten Socket-Set verstauen würde. Wenn das der Fall ist, oder generell wenn irgendwelche Fehler aufterten, reagierst du darauf generell nicht.

    Von WebSockets hab ich selbst keine Ahnung, also kann ich nicht wirklich beurteilen ob das der Fehler sein könnte.

    MfG kackb00n