連載
» 2012年02月09日 11時55分 公開

ソフトウェア完全自作のWebサーバを動かしてみようH8マイコンボードで動作する組み込みOSを自作してみよう!(7)(2/4 ページ)

[坂井弘亮,@IT MONOist]

5.TCP処理の実装

 では、実装を説明していきましょう。まずは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.