(H8移植編その2第11回)TCPの再送処理の実装準備その2

2010/11/25

あなたは 人目のお客様です.

前回から引き続き,たまっていた改造を一気にアップ してしまおう.

TCPの再送を行うためには,パケットを送信したらすぐに削除してしまうのでなく 保存しておいて,ACKが来たらそこで削除して次のパケットを送信する,という 手順に書き換える必要がある.つまり送信用の一時バッファが必要になる.

実は,このバッファリング処理の設置にはちょっと抵抗がある.というのは, バッファリングしてパケットデータを残すことで再送に備えるということは, 送信時にはパケットをコピーして送信するということになるからだ. これは性能的にも影響があるのだが,なんといってもメモリを余計に使ってしまう. 従来はhttpdから受け取ったデータをそのまま加工して送信していたため, まあ頑張ればDRAMを利用せずに内蔵RAMだけの範囲でTCPの処理も行えるような 気もしていたのだが,このようなコピー処理が入ってしまっては, 内蔵RAMだけの動作はいよいよ難しくなってくるだろう.

しかし前回も書いたように,TCPの安定通信のためには 再送処理は必須だと思うし,タイマの実用的な使いかたの見本にもなる. ということで,まああまり深く考えずに実装してみようと思う.

で,そんなかんじでTCPの処理を書き換えてみた.

主な修正点を説明しよう.

まず接続管理用の connection 構造体に,送信バッファとして sending という メンバを追加する.

 struct connection {
   int number;
   kz_msgbox_id_t id;
   uint32 src_ipaddr;
   uint32 dst_ipaddr;
   uint16 src_port;
   uint16 dst_port;
-  uint32 snd_number;
-  uint32 seq_number;
-  uint32 ack_number;
+  uint32 snd_number; /* 自分がどこまで送信したか */
+  uint32 seq_number; /* 相手がどこまで受信したか */
+  uint32 ack_number; /* 自分がどこまで受信したか */
   enum {
     TCP_CONNECTION_STATUS_CLOSED,
     TCP_CONNECTION_STATUS_LISTEN,
-    TCP_CONNECTION_STATUS_SYNSENT,
+    TCP_CONNECTION_STATUS_SYNSENT1,
+    TCP_CONNECTION_STATUS_SYNSENT2,
     TCP_CONNECTION_STATUS_SYNRECV,
     TCP_CONNECTION_STATUS_ESTAB,
     TCP_CONNECTION_STATUS_FINWAIT1,
     TCP_CONNECTION_STATUS_FINWAIT2,
     TCP_CONNECTION_STATUS_CLOSEWAIT,
     TCP_CONNECTION_STATUS_LASTACK,
   } status;
+  struct netbuf *sending;
   struct netbuf *send_queue;
   struct netbuf **send_queue_end;
   struct netbuf *recv_queue;
   struct netbuf **recv_queue_end;
 };
あとついでにステータスの管理のために SYNSENT をSYNSENT1とSYNSENT2の2つに 分けている.これは,そうしたほうがなんかすっきり書けたから.

コネクションの削除時に,送信バッファも解放する処理を追加.

 static int tcp_free_connection(struct connection *con)
 {
   struct netbuf *buf, *next;
 
+  if (con->sending)
+    kz_kmfree(con->sending);
   for (buf = con->send_queue; buf; buf = next) {
     next = buf->next;
     kz_kmfree(buf);
   }
   for (buf = con->recv_queue; buf; buf = next) {
     next = buf->next;
     kz_kmfree(buf);
   }
   memset(con, 0, sizeof(*con));
   kz_kmfree(con);
 
   return 0;
 }
