Доброго всем! Решил поделиться своими наработками в области создания сетевых приложений под Андроид. Данное сообщение не относится непосредственно к 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 Не воспринимайте данный класс как панацею, хотя он и вполне работоспособен, небольшие сомнения все-таки вызывает отправка/очистка данных и если у кого-то появится желание по усовершенствованию данного класса мы будем рады совету или подсказке. Изменения я постараюсь сразу же внести.
Спасибо за внимание!