では、実装を説明していきましょう。まずは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.
組み込み開発の記事ランキング
コーナーリンク
よく読まれている編集記者コラム