再送のためには送信バッファを設置して,ACKが来たら送信パケットを削除する という処理にする必要がある.つまり,パケットはそのまま送信するのでなく, 送信バッファにあるものをコピーして送信することになる.このためパケットの コピー用のサービス関数を追加.
+static struct netbuf *tcp_copypkt(struct netbuf *pkt)
+{
+  struct netbuf *copypkt;
+
+  copypkt = kz_kmalloc(DEFAULT_NETBUF_SIZE);
+  memcpy(copypkt, pkt, DEFAULT_NETBUF_SIZE);
+
+  copypkt->next = NULL;
+  copypkt->top = (char *)copypkt + (pkt->top - (char *)pkt);
+
+  return copypkt;
+}
パケットの送信関数では,パケットを送信バッファに格納して,さらにコピーして 送信するように修正する.これにより送信パケットはバッファに残るので, 必要に応じて再送することが可能になる.
 static int tcp_send_flush(struct connection *con)
 {
   struct netbuf *pkt;
 
   if (con->snd_number == con->seq_number) {
     pkt = con->send_queue;
     if (pkt) {
       con->send_queue = pkt->next;
       if (!con->send_queue)
 	con->send_queue_end = &(con->send_queue);
       pkt->next = NULL;
+
+      if (con->sending)
+	kz_kmfree(con->sending);
+      con->sending = pkt;
+      pkt = tcp_copypkt(pkt);
+
       tcp_sendpkt(pkt, con);
     }
   }
 
   return 0;
 }
なお tcp_recv_open(),tcp_recv_close(),tcp_recv_data() に関してはとくに差分は 掲載しないが,前回は #if 0 でくくるだけだったが,もう利用しないので, 今回ごっそり削除した.

