Работа с сокетом во Flutter

В прошлой статье я рассказывал о том, как сделать простое приложение по мониторингу на flutter. Но для полноты картины я решил описать как отправлять полученные координаты на сервер. Для передачи данных я решил использовать protobuf.

Создание protobuf схемы.

Для того чтобы быть уверенными что данные в нашем приложении имеют строго определенный формат на нужна какая-то схема, при валидации которой сразу можно понять валидное сообщение или нет. Для текстовых форматов эта может быть XSD схема для XML или JSON схема для JSON. Однако передавать текстовые данные по сети накладно, из-за большого их объема. Тут на помощь и приходят бинарные форматы данных, такие как protobuf.

Для начала работы нужно составить схему данных в моем случае она будет следующая:

syntax = "proto3";
package test;

message Packet {
    int32 client = 1;
    double latitude = 2;
    double longitude = 3;
}

В этой схеме мы описываем версию протокола, имя пакета и структуру сообщений. В структуре сообщений мы задаем тип поля, его название и номер. После того схема составлена, её нужно сохранить.

Далее на основе этой схемы мы можем генерировать структру данных для различных языков (они описаны в руководстве).

В моем случае мне нужны 2 языка - Dart и Go. Первый используется в мобильном приложении, а на втором будет написан тестовый сервер. Для этого необходимо в папке со схемой выполнить:

protoc --go_out=. test.proto 
protoc --dart_out=. test.proto 

После того как эти команады отработают в папке появится файл test.pb.go с описание Go структуры и 4 файла для мобильного приложения:

test.pb.dart
test.pbenum.dart
test.pbjson.dart
test.pbserver.dart

Соответственно надо перенести в папки где находятся ваши приложения.

Тестовый сервер

Для проверки того, что приложение нам отправляет данные необходим простенький сервер. Я решил написать его на Go:

package main

import (
	"log"
	"net"
	"github.com/golang/protobuf/proto"
	"io"
)

func main() {
	srvAddress := "localhost:5555"

	l, err := net.Listen("tcp", srvAddress)
	if err != nil {
		log.Fatalf("Не удалось открыть соединение: %v", err)
	}
	defer l.Close()

	log.Printf("Запущен сервер %s...", srvAddress)
	for {
		conn, err := l.Accept()
		if err != nil {
			log.Printf("Ошибка соединения: %v", err)
		} else {
			go handleRecvPkg(conn)
		}
	}
}

func handleRecvPkg(conn net.Conn) {
	defer conn.Close()

	buf := make([]byte, 1024)
	log.Printf("Установлено соединение с %s", conn.RemoteAddr())
	for {
		packetLen, err := conn.Read(buf)

		switch err {
		case nil:
			recvPacket := buf[:packetLen]
			log.Printf("Принят пакет: %X", buf[:packetLen])

			outPkg := Packet{}
			if err := proto.Unmarshal(recvPacket, &outPkg); err != nil {
				log.Printf("Ошибка декодирования пакета: %v", err)
				return
			}

			log.Println("Данные в пакете: ", outPkg)
		case io.EOF:
			continue
		default:
			log.Printf("Ошибка при получении: %v", err)
			return
		}
	}
}

Сервер открывает сокет на порту 5555 и на каждое соединение запускает горутину. Горутина в свою очередь считывает принятые данные и с помощью библиотеки proto разбирает данные в структуру Packet из файла test.pb.go, который была сгенерирован из схемы protobuf с помощью protoc.

Модификация мобильного приложения

Для работы с protobuf необходимо добавть соответствующий модуль в pubspec.yaml:

dependencies:
    protobuf: ^0.13.15

Далее в файле нашего приложения необходимо импортировать нужные модули:

import 'dart:io';
import 'package:mobileapp/test.pb.dart'; // сгенерированный protoc файл

....

После этого в конец кода обработки геопозиции нужно добавить:

...

location.getLocation().then((p) {
    ...
          locPkg = Packet();
          locPkg.client = 1;
          locPkg.longitude = p.latitude;
          locPkg.latitude = p.longitude;

          Socket.connect('localhost', 5555).then((s) {
            s.add(locPkg.writeToBuffer());
            s.destroy();
          }).catchError((e) {
            print(e.toString());
          });
});          

...

В этом куске кода мы создаем класс нашего пакета из protobuf схемы, а затем добавляем в него необходимые поля. Затем мы создаем tcp соединение с сервером и отправляем на него бинарный пакет, который создает функция writeToBuffer, затем соедиение закрывается.

В итоге после запуска приложения в логах отладочного сервиса можно увидеть следующее:

демо

Заключение

В статье показан базовый уровень взаимодействия с сервером. Данный код каждый раз в момент отправки пакета создает новое соединение, что в боевоей системе не очень хорошо. Также если возникнет необходимость использовать UDP протокол для передачи данных, для уменшения издержек, то в место Socket, можно использовать RawDatagramSocket. Хороший пример по работе с UDP, можно посмотреть в статье.

Ссылки

  1. Protocol buffers developer guide
  2. Proto
  3. UDP Socket Programming with Dart
  4. How to set up Flutter platform channels with Protobuf
 
comments powered by Disqus