Pull to refresh

Стабильный socket клиент для android

Недавно столкнулся с проблемой создания приложения (наподобие ICQ) с клиентом под андроид, в связи с чем, появилась необходимость реализовать сокет-соединение, которое выполняло бы следующие функции:
  1. Инициировало соединение с сервером
  2. Посылало серверу сообщение типа «Привет, я тут!», чтобы сервер знал, что за устройство к нему подключилось
  3. Сидело и спокойно ждало входящие сообщения от сервера

Сама по себе задача довольно простая и ее решение описывается много где, НО есть следующие заморочки, с которыми я столкнулся:
  • данное подключение должно работать в фоновом режиме и не грузить основной процесс приложения, т.е. отрабатывать в отдельном потоке;
  • В случае потери соединения — оно должно автоматом восстанавливаться;

Совместно эти 2 «НО» явились для меня серьезной головной болью на 2 дня. Гугл по данной проблеме выдал несколько более-менее адекватных ссылок, но конечного работоспособного решения я так и не нашел (возможно, плохо искал, конечно, но не суть).

Собственно, предлагаю вам на растерзание рассмотрение свое решение.

1. Фоновый режим

Для того, чтобы наш сокет-клиент отрабатывал в фоновом режиме и принимал сообщения от сервера даже тогда, когда приложение не активно, запустим его внутри сервиса.

Сделать это можно, например, так:

