魔術師見習いのノート

プロフィール

魔術師見習い
Author魔術師見習い-_-.
Twitter魔術師見習い

コンピュータ関係のメモを主に書きます.

MENU

コンピュータ・ネットワーク(5) -トランスポート層-

投稿日:
修正日:
タグ:

目次

トランスポート層は5階層TCP/IPモデルの下から4番目であり、上位の層にサービスを行う最後の層である。

アプリケーション層
トランスポート層
ネットワーク層
データリンク層
物理層

トランスポート層は、アプリケーション層の各サービスに対し、通信を提供する。

サービス
    サービスを行ったり受けたりするために、そのプロセスに通信を提供。
  • フロー制御
  • 輻輳制御
  • 誤り検出
インタフェースデータ単位ソケット
アドレスポート番号
代表的なプロトコル
  • TCP
  • UDP
※UDPはフロー制御や輻輳制御などは行わず、アプリケーション層に任せているため、TCPだけといえなくもない
トランスポート層の代表的なプロトコルには、コネクション型のTCPと、コネクションレス型のUDPがある。TCPは電話のように互いにコンタクトを取りながらデータ通信を行うことで、品質の高い通信を提供する。UDPは手紙のように送信者が一方的に受信側にデータを送るようなものである。UDPの場合、送りつけられたデータが届いたかどうか、それが完全なものであるかの確認や、受信側やネットワークへの負荷管理などはアプリケーション層で確認しなければならない。
TCP(Transmission Control Protocol)
フロー制御や輻輳制御、誤り検出などを行うことで、高品質な通信をアプリケーション層に提供。
UDP(User Datagram Protocol)
TCPのような高品質な通信を行うための機能をアプリケーション層に任せることで、トランスポート層での高速通信を可能にする。
TCPの通信は、次の2種類の通信から成る。
  • データの送受信のための通信
  • 通信制御のための通信
本稿ではTCPを中心に説明する。

TCPの通信では、あるノードAがノードBに対してデータを送る場合、ノードAを送信側、ノードBを受信側と呼ぶ。ノードBからノードAに対して通信制御のための情報を送信することもあるが、多くの場合、その呼び方は変わらない。


3ウェイ・ハンドシェイク

TCPではコネクション型の通信を行う。そのために3ウェイ・ハンドシェイクと呼ばれる接続の確立を行う。接続の確立には、データ通信を開始するのを含め次の5つの工程が必要である。

  1. 送信者がSYNパケットを送る。
  2. 受信者がACKパケットを送る。
  3. 受信者がSYNパケットを送る。
  4. 送信者がACKパケットを送る。
  5. データを送信する。
SYNパケットは同期開始の合図であり、ACKパケットは確認の合図である。

TCPでは、これらの工程の次のように3つの工程にまとめている。

  1. 送信側がSYNパケットを送信する。
  2. 受信側がSYNパケットとACKパケットを送信する。
  3. ACKパケットと、データを送信する。

3ウェイ・ハンドシェイクで接続が確立されなければ、アプリケーション層はアクセスがあったかどうかも認識できない。それゆえ、SYNパケットを送り、SYN+ACKパケットが返ってくるかを待ち、返ってきても最後のACKを送信しないことで、相手に悟られることなくそのポートでTCPを使った通信を行なっているかを確認することができる。このような処理をポートスキャンという。これを防ぐには、1階層下のネットワーク層で、通信を監視する必要がある。このような機能や、それによって通信を防ぐ機能をファイアウォールという。

フロー制御

TCPのフロー制御では、受信相手の状況に応じて送信量を制御することで、受信側の負荷を小さくしようとする。そのために、受信側は受信可能な通信量、つまり受信者のバッファの空きサイズを送信側に知らせる。この通信量を制御する仕組みをスライディングウィンドウと、受信可能なサイズのことをウィンドウ・サイズという。受信側は,受信可能なウィンドウサイズを,ソケットのウィンドウ・サイズフィールドによって通知する。

輻輳制御

