Как стать автором
Поиск
Написать публикацию
Обновить

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

Время на прочтение6 мин
Количество просмотров12K
Итак, обещанное продолжение моей первой статьи из песочницы, в котором будет немного технических деталей по реализации простой многопользовательской игры с возможностью играть с клиентов на разных платформах.
Предыдущую часть я закончил тем, что в последней версии моей игры «Магический 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 пакет с нугета).

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

Прочитал, что написал и понимаю, что вопросов, возможно, больше чем ответов. К сожалению описать все в одной статье физически не возможно, а она и так уже получается не самой короткой… но я буду продолжать тренироваться.
Теги:
Хабы:
Всего голосов 20: ↑15 и ↓5+10
Комментарии8

Публикации

Ближайшие события