Schreiben eines Richtlinienservers in C ++ für Unity3d



Warum brauchen wir einen Policy Server?


In der Einheit , beginnend mit Version 3.0, für die Montage unter der Web Player verwendet Sicherheitsmechanismen ähnlich denen im Adobe Flash Player verwendet. Das Wesentliche besteht darin, dass der Client beim Zugriff auf den Server nach "Erlaubnis" fragt. Wenn der Server keine "Erlaubnis" erteilt, versucht der Client nicht, eine Verbindung zu ihm herzustellen. Diese Einschränkungen gelten für den Zugriff auf Remoteserver über die WWW- Klasse und mithilfe von Sockets. Wenn Sie mithilfe des Restprotokolls eine Anforderung von Ihrem Client an einen Remoteserver stellen möchten, benötigen Sie eine spezielle XML-Datei im Stammverzeichnis der Domäne. Es sollte crossdomain.xml heißen und das folgende Format haben:


Vor der Anforderung lädt der Client die Sicherheitsrichtliniendatei herunter, überprüft sie und erfüllt die von Ihnen gestellte Anforderung, nachdem festgestellt wurde, dass alle Domänen zulässig sind.

Wenn Sie eine Verbindung zum Remote-Server über Sockets (tcp / udp) herstellen müssen, fordert der Client vor dem Herstellen der Verbindung den Server an Port 843 auf, eine Sicherheitsrichtliniendatei zu erhalten, in der beschrieben wird, mit welchen Ports und Domänen Sie eine Verbindung herstellen können:

"

Wenn die Clientdaten nicht alle Parameter (Domäne, Port) erfüllen, löst der Client eine SecurityException aus und versucht nicht, eine Verbindung zum Server herzustellen.

Dieser Artikel wird sich auf das Schreiben eines Servers konzentrieren, der Sicherheitsrichtliniendateien ausgibt. In Zukunft werde ich ihn Richtlinienserver nennen.

Wie soll der Policy Server funktionieren?


Das Serverbetriebsschema ist einfach:

  1. Der Server wird gestartet und überwacht Port 843 mithilfe des TCP-Protokolls. Es ist möglich, den Port Security.PrefetchSocketPolicy () zu überschreiben.
  2. Der Client stellt über das TCP-Protokoll eine Verbindung zum Server her und sendet XML mit einer Anforderung für die Sicherheitsrichtliniendatei:

  3. Der Server analysiert die Anforderung und sendet den XML-Client mit der Sicherheitsrichtlinie

In der Praxis macht das Parsen einer Anfrage keinen Sinn. Der Wert hat die Zeit, die der Client wartet, bis die Sicherheitsrichtliniendatei empfangen wird, da dies die Verzögerung vor dem Herstellen einer Verbindung mit dem Zielport erhöht. Wir können den Prozess des Servers ändern und dem Client unmittelbar nach der Verbindung eine Sicherheitsrichtliniendatei übergeben.

Was ist schon da?


Derzeit gibt es einen Server in Java + Netty , Quellcode mit Anweisungen und Jar . Eine seiner Hauptschwächen ist die Abhängigkeit. Im Allgemeinen ist die Bereitstellung von jre auf einem Linux-Server kein Problem, aber Spieleentwickler sind häufig Client-Programmierer, die so wenig Körperbewegungen wie möglich ausführen möchten, zumal sie jre nicht installieren und später verwalten möchten. Aus diesem Grund wurde beschlossen, einen Richtlinienserver in C ++ zu schreiben, der als native Anwendung auf einem Linux-Computer funktioniert.

Ein in C ++ geschriebener Policy Server sollte in seiner Leistung nicht schlechter sein als der alte, idealerweise sollte er ein viel besseres Ergebnis liefern. Die wichtigsten Leistungskennzahlen sind: die Zeit, die der Client mit dem Warten auf die Sicherheitsrichtliniendatei verbringt, und die Anzahl der Clients, die gleichzeitig Sicherheitsrichtliniendateien empfangen können, was im Wesentlichen auch auf das Zeitlimit für die Richtliniendatei zurückzuführen ist.

Zum Testen habe ich dieses Skript verwendet . Es funktioniert wie folgt:

  1. Berechnet den durchschnittlichen Ping zum Server
  2. Startet mehrere Threads (die Nummer ist im Skript angegeben)
  3. In jedem Thread wird eine Richtliniendatei vom Richtlinienserver angefordert.
  4. Wenn die Richtliniendatei mit der erwarteten übereinstimmt, wird für jede Anforderung die Wartezeit angegeben
  5. Druckt die Ergebnisse auf die Konsole. Wir interessieren uns für die folgenden Werte: minimale Latenz, maximale Latenz, durchschnittliche Latenz und dieselben Parameter ohne Ping

