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