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

от автора

Итак, обещанное продолжение моей первой статьи из песочницы, в котором будет немного технических деталей по реализации простой многопользовательской игры с возможностью играть с клиентов на разных платформах.
Предыдущую часть я закончил тем, что в последней версии моей игры «Магический Yatzy» в качестве инструмента клиент-серверного взаимодействия я использую WebSocket’ы. Теперь немного технических подробностей.

1. Общее

В общем, все выглядит как показано на этой схеме:

image

Выше представлена схема взаимодействия между тремя клиентами на различных платформах и сервером. Рассмотрим каждую часть по-подробнее.

2. Сервер

Сервер у меня на базе MVC4, работающего как «Cloud service» в Windows Azure. Почему такой выбор. Все просто:
1) Ничего кроме .NET я не знаю.
2) WebSocket у меня только для взаимодействий, касающихся игры, все остальное, такое как проверка статуса сервера, получение/сохранение очков и прочее – через WebApi – поэтому MVC.
3) У меня есть подписка на сервисы Azure.

Согласно схеме выше – сервер состоит из трех частей:
1) ServerGame – реализация всей логики игры;
2) ServerClient – своего рода посредник между игрой и сетевой частью;
3) WSCommunicator – часть, ответственная за сетевое взаимодействие с клиентом – прием/отправка команд.

Конкретная реализация ServerGame и ServerClient зависит от конкретной игры, которую вы разрабатываете. В общем случае ServerClient получает комманду от клиента, обрабатывает ее и оповещает игру о действии клиента. В тоже время он следит за изменением состояния игры (ServerGame) и оповещает (отправляет информацию через WSCommunicator) своего клиента о любых изменениях.
Например, касательно моей игры в кости: в свой ход пользователь на Windows 8 клиенте закрепил несколько костей (сделал так, чтобы их значение не изменилось при следующем броске). Эта информация была передана на сервер и ServerClient оповестил об этом класс ServerGame, который сделал необходимые изменения в состоянии игры. Об этом изменении были оповещены все другие ServerClient’ы, подключенные к данной игре (в рассматриваемом случае – WP и Android), а они в свою очередь отправили информацию на устройства для оповещения пользователей через UI.
Следует сказать, что в самом классе ServerGame ничего «серверного» нету. Это обычный .NET класс, имеющий общий интерфейс с ClientGame. Таким образом мы может подставить его вместо ClientGame в клиентской программе и таким образом получить локальную игру. Именно так и работает локальная игра в моем «книффеле»– когда из одной UI странички возможна как локальная так и сетевая игра.
WSCommunicator – как я уже сказал, класс ответственный за сетевое взаимодействие. Конкретно этот реализует это взаимодействие посредством WebSocket’ов. В .NET 4.5 появилась собственная реализация вебсокетов. Основным в этой реализации является класс WebSocket, WSCommunicator по сути является оберткой над ним, реализующей открытие/закрытие соединения, попытки переподключения, отправки/получения данных в определенном формате.
Теперь немного кода. Для первоначального соединения используется Http Handler. Физическую страницу добавлять не обязательно. Достаточно задать параметры в WebConfig’e:

… <system.webServer>     <handlers>       <remove name="ExtensionlessUrlHandler-Integrated-4.0" />       <add name="app" path="app.ashx" verb="*" type="Sanet.Kniffel.Server.ClientRequestHandler" />       <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" resourceType="Unspecified" requireAccess="Script" preCondition="integratedMode,runtimeVersionv4.0" />     </handlers>   </system.webServer> … 

Таким образом, при обращении к страничке (виртуальной) «app.ashx» на сервере будет вызван код из класса «Sanet.Kniffel.Server.ClientRequestHandler». Вот этот код:

public class ClientRequestHandler : IHttpHandler     {         public void ProcessRequest(HttpContext context)         {             if (context.IsWebSocketRequest) //обращение через WebSocket                 context.AcceptWebSocketRequest(new Func<AspNetWebSocketContext, Task>(MyWebSocket));             else                                                    //обращение через Http                 context.Response.Output.Write("Здесь ничего нет...");         }          public async Task MyWebSocket(AspNetWebSocketContext context)         {             string playerId = context.QueryString["playerId"];             if (playerId == null) playerId = string.Empty;                          try 	    { 		WebSocket socket = context.WebSocket;                 //новый класс, унаследованный от WSCommunicator'а и имеющий дополнительный функционал по подключению клиента к игре 		ServerClientLobby clientLobby = null;                 if (!string.IsNullOrEmpty(playerId)) 		{ 			//проверяем не подключен ли уже клиент с таким айди 			if ( !ServerClientLobby.playerToServerClientLobbyMapping.TryGetValue(playerId, out clientLobby)) 			{                                 //если нет - создаем новый 				clientLobby = new ServerClientLobby(ServerLobby, playerId); 				ServerClientLobby.playerToServerClientLobbyMapping.TryAdd(playerId,  clientLobby); 				} 			} 		        else  			{ 				//запрос с пустым айди оставляем без внимания                                 return; 			}  			//устанавливаем новый вебсокет и запускаем 			clientLobby.WebSocket = socket; 			await clientLobby.Start();              		} 		catch (Exception ex) 		{ 			//что-то пошло не так... 		}         }      } 

Думаю, с учетом комментариев все должно быть понятно. Метод WSCommunicator.Start() запускает «режим ожидания» команды от клиента. Вот как это выглядит ():