Das Skript ist in Ruby geschrieben, aber da der Standard-Ruby-Interpreter keine Threads auf Betriebssystemebene unterstützt, habe ich jruby zum Arbeiten verwendet . Am bequemsten ist es, rvm zu verwenden . Der Befehl zum Ausführen des Skripts sieht folgendermaßen aus:

rvm jruby do ruby test.rb

Testergebnisse für Policy Server geschrieben in Java + Netty :
Durchschnittliche ms245
Minimum, ms116
Maximale ms693

Was brauchst du


Im Wesentlichen besteht die Aufgabe darin, einen Daemon in C ++ zu schreiben, der beim Verbinden von Clients mehrere Ports überwachen, einen Socket erstellen, Textinformationen in den Socket kopieren und diesen schließen kann. Es ist ratsam, so wenig Abhängigkeiten wie möglich zu haben, und wenn sie existieren, sollten sie sich in den Repositorys der gängigsten Linux-Distributionen befinden. Wir werden den c ++ 11-Standard zum Schreiben von Code verwenden. Als Mindestmenge an Bibliotheken nehmen wir:


Ein Port - ein Thread


Die Struktur der Anwendung ist recht einfach: Sie benötigen Funktionen für die Arbeit mit Befehlszeilenparametern, Klassen für die Arbeit mit Streams, Funktionen für die Arbeit mit einem Netzwerk und Funktionen für die Arbeit mit Protokollen. Dies sind einfache Dinge, die kein Problem sein sollten, daher werde ich nicht näher darauf eingehen. Den Code finden Sie hier . Die Problemstelle ist die Organisation der Bearbeitung von Kundenanfragen. Die einfachste Lösung ist, alle Daten nach dem Anschließen des Client-Sockets zu senden und den Socket sofort zu schließen. Das heißt Der Code, der für die Verarbeitung der neuen Verbindung verantwortlich ist, sieht folgendermaßen aus:

void Connector::connnect(ev::io& connect_event, int )
{
	struct sockaddr_in client_addr;
	socklen_t client_len = sizeof(client_addr);
	int client_sd;
	client_sd = accept(connect_event.fd, (struct sockaddr *)&client_addr, &client_len);
	if(client_sd < 0)
		return;
	const char *data = this->server->get_text()->c_str();
	send(client_sd, (void*)data, sizeof(char) * strlen(data), 0);
	shutdown(client_sd, 2);
	close(client_sd);
}

Als ich versuchte, auf einer großen Anzahl von Threads (300, je 10 Verbindungen) zu testen, konnte ich nicht warten, bis das Testskript fertig war. Daraus können wir schließen, dass diese Entscheidung nicht zu uns passt.

Async


Das Übertragen von Daten über das Netzwerk ist zeitaufwändig. Es ist offensichtlich, dass der Prozess des Erstellens eines Client-Sockets und der Prozess des Sendens von Daten voneinander getrennt werden müssen. Es wäre auch schön, Daten in mehreren Threads anzugeben. Eine gute Lösung ist die Verwendung von std :: async , das im async C ++ 11-Standard enthalten ist. Der Code, der für die Behandlung der neuen Verbindung verantwortlich ist, sieht folgendermaßen aus:

void Connector::connnect(ev::io& connect_event, int )
{
	struct sockaddr_in client_addr;
	socklen_t client_len = sizeof(client_addr);
	int client_sd;
	client_sd = accept(connect_event.fd, (struct sockaddr *)&client_addr, &client_len);
	std::async(std::launch::async, [client_addr, this](int client_socket) {
		const char * data = this->server->get_text()->c_str();
		send(client_socket, (void*)data, sizeof(char) * strlen(data), 0);
		shutdown(client_socket, 2);
		close(client_socket);
	}, client_sd);
}

Der Nachteil dieser Entscheidung ist die mangelnde Kontrolle über die Ressourcen. Mit minimalen Eingriffen in den Code erhalten wir die Möglichkeit, Daten asynchron an den Client zu senden, während wir den Prozess der Generierung neuer Threads nicht steuern können. Das Erstellen eines neuen Threads ist für das Betriebssystem teuer, und eine große Anzahl von Threads kann die Serverleistung beeinträchtigen.

Pub / Sub


Eine geeignete Lösung für diese Aufgabe ist das Publisher-Subscriber-Pattern. Das Serverbetriebsschema sollte folgendermaßen aussehen:
  • Mehrere Herausgeber, einer für jeden Port, speichern die Kennungen der Client-Sockets, an die die Sicherheitsrichtliniendatei gesendet werden muss, im Puffer
  • Mehrere Abonnenten erhalten Socket-IDs aus dem Puffer, kopieren die Sicherheitsrichtliniendatei in sie und schließen den Socket.

