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