2つのノードが通信する場合,それが同一のネットワークであるとは限らない。そのような場合,パケットはノード間の転送速度は間にあるいくつかのネットワークやルータによって処理速度が変化する。それゆえ,それぞれのノードが好き放題な送信を行い,ルータがそれを処理しきれずにパケットをロスすれば結果として遅くなる。また,ネットワーク全体としての性能(スループット)も低下する。TCPにはこのような問題を防ぎ、ネットワークの利用効率を良くするための仕組み(輻輳制御の機能)がある。その仕組みがスロースタートアルゴリズムである。

送信側は、はじめに送出するサイズを小さく設定し、それから指数オーダ的に大きくしていく。もし輻輳が発生した場合,受信側はそれを送信側に知らせ,送出するサイズを小さくさせる。TCPでは送信側と受信側がそれぞれパケットを送りあうことでコネクションを確立するが,もし輻輳が発生した場合,受信側は送信側に送るパケットサイズを小さくすることで,それを知らせる。これはTCPには元々輻輳制御が備わっておらず,後に追加されたためにこのような通知手段をとっている。送出するサイズもまたウィンドウサイズという単位で扱われるが,輻輳制御の場合ウィンドウサイズはOSによって管理される。輻輳が発生してウィンドウサイズを減らした後,送信側は今度は線形的にウィンドウサイズを増やしていくことで,送出するサイズを理想的な状態に近づける。

ルータがパケットをロスするような場合,ルータはICMPパケットを使ってそれを送信側に知らせる。通信経路を特定するためのコマンドtracerouteは,この特性を利用して徐々にTTL(Time To Live)を増やしていく事で実装される。


ソケット

TCP通信で使用するソケットは次のような構造である。ここで各セルはビット単位を表す。

01234567 89101112131415 1617181920212223 2425262728293031
送信元ポート番号宛先ポート番号
シーケンス番号
ACK番号
データオフセット 予約領域 NS CWR ECE URG ACK PSH RST SYN FIN ウィンドウ・サイズ
チェックサム 緊急ポインタ
オプション(32ビット単位で可変長)
データ部分(可変長)
送信元ポート番号
サービスを行うトランスポート層のアドレス。
宛先ポート番号
サービスを受けるトランスポート層のアドレス。
シーケンス番号
データを分割して送信する場合、その順番を表す値。送信するデータには、1バイト毎に順にシーケンス番号が割り当てられている。シーケンス番号の値は、データの先頭が0とは限らず、最大値まで使用すると0から順に割り当てられる。シーケンス番号はTCPデータの送信側で管理される。例えば30バイトのデータを10バイトずつ送信する場合、ソケットのシーケンス番号は1つ目はn、2つ目はn+10、3つ目はn+20となる。
ACK(Acknowledge)番号
受信側がデータをどのシーケンス番号まで受信したか示す値。ACK番号は受信したデータのシーケンス番号+1である。例えば10バイトのデータを受信した場合、初期値+10の値になる。
データオフセット
ヘッダ長
TCPソケットのデータが始まる位置、またはTCPソケットのサイズを示す値。
予約領域
将来的に使用されるために予約されているフィールド。現在は未使用で値は常に0である。
フラグ
パケット(ソケット)の種類を表すフィールド群。各フィールドは1の時にオン、0の時にオフを表す。標準では全て0である。RFC793ではフラグが6つであったが、後に追加されていき、RFC3540までに3つ追加されて9ビットとなり、その分予約領域が縮小された。
URG
URGentフラグ。緊急データが含まれていることを表す。
ACK
ACKnowledgeフラグ。ACK番号が含まれていることを表す。
PSH
PuSHフラグ。受信したデータをすぐに上位レイヤに提供することを表す。
RST
ReSeTフラグ。TCP接続を中断、拒否することを表す。ある接続要求に対してACKならば接続許可、RSTならば接続拒否を表す。
SYN
SYNchronizeフラグ。接続要求を表し、ACK番号を同期する。
FIN
FINishフラグ。接続終了を表す。
後に追加されたフラグ
ECE
RFC3168で追加されたフラグで、輻輳が起こっていることや輻輳情報を通知するための機能が備わっていることを示す。ECN-Echoフラグ。具体的には、3ウェイハンドシェイク(SYNフラグがオン)の際、ECN(輻輳情報通知機能)対応であること、すなわちIPパケットのTOSの予備フィールドをECNのために使用できる(10か01、11の値かもしれない)ことを示す。それ以外(SYNフラグがオフ)の時、IPパケットのCEフラグが1だったことを示す。
CWR
RFC3168で追加されたフラグで、輻輳ウィンドウ・サイズを小さくしたことを示す。すなわち受信側がENEフラグがオンであるソケットを受信した場合、これをオンにしたソケットを送信することで、輻輳制御機構で対応したことを通知する。Congestion Window Reducedフラグ。
NS
RFC3540で追加されたENC Nonce Sumフラグ。
ウィンドウ・サイズ
受信側が受信可能なソケットのサイズ。
チェックサム
ヘッダやデータの誤りを検査するための値。
緊急ポインタ
URGフラグがオンの時に使用されるフィールド。 緊急データが シーケンス番号からのオフセットを示す。
オプション
拡張情報のためのフィールド。32ビット単位で可変長で、最小サイズは0バイトである。MSS(最大セグメントサイズ)やウィンドウ・スケーリングなどを設定するのに使用され、情報が32ビット単位にならない場合はその分0を詰められる(これをパディングと呼ぶ)。


