Предыдущую часть я закончил тем, что в последней версии моей игры «Магический Yatzy» в качестве инструмента клиент-серверного взаимодействия я использую WebSocket’ы. Теперь немного технических подробностей.
1. Общее
В общем, все выглядит как показано на этой схеме:
Выше представлена схема взаимодействия между тремя клиентами на различных платформах и сервером. Рассмотрим каждую часть по-подробнее.
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/
Добавить комментарий