Для вашей задачи не нужны парсеры, ведь если мы заглянем в "Средства разработчика" (F12 в браузере), то увидим там следующую картину

Заметили один постоянно выполняющийся запрос? Вот он и содержит в себе все данные, ведь это WebSocket, к которому нам достаточно лишь подключиться и запросить всю необходимую информацию.
В C# для таких целей есть специальный класс, зовется ClientWebSocket. Я однажды уже давал ответ на подобный вопрос, можно увидеть его здесь, сейчас я на его основе постараюсь реализовать клиент именно для указанного сайта, где конечной целью будет чтение сообщений с чата.
И так, приступим:
Создадим пустой класс, назовем его к примеру RUSTreaperClient и унаследуем от IDisposable, ведь мы делаем некую обертку над ClientWebSocket, а он должен быть закрыт.
Создадим внутри приватное поле ClientWebSocket и реализуем метод Dispose.
Получим в итоге следующее:
public class RUSTreaperClient : IDisposable
{
private readonly ClientWebSocket client = new();
public void Dispose() => client.Dispose();
}
Теперь давайте реализуем 3 метода:
Соединения:
public async Task ConnectAsync(CancellationToken cancellationToken = default)
{
await client.ConnectAsync(uri, cancellationToken);
}
Отправки запроса:
public Task SendAsync(string message, CancellationToken cancellationToken = default)
{
if (!string.IsNullOrEmpty(message))
{
ArraySegment<byte> arraySegment = new(Encoding.UTF8.GetBytes(message));
return client.SendAsync(arraySegment, WebSocketMessageType.Text, true, cancellationToken);
}
return Task.CompletedTask;
}
Чтение ответа:
public async Task<string> ReadAsync(CancellationToken cancellationToken = default)
{
if (client is { State: WebSocketState.Open })
{
ArraySegment<byte> bytesReceived = new ArraySegment<byte>(new byte[1024]);
WebSocketReceiveResult result = await client.ReceiveAsync(bytesReceived, cancellationToken);
return Encoding.UTF8.GetString(bytesReceived.Array, 0, result.Count);
}
return "Closed";
}
Имея это, давайте попробуем подключиться
static async Task Main()
{
using var client = new RUSTreaperClient();
await client.ConnectAsync();
Console.WriteLine(await client.ReadAsync());
}
Но получаем вдруг ошибку
System.Net.WebSockets.WebSocketException: "The server returned status code '400' when status code '101' was expected."
400 - это Bad Request, то есть нашему запросу не хватает неких данных, обычно это заголовки. Посмотрим опять в браузере на подключение и пытаемся методом подбора подставить заголовок. Находим, что сервер требует обязательно Origin, ну так давайте его добавим нашему клиенту в конструкторе класса:
public RUSTreaperClient()
{
client.Options.SetRequestHeader("Origin", "https://www.rustreaper.com");
}
Пробуем повторно соединится и видим ответ от сервера, который имеет вид
0{"sid":"HU29rYgkIM8s1Ss5BmLY","upgrades":[],"pingInterval":25000,"pingTimeout":5000}
На вид простой JSON (с мусором), в котором нас интересует pingInterval, ведь это значение, в течении которого сервер ждет от нас пинг запрос.
Давайте приведем ответ в удобный для нас вид:
Создадим класс, я назову его Session, пусть содержит id и нужный timeout:
public class Session
{
[JsonPropertyName("sid")]
public string Id { get; set; }
[JsonPropertyName("pingInterval")]
public int Timeout { get; set; }
}
Далее допишем метод подключения, пусть дополнительно десериализует первый ответ сервера и заносит это в приватное поле класса:
public async Task ConnectAsync(CancellationToken cancellationToken = default)
{
await client.ConnectAsync(uri, cancellationToken);
var response = await ReadAsync(cancellationToken);
session = JsonSerializer.Deserialize<Session>(response.Substring(1));
Console.WriteLine($"Сессия {session.Id}, Ping/Pong раз в {session.Timeout}ms.");
}
Отлично, получили данные, десериализовали их, осталось дело за малым, а именно начать непрерывное чтение данных, а также Ping запрос отправлять.
Чтение данных фоном. Создадим еще одну асинхронную задачу, пусть в цикле бесконечно вызывает метод чтения ответа:
private async Task ReadMessages(CancellationToken cancellationToken = default)
{
while (!cancellationToken.IsCancellationRequested)
{
var message = await ReadAsync(cancellationToken);
Console.WriteLine(message);
}
}
Ping запрос. Цель - отправить сообщение с цифрой 2 раз в N сек, где N - значение с сервера.
private async Task PingMessage(CancellationToken cancellationToken = default)
{
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(session.Timeout);
await SendAsync("2", cancellationToken);
}
}
Допишем метод соединения, добавив в конец запуск этих двух задач:
var pingMessage = PingMessage(cancellationToken);
var readMessages = ReadMessages(cancellationToken);
await Task.WhenAll(new[] { pingMessage, readMessages });
На данном этапе, если попытаться подключиться, то мы увидим нечто такое:
Сессия -5iYNPHK-NmTzGn_BmMQ, Ping/Pong раз в 25000сек.
40
3
3
Это означает, что соединение есть, оно стабильно, сервер ждет указаний.
Осталось нам подключиться к чату. Для этого сервер просит отправить 2 запроса:
40/chat - цифра, это то, что отправляет нам сервер в последующих ответах после инициализации, можно (и наверно нужно) брать от туда, но я лично не стал, ибо она всегда 40. Ну а chat - это некая команда инициализации.
42/chat,12["join",["english","russian"]] - 42 - эта цифра всегда больше цифры "инициализации" на 2, 12 - это скажем так "вкладка чата" (начинается с 0), join - команда подключения, ну а языки - это вроде как какие языки слушать.
Стоит учесть тот факт, что между первой и 2-й командой сервер ожидает задержу.
В итоге у нас получается новая задача:
private async Task JoinToChat(string[] languages, CancellationToken cancellationToken = default)
{
var data = JsonSerializer.Serialize(new object[] { "join", languages });
await SendAsync("40/chat", cancellationToken);
await Task.Delay(1000);
await SendAsync($"42/chat,0{data}");
}
Я как видите, не стал тут мудрить, делать каналы и ожидать ответа от сервера (задержка), это оставлю на вас. Не забываем вызвать этот Task в том же методе подключения
await JoinToChat(new[] { "english", "russian" }, cancellationToken);
Собственно, вот и все, сервер нам успешно отдает данные чата. Если нужна еще информация, то отправляем ему аналогичные запросы (например 40/general). Весь код получается следующий:
public class Session
{
[JsonPropertyName("sid")]
public string Id { get; set; }
[JsonPropertyName("pingInterval")]
public int Timeout { get; set; }
}
public class RUSTreaperClient : IDisposable
{
private readonly Uri uri = new Uri("wss://www.rustreaper.com/socket.io/?EIO=3&transport=websocket");
private readonly ClientWebSocket client = new();
private Session session;
public RUSTreaperClient()
{
client.Options.SetRequestHeader("Origin", "https://www.rustreaper.com");
}
public async Task ConnectAsync(CancellationToken cancellationToken = default)
{
await client.ConnectAsync(uri, cancellationToken);
var response = await ReadAsync(cancellationToken);
session = JsonSerializer.Deserialize<Session>(response.Substring(1));
Console.WriteLine($"Сессия {session.Id}, Ping/Pong раз в {session.Timeout}сек.");
await JoinToChat(new[] { "english", "russian" }, cancellationToken);
var pingMessage = PingMessage(cancellationToken);
var readMessages = ReadMessages(cancellationToken);
await Task.WhenAll(new[] { pingMessage, readMessages });
}
public Task SendAsync(string message, CancellationToken cancellationToken = default)
{
if (!string.IsNullOrEmpty(message))
{
ArraySegment<byte> arraySegment = new(Encoding.UTF8.GetBytes(message));
return client.SendAsync(arraySegment, WebSocketMessageType.Text, true, cancellationToken);
}
return Task.CompletedTask;
}
public async Task<string> ReadAsync(CancellationToken cancellationToken = default)
{
if (client is { State: WebSocketState.Open })
{
ArraySegment<byte> bytesReceived = new ArraySegment<byte>(new byte[1024]);
WebSocketReceiveResult result = await client.ReceiveAsync(bytesReceived, cancellationToken);
return Encoding.UTF8.GetString(bytesReceived.Array, 0, result.Count);
}
return "Closed";
}
private async Task ReadMessages(CancellationToken cancellationToken = default)
{
while (!cancellationToken.IsCancellationRequested)
{
var message = await ReadAsync(cancellationToken);
Console.WriteLine(message);
}
}
private async Task PingMessage(CancellationToken cancellationToken = default)
{
while (!cancellationToken.IsCancellationRequested)
{
await Task.Delay(session.Timeout);
await SendAsync("2", cancellationToken);
}
}
private async Task JoinToChat(string[] languages, CancellationToken cancellationToken = default)
{
var data = JsonSerializer.Serialize(new object[] { "join", languages });
await SendAsync("40/chat", cancellationToken);
await Task.Delay(1000);
await SendAsync($"42/chat,0{data}");
}
public void Dispose() => client.Dispose();
}
class Program
{
static async Task Main()
{
using var client = new RUSTreaperClient();
await client.ConnectAsync();
}
}
Результат:

Что поправить:
- Как я уже писал ранее, первое число 40, приходит ответом от сервера, стоит его также как и интервал взять и использовать.
- Ответ от сервера может быть в разы больше, чем указанный буфер в размере
1024.
- Если соединение закрывается, то идет спам в консоль (стоит дописать в условие циклов есть ли соединение или нет).
- Ping/Pong - не уверен, что всегда 2 и 3, стоит проверить и сделать проверку ответа от сервера.
- Полученные JSON значения десериализировать в классы. Но там правда каша, сервер присылает
object[], что не есть хорошо, ибо придется писать конвертор.
- Метод
ConnectAsync выполняет не свои обязанности, стоит вынести инициализацию и прочее в другие методы.
- В некоторые асинхронные методы не закинул
CancellationToken (например в Task.Delay.
- Ну и другие мелочи... Оставлю все это для вас)
ws.Options.SetRequestHeader()метод). Я вам даже подсказку дам:Origin. – EvgeniyZ Sep 17 '21 at 16:38