 public async Task Start()         {             if (Interlocked.CompareExchange(ref isRunning, 1, 0) == 0)             {                await Run();             }         }          protected virtual async Task Run()         {             while (WebSocket != null && WebSocket.State == WebSocketState.Open)             {                 try                 {                     string result = await Receive();                     if (result == null)                     {                         return;                     }                 }                 catch (OperationCanceledException) //это нормально при отмене операции                 { }                 catch (Exception e)                 {                     //что-то непоправимое                     //закрываем соединение                     CloseConnections();                     //оповещаем всех, что этот клиент отключен от игры                     OnReceiveCrashed(e);                 }             }                      } 

Это общая часть, дальнейшее описание сервера опускаю, так как оно будет в большей степени зависеть от игры, которую вы делаете. Скажу только, что команды через WebSocket передаются (в том числе) в текстовом формате. Конкретная реализация этих команд опять таки в основном зависит от игры. При получении команды от клиента, она будет обработана методом WSCommunicator.Receive(), для отправки клиенту — WSCommunicator.Send(). Все, что между – опять же зависит от логики игры.

3. Клиент

3.1 WinRT.

Если бы клиент был на полноценной .NET 4.5, то для него можно было бы использовать тот же класс WSCommunicator, что и на серевере с небольшими лишь дополнениями – вместо класса WebSocket необходим был бы класс ClientWebSocket, плюс добавить логику по запросу на соединение с сервером. Но в WinRT используется своя реализация вебсокетов с классами StreamWebSocket и MessageWebSocket. Для передачи текстовых сообщений используется второй. Вот код по установлению соединения с сервером с его использованием:

public async Task<bool> ConnectAsync(string id, bool isreconnect = false)         {             try             {                 //работаем с локальной копией вебсокета, чтобы избежать его закрытия из другого потока во время асинхронной операции                 //(маловероятно, но возможно)                 MessageWebSocket webSocket = ClientWebSocket;                  // Проверяем что не подключены                 if (!IsConnected)                 {                     //получаем адрес сервера (ws://myserver/app.ashx")                     var uri = ServerUri();                     webSocket = new MessageWebSocket();                     webSocket.Control.MessageType = SocketMessageType.Utf8;                     //устанавливаем обработчики                     webSocket.MessageReceived += Receive;                     webSocket.Closed += webSocket_Closed;                                              await webSocket.ConnectAsync(uri);                     ClientWebSocket = webSocket; //устанавливаем в переменную класса только после успешного подключения                     if (Connected != null)                         Connected();             //сообщаем, что мы подключились                     return true;                 }                 return false;             }             catch (Exception e)              {                 //что-то не так                 return false;             }         } 

Далее все как на сервере: WSCommunicator.Receive() получает сообщения с сервера, WSCommunicator.Send() – отправляет. GameClient работает в соответствии с данными, получаемыми с сервера и от пользователя.

3.2 Windows Phone, Xamarin и Silverlight (а также .NET 2.0)

Во всех этих платформах нет поддержки вебсокетов «из коробки». К счастью есть отличная опенсорс библиотека WebSocket4Net, которую я упоминал в предыдущей статье. Заменив в WSCommunicatare класс вебсокета на реализованный в этой библиотеке, мы получим возможность подключения к серверу с указанных платформ. Вот как изменится код по установке соединения:

public async Task<bool> ConnectAsync(string id, bool isreconnect = false)         {            try             {                 //работаем с локальной копией вебсокета, чтобы избежать его закрытия из другого потока во время асинхронной операции                 //(маловероятно, но возможно)                 WebSocket webSocket = ClientWebSocket;                  // Проверяем что не поделючены                 if (!IsConnected)                 {                     //получаем адресс сервера (ws://myserver/app.ashx")                     var uri = ServerUri();                     webSocket = new WebSocket(uri.ToString());                     //устанавливаем обработчики                     webSocket.Error += webSocket_Error;                     webSocket.MessageReceived += Receive;                     webSocket.Closed += webSocket_Closed;                     //соединение не асинхронное, поэтому "асинхронизируем" его принудительно                     var tcs = new TaskCompletionSource<bool>();                     webSocket.Opened += (s, e) =>                      {                         //устанавливаем в переменную класса только после успешного подключения                         ClientWebSocket = webSocket;                         if (Connected != null)                             Connected();        //сообщаем, что мы подключились                          else tcs.SetResult(true);                                                                   };                     webSocket.Open();                      return await tcs.Task;                  }                  return false;             }             catch (Exception ex)             {                 //что-то не так                 return false;             }                      } 

Как видим отличия есть, но их не так много, основное -это не асинхронное открытие соединения с сервером, но это легко исправить (правда для поддержки async await в старых версиях .NET необходимо установить Microsoft.Bcl пакет с нугета).

Вместо заключения

Прочитал, что написал и понимаю, что вопросов, возможно, больше чем ответов. К сожалению описать все в одной статье физически не возможно, а она и так уже получается не самой короткой… но я буду продолжать тренироваться.

ссылка на оригинал статьи http://habrahabr.ru/post/187282/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *