Обычного REST API недостаточно для создания чата или многопользовательской игры. Для этого нам нужно получать уведомления каждый раз, когда сервер получает новые данные. SignalR приходит на помощь!

Представим себе приложение, помогающее друзьям найти себя. Каждую минуту приложение собирает текущее местоположение пользователя и отправляет его на сервер, который повторно отправляет это местоположение другим подключенным пользователям.

В этом сообщении блога мы покажем вам, как настроить SingalR как на стороне сервера, так и на мобильном приложении.

Что такое SignalR?

SignalR - это библиотека .NET для создания приложений в реальном времени. В отличие от обычного обмена данными по протоколу HTTP, SignalR позволяет отправлять сообщения со стороны сервера подключенным клиентам без предварительного запроса. Благодаря двунаправленной связи мобильному приложению не нужно запрашивать у сервера актуальность содержимого сервера.

SignalR освобождает разработчика от выбора типа транспорта, выбирая наилучший доступный транспорт, предоставляемый клиентом и сервером. Websockets не поддерживаются сервером? Нет проблем, давай поговорим с Long Pooling или Server Sent Events. SignalR предоставляет API, который делает выбранный транспорт прозрачным.

Настройка сервера SignalR

Весь код на стороне сервера SignalR содержится в концентраторе, который содержит методы, которые можно вызывать аналогично контроллерам MVC. Чтобы использовать их в полной мере, необходимо зарегистрировать его в Startup.Configure следующим образом:

app.UseSignalR(route => 
{ 
    route.MapHub<SomeFancyNameHub>("/FancyHub"); 
});

Благодаря наследованию от SignalR Hub SomeFancyNameHub: Hub у вас уже есть доступ к свойствам Context, Clients и Groups.

public abstract class Hub : IDisposable 
   { 
       protected Hub(); 
       public IHubCallerClients Clients { get; set; } 
       public HubCallerContext Context { get; set; } 
       public IGroupManager Groups { get; set; } 
       public void Dispose(); 
       public virtual Task OnConnectedAsync(); 
       public virtual Task OnDisconnectedAsync(Exception exception); 
       protected virtual void Dispose(bool disposing); 
   }

Это означает, например, что, переопределив метод OnConnectedAsync () для каждого подключенного клиента, вы можете получить его connectionId из Context.ConnectionId, поместить его в одну из групп из групп с помощью Groups.AddToGroupAsync () и уведомить всех других клиентов о том, что новый только что вступил в клуб (Hub)! С этого момента и до тех пор, пока концентратор не будет удален, сервер может получить доступ ко всем этим соединениям и уведомить клиентов, когда в этом возникнет необходимость. Единственное, что вас ограничивает, - это ваше

В нашем случае есть мобильное приложение, которое отображает местоположение члена команды на карте. Команды определены ранее, поэтому серверная часть знает, сколько их членов, какие из них активны и пока они находятся, каково их местонахождение. Хаб запускается, когда присоединяется первый член команды, и на основе названия его команды он уже сгруппирован.

Groups.AddToGroupAsync(Context.ConnectionId, usersTeamName);

Отныне все присоединяющиеся члены команды будут зачислены в эту группу. И когда они это сделают, будут вызваны эти методы:

await Clients.Caller.SendAsync("TeamJoined", JsonConvert.SerializeObject(teammates)); 
await Clients.OthersInGroup(usersTeamName).SendAsync("TeammateJoined", JsonConvert.SerializeObject(teammember));

где TeamMates - это набор TeamMember, а TeamJoined вместе с TeammateJoined - это сообщения / методы, которые прослушивает наш клиент iOS.

Поэтому, когда к группе присоединяется новый член команды, его имя, изображение профиля и местоположение отправляются другим членам команды. Это дает нам возможность нарисовать его аватар на карте в точном месте, чтобы его товарищи по команде могли знать, где он сейчас находится. В то же время член команды получает ту же информацию о своих товарищах, которую он нарисовал на своей карте.

Чтобы поддерживать хаб в рабочем состоянии и предотвратить его удаление, каждый член команды время от времени отправляет свое местоположение. Это обновленное местоположение затем отправляется другим товарищам по команде, чтобы карта могла обновиться.

Благодаря концентратору SignalR мы можем иметь столько групп, сколько захотим, и поддерживать коммуникационный поток внутри этой группы (или команды в нашем случае).

Настройка клиента SignalR для iOS

Для настройки клиента iOS воспользуемся репозиторием github mooozyk / SignalR-Client-Swift.

Начнем с инициализации экземпляра Hub. Для этого используйте класс HubConnectionBuilder.

let hubConnection = HubConnectionBuilder(url: URL(string: "https://angrynerds.pl/stream")!).build()

HubConnectionBuilder позволяет нам настраивать параметры подключения, задавая собственные заголовки HTTP или добавляя токен авторизации.

let hubConnection = HubConnectionBuilder(url: URL(string: "https://angrynerds.pl/stream")!) 
    .withHttpConnectionOptions { options in 
        options.headers = ["foo": "bar"] 
        options.accessTokenProvider = { 
            return userProvider.token 
        } 
}.build()

После вызова метода build() мы готовы начать общение с сервером. Для начала соединения вызовите start() на HubConnection объект.

SignalR API предоставляет 2 основных метода связи: один для отправки сообщения, а другой для прослушивания любого.

В нашем примере мы будем уведомлять сервер о текущем местоположении пользователя. Для этого нам нужно позвонить invoke(method: String, arguments: [Any?], invocationDidComplete: (Error?) -> Void)

  • method: String - сервер метода слушает
  • arguments: [Any?] - массив объектов, которые мы хотим отправить
  • invocationDidComplete: (Error?) -> Void) - обработчик завершения после завершения вызова
hub.invoke(method: "updateLocation", arguments: [email, latitude, longitude], invocationDidComplete: nil)

Чтобы получать уведомления о каждом обновлении местоположения игрока, мы будем использовать hub.on(method: String, callback: ([Any?], TypeConverter) -> Void)

  • method: String - сервер метода слушает
  • callback: ([Any?], TypeConverter) -> Void) - метод обратного вызова с 2 параметрами, аргументами и объектом-преобразователем, который помогает нам декодировать аргументы в желаемый тип

Мы ожидаем, что данные json будут единственным параметром в сообщении сервера. JSON должен содержать список подключенных пользователей с их текущим местоположением.

hub.on(method: method.rawValue, callback: { (args, typeConverter) in
    guard let jsonString = try? typeConverter.convertFromWireType(obj: args[0], targetType: String.self), 
      let data = jsonString?.data(using: .utf8), 
      let users = try? JSONDecoder().decode([User].self, for: data) 
      else { return } 
    self.updateUI(with: users)

Если нам нужно быть в курсе статуса подключения, мы можем реализовать HubConnectionDelegate методов.

func connectionDidOpen(hubConnection: HubConnection!) 
func connectionDidFailToOpen(error: Error) 
func connectionDidClose(error: Error?)

Хотите узнать больше о волшебстве, которое можно творить с SignalR? Сообщите нам в комментариях, если есть конкретная тема, которую вы хотите, чтобы мы затронули!

Статья Лукаша Леха и Якуба Томальского. Первоначально опубликовано на сайте angrynerds.co.