下記の記事の続きになります。
前回の記事ではLightWieghtIPのデータ構造にフォーカスしてまとめました。
今回の記事は実際にUDP,TCPの送受信をした際に呼び出すAPIとLwIPの振る舞いについてまとめていきます。
また、この記事ではソケット通信に関しては考えずに、一旦UDP/TCPの送受信をどのような流れで行っているかに関してフォーカスして説明していきます。ソケット通信に関してはまた別の記事にまとめようかと思います。
UDP
UDPの送受信時に呼び出されるLwIPの関数の流れは下記の図のようになります。
下記に送信と受信に分けて説明します。
送信
UDPの通信は、コネクションレスで行われるため、送信はAPI「udp_send()」を呼び出すことでUDPのパケットを送信します。
err_t udp_send(struct udp_pcb *pcb, struct pbuf *p);
引数に指定する管理用構造体udp_pcbのメンバ「remote_ip」に指定されているIPアドレスと、別のIPアドレスにパケットを送りたい場合は、API「udp_sendto()」を呼び出すことでUDPのパケットを送信することが可能です。
受信
UDPの受信に関しては、初期化時にAPI「udp_recv()」でUDPのパケットを受信した際に呼び出す関数を設定することで、パケットを受信します。
void udp_recv(struct udp_pcb *pcb, udp_recv_fn recv, void *recv_arg);
設定する関数の型「udp_recv_fn」は下記のようになります。これと同じ引数、返り値の関数を指定します。
typedef void (*udp_recv_fn)(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port);
また、「udp_recv()」に設定する関数は引数に指定する管理用構造体udp_pcbと紐づけられるので、udp_pcbに指定したIPアドレスとポート番号のUDPパケットが受信された際に、設定した関数が呼び出されます。
TCP
TCPの送受信時に呼び出されるLwIPの関数の流れは下記の図のようになります。
下記に送信と受信に分けて説明します。
送信
TCPの送信を行いたい場合は、アプリケーション層から送信API tcp_write()を呼び出します。
err_t tcp_write(struct tcp_pcb *pcb, const void *arg, u16_t len, u8_t apiflags);
ただし、UDPの場合と異なり、TCPの送信時は上記のAPIを呼び出してからすぐにデータは送信されません。
引数に指定した「arg」が送信データに該当するのですが、このargがTCP管理構造体の未送信データキュー(pcb->unsent)の末尾に設定され、周期タスク(タイマータスク)から送信されます(TCPの輻輳制御を考慮し、ウインドウサイズを確認して送れそうなら送る、といった振る舞いになります)。
一見、送信関数を呼び出してから送信するまでにかなりのタイムラグが出そうですが、
TCPのデータを受信してACKを返すタイミングで、未送信のデータが残っていれば送信するような仕組みになっており、何らかのデータを送信中に「tcp_write()」が呼ばれることも多いので、そこまで気にするほどのものではないかと思います。
受信
TCPの受信に関しては、初期化時にAPI「tcp_recv()」でTCPのパケットを受信した際に呼び出す関数を設定することで、パケットを受信します。
void tcp_recv(struct tcp_pcb *pcb, tcp_recv_fn recv);
設定する関数の型「tcp_recv_fn」は下記のようになります。これと同じ引数、返り値の関数を指定します。
typedef err_t (*tcp_recv_fn)(void *arg, struct tcp_pcb *tpcb,
struct pbuf *p, err_t err);
設定された関数は、パケットを受信した際に呼び出されるネットワークインターフェースの受信関数「netif->input()」を通して、最終的にtcp_input()関数内で呼び出されます。
参考資料
- Githubリポジトリ
https://github.com/lwip-devs/lwip - LwIP初版設計書
https://www.artila.com/download/RIO/RIO-2010PG/lwip.pdf