では、実装を説明していきましょう。まずはTCPの処理用のタスク(tcp.c)についてです。
tcpタスクのメインループを見てみましょう。リスト2のtcp_main()内のwhile()ループが、tcpタスクのメインループ処理になります。
int tcp_main(int argc, char *argv[]) { ……(中略)…… buf->option.ip.regproto.protocol = IP_PROTOCOL_TCP; buf->option.ip.regproto.cmd = TCP_CMD_RECV; buf->option.ip.regproto.id = MSGBOX_ID_TCPPROC; kz_send(MSGBOX_ID_IPPROC, 0, (char *)buf); while (1) { kz_recv(MSGBOX_ID_TCPPROC, NULL, (char **)&buf); ret = tcp_proc(buf); if (!ret) kz_kmfree(buf); } return 0; }
リスト2 tcpタスクのメインループ(tcp.c) |
最初にipタスクに対してTCPのプロトコル番号を通知し、当該のプロトコル番号を持つパケットを「MSGBOX_ID_TCPPROC」というメッセージボックスに転送するように依頼します。これにより、ipタスクはTCPのパケットをtcpタスクに転送し始めます。
while()ループの中では、kz_recv()により、ipタスクからの転送パケットをMSGBOX_ID_TCPPROCで待ち受けます。また、ここでは、httpdタスクからの送信要求も受け付けます。メッセージを受けるとtcp_proc()を呼び出して、受信メッセージに応じた処理を行います。
tcp_proc()は、リスト3のように実装されています。
static int tcp_proc(struct netbuf *buf) { ……(中略)…… switch (buf->cmd) { ……(中略)…… case TCP_CMD_ACCEPT: ……(中略)…… kz_send(MSGBOX_ID_TCPCONLIST, 0, (char *)con); ……(中略)…… break; ……(中略)…… case TCP_CMD_RECV: ret = tcp_recv(buf); break; case TCP_CMD_SEND: ret = tcp_send(buf); break; ……(中略)…… }
リスト3 tcpタスクの受信メッセージ処理(tcp.c) |
tcpタスクには幾つかの要求が送られてきます。リスト3では「TCP_CMD_ACCEPT」「TCP_CMD_RECV」「TCP_CMD_SEND」という3つの要求の処理を行っています。
TCP_CMD_ACCEPTは、クライアントからの接続待ちを行うための要求です。これはUNIXのソケットプログラミングでいうaccept()に相当するもので、httpdタスクから発行されます。接続待ち要求が来た場合、tcpタスクは接続待ち用のデータベースを作成し、それを「MSGBOX_ID_TCPCONLIST」というリンクリストにつなげます。kz_send()を呼んでいるのは、メッセージ通信をタスク間通信のためでなく、リンクリストの代わりに利用しているためです。つまり、MSGBOX_ID_TCPCONLISTというメッセージボックスは、tcpタスクだけが送受信します。
TCP_CMD_RECVは、ipタスクからのパケット受信通知です。つまり、TCPのパケットを受信した場合の処理です。逆に、TCP_CMD_SENDはhttpdからの送信要求です。これらはそれぞれ「tcp_recv()」「tcp_send()」という関数で実際の処理が行われます。
まずは、受信処理(tcp_recv())を見てみましょう。リスト4がtcp_recv()の実装になります。
static int tcp_recv(struct netbuf *pkt) { ……(中略)…… switch (con->status) { case TCP_CONNECTION_STATUS_LISTEN: case TCP_CONNECTION_STATUS_SYNSENT: case TCP_CONNECTION_STATUS_SYNRECV: return tcp_recv_open(pkt, con, tcphdr); case TCP_CONNECTION_STATUS_ESTAB: if (tcphdr->flags & TCP_HEADER_FLAG_FIN) if (tcp_recv_close(pkt, con, tcphdr)) ……(中略)…… return tcp_recv_data(pkt, con, tcphdr); case TCP_CONNECTION_STATUS_FINWAIT1: case TCP_CONNECTION_STATUS_FINWAIT2: case TCP_CONNECTION_STATUS_CLOSEWAIT: case TCP_CONNECTION_STATUS_LASTACK: return tcp_recv_close(pkt, con, tcphdr); ……(中略)…… }
リスト4 TCPパケットの受信処理(tcp.c) |
tcp_recv()の内部では“コネクションの状態”によって動作を切り替えています。これは、例えば同じACKを受信したとしても、“接続開始中”なのか“接続後”なのかによって、その意味が変わってくるためです。接続開始中の場合にはtcp_recv_open()が、接続確立してデータ通信を行っている場合にはtcp_recv_data()が、接続のクローズ中にはtcp_recv_close()が呼ばれます。
リスト5は、tcp_recv_open()による接続開始時の処理です。
static int tcp_recv_open(struct netbuf *pkt, struct connection *con, struct tcp_header *tcphdr) { ……(中略)…… switch (tcphdr->flags & TCP_HEADER_FLAG_SYNACK) { case TCP_HEADER_FLAG_SYN: ……(中略)…… tcp_makesendpkt(con, TCP_HEADER_FLAG_SYNACK, 1460, 1460, 1, 0, NULL); con->status = TCP_CONNECTION_STATUS_SYNRECV; break; case TCP_HEADER_FLAG_SYNACK: ……(中略)…… tcp_makesendpkt(con, TCP_HEADER_FLAG_ACK, 1460, 0, 0, 0, NULL); ……(中略)…… buf->cmd = TCP_CMD_ESTAB; ……(中略)…… kz_send(con->id, 0, (char *)buf); con->status = TCP_CONNECTION_STATUS_ESTAB; break; case TCP_HEADER_FLAG_ACK: ……(中略)…… buf->cmd = TCP_CMD_ESTAB; ……(中略)…… kz_send(con->id, 0, (char *)buf); con->status = TCP_CONNECTION_STATUS_ESTAB; break; ……(中略)…… }
リスト5 TCPの接続開始処理(tcp.c) |
図2で説明したように、TCPの接続開始時はSYN、SYN+ACK、ACKの3つのパケットが往復します。これらのパケットを受信して状態遷移していき、最終的に接続確立状態(TCP_CONNECTION_STATUS_ESTAB)に移行するようになっています。さらに、SYNを受けたときにはSYN+ACKを、SYN+ACKを受けたときにはACKを返します。
リスト6は、接続確立後のデータ通信の処理(tcp_recv_data())です。
static int tcp_recv_data(struct netbuf *pkt, struct connection *con, struct tcp_header *tcphdr) { ……(中略)…… /* ACKを受信 */ if (tcphdr->flags & TCP_HEADER_FLAG_ACK) { ……(中略)…… con->seq_number = tcphdr->ack_number; tcp_send_flush(con); } ……(中略)…… /* データを受信 */ if (tcphdr->flags & TCP_HEADER_FLAG_PSH) { ……(中略)…… *(con->recv_queue_end) = pkt; con->recv_queue_end = &(pkt->next); tcp_recv_flush(con); ……(中略)…… }
リスト6 TCPの接続中のデータ通信処理(tcp.c) |
データを受信した場合には、受信データを“受信キュー”につないで、tcp_recv_flush()を呼ぶことでACKを返送し、httpdに受信データをメッセージ通信で送ります。ACKを受信した場合には、tcp_send_flush()を呼び出すことで次のデータを送信します。
tcp_recv_flush()の実装は、リスト7のようになっています。
static int tcp_recv_flush(struct connection *con) { ……(中略)…… for (pkt = con->recv_queue; pkt; pkt = next) { ……(中略)…… /* ACKを返す */ ……(中略)…… tcp_makesendpkt(con, TCP_HEADER_FLAG_ACK, 1460, 0, 0, 0, NULL); pkt->cmd = TCP_CMD_RECV; kz_send(con->id, 0, (char *)pkt); ……(中略)…… }
リスト7 TCPの受信キューのフラッシュ処理(tcp.c) |
ループで受信キューからデータを取り出し、ACKを返した後に、kz_send()によってhttpdに対して受信データを送信しています。
ここまでがTCPの受信処理です。
次に、送信処理について見てみましょう。リスト8はリスト3のtcp_proc()から呼ばれているtcp_send()の本体と、そこから呼ばれているtcp_send_enqueue()という関数の実装です。
static int tcp_send_enqueue(struct connection *con, uint8 flags, uint16 window, int mss, int window_scale, int size, char *data) { ……(中略)…… pkt = tcp_makepkt(con, flags, window, mss, window_scale, size, data); ……(中略)…… *(con->send_queue_end) = pkt; con->send_queue_end = &(pkt->next); tcp_send_flush(con); ……(中略)…… } static int tcp_send(struct netbuf *pkt) { ……(中略)…… tcp_send_enqueue(con, TCP_HEADER_FLAG_PSH|TCP_HEADER_FLAG_ACK, 1460, 0, 0, pkt->size, pkt->top); return 0; }
リスト8 TCPパケットの送信処理(tcp.c) |
ご覧の通り、tcp_send()は適切な引数でtcp_send_enqueue()を呼び出しているだけです。
一方、tcp_send_enqueue()では、tcp_makepkt()によりTCPパケットを適切なパラメータで作成し、送信キューに接続します。さらに、tcp_send_flush()が呼ばれることで、実際の送信処理が行われます。
tcp_send_flush()の実装は、リスト9のようになります。
static int tcp_sendpkt(struct netbuf *pkt, struct connection *con) { ……(中略)…… pkt->cmd = IP_CMD_SEND; pkt->option.ip.send.protocol = IP_PROTOCOL_TCP; pkt->option.ip.send.dst_addr = con->dst_ipaddr; kz_send(MSGBOX_ID_IPPROC, 0, (char *)pkt); ……(中略)…… } static int tcp_send_flush(struct connection *con) { ……(中略)…… pkt = con->send_queue; if (pkt) { ……(中略)…… tcp_sendpkt(pkt, con); ……(中略)…… }
リスト9 TCPの送信キューのフラッシュ処理(tcp.c) |
送信キューからパケットを取り出してtcp_sendpkt()を呼び出します。tcp_sendpkt()では、「MSGBOX_ID_IPPROC」というipタスクが持っているメッセージボックスに対して、kz_send()によりパケットをメッセージ送信することで、ipタスクに対して送信依頼を行います。
後は、ipタスクがIPヘッダを適切に付加し、ARPなどの処理が行われて、Ethernet上に送信されることになります。このあたりの処理は、他のタスクが「よきに計らってくれる」ため、tcpタスク側で特に考える必要はありません。
Copyright © ITmedia, Inc. All Rights Reserved.