次に TCP の受信処理の改造.

 static int tcp_recv(struct netbuf *pkt)
 {
   struct netbuf *buf;
   struct connection *con;
   struct tcp_header *tcphdr;
-  int new_status;
-  int closed = 0, ret = 0;
+  int opened = 0, closed = 0, ret = 0;
 
   tcphdr = (struct tcp_header *)pkt->top;
 
   con = tcp_search_connection_from_addr(0,
 					pkt->option.common.ipaddr.addr,
 					tcphdr->dst_port,
 					tcphdr->src_port);
   if (!con)
     con = tcp_search_connection_from_addr(0, 0, tcphdr->dst_port, 0);
 
   if (!con) return 0;
 
-  new_status = con->status;
-
   if (tcphdr->flags & TCP_HEADER_FLAG_RST) {
     closed++;
   }
 
   if (tcphdr->flags & TCP_HEADER_FLAG_ACK) {
-    /* ここで送信キューからパケット削除する */
+    con->seq_number = tcphdr->ack_number;
+
+    /* 送信バッファからパケットを削除する */
+    if (con->snd_number == con->seq_number) {
+      if (con->sending) {
+	kz_kmfree(con->sending);
+	con->sending = NULL;
+      }
+    }
 
     /* データ送信に対してACKが返ってきたので,次のデータを送信する */
     if (con->status == TCP_CONNECTION_STATUS_ESTAB) {
-      con->seq_number = tcphdr->ack_number;
       tcp_send_flush(con);
     }
 
-    /* SYNSENT,SYNRECVならばESTABに */
-    if ((con->status == TCP_CONNECTION_STATUS_SYNSENT) ||
-	(con->status == TCP_CONNECTION_STATUS_SYNRECV)) {
-      con->seq_number = tcphdr->ack_number;
-
-      /* セッション確立を上位タスクに通知 */
-      buf = kz_kmalloc(sizeof(*buf));
-      buf->cmd = TCP_CMD_ESTAB;
-      buf->option.tcp.establish.number = con->number;
-      kz_send(con->id, 0, (char *)buf);
+    if (con->status == TCP_CONNECTION_STATUS_SYNSENT1) {
+      con->status = TCP_CONNECTION_STATUS_SYNSENT2;
+    }
 
-      new_status = TCP_CONNECTION_STATUS_ESTAB;
+    if (con->status == TCP_CONNECTION_STATUS_SYNRECV) {
+      con->status = TCP_CONNECTION_STATUS_ESTAB;
+      opened++;
     }
 
     if (con->status == TCP_CONNECTION_STATUS_FINWAIT1) {
-      con->seq_number = tcphdr->ack_number;
-      new_status = TCP_CONNECTION_STATUS_FINWAIT2;
+      con->status = TCP_CONNECTION_STATUS_FINWAIT2;
     }
     if (con->status == TCP_CONNECTION_STATUS_LASTACK) {
-      con->seq_number = tcphdr->ack_number;
       con->status = TCP_CONNECTION_STATUS_CLOSED;
       closed++;
     }
   }
 
   if (tcphdr->flags & TCP_HEADER_FLAG_SYN) {
     /* LISTENならばSYN+ACKを返す */
     if (con->status == TCP_CONNECTION_STATUS_LISTEN) {
       con->snd_number = con->seq_number = 1;
       con->dst_ipaddr = pkt->option.common.ipaddr.addr;
       con->dst_port   = tcphdr->src_port;
       con->ack_number = tcphdr->seq_number + 1;
 
-      tcp_makesendpkt(con, TCP_HEADER_FLAG_SYNACK, 1460, 1460, 1, 0, NULL);
-      new_status = TCP_CONNECTION_STATUS_SYNRECV;
+      tcp_send_enqueue(con, TCP_HEADER_FLAG_SYNACK, 1460, 1460, 1, 0, NULL);
+      con->status = TCP_CONNECTION_STATUS_SYNRECV;
     }
 
     /* SYNSENTならばACKを返す(たぶんSYN+ACKが来ている) */
-    if (con->status == TCP_CONNECTION_STATUS_SYNSENT) {
+    if (con->status == TCP_CONNECTION_STATUS_SYNSENT2) {
       con->ack_number = tcphdr->seq_number + 1;
       tcp_makesendpkt(con, TCP_HEADER_FLAG_ACK, 1460, 0, 0, 0, NULL);
+      opened++;
+      con->status = TCP_CONNECTION_STATUS_ESTAB;
     }
   }
 
   if (tcphdr->flags & TCP_HEADER_FLAG_FIN) {
     /* FINWAIT2なら,ACKを返してCLOSEDに遷移 */
     if (con->status == TCP_CONNECTION_STATUS_FINWAIT2) {
       con->ack_number = tcphdr->seq_number + 1;
       tcp_makesendpkt(con, TCP_HEADER_FLAG_ACK, 1460, 0, 0, 0, NULL);
-      new_status = TCP_CONNECTION_STATUS_CLOSED;
+      con->status = TCP_CONNECTION_STATUS_CLOSED;
       closed++;
     }
 
     /* ESTABなら,ACK, FIN+ACK を返してLASTACKに遷移 */
     if (con->status == TCP_CONNECTION_STATUS_ESTAB) {
       con->ack_number = tcphdr->seq_number + 1;
       tcp_makesendpkt(con, TCP_HEADER_FLAG_ACK, 1460, 0, 0, 0, NULL);
-      /* new_status = TCP_CONNECTION_STATUS_CLOSEWAIT; */
-      tcp_makesendpkt(con,
+      /* con->status = TCP_CONNECTION_STATUS_CLOSEWAIT; */
+      tcp_send_enqueue(con,
 #if 0
-		      TCP_HEADER_FLAG_FIN,
+		       TCP_HEADER_FLAG_FIN,
 #else
-		      TCP_HEADER_FLAG_FINACK,
+		       TCP_HEADER_FLAG_FINACK,
 #endif		  
-		      1460, 0, 0, 0, NULL);
-      new_status = TCP_CONNECTION_STATUS_LASTACK;
+		       1460, 0, 0, 0, NULL);
+      con->status = TCP_CONNECTION_STATUS_LASTACK;
     }
   }
 
   if (tcphdr->flags & TCP_HEADER_FLAG_PSH) {
     /* データを受信 */
     if (con->status == TCP_CONNECTION_STATUS_ESTAB) {
       pkt->next = NULL;
       *(con->recv_queue_end) = pkt;
       con->recv_queue_end = &(pkt->next);
       tcp_recv_flush(con); /* データを上位タスクに通知してACKを返す */
       ret = 1;
     }
   }
 
-#if 0
-  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 1; /* conが削除されたので処理継続できないので返る.要修正 */
-    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);
-
-  case TCP_CONNECTION_STATUS_CLOSED:
-  default:
-    break;
+  if (opened) {
+    /* セッション確立を上位タスクに通知 */
+    buf = kz_kmalloc(sizeof(*buf));
+    buf->cmd = TCP_CMD_ESTAB;
+    buf->option.tcp.establish.number = con->number;
+    kz_send(con->id, 0, (char *)buf);
   }
-#endif
 
-  con->status = new_status;
   if (closed) {
     /* セッション終了を上位タスクに通知 */
     buf = kz_kmalloc(sizeof(*buf));
     buf->cmd = TCP_CMD_CLOSE;
     buf->option.tcp.close.number = con->number;
     kz_send(con->id, 0, (char *)buf);
 
     con = tcp_delete_connection(con->number);
     tcp_free_connection(con);
   }
 
   return ret;
 }
tcp_recv()は送信バッファの対処を追加して,さらに整理しなおした. ACK以外の制御パケット(SYNACKやFIN)は tcp_makesendpkt() で直接送信するのでなく, tcp_send_enqueue() を利用することで,送信バッファ経由で送信するようにした. またSYNSENTはSYNSENT1,SYNSENT2に分けて遷移するようにするようにした. これにより変数 new_status の処理が不要になってすっきりする.

最後に tcp_proc() の修正.これも今まで同様,送信バッファ設置と SYNSENT1,SYNSENT2に関する修正だ.

 static int tcp_proc(struct netbuf *buf)
 {
   struct connection *con;
   int ret = 0;
 
   switch (buf->cmd) {
   case TCP_CMD_IPADDR:
     my_ipaddr = buf->option.common.ipaddr.addr;
     break;
 
   case TCP_CMD_ACCEPT:
     con = kz_kmalloc(sizeof(*con));
     memset(con, 0, sizeof(*con));
 
     con->number     = ++number;
     con->id         = buf->option.tcp.accept.id;
     con->src_ipaddr = my_ipaddr;
     con->src_port   = buf->option.tcp.accept.port;
     con->snd_number = 1;
     con->seq_number = 1;
     con->ack_number = 0;
     con->status = TCP_CONNECTION_STATUS_LISTEN;
 
+    con->sending = NULL;
     con->send_queue = NULL;
     con->recv_queue = NULL;
     con->send_queue_end = &con->send_queue;
     con->recv_queue_end = &con->recv_queue;
 
     kz_send(MSGBOX_ID_TCPCONLIST, 0, (char *)con);
     tcp_search_connection_from_number(0); /* 頭出し */
     break;
 
   case TCP_CMD_CONNECT:
     con = kz_kmalloc(sizeof(*con));
     memset(con, 0, sizeof(*con));
 
     con->number     = ++number;
     con->id         = buf->option.tcp.connect.id;
     con->src_ipaddr = my_ipaddr;
     con->dst_ipaddr = buf->option.tcp.connect.ipaddr;
     con->src_port   = port++;
     con->dst_port   = buf->option.tcp.connect.port;
     con->snd_number = 1;
     con->seq_number = 1;
     con->ack_number = 0;
-    con->status = TCP_CONNECTION_STATUS_SYNSENT;
+    con->status = TCP_CONNECTION_STATUS_SYNSENT1;
 
+    con->sending = NULL;
     con->send_queue = NULL;
     con->recv_queue = NULL;
     con->send_queue_end = &con->send_queue;
     con->recv_queue_end = &con->recv_queue;
 
     kz_send(MSGBOX_ID_TCPCONLIST, 0, (char *)con);
     tcp_search_connection_from_number(0); /* 頭出し */
 
     /* SYNを送信 */
-    tcp_makesendpkt(con, TCP_HEADER_FLAG_SYN, 1460, 1460, 1, 0, NULL);
+    tcp_send_enqueue(con, TCP_HEADER_FLAG_SYN, 1460, 1460, 1, 0, NULL);
     break;
 
   case TCP_CMD_CLOSE:
     con = tcp_search_connection_from_number(buf->option.tcp.close.number);
     if (!con) break;
 
     /* FINを送信 */
     tcp_send_enqueue(con,
 #if 0
 		     TCP_HEADER_FLAG_FIN,
 #else
 		     TCP_HEADER_FLAG_FINACK,
 #endif
 		     1460, 0, 0, 0, NULL);
 #if 0
     con->status = TCP_CONNECTION_STATUS_FINWAIT1;
 #endif
     break;
 
   case TCP_CMD_RECV:
     ret = tcp_recv(buf);
     break;
 
   case TCP_CMD_SEND:
     ret = tcp_send(buf);
     break;
 
   default:
     break;
   }
動作はシミュレータで確認した.ちゃんとwebサーバが動いている. 実はMTM06の出展でのデモは前回あたりのソースコードで やっていたのだけど,こころなしか以前よりも安定して 動作しているように感じる.

これで再送処理をするための準備はできた.次はタイマを利用できるようにして, いよいよ再送処理の実装だ.


メールは kozos(アットマーク)kozos.jp まで