Доброго всем! Решил поделиться своими наработками в области создания сетевых приложений под Андроид. Данное сообщение не относится непосредственно к AndEngine, но имеет прямое отношение к геймдеву, ведь каждому игроделу когда-нибудь приходит в голову идея организовать взаимодействие игроков через интернет (настольные игры, общение, пошаговые стратегии и прочее).
Сразу оговорюсь, что приведенный здесь класс не обеспечивает "прямого" взаимодействия пользователей. Между пользователями необходимо наличие веб-сервера на котором должна быть реализована логика аутентификации, игровая логика и который будет координировать команды и события от клиентов.
В процессе портирования одной популярной настольной игры под Android, мы решили прикрутить к ней возможность игры через интернет.
Задача стояла следующая: получить инструмент, позволяющий легко реализовывать пошаговый протокол игры, имеющий события подключения, отключения, перехват ошибок, ну и конечно отправки/приема данных. Я намеренно пишу "получить", потому что не являюсь хардкорным программером и если нахожу хорошее опенсурс-решение со свободной лицензией, то ничуть не стесняюсь его использовать. Такого же мнения придерживаюсь и для своего кода в этом блоге: если кому-то что-то глянется - берите не стесняйтесь. =)
Итак, начали мы с активных поисков готового кода. Через пару дней скитаний по интернету стало понятно, что устойчивого и законченного клиента для WEB для нас никто не выложил =). Конечно, нашлось немало статей по созданию таких клиентов, но все они были настолько печальны, что их юзабельность сводилась к нулю.
Я не могу привести здесь все источники из которых мы брали информацию, во-первых их очень много, а во-вторых ни один из них не стал для нас отправной точкой, поэтому, без лишних слов привожу здесь код который у нас получился из собранной инфы и собственных умозаключений.
Работа сокета по чтению и записи данных должна происходить в отдельном потоке и ни в коем случае не в потоке UI., именно поэтому, чтобы получить доступ из UI к данным (а ведь это нам и нужно) которые приняты/переданы мы создаем Handler и работаем через него.
Синхронизированные методы нужны для синхронизации доступа к volatile переменным, т.е. переменным которые могут использоваться и потоком UI и потоком сокета.
В коде фигурирует переменная mSocketStatus использование которой обоснованно только нашими способами отладки =).
Использование класса заключается в перекрытии его методов прямо в UI потоке:
Чтение данных и обработка ошибок:
Подключение к серверу:
Отправка данных:
Отключение от сервера:
Отсюда видно, что с этим классом не надо заморачиваться с работой сокетов, а сразу приступить к реализации своего протокола, а при наличии хорошего стабильного соединения - не только пошагового. Здесь имеется возможность контролировать момент подключения/отключения к серверу, ошибки сокета, отправку и прием данных.
Протокол для своей игрушки мы реализовали на базе XML. Советую использовать вот этот класс для парсинга, очень помогает не запутаться в ветках.
PS Не воспринимайте данный класс как панацею, хотя он и вполне работоспособен, небольшие сомнения все-таки вызывает отправка/очистка данных и если у кого-то появится желание по усовершенствованию данного класса мы будем рады совету или подсказке. Изменения я постараюсь сразу же внести.
Спасибо за внимание!
Сразу оговорюсь, что приведенный здесь класс не обеспечивает "прямого" взаимодействия пользователей. Между пользователями необходимо наличие веб-сервера на котором должна быть реализована логика аутентификации, игровая логика и который будет координировать команды и события от клиентов.
В процессе портирования одной популярной настольной игры под Android, мы решили прикрутить к ней возможность игры через интернет.
Задача стояла следующая: получить инструмент, позволяющий легко реализовывать пошаговый протокол игры, имеющий события подключения, отключения, перехват ошибок, ну и конечно отправки/приема данных. Я намеренно пишу "получить", потому что не являюсь хардкорным программером и если нахожу хорошее опенсурс-решение со свободной лицензией, то ничуть не стесняюсь его использовать. Такого же мнения придерживаюсь и для своего кода в этом блоге: если кому-то что-то глянется - берите не стесняйтесь. =)
Итак, начали мы с активных поисков готового кода. Через пару дней скитаний по интернету стало понятно, что устойчивого и законченного клиента для WEB для нас никто не выложил =). Конечно, нашлось немало статей по созданию таких клиентов, но все они были настолько печальны, что их юзабельность сводилась к нулю.
Я не могу привести здесь все источники из которых мы брали информацию, во-первых их очень много, а во-вторых ни один из них не стал для нас отправной точкой, поэтому, без лишних слов привожу здесь код который у нас получился из собранной инфы и собственных умозаключений.
package com.expedition107.mytreasures; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.net.InetSocketAddress; import java.net.Socket; import java.net.SocketException; import java.net.UnknownHostException; import android.os.Handler; import android.util.Log; public class ClientSocket { private Handler handler = new Handler(); private static String mHostIpAddress; private static int mPort; private Thread mServiceThread; private Runnable mServiceRunnable; private Socket mSocket; private volatile boolean isConnected = false; private int mSocketStatus = 0;// 0-disconnected, 1- connection, 2- // io_pending, 3 - write out, 4 - read in, -1 - unknown state private volatile String mOutString = ""; private volatile boolean mIsNewData; public ClientSocket(String pHostIp, int pPort) { mHostIpAddress = pHostIp; mPort = pPort; } public void connect(final int pTimeoutMillisecs) { if (mSocketStatus != 0) { return; } mServiceRunnable = new Runnable() { @Override public void run() { mSocketStatus = 1; //соединяемся // connect try { mSocket = new Socket(); mSocket.connect(new InetSocketAddress(mHostIpAddress,mPort), pTimeoutMillisecs); mSocket.setKeepAlive(true); setConnected(true); onConnect(); mSocketStatus = 2;//уходим в прослушку сокета char[] inBuff = new char[1024]; while (isConnected()) { String tmpSendData = getSendData(); if (!tmpSendData.equals("")) { mSocketStatus = 3;//пишем в сокет PrintWriter out = new PrintWriter( new BufferedWriter(new OutputStreamWriter( mSocket.getOutputStream())), true); out.println(tmpSendData); handler.post(new Runnable() { @Override public void run() { onSendData(); } }); clearSendData(); } mSocketStatus = 4;//читаем из сокета BufferedReader in = new BufferedReader( new InputStreamReader(mSocket.getInputStream()), 1024); String line = ""; if (in.ready()) { int length = in.read(inBuff); while ((length) != -1) { for (int i = 0; i < length; i++) { line = line + inBuff[i]; } length = -1; if (in.ready()){ length = in.read(inBuff); } } final String inStr = line; handler.post(new Runnable() { @Override public void run() { onReadData(inStr); } }); } } mSocketStatus = 0;//отсоединяемся mSocket.close(); handler.post(new Runnable() { @Override public void run() { onDisconnect(); } }); } catch (UnknownHostException e) { final UnknownHostException _e = e; handler.post(new Runnable() { @Override public void run() { mSocketStatus = -1; onSocketError(mSocketStatus,_e); mSocketStatus = 0; } }); e.printStackTrace(); } catch (SocketException e) { final SocketException _e = e; handler.post(new Runnable() { @Override public void run() { onSocketError(mSocketStatus, _e); mSocketStatus = 0; } }); e.printStackTrace(); } catch (IOException e) { final IOException _e = e; handler.post(new Runnable() { @Override public void run() { onSocketError(mSocketStatus, _e); mSocketStatus = 0; } }); e.printStackTrace(); } } }; mServiceThread = new Thread(mServiceRunnable); mServiceThread.start(); } //эти методы вызываются из UI-потока public void disconnect() { setConnected(false); } public void onConnect() { } public void onDisconnect() { setConnected(false); } public void onSocketError(int pSocketStatus, Exception e) { } public void onReadData(String pDataString) { } public void onSendData() { } //синхронизированные методы чтения/записи protected synchronized boolean isConnected(){ return this.isConnected; } private synchronized boolean setConnected(boolean pIsConnected){ this.isConnected = pIsConnected; return this.isConnected; } //этот метод вызывается из UI-потока protected synchronized void send(String pOutString){ mOutString = pOutString; mIsNewData = true; } private synchronized String getSendData(){ mIsNewData = false; return mOutString; } private synchronized void clearSendData(){ if (!mIsNewData) mOutString = ""; } }И, как обычно, немного пояснений.
Работа сокета по чтению и записи данных должна происходить в отдельном потоке и ни в коем случае не в потоке UI., именно поэтому, чтобы получить доступ из UI к данным (а ведь это нам и нужно) которые приняты/переданы мы создаем Handler и работаем через него.
Синхронизированные методы нужны для синхронизации доступа к volatile переменным, т.е. переменным которые могут использоваться и потоком UI и потоком сокета.
В коде фигурирует переменная mSocketStatus использование которой обоснованно только нашими способами отладки =).
Использование класса заключается в перекрытии его методов прямо в UI потоке:
Чтение данных и обработка ошибок:
final ClientSocket cs = new ClientSocket(serverIpAddress, 7777){ @Override public void onConnect(){ //Например показать Toast уведомление } @Override public void onDisconnect(){ //обязательно вызвать метод супера //иначе разрыва связи не произойдет super.onDisconnect(); //тут ваш код } @Override public void onReadData(String pStr){ //В pStr хранятся принятые данные textView.setText(textView.getText()+pStr+"\n"); } @Override public void onSocketError(int pSocketStatus, Exception e) { Log.v("game", e.getMessage()); } };
Подключение к серверу:
... cs.connect(5000); ...
Отправка данных:
... cs.send("СТРОКА ДАННЫХ НА ОТПРАВКУ"); ...
Отключение от сервера:
... cs.disconnect(); ...
Отсюда видно, что с этим классом не надо заморачиваться с работой сокетов, а сразу приступить к реализации своего протокола, а при наличии хорошего стабильного соединения - не только пошагового. Здесь имеется возможность контролировать момент подключения/отключения к серверу, ошибки сокета, отправку и прием данных.
Протокол для своей игрушки мы реализовали на базе XML. Советую использовать вот этот класс для парсинга, очень помогает не запутаться в ветках.
PS Не воспринимайте данный класс как панацею, хотя он и вполне работоспособен, небольшие сомнения все-таки вызывает отправка/очистка данных и если у кого-то появится желание по усовершенствованию данного класса мы будем рады совету или подсказке. Изменения я постараюсь сразу же внести.
Спасибо за внимание!
Может у Вас есть решения для увеличения производительности particle? А то из-за производительности приходится отказываться в сторону анимированных спрайтов.
ОтветитьУдалитьДоброго Вам, Олег! Если Вы чуть более подробно напишите как именно Вы используете партиклы, возможно, я смогу Вам ответить. В одной из прототипов у нас партиклы используются очень активно (пыль из под ног, дым от костра, труб и т.д.) порядка 10-15 инстанций и проблем не замечено. Работало даже на сони эриксон мини.
УдалитьАлексей, добрый день!
ОтветитьУдалитьОчень интересная статья, спасибо, сохранил себе в блокнот на всякий случай. Подскажите, пожалуйста, как организовать связь Андроид и Веб-сайта. Т.е. нужно отправить get-запрос из Android и получить ответ в xml. Где можно почитать об этом? Желательно с исходником.
Буду благодарен, если продублируете ответ мне в gMail: gigabyte.artur87@gmail.com
Вот тут http://expedition107.blogspot.ru/2012/07/android-achartengine-asynctask.html?m=1
ОтветитьУдалитьПосмотрите реализацию AsyncTask. Там нужно только парсинг свой сделать. У меня в статье просто текст приходит а у Вас xml. Ну и сервер должен уметь обработать запрос.
Благодарю за оперативный ответ!
ОтветитьУдалитьНавскидку, именно то, что искал. Попробую, когда доберусь до компилятора.
[spoiler]PS. Заходите и ко мне на огонек blog.livegig.ru :)[/spoiler]
Этот комментарий был удален автором.
ОтветитьУдалитьВ примере затруднительно, потому что на текущий момент у меня нет тестового сервера для демонстрации. Что именно не получается? Создаете класс, копипастите его отсюда, а у себя (в активити например) используете так как написано после "Чтение данных и обработка ошибок"
УдалитьИзвените, в другой теме хотел написать, а отобразилось здесь.
УдалитьА по повуду данного примера, скажите ваш вариант, работает во всех режимах, когда приложение свернуто длительное время. т.е хочу сказать не нужно ли его обернуть в сервис или как Asynctask, а то хочу написать клиента месенджера, как будет вести данный код. заранее блогадорю за ответ
О месенджерах ничего толком не читал, но думается, что для них лучше подойдет сервис+asynctask. В этой статье написан только класс и его применение, время его жизни зависит только от процесса в котором он будет существовать.
Удалить