📜 ⬆️ ⬇️

Game client download tracking system. Part 2

In this article we will talk about the GeoIP service, which determines the geodata by IP address of the request, web sockets, polling server implementations, AngularJS, Highcharts and will conduct a brief analysis of the game client download tracking system.



Game client download tracking system. Part 1.

GeoIP service
')
Part of the project was the development of a service for determining geodata. He had to determine the geodata by IP-address of the request, as well as find geo-information by coordinates. There are many similar online services available, but we needed a locally running and monitored service that can handle a large number of simultaneous and daily requests. As a result, the DB-IP base was acquired and its service was written. The base of this service contains about 7 million address ranges (both IPv4 and IPv6) and is updated monthly. The database is distributed in CSV format.

Already used in our project, MongoDB perfectly suited for storing and monthly updating of the database of IP addresses. It was decided to distribute the ranges of IP-addresses for different collections. As a result, the search does not occur from all the data, but only from a specific collection, which together with the indexing gave a good search speed: 2–8 milliseconds.

In addition, it was necessary to find geodata by geographic coordinates. This time, MongoDB was pleased with the opportunity to easily and conveniently work with coordinates and geographical objects. GeoJson2DGeographicCoordinates and the GeoSpatialSpherical index were used to store the coordinates. The base is able to find the nearest data by coordinates, for which it suffices to specify the source coordinates and the distance to search in measures.

Search example:

var queryList = new List<IMongoQuery> { Query.Near("Coordinates", lng, lat, distanceInMeters, true) …    }; MongoCursor<HighChartRegion> result = GeoDataCollection .Find(Query.And(queryList)) 


Web sockets (WebSockets), implementation of polling server

Since the system was designed for corporate use, it was not necessary to support all web browsers. This allowed us to use the WebSocket technology without any problems. The technology itself is not new, but has not yet received widespread use, and not all browsers are supported by it and there are not so many ready-made solutions for .net. Of the most popular - Supersocket , Fleck , XSockets , SignalR , and each has its advantages and disadvantages. Alternatively, you can use the WebSocket- class from Microsoft to write your socket server.

When choosing, you should pay attention to the possibility of working under the IIS process, as well as to support secure connection (WSS). For example, SignalR requires IIS8, and XSockets is a paid enterprise solution.

The idea of ​​use is that instead of periodic requests to the server, respectively, initialization of new and new requests, a connection is established and data is exchanged between the client and the server. In addition, the use of web sockets allows us to solve the problem when several clients often request the same data, thereby loading the server in complex calculations.
In our case, we used a Fleck socket server, on the basis of which we wrote a wrapper - PollerWebSocketServer. PollerWebSocketServer can handle incoming requests, as well as register subscribers and send push notifications.