public class NotificationService extends Service {

String acc_email;

public class LocalBinder extends Binder {
NotificationService getService() {
return NotificationService.this;
}
}

@Override
public void onCreate() {
super.onCreate();

startService();
}

@Override
public IBinder onBind(Intent intent) {
return mBinder;
}

@Override
public void onDestroy() { }

// Здесь выполняем инициализацию нужных нам значений
// и открываем наше сокет-соединение
private void startService() {

acc_email = "test@test.com";

try {
openConnection();

} catch (InterruptedException e) {
e.printStackTrace();
}
}

// данный метод открыает соединение
public void openConnection() throws InterruptedException
{
try {

// WatchData - это класс, с помощью которого мы передадим параметры в
// создаваемый поток
WatchData data = new WatchData();
data.email = acc_email;
data.ctx = this;

// создаем новый поток для сокет-соединения
new WatchSocket().execute(data);

} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}


Этот сервис делает 2 вещи:
  1. Получает каким-либо образом e-mail пользователя, который нужно будет передать на сервер (в примере я его просто присвоил);
  2. Передает этот e-mail в поток, который создаст сокет-соединение.

В данном коде интерес представляет метод public void openConnection(), а если быть точнее, классы WatchData и WatchSocket, которые используются для создания нового потока, рассмотим их подробнее.

2. Создание потока

Зачем мы вообще создаем поток? Затем, что сервисы в андроиде не создают собственного, а выполняются в потоке приложения, что может (и будет!) тормозить наше приложение. Подробнее про сервисы — тут.
Итак, поток создавать нужно. Я для этой задачи решил использовать хрень под названием AsyncTask.
Для создания нужного мне AsyncTask'а я сделал два класса, о которых писал выше: WatchData и WatchSocket

WatchData:
class WatchData
{
String email;
Context ctx;
}

Поле email — нужно для того, чтобы передать в поток email, который мы хотим отправить на сервер (естественно, тут могут быть любые другие данные). Поле ctx передает контекст приложения. Мне этот контекст, например, нужен, чтобы сохранять получаемые от сервера сообщения в sqlite'овскую базу данных андроида.

WatchSocket:
class WatchSocket extends AsyncTask<WatchData , Integer, Integer>
{
Context mCtx;
Socket mySock;

protected void onProgressUpdate(Integer... progress)
{ }

protected void onPostExecute(Integer result)
{
// Это выполнится после завершения работы потока
}

protected void onCancelled(Object result)
{
// Это выполнится после завершения работы потока
}


protected Integer doInBackground(WatchData... param)
{
InetAddress serverAddr;

mCtx = param[0].ctx;
String email = param[0].email;

try {
while(true)
{
serverAddr = InetAddress.getByName("192.168.0.10");
mySock = new Socket(serverAddr, 4505);

// открываем сокет-соединение
SocketData data = new SocketData();
data.ctx = mCtx;
data.sock = mySock;

// ВНИМАНИЕ! Финт ушами - еще один поток =)
// Именно он будет принимать входящие сообщения
GetPacket pack = new GetPacket();
AsyncTask<SocketData, Integer, Integer> running = pack.execute(data);

String message = email;
// Посылаем email на сервер
try {
PrintWriter out = new PrintWriter( new BufferedWriter( new OutputStreamWriter(mySock.getOutputStream())),true);

out.println(message);

} catch(Exception e)

// Следим за потоком, принимающим сообщения
while(running.getStatus().equals(AsyncTask.Status.RUNNING))
{

}

// Если поток закончил принимать сообщения - это означает,
// что соединение разорвано (других причин нет).
// Это означает, что нужно закрыть сокет
// и открыть его опять в бесконечном цикле (см. while(true) выше)
try
{
mySock.close();
}
catch(Exception e)
{}
}
} catch (Exception e) {
return -1;
}
}
}


Тут нас интересует метод protected Integer doInBackground(WatchData… param), который выполняет все, что нам нужно.
В вечном цикле производятся следующие действия:
  1. Открывается сокет-соединение
  2. Запускается новый поток, слушающий сервер
  3. Посылается сообщение на сервер
  4. Отслеживается слушающий поток на предмет закрытия соединения
  5. В случае обрыва соединения цикл срабатывает заново


Здесь может возникнуть вопрос: «Зачем делать еще один поток? Почему не слушать сервер в этом же потоке?»

Отвечаю:
Во время моих экспериментов с AsyncTask'ами выяснилось, что при закрытии соединения методом «сервер взял и вырубился», слушающий его AsyncTask тупо берет и заканчивает работу. Пересоединить сокет с сервером прямо в том же таске у меня, как я ни бился — не вышло. Не берусь заявлять, что этого сделать невозможно — просто предлагаю свое рабочее решение.
Итак, чтобы слушать сервер, создается дополнительный поток GetPacket. Ему, в качестве параметров, в классе SocketData передаются открытый нами сокет и контекст приложения.

3. Слушаем сервер


Собственно, код второго потока:

class SocketData
{
Socket sock;
Context ctx;
}

class GetPacket extends AsyncTask<SocketData, Integer, Integer>
{
Context mCtx;
char[] mData;
Socket mySock;

protected void onProgressUpdate(Integer... progress)
{
try
{
// Получаем принятое от сервера сообщение
String prop = String.valueOf(mData);
// Делаем с сообщением, что хотим. Я, например, пишу в базу

}
catch(Exception e)
{
Toast.makeText(mCtx, "Socket error: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
}

protected void onPostExecute(Integer result)
{
// Это выполнится после завершения работы потока
}

protected void onCancelled(Object result)
{
// Это выполнится после завершения работы потока
}

protected Integer doInBackground(SocketData... param)
{
mySock = param[0].sock;
mCtx = param[0].ctx;
mData = new char[4096];

try {
BufferedReader reader = new BufferedReader(new InputStreamReader(mySock.getInputStream()));
int read = 0;

// Принимаем сообщение от сервера
// Данный цикл будет работать, пока соединение не оборвется
// или внешний поток не скажет данному cancel()
while ((read = reader.read(mData)) >= 0 && !isCancelled())
{
// "Вызываем" onProgressUpdate каждый раз, когда принято сообщение
if(read > 0) publishProgress(read);
}
reader.close();
} catch (IOException e) {
return -1;
}
catch (Exception e) {
return -1;
}
return 0;
}
}


Собственно, все.

Итого

Мы получили:
  • сокет-соединение, работающее в фоновом режиме
  • поток 1, который слушает сервер и принимает от него сообщения
  • поток 2, который следит за потоком 1 и, в случае его завершения (обрыва соединения), запускает заново

Надеюсь, это поможет кому-нибудь сэкономить немного времени и нервов
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.