関連コマンド

telnet
TELNETプロトコルのインタフェース。標準では23番が使われるが、ポート番号を指定することで他のポートにアクセスしてTCP通信が可能である。
user% telnet pied-piper.net 80
GET /note/note.cgi HTTP/1.1
Accept: image/gif, image/jpeg, */*
Accept-Language: ja
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (Compatible; MSIE 6.0; Windows NT 5.1;)
Host: pied-piper.net
Connection: Keep-Alive

nc
netcat
TCPかUDPで通信を行うためのユーティリティ。telnetと異なりtcpだけでなく、UDPも扱え、ポートスキャンやソケットの受け取り側としても起動可能。
-u
UDPモードで起動。
-l
リッスンモードで起動。
-p port
リッスンモードの際に開くポート番号を指定。
-s port
ソースポートを指定。
-z
スキャンのみでデータを送らない。
-v
詳細な情報を出力。
接続
user% nc pied-piper.net 80
GET /note/note.cgi HTTP/1.1
Accept: image/gif, image/jpeg, */*
Accept-Language: ja
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0 (Compatible; MSIE 6.0; Windows NT 5.1;)
Host: pied-piper.net
Connection: Keep-Alive

user% nc -u -z localhost 5555
リッスンモード
user% nc -p 5555 -l localhost
user% nc -p 5555 -u -l localhost
ポートスキャン
user% nc -z -v localhost 0-65535
user% nc -z -u -v localhost 0-65535
nmap
高性能ポートスキャニングツール。
user% nmap localhost -p1-1024 -sT
user% nmap localhost -p1-1024 -sT -sU
user% nmap 192.168.1.1,2 -p1-100,500-1024 -sT -sU
-p
ポート番号やポート番号の範囲を指定。
-T0
-T1
-T2
-T3
-T4
-T5
スキャンのタイミングを設定。数字が大きいほどスキャン速度は速い。
-r
検査するポートの順番は標準では無作為だが、順番に行うように設定。
-sT
TCP接続でスキャン。
-sU
UDPでスキャン
-sS
TCP SYNパケットでスキャン。
-sA
TCP ACKパケットでスキャン。
-sF
TCP FINパケットでスキャン。
-s0
IPパケットでスキャン。
-PE
ICMP Echoパケットでスキャン。
-PP
ICMP Timestampパケットでスキャン。
-PM
ICMP Address Maskパケットでスキャン。
-O
アクセス先マシンのOSを推測。ルート権限でのみ使用可能
--traceroute
tracerouteを行う。
tcpdump
パケットスキャニングツール.さまざまな機能を持ち、5階層TCP/IPモデルのいくつかの層で利用可能であり、プロトコルや宛先ポート番号、送信元ポート番号でフィルタリングすることができる。
user% tcpdump -X src port 80 -i eth0
proto
プロトコルを指定.
src port port
送信元のポート番号を指定.
dst port port
宛先のポート番号を指定.
and
or
not
いくつかの条件を組み合わせる


ソケットプログラミング