Die Warteschlange eignet sich als Puffer, der als erster eine Verbindung zum Server herstellt - der als erster eine Richtliniendatei erhält. In der Standard-C ++ - Bibliothek ist ein vorgefertigter Warteschlangencontainer vorhanden, der jedoch für uns nicht funktioniert, da eine thread-sichere Warteschlange erforderlich ist. Gleichzeitig muss die Operation zum Hinzufügen eines neuen Elements nicht blockierend sein, während die Leseoperation blockierend sein muss. Das heißt, wenn der Server gestartet wird, werden mehrere Abonnenten gestartet, die warten, bis die Warteschlange leer ist. Sobald dort Daten angezeigt werden, werden ein oder mehrere Handler ausgelöst. Publisher schreiben Socket-IDs asynchron in diese Warteschlange.

Beim Googeln habe ich einige fertige Implementierungen gefunden:
  1. https://github.com/cameron314/concurrentqueue .
    In diesem Fall interessiert uns die Blockingconcurrentqueue , die einfach als Header-H-Datei in das Projekt kopiert wird. Praktischerweise und es gibt keine Abhängigkeiten. Diese Lösung hat folgende Nachteile:
    • Es gibt keine Methoden, um Abonnenten zu stoppen. Sie können sie nur stoppen, indem Sie der Warteschlange Daten hinzufügen, die den Abonnenten signalisieren, dass sie die Arbeit beenden müssen. Dies ist sehr unpraktisch und kann zu einem Deadlock führen.
    • Es wird von einer Person unterstützt, Commits sind in letzter Zeit selten genug aufgetreten

  2. TBB gleichzeitige Warteschlange .
    Multithread-Warteschlange aus der TBB- Bibliothek (Threading Building Blocks). Die Bibliothek wird von Intel entwickelt und gepflegt und bietet alles, was wir brauchen:
    • Block Read from Queue
    • Nicht blockierender Queue-Eintrag
    • Möglichkeit, blockierte Streams zu stoppen, während auf Daten gewartet wird

    Unter den Minuspunkten kann angemerkt werden, dass eine solche Lösung die Anzahl von Abhängigkeiten erhöht, d.h. Endbenutzer müssen tbb auf ihrem Server installieren. In den gängigsten Linux-Repositorys kann tbb über den Paket-Manager des Betriebssystems installiert werden, sodass es keine Probleme mit Abhängigkeiten geben sollte.

Daher sieht der Code zum Erstellen einer neuen Verbindung folgendermaßen aus:

void Connector::connnect(ev::io& connect_event, int )
{
	struct sockaddr_in client_addr;
	socklen_t client_len = sizeof(client_addr);
	int client_sd;
	client_sd = accept(connect_event.fd, (struct sockaddr *)&client_addr, &client_len);
	clients_queue()->push(client_sd);
	this->handled_clients++;
}

Client-Socket-Verarbeitungscode:

void Handler::run()
{
	LOG(INFO) << "Handler with thread id " << this->thread.get_id() << " started";
	while(this->is_run)
	{
		int socket_fd = clients_queue()->pop();
		this->handle(socket_fd);
	}
	LOG(INFO) << "Handler with thread id " << this->thread.get_id() << " stopped";
}

Der Code für die Arbeit mit der Warteschlange:

void ClientsQueue::push(int client)
{
	if(!this->queue.try_push(client))
		LOG(WARNING) << "Can't push socket " << client << " to queue";
}
int ClientsQueue::pop()
{
	int result;
	try
	{
		this->queue.pop(result);
	}
	catch(...)
	{
		result = -1;
	}
	return result;
}
void ClientsQueue::stop()
{
	this->queue.abort();
}

Den Code für das gesamte Projekt mit Installationsanleitung finden Sie hier . Das Ergebnis eines Testlaufs mit zehn Thread-Handlern:
Durchschnittliche ms151
Minimum, ms100
Maximale ms1322

Zusammenfassung


Vergleich der Ergebnistabelle
Java + NettyC ++ Pub / Sub
Durchschnittliche ms245151
Minimum, ms116100
Maximale ms6931322

Referenzen:

PS: Momentan macht Unity Web Player schwierige Zeiten durch, da npapi in Top-Browsern geschlossen wurde. Aber wenn jemand anderes es benutzt und den Server unter Linux laufen lässt, dann kann er diesen Server benutzen. Ich hoffe, es wird Ihnen nützlich sein. Besonderer Dank geht immer an die Leute , die Angst haben, den Artikel zu illustrieren.

Jetzt auch beliebt: