Установка TCP-соединения в стеке lwIP

Разберём процесс установки TCP-соединения в стеке lwIP по шагам, на примере кода из статьи «JSON-транспорт между микроконтроллером и сервером». Я буду сначала писать код, а потом давать к нему комментарии.

Инициализация

void InitJSON()
{
struct ip_addr server_ip;
IP4_ADDR(&server_ip, 192,168,1,152);
struct tcp_pcb *TCPSockJSON;

TCPSockJSON = tcp_new();
tcp_recv(TCPSockJSON, json_receive);
tcp_sent(TCPSockJSON, json_client_sent);
tcp_connect(TCPSockJSON, &server_ip, 9090, json_connected);
}

Стек lwIP устроен по принципу конечного автомата. При наступлении определённых событий (подключение, приём данных, ошибки) он вызывает функции-обработчики этих событий, и на старте вы должны передать ему адреса этих функций.

Структура TCPSockJSON — ядро соединения, именно в ней будут лежать все настройки и состояние подключения. Она будет передана во все функции-обработчики событий. Первое что нужно сделать — проинициализировать её функцией lwIP tcp_new.

Далее расставляем обработчики интересующих нас событий. Это будут:

  • Успешное подключение — обработчик json_connected
  • Приём данных — обработчик json_receive
  • Передача данных — обработчик json_client_sent

Функция tcp_connect подключается к серверу (IP = &server_ip, порт = 9090) и сразу вызывает обработчик json_connected.

Обработчик подключения

static err_t json_connected(void *arg, struct tcp_pcb *pcb, err_t err)
{
char *string = "{\"hop\": 0}";
LWIP_UNUSED_ARG(arg);
if(err == ERR_OK)
{
tcp_write(pcb, string, strlen(string), 0);
tcp_output(pcb);
}
return err;
}

Сразу после подключения проверяем ошибки, и если всё ОК — передаём серверу строку «{«hop»: 0}». Это — приветственная строка, сообщение «я включился и со мной всё нормально». Здесь можно просто передать какой-нибудь статус устройства, к примеру.

Для записи данных в буфер передачи используем функцию tcp_write, ей требуется сокет, строка и её длина. Для реальной передачи данных — функция tcp_output. Вы можете накопить в буфере много данных после нескольких вызовов функции tcp_write, и потом отправить всё одним пакетом с помощью tcp_output.

lwIP вызывает обработчики по цепочке, поэтому в конце обязательно передаём ошибку дальше: return err.

Приём данных

static err_t json_receive(void *arg, struct tcp_pcb *pcb, struct pbuf *p, err_t err)
{
LWIP_UNUSED_ARG(arg);

if(err == ERR_OK && p != NULL)
{
char *string = p->payload;
string[p->len] = 0;

processServerJSON(pcb, cJSON_Parse(string));

tcp_recved(pcb, p->tot_len);
pbuf_free(p);
}
else
json_client_close(pcb);
return ERR_OK;
}

Снова проверяем ошибки, если их нет — обрабатываем принятые данные.

Сами данные лежат в поле p->payload, а их длина — в p->len. Мы знаем что принимаем от сервера не абстрактные данные, а именно строку — и чтобы сделать из этого нормальную строку, просто добавим в её конце (по адресу p->len) терминирующий ноль. Это не очень правильно, потому что так мы лезем прямо в структуру p, и можем испортить данные, лежащие сразу за строкой. Лучше скопировать строку через memcpy.

Далее парсим принятый текст в JSON функцией библиотеки cJSON (cJSON_Parse), и обработаем его нашей функцией processServerJSON.

Забрав все данные, скажем lwIP что данные получены и их можно стирать и принимать новые. Для этого вызовем функцию tcp_recved. Потом просто освободим память, занимаемую буфером p.

В случае ошибок при приёме — вызовем нашу функцию json_client_close.

В конце — передадим код ошибки дальше.

Закрытие соединения

static void json_client_close(struct tcp_pcb *pcb)
{
tcp_arg(pcb, NULL);
tcp_sent(pcb, NULL);
tcp_close(pcb);
}

Сбросим обработчики состояний (передав NULL в качестве адресов обработчиков) и закроем соединение функцией tcp_close. Всё просто.

Обработка принятых данных

static void processServerJSON(struct tcp_pcb *pcb, cJSON *json)
{
cJSON *json_out = cJSON_CreateObject();
cJSON_AddNumberToObject(json_out, "data", 500);

char *json_string = cJSON_Print(json_out);
tcp_write(pcb, json_string, strlen(json_string), 0);
tcp_output(pcb);
free(json_string);
cJSON_Delete(json_out);
cJSON_Delete(json);
}

Принимаем готовый объект cJSON, создаём новый объект cJSON, добавляем в него поле data и превращаем его в строку.

Для передачи — снова вызываем tcp_write/tcp_output.

В конце — ни в коем случае не забывайте удалять весь мусор! К примеру, код с неудалёнными cJSON-объектами проработает всего лишь в течение 500 циклов приёма/передачи и память закончится.

Забыв об освобождении строки json_string вы обеспечите падение после 2-3 тысяч циклов приёма-передачи.

Обработчик передачи

static err_t json_client_sent(void *arg, struct tcp_pcb *pcb, u16_t len)
{
LWIP_UNUSED_ARG(arg);
return ERR_OK;
}

Просто передаём код ошибки дальше, никаких действий здесь не требуется.