UDPやTCPソケット通信を利用したプログラム、すなわちアプリケーション層のプログラムは次のように書くことができる。

TCP

サーバ・プログラム

#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>


int main(int argc, char *argv[])
{
  const char* SERVER_ADDR = "192.168.1.14";
  const int SERVER_PORT = 50000;
  int server_fd, client_fd;
  FILE *fp;
  struct sockaddr_in sin, pin;
  char buf[20];
  size_t size = sizeof(struct sockaddr_in);
  
  memset(&sin, 0, sizeof(struct sockaddr_in));
  sin.sin_family = PF_INET;
  sin.sin_addr.s_addr = inet_addr(SERVER_ADDR);
  sin.sin_port = htons(SERVER_PORT);
  
  server_fd = socket(PF_INET, SOCK_STREAM, 0);
  bind(server_fd, (struct sockaddr*)&sin, sizeof(struct sockaddr_in));
  listen(server_fd, 50);
  
  client_fd = accept(server_fd, (struct sockaddr*)&pin, &size);
  fp = fdopen(client_fd, "w+");
  
  fprintf(fp, "Hello, Client\n");
  
  fgets(buf, 20, fp);
  printf("client ip:%s\n", inet_ntoa(pin.sin_addr));
  printf("Message: %s\n", buf);
  
  fclose(fp);
  return 0;
}

クライアント・プログラム

#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>


int main(int argc, char *argv[])
{
  const char* SERVER_ADDR = "192.168.1.14";
  const int SERVER_PORT = 50000;
  int server_fd;
  FILE *fp;
  struct sockaddr_in sin;
  char buf[20];
  
  memset(&sin, 0, sizeof(struct sockaddr_in));
  sin.sin_family = PF_INET;
  sin.sin_addr.s_addr = inet_addr(SERVER_ADDR);
  sin.sin_port = htons(SERVER_PORT);
  
  server_fd = socket(PF_INET, SOCK_STREAM, 0);
  connect(server_fd, (struct sockaddr*)&sin, sizeof(struct sockaddr_in));
  fp = fdopen(server_fd, "w+");
    
  fgets(buf, 20, fp);
  
  fprintf(fp, "Hello, Server\n");
  printf("Server ip:%s\n", inet_ntoa(sin.sin_addr));
  printf("Message: %s\n", buf);
  
  fclose(fp);
  return 0;
}


UDP

送信側プログラム

#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>


int main(int argc, char *argv[])
{
  const char* SERVER_ADDR = "192.168.1.14";
  const int SERVER_PORT = 50000;
  const char HELLO[] = "Hello, Server";
  int sock;
  struct sockaddr_in sin;
  char buf[20];
  
  memset(&sin, 0, sizeof(struct sockaddr_in));
  sin.sin_family = PF_INET;
  sin.sin_port = htons(SERVER_PORT);
  sin.sin_addr.s_addr = inet_addr(SERVER_ADDR);
  
  sock = socket(PF_INET, SOCK_DGRAM, 0);
  
  sendto(sock, HELLO, strlen(HELLO), 0, (struct sockaddr*)&sin, sizeof(struct sockaddr_in));
  
  close(sock);
  return 0;
}

受信側プログラム

#include <sys/socket.h>
#include <netinet/in.h>
#include <stdio.h>
#include <string.h>


int main(int argc, char *argv[])
{
  const char* SERVER_ADDR = "192.168.1.14";
  const int SERVER_PORT = 50000;
  const char HELLO[] = "Hello, Client";
  size_t len = sizeof(struct sockaddr_in);
  int sock;
  struct sockaddr_in sin, pin;
  char buf[20];
  
  memset(&sin, 0, sizeof(struct sockaddr_in));
  sin.sin_family = PF_INET;
  sin.sin_port = htons(SERVER_PORT);
  sin.sin_addr.s_addr = inet_addr(SERVER_ADDR);
  
  sock = socket(PF_INET, SOCK_DGRAM, 0);
  bind(sock, (struct sockaddr*)&sin, len);
  
  recvfrom(sock, (void*)buf, 13, 0, (struct sockaddr*)&pin, &len);
  buf[13] = '\0';
  printf("Message: %s\n", buf);
  
  close(sock);
  return 0;
}

一覧