For the client to communicate with the server, its own communication protocol was developed so that the client could request various data, subscribe to receive data or unsubscribe. The WebSocketMessage class handling the request from the client looks like this:

 public class WebSocketMessage : RequestFilters, IWebSocketMessage { [Json("id")] public string Id { get; set; } [JsonSkip] public Guid SocketId { get; set; } [Json("command")] public WebSocketCommand Command { get; set; } [Json("type")] public WebSocketCommandType CommandType { get; set; } [JsonSkip] public string Hash { get { return GetHash(); } } 


Hash is created based on the requested data (filters) and is used to track unique requests. A group of such subscribers is sent once generated data.
In response to such a request, the ID and the command of the request, the time of the next push notification and data are returned.

The initialization of the PollerWebSocketServer is as follows:

 var wss = new WebSocketServer(Settings.SocketPort, Settings.Certificate != null); wss.SupportedSubProtocols = new[] { "map", "stats"}; wss.ServerSettings = SettingsProvider.WebSockerServerSettings; if (SettingsProvider.Certificate != null) wss.Certificate = SettingsProvider.Certificate; var dataProvider = new WebSocketJsonDataProvider(); _pollerWebSocketServer = new PollerWebSocketServer<WebSocketMessage>(wss, dataProvider, OnProblemDetected); 

where OnProblemDetected is a handler that is called if the server has detected a problem situation.

To start the server, you need to set delegates for three events:

 _webSocketServer.Start(socket => { socket.OnOpen = () => OpenSocket(socket); socket.OnClose = () => CloseSocket(socket); socket.OnMessage = message => ProcessSocketMessage(socket, message); }); 

Of interest is the handler WebSocketMessage:

  private void ProcessSocketMessage(IWebSocketConnection socket, string message) { if (!IsValidOrigin(socket)) return; TM wsMessage; try { wsMessage = message.FromJsonStr<TM>(); } catch (Exception ex) { ReturnError(socket, ex, "Fail to parse socket message"); return; } if (wsMessage == null) { ReturnError(socket, null, "Message is null"); return; } switch (wsMessage.CommandType) { case WebSocketCommandType.Subscribe: wsMessage.SocketId = socket.ConnectionInfo.Id; _subscriptions.AddOrUpdate(wsMessage.Id ?? Guid.NewGuid().ToString("N"), wsMessage, (guid, connection) => wsMessage); break; case WebSocketCommandType.Unsubscribe: Unsubscribe(wsMessage); return; } var data = _webSocketJsonDataProvider.GetJsonData(wsMessage); var response = new WebSocketResponseMessage(wsMessage.Id, wsMessage.Command, data, NextPollingInSec); socket.Send(response.JsonData); } 

Processing the mailing event essentially groups all subscribers by Hash, for each unique request it receives data from the JSON provider and sends it to subscribers:

 private void OnPollingTimerExecution() { var hashGroupedSubs = _subscriptions.GroupBy(s => s.Value.Hash); foreach (var hashGroup in hashGroupedSubs) { var requestMessage = hashGroup.First().Value; var data = _webSocketJsonDataProvider.GetJsonData(requestMessage); foreach (var msg in hashGroup) { IWebSocketConnection socket; if (_activeSockets.TryGetValue(msg.Value.SocketId, out socket)) { var response = new WebSocketResponseMessage(msg.Value.Id, requestMessage.Command, data, NextPollingInSec); socket.Send(response.JsonData); } } } } 


WebSocket-north can be launched either in a separate application or service, or under a web service. Note that the protocol works on top of the TCP protocol and the instance needs a separate port. When running under a web service, you need to pay attention to the features of restarting the web service (disable restarting with overlapping for the pool) and correctly handle restarting the WebSocket server or synchronization between processes.

As mentioned above, each message has a specific format.
Example:
 { id: “messageId”, command: 100, type: 1, nextDataTime: 30, data: “...”, ... }. 

Consider more:
â—Ź command is an internal command to perform an action or get data. It is sent both from the client and from the server;
● id - a unique identifier of the message that is sent from the client and the server always sends it back when responding. This is a very important part, as you can easily identify each message and not be afraid that a request with a specific command will cause a “foreign” handler. Thus, there may be several subscribers to the same team, and they will not conflict;
â—Ź type - type of request. It can take 3 values: 0 - executed once, 1 - subscribe to receive updated data, 2 - unsubscribe from receiving data (transmitted from the client);
â—Ź nextDataTime - time until the next data update. Used to display the countdown in the form of a progress bar. It comes only from the server;
â—Ź data - data specific to each command;
â—Ź Additional fields, such as countryCode, appIds, fromTime, toTime etc, which act as filters. Sent from the client.

AngularJS and the use of the Highcharts library on the client side

To work with web sockets on the client side, the WS service is used, a wrapper over the standard WebSocket- class.
The connection is established at the start of the application. At the same time, the service subscribes to standard web socket events (open, close, message, error) and then reacts to them, implementing its own logic of operation. Also, when the application starts, the dictionaries needed for further work (countries, regions, etc.) are loaded.

If the connection is broken, the service will automatically try to restore it with a certain timeout.
The service provides 4 basic methods: connect, disconnect, createSubscriber and get. If the functionality of the first two methods is clear from the names, then the last two will take a closer look. The createSubscriber method allows you to create a subscriber object that can subscribe to receive data from a given command based on certain filters, unsubscribe from receiving data, and also change the set of filters.
Call example:

  WS.createSubscriber(command, filters, callback); 


The callback is executed when new data arrives (only data from the data field is passed to this function).
It would be possible to return a promise, but since it cannot be resolved several times, this option is not suitable. But you can use the notify method, which will be called when the data arrives. Perhaps in the future we will try to use this approach.
There is also an important nuance. When you disconnect and successfully reconnect, the WS service will automatically re-subscribe all active subscribers.
The get method allows you to get data once, emulating the get $ http method of the service, but within a socket connection. Returns promise:

WS.get (command, filters) .then (function (data) {...});

From the interesting moments on sockets on the client it is all.

To display charts, we use the Highcharts library and Highmaps for cards. A map of the world, as well as detailed maps of countries are used to display detailed information.
Components for maps are wrapped in directives that display data.

The loading of geoJSON data for maps is rendered into a separate service, the methods of which return promises.
Call examples:

  GeoJSON.getCountry(countryCode).then(function(data) { ... }); GeoJSON.getWorld(isHighRes).then(function(data) { ... }); 


The service will automatically build a link to retrieve data and upload it. Exceptions are also regulated here: for example, for France we use a more detailed map, the link for which differs from the standard scheme.

There was a need to display on the map of individual countries a large number of points (1000 and more). But rendering in this case is very slow, as the points are added to the DOM, and this is not always fast. The amount of RAM used by the browser, in this case, also greatly increases, so temporarily abandoned the idea. Perhaps to accelerate it is worth using canvas, but this imposes certain restrictions.

Brief system analysis

The tracker processes ~ 6–7 thousand requests per minute and ~ 14 thousand requests at peak load, with geodata being determined for each request. The time of geodata determination is 2–8 milliseconds. Data in MongoDB is stored cyclically for a month. The service displays the status of downloads in real time with periodic updates. In the event of mass errors, they are displayed on the map as an indication of the color of the country. You can also view statistics in detail on areas and types of errors for a selected period of time and specified criteria. When abnormalities occur, notifications are automatically sent and a warning is displayed.

How it looks on the client:





Source: https://habr.com/ru/post/267729/


All Articles