2 TCP/IPプログラミング

2 TCP/IPプログラミング

Menu Menu

1 目的

この実験では、インターネットで使われている通信規約であるTCP/IPの利用法について学ぶ。具体的は、NNTPサーバやWWWのサーバと telnet コマンドにより通信を行い、通信方法を調べる。そして、C言語によりクライアントのプログラムを作成、および、サーバのプログラムを作成する。


2 関連科目

    情101	ソフトウエア基礎 I	2
    情102	ソフトウエア基礎 II	2
    情103	ソフトウエア基礎演習 I	1
    情104	ソフトウエア基礎演習 II	1
    情211	情報通信	2


3 準備


3.1 TCP/IP


ストリーム

TCP/IPは、信頼性のある(reliable)双方向のストリーム転送サービス(stream transport service)を提供する通信プロトコルである(図1)。ストリームとは、通信する2つのプロセス間に結合(connection)が形成され、複数回に分けて送り出したデータでも、順番が入れ替わらないが、データの区切りがわからなくなるような転送サービスである。UNIXのパイプは、双方向ではなく、単方向であるが、同じストリームに分類される転送サービスである。

なお、C言語のライブラリ関数である fopen(), fgets(), fputs() なども、ストリームと呼ばれることがある。これは、もともとランダム・アクセス可能で、メモリ中の配列と同じようにアクセスするすることもできるファイルを、まるでプロセス間通信のストリームと同じように扱うことができることに由来する。ストリームの元の意味は、プロセス間通信である。


TCP/IPによる通信では、図2に示すように、4つのプロトコル(規約、約束事)の層を構成される。TCP/IP自身は、TCP層と IP層という2つの層に分解される。このようにさまざまなプロトコルが決められ、全体として層をなしている。この様子を、プロトコル・スタックと呼ぶ。

TCP は、IP という通信プロトコルを利用して実現されている。IPは、(信頼性がない)データグラム(datagram)転送サービスを提供する通信プロトコルである。データグラムでは、データの送り手と受けての間に結合が形成されず、途中で順番が変ることや失われることがある。データグラムは、書留めではない郵便に似ている。IPのデータグラムが配達されるときに使われる番地が、IP アドレスである。IPアドレスとしては、現在32ビットの整数が使われている。

TCP層の上は、応用層がある。この層では、ftp, rlogin, WWW, mnews, sendmail などの、TCP/IP を利用するプログラムの間の会話の方法が定義される。TCP/IPを使った通信は、まるでプロセス同士が電話で会話をしているように進められる。普通の電話では、日本語を話す人と英語を話す人は、電話で情報交換ができない。同様に、同じTCP/IPを使っていても、会話の約束事が違うと、まったく情報交換ができない。ゆえに、TCP/IPの上にさらに、情報交換のためにさまざまなプロトコルが取り決められている。

TCP/IPの上に構築されているプロトコルの例を、表1に示す。ポート番号については、後述する。


表1 TCP/IPの上に構築されているプロトコルの例

表1 TCP/IPの上に構築されているプロトコルの例
ポート番号 名前目的
21 FTP(File Transfer Protocol) ファイル転送
23 Telnet 遠隔ログイン(telnet)
25 SMTP(Simple Mail Transfer Protocol) 電子メールの転送
79 finger fingerコマンド
80 HTTP(HyperText Transfer Protocol) WWWのデータ転送
119 NNTP(Network News Transfer Protocol) ネットワーク・ニュースの記事の転送
513 login 遠隔ログイン(rlogin)

IPのデータグラムを転送するためには、さまざまな物理的な媒体が使われる。現在LANでは、インターネットやFDDIがよく使われいる。イーサネットは、同軸ケーブルやより対線(Twisted Pair)を使ってデータを転送する。FDDIは、光ケーブルを使っている。モデムなどを使ったシリアル回線では、PPP(Point to Point Protocol)というプロトコルの上に、IPデータグラムが流される。

データグラムは、ネットワーク通信では、最も基本的な転送サービスである。IP上に構築された UDP(User Datagram Protocol)も、IPとほとんど同じ機能を提供する。また、イーサネットやFDDIが提供する転送サービスも、データグラムである。


ホストとルータ

ネットワークに接続されている計算機の中で、ネットワークに1ヵ所の出入り口(インタフェース)を持っているものは、ホストと呼ばれる。2ヵ所以上の出入り口を持っている計算機は、ルータと呼ばれる。ルータは、ネットワークとネットワークを接続するための計算機である。ルータは、入ってきたIPのパケットのIPアドレスを見て、どのネットワークに送ればよいかを判断する。

図1で、左端と右端にあり、4層全てそろっている部分がホストである。TCP/IPの通信は、ホストとホストの間で行われる。中央の、2層しかない部分は、ルータである。ルータの仕事は、IP層において行われる。


仮想回線

TCP/IP では、プロセスとプロセスが、電話で会話をするように通信が行われる。普通の電話で人間同士が話をするには、まず電話番号を指定して、話相手に電話をとってもらわなければならない。TCP/IPでは、電話を掛ける方をクライアント・プロセス、電話を待つ方をサーバ・プロセスと言いう。

TCP/IPにおいて、プロセス間に形成されたストリーム通信路のことを、計算機間に張られた物理的な回線に似ていることから、仮想的回線(virtual circuit)とも言う。TCP/IP では、回線を接続する段階では、クライアント・プロセスとサーバ・プロセスは非対称である。一度仮想回線が接続された後は、両方のプロセスは、TCP/IPのレベルでは、まったく対称的になる。

TCP/IPでプロセス間に仮想回線を開設するには、IPアドレスとポート番号が使われる。ポート番号は、同じIPアドレスを持つホスト上で動いているプロセスを区別するために使われる。

図3に、通信路が開設される手順を示す。

(1)サーバ・プロセスがポート番号を指定して、要求受付用ポートを作る。サーバ・プロセスは、クライアント・プロセスからの接続要求を待つ。要求受付用ポートでは、データの送受信はできない。

(2)クライアント・プロセスが通信用ポートを作る。このポートを、サーバ・プロセスが動いているIPアドレスと、サーバ・プロセスが作った通信ポートのポート番号を使って、接続要求を行う(図3(a))。

(3)接続要求が受け付けられると、サーバ・プロセスには、新たに通信用ポートが作られる(図3(b))。これは、特定のクライアントとの通信のために使われる。

こうして一度通信路が開設されると、クライアントとサーバは、どちらからでもデータを送り始めることができる。

表1に示されているポート番号の例は、公に利用目的が決められているものである。それ以外に、1024以上のポート番号は、特に利用目的が決められていないので、空いていれば利用者が自由に使ってもよい。


プロセス間通信におけるクライアント・サーバ・モデル

プロセス間通信は、本来自由に行うことができる。どのプロセスも自由にメッセージを送信する権利がある。

クライアント・サーバ・モデルは、本来対称的なプロセスを最初にメッセージを送る方(クライアント・プロセス)と受ける方(サーバ・プロセス)に分類することで、プロセス間通信を構造化し、わかりやすくするものである。TCP/IPの接続におけるクライアントとサーバの役割は、このプロセス間通信におけるクライアント・サーバ・モデルの1つの例になっている。

その他に、クライアントとサーバという言葉は、サービスを受ける方と提供する方の意味で使われることがある。インターネットにおけるプロセス間通信では、多くの場合、サービスの授受の関係におけるクライアントとサーバと、プロセス間通信におけるクライアントとサーバが一致している。


ソケット

UNIX 上で動作するプログラムがTCP/IPの機能を使う場合、UNIXが提供するソケットというインタフェースを通じて利用することになる。ソケットは、TCP/IP を始めとして、XNS, OSI などさまざまな通信プロトコルを UNIX 上で使うために設計されたものである。TCP/IP だけを考えると、ソケットのインタフェースは、繁雑であり、使いにくくなっている。


DNS(Domain Name Service)

TCP/IPによる通信は、通信相手のIPアドレス(32ビットの整数)とポート番号さえわかれば、可能である。しかしながら、人間にとってIPアドレスは扱いやすいものではない。人間にとってわかりやすい記号の名前から、IPアドレスに変換するサービスがあれば便利である。このサービスを、名前サービス、それを行うプログラムを名前サーバという。

インターネットにおける名前サービスは、名前空間をドメイン(領域)に分割して、階層的管理することで実現されている。これを、ドメイン・ネーム・サービス(Domain Name Service, DNS)という。DNSという言葉は、名前サービスを提供するプログラム(名前サーバ)を差すこともある。

DNSでは、主に名前をIPアドレスへ変換するサービスが使われている。その他に、名前から電子メールの配送先、名前から名前サーバが動いているホストの名前、名前から、名前サーバ自身の管理情報、逆にIPアドレスから名前を引くためにも使われる。


3.2 telnet コマンド

ソフトウェア基礎で勉強した telnet コマンドの使い方を復習しなさい。


4 実験

以下に、課題の必修・選択の別を示す。

  1. 課題1必修。
  2. 課題2必修。
  3. 課題3,5から1つ以上選択。課題4,6,7は、オプション。
  4. 課題8必修。
  5. 課題9-11から1つ以上選択。
  6. 課題12必修。
  7. 課題13-15から1つ以上選択。


4.1 telnet コマンドによるサーバへのアクセス


4.1.1 telnet コマンドの使い方

telnet コマンドは、通常、次のように使われる。

    -----------------------------------------------------------------------
    % telnet host1
    -----------------------------------------------------------------------

これは、次の省略形である。

    -----------------------------------------------------------------------
    % telnet host1 telnet
    -----------------------------------------------------------------------

ここで、第2引数の "telnet" は、TCP/IP のポート番号を示す記号である。この記号は、/etc/services というファイルに次のように格納されている。
    -----------------------------------------------------------------------
    telnet		23/tcp
    -----------------------------------------------------------------------

telnet コマンドは、/etc/services ファイルを検索し、与えられた記号からポート番号(この例では23)を得る。また、host1 の IP アドレスを、/etc/hosts や DNS から得る。telnet コマンドは、この IP アドレスとポート番号の2つを使って、TCP/IP の接続を行う。(注意:NISが動いている場合には、/etc/services や /etc/hosts の代わりに、NISのデータベース(マップ)が検索される。)

telnet コマンドでは、次のように、ホストのIPアドレスとポート番号を数字で打ち込むこともできる。

    -----------------------------------------------------------------------
    % telnet 133.13.30.131 23
    -----------------------------------------------------------------------

ここで、133.13.30.131 とは、32ビットのIPアドレスを8ビットづつに区切り、それぞれの8ビットの整数を10進数で表記したものである。よって、実際のIP アドレスは、次のようしてに計算できる。
	((((133-256)+13)-256)+30)-256+131
	== 2232229507

よって、telnet コマンドに次のようIPアドレスを与えてもよい。
    -----------------------------------------------------------------------
    % telnet 2232229507
    -----------------------------------------------------------------------


4.1.2 telnet コマンドによる fingerd へのアクセス

finger コマンドは、利用者の情報を表示するコマンドである。finger コマンドは、ネットワーク経由で遠隔の利用者の情報を調べることもできる。そのような finger コマンドの利用例を以下に示す。

    -----------------------------------------------------------------------
    % finger shinjo@pw001↓
      ^^^^^^^^^^^^^^^^^^^
    [pw001] 
    Login name: yas       			In real life: Yasushi SHINJO
    Directory: /home/teacher/yas        	Shell: /usr/local/bin/tcsh
    On since Sep 08 19:48:13 on pts/0 from top.ie.u-ryukyu.
    9 minutes 29 seconds Idle Time
    No Plan.
    Login name: j94070    			In real life: Yasushi [j94] Shinjo
    Directory: /home/b5/y94/j94070      	Shell: /usr/local/bin/tcsh
    No Plan.
    Login name: j94077    			In real life: Yasushi SHINJO
    Directory: /home/b2/y94/j94077      	Shell: /usr/local/bin/tcsh
    No Plan.
    % 
    -----------------------------------------------------------------------

ここで、下線(^^^^)を付けた部分が、利用者のタイプである。

この例で、finger コマンドは、クライアントとして、ホスト pw01 のポート番号79(finger)のポートに対して、TCP/IP による仮想回線を開設する。そして、次のような文字列を送る。

    -----------------------------------------------------------------------
    shinjo↓
    -----------------------------------------------------------------------

すると、pw001 上のサーバ(fingerd, finger daemon)は、上の例で示した情報をクライアントに返す。サーバは、データ転送が完了すると、TCP/IP の結合を切断する。finger コマンド(クライアント)は、受け取ったデータをを整形して利用者に対して表示する。


■課題1:

telnet コマンドを使って、finger サーバにアクセスして、自分の情報を検索してみなさい。アクセス先のホストとしては、pw の中から選びなさい。その様子を、レポートに添付しなさい。script コマンドや emacs 中のshell を使うとよい。また、nirai, kanai に対して行ってみなさい。その結果と、前の結果を比較しなさい。


4.1.3 telnet コマンドによる WWW サーバへのアクセス

WWW (the World-Wide Web)では、TCP/IP の上にさらにHTTP(HyperText Transfer Protocol)と呼ばれるプロトコルを構築し、データの転送を行っている。Netscape や Lynx などのブラウザは、WWW サーバとの間に TCP/IP による通信路を開設する。そして、クライアントは、必要なデータを得るための命令を送る。この命令の形式を定めたものが、HTTP である。HTTP 通信プロトコルを受け付けるサーバを、HTTP サーバと呼ぶ。

表2に、HTTP で定義されている命令の例を示す。これらの命令に対して、サーバは、表3に定義されたような応答を行う。


表2 HTTPで定義されている命令(methods)の例

HTTPで定義されている命令(methods)の例
命令 説明
GET 情報を得る(ヘッダと本体の両方)
HEAD 情報のヘッダのみを得る
POST 新しく情報を作る


表3 HTTPで定義されている状態コードの例

HTTPで定義されている状態コードの例
状態コード 説明
200 OK(エラーなし)
301 要求されたデータが移動した
400 要求の形式にエラーがある。
404 要求されたデータが見つからない。


URL を持つデータへのアクセス

たとえば、次のような URL を持つデータをアクセスすることを考える。

	http://www.ie.u-ryukyu.ac.jp:80/Welcome.html

Netscape などのクライアントは、まずホスト名 www.ie.u-ryukyu.ac.jp とポート番号80 を使ってサーバとの間に TCP/IP の通信路を開設する。そして、次のような文字列を送る。

    -----------------------------------------------------------------------
    GET /Welcome.html HTTP/1.0↓
    ↓
    -----------------------------------------------------------------------

ここで、"GET" が命令の種類、"/Welcome.html" は、要求しているデータを表わす URL (ファイル名)、"HTTP/1.0" は、使っているプロトコルのバージョンである。次の空行は、命令の終りを意味するものであり、必要である。

すると、サーバは、次のようなデータを送る。

    -----------------------------------------------------------------------
    HTTP/1.0 200 Document follows
    MIME-Version: 1.0
    Server: CERN/3.0pre6
    Date: Thursday, 07-Sep-95 13:14:34 GMT
    Content-Type: text/html
    Content-Length: 782
    Last-Modified: Monday, 07-Aug-95 03:55:13 GMT
    <HEAD>
    <TITLE>University of Ryukyus WWW Home Page.</TITLE>
    </HEAD>
    <BODY>
    <IMG SRC="title.gif">
    <H3>
    Welcome to the University of the Ryukyus.
    </H3>
    <HR>
    <H2>
    <UL>
    <LI> <A HREF="http://www.ie.u-ryukyu.ac.jp:20080/Welcome.html"> English</A>
    <LI> <A HREF="http://www.ie.u-ryukyu.ac.jp:20080/Welcome-j.html">
	    Japanese (any,mixed)</A>
    <LI> <A HREF="http://www.ie.u-ryukyu.ac.jp:8002/Welcome-j.html">
	    Japanese (EUC only)</A>
    <LI> <A HREF="http://www.ie.u-ryukyu.ac.jp:8003/Welcome-j.html">
	    Japanese (JIS only)</A>
    <LI> <A HREF="http://www.ie.u-ryukyu.ac.jp:8004/Welcome-j.html">
	    Japanese (Shift-JIS only)</A>
    <LI> <A HREF="kanji-menu.html"> Kanji code menu </A>
    </UL>
    </H2>
    <HR>
    <ADDRESS>
    <a >Web Master </a>of University of the Ryukyus.
    </ADDRESS>
    </BODY>
    -----------------------------------------------------------------------

最初の行が、状態行(status line)と呼ばれる、要求が成功したか失敗したかわ表わしている。"200" とは、成功したという意味である。2行目から最初の空行までは、これから送るデータのメタ情報である。具体的には、データの型や、サーバのバージョン、データが更新された日付と時刻、バイト数などが記録されている。

最初の空行の次が、データの本体である。この例では、HTMLで記述されたデータが返されている。サーバは、データ転送が完了すると、TCP/IP の結合を切断する。

クライアントは、受け取ったデータをを整形して利用者に対して表示する。たとえば、インライン・イメージとして指定されたデータを続けてサーバに要求して展開したり、フォントを変えたりして表示する。


■課題2:

telnet コマンドを使って、WWW サーバにアクセスし、データを画面に表示させなさい。そのデータの URL とデータの先頭の20行程度を報告書に添付しなさい。


4.1.4 telnet コマンドによる NNTP サーバへのアクセス

NNTP(Network News Transfer Protocol) とは、ネットワーク・ニュースの記事の転送や、記事の読み書きを行うためのプロトコルである。mnews や GNUS などのネットワーク・ニュースを読み書きするソフトウェアは、NNTP サーバとの間に TCP/IP による結合を形成する。そして、クライアントは、記事を要求する文字列や、ニュース・グループの一覧を要求する文字列を送る。これに対してサーバは、要求された記事やニュース・グループの一覧を返す。表4に、クライアントからサーバへ送られるNNTPのコマンド、表5に、サーバからクライアントへ返され応答を示す。


表4 NNTPのコマンド

NNTPのコマンド
GROUP ニュース・グループ名
	    ニュース・グループを選択する。結果として、記事の数、記事の番号
	    の上限と下限が返される。

ARTICLE 記事番号
	    その記事の内容を得る。ニュース・グループが選択されている状態の
	    時に使える。

ARTICLE <message-id>
	    メッセージID &lt;message-id&gt; の記事の内容を得る。

HELP
	    ヘルプ・メッセージの表示

QUIT
	    終了


表5 NNTPの応答

NNTPの応答
応答コード 説明
100 ヘルプのテキストが続く。
200 要求受け付け可能である(投稿可能)。
201 要求受け付け可能である(投稿不可能)。
205 通信路を切断する。
211 ニュース・グループが選ばれた。
		    記事の数、記事番号の上限、下限、ニュース・グループ名。

400 サービスを中断する。
411 そのようなニュース・グループがない。
421 もうそのニュース・グループには次の記事がない。
500 コマンドが認識できなった。
501 コマンドの文法に誤りがあった。
502 アクセスが制限されている。


telnet コマンドを利用して、NNTP サーバに接続

以下に、telnet コマンドを利用して、NNTP サーバに接続した様子を示す。
    -----------------------------------------------------------------------
      1: % telnet spn.ie.u-ryukyu.ac.jp nntp↓
	   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      2: Trying 133.13.31.5 ...
      3: Connected to spn.ie.u-ryukyu.ac.jp.
      4: Escape character is '^]'.
      5: 200 spn NNTP[auth] server version 1.5.12.1 (1 Jan 1995) ready at Thu Sep  7 22:56:28 1995 (posting ok).
      6: help↓
	 ^^^^^^
      7: 100 This server accepts the following commands:
      8: ARTICLE     BODY         GROUP
      9: HEAD        LAST         LIST
     10: NEXT        POST         QUIT
     11: STAT        NEWGROUPS    HELP
     12: IHAVE       NEWNEWS      SLAVE
     13: DATE
     14: 
     15: Additionally, the following extensions are supported:
     16: 
     17: XHDR        Retrieve a single header line from a range of articles.
     18: LISTGROUP   Retrieve a list of valid article-numbers.
     19: XOVER       Return news overview data.
     20: XGTITLE     Same as LIST NEWSGROUPS (for backward compatibility).
     21: 
     22: Bugs to Stan Barber (Internet: nntp@academ.com)
     23: .
     24: quit↓
	 ^^^^
     25: 205 spn closing connection.  Goodbye.
     26: Connection closed by foreign host.
     27: %
    -----------------------------------------------------------------------

ここで、下線(^^^^)で示した部分が、キーボードからのタイプである。この例では、ホスト spn.ie.u-ryukyu.ac.jp のポート番号119(nntp)のポートに、TCP/IPにより接続を試みている。2行目から4行目は、telnet コマンドによる定型的な表示である。ここで、通信路が確立されている。すると、サーバは、200 という応答を返している。これは、NNTP で定義されている応答であり、サーバが、要求を受け付け可能であり、かつ、要求としては投稿要求も受け付けることを意味している。200 以降の文字列は、コメントである。

第6行では、"help" というコマンドをサーバに送っている。これに対して、サーバは、100 という応答に続けて、受け付け可能なコマンドなど、簡単な使い方を返している。23行目に「.」からなる行がある。これが、1つのコマンドに対する応答の終りを示している。

次に、24行において、"quit" というコマンドをサーバに送っている。これにたいして、サーバは、205 という応答を返し、続いて TCP/IP の結合を切断している。26行目は、telnet コマンドが生成したメッセージである。


■課題3:

telnet コマンドを使って、NNTP サーバ spn.ie.u-ryukyu.ac.jp にアクセスし、ネットワーク・ニュースの記事を画面に表示させなさい。その記事のニュース・グループ、ニュース・グループ内の番号、記事の先頭の20行程度を報告書に添付しなさい。このとき、表NNTP のコマンドを使うとよい。


■課題4:

その他の NNTP のコマンドを使ってみなさい。NNTP の定義は、RFC977 というドキュメントにある。RFC977は、/usr/open/doc/rfc/rfc977.txt にある。


4.1.5 telnet コマンドによる SMTP サーバへのアクセス

SMTP(Simple Mail Transfer Protocol)とは、電子メールの転送を行うためのプロトコルである。このプロトコルは、MTA (Mail Transfer Agent) と呼ばれるプログラムの間で電子メールを配送するために使われている。MTA の例としては、UNIX 上で動作するsendmailと呼ばれるプログラムがあげられる。MTA は、一方が SMTP サーバ、もう一方が SMTP クライアントとなり、電子メールを転送する。表6に、SMTPで定義されている要求、表7に応答を示す。


表6 SMTPで定義されている手続き

    -----------------------------------------------------------------------

SMTPで定義されている手続き
MAIL FROM:<reverse-path>
	    <td>新しいメールの転送を開始する。
	    &lt;reverse-path&gt;は、エラーが起きた時の送り返し先のアドレス。

RCPT TO:<forward-path>
	    <td>メールの送り先として &lt;forward-path&gt; を指定する。

DATA
	    <td>メールのデータを送り始める。

VRFY <login-name>
	    <td>メールの受け手を確認する。

EXPN <ml-name>
	    <td>メーリング・リストの受取人を表示する。

SEND FROM:<reverse-path>
	    <td>メールを転送する。

HELO <domain>
	    <td>最初に接続した時に自分自身を相手に知らせる。

QUIT
	    <td>接続を切る。


表7 SMTPの応答コード

SMTPの応答コード
コード 説明
220 <domain> というドメインで要求受け付け可能である。
250 そのアドレスへのメールは、受け付け可能である。
251 そのアドレスは、ローカルには受取人がいない。


telnet コマンドを利用して、SMTP サーバに接続

以下に、telnet コマンドを利用して、SMTP サーバに接続した様子を示す。

    -----------------------------------------------------------------------
      1: % telnet p001 smtp↓
	   ^^^^^^^^^^^^^^^^^^
      2: Trying 133.13.30.131 ...
      3: Connected to pw001.
      4: Escape character is '^]'.
      5: 220 pw001.ie.u-ryukyu.ac.jp Sendmail ALPHA-6.58/6.4J.6-UReecs-UG-21.0 ready at Fri, 8 Sep 1995 00:25:05 +0900
      6: vrfy root↓
	 ^^^^^^^^^^^
      7: 250 <root@pw001.st.ie.u-ryukyu.ac.jp>
      8: quit↓
	 ^^^^^^
      9: 221 pw001.st.ie.u-ryukyu.ac.jp closing connection
     10: Connection closed by foreign host.
     11: %
    -----------------------------------------------------------------------

ここで、下線(^^^^)で示した部分が、キーボードからのタイプである。この例では、ホスト pw001 のポート番号25(smtp)のポートに、TCP/IPにより接続を試みている。2行目から4行目は、telnet コマンドによる定型的な表示である。ここで、接続が確立されている。すると、サーバは、220 という応答を返している。これは、SMTP で定義されている応答であり、サーバが、コマンド要求を受け付け可能であり、

第6行では、"vrfy" (verify) というコマンドをサーバに送っている。これに対して、サーバは、250 という応答に続けて、電子メールのアドレスを返している。

次に、8行において、"quit" というコマンドをサーバに送っている。これにたいして、サーバは、221 という応答を返し、続いて TCP/IP の結合を切断している。10行目は、telnet コマンドが生成したメッセージである。


■課題5:

telnet コマンドを使って、pw001 と pw002 にアクセスし、自分のログイン名と存在しないログイン名について、VRFY で調べてみなさい。そして、ホストによる応答の違いを比較しなさい。


■課題6:

pw001 以外のホストで、mail コマンドに -v オプションを付けて、メールを送って、その様子を観察しなさい。すると、SMTP による電子メールの配送の様子が観察される。ただし、mail -v では、電子メールのデータ(DATAの部分)は、表示されない。ここでデータとは、To: 行や From: 行などのヘッダも含む。SMTP のレベルにおいては、データの部分の To: 行は、配送には使われない。その代わりに、SMTP で定義されている RCPT が使われる。


■課題7:

SMTP コマンドを使って電子メールを送ってみなさい。SMTP の定義は、RFC821 というドキュメントにある。RFC821は、/usr/open/doc/rfc/rfc821.txt にある。mail -v の結果を参考にするとよい。


4.2 クライアントの作成

4.1節で telnet でやったことを、C言語のプログラムで実現する。stream ライブラリを使う。


4.2.1 initport() と fdopen2()


streamライブラリ

この実験では、Don Libes 氏により開発された stream ライブラリを用いる。これは、UNIX 上で TCP/IP を使うプログラムを書きやすくするためのライブラリである。このライブラリを使うことで、UNIX固有の繁雑なソケットを利用することなく、もともとの TCP/IP のモデル(3.1節の図3参照)に近い形でTCP/IPを利用するプログラムを書くことが可能になる。


initport()

steam ライブラリでは、initport() という関数が、中心的な役割を果たす。initport() は、TCP/IP のクライアント側(接続を行う方)でも、サーバ側(接続されるのを待つ方)でも使われる。この項では、クライアント側の使い方を示す。

initport() は、クライアント側では、次の2種類の形式で使われる。

    -----------------------------------------------------------------------
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include "inet.h"
    int s = initport(PORT_NUMBER(port), CLIENT,SOCK_STREAM,host);
    int s = initport(PORT_NAME(name),CLIENT,SOCK_STREAM,host);
    int port ;
    char -name ;
    char -host;
    -----------------------------------------------------------------------

前者は、ポート番号を指定して、TCP/IP の接続を確立するものである(3.1節の図3参照)。重要な引数は、TCP/IP のポート番号 port と、ホスト名 host である。その他の引数 CLIENT と SOCK_STREAM は、定数であ。これらは、クライアント側で使う限り、必ず指定されなければならない。

結果として、1つのファイル記述子 s が返される。これは、TCP/IP の双方向ストリームに対応している。よって返されたファイル記述子に対して次のように write() システム・コールでデータを書き込むと、そのデータは、接続先に送り届けられる。

	write( s, buf, size );

逆に、接続先から送られてきたデータは、read() システム・コールにより受け取ることができる。
	read( s, buf, size );

1つのファイル記述子にたいして、read(),write() もと利用することができる。これは、ファイルを読み書き(O_RDWR)モードで開いた時と似ている。

もう1つの形式は、ポート番号の代わりにポート名(サービス名)を指定する方法である。たとえば、"telnet" というサービスの場合、次のようにして使うことができる。

	s = initport(PORT_NAME("telnet"),CLIENT,SOCK_STREAM,host);

エラーが生じると、initport() は、-1 を返す。

なお、ネットワーク・プログラミングの教科書の多くは、ソケットを直接利用するように書かれている。この実験においても、stream ライブラリを使うことなく、直接ソケットを利用してプログラムを作成してもよい。


fdopen2()

fdopen2() は、この実験のために標準のライブラリ関数 fdopen() を拡張して作っ関数である。fdopen() は、open() システム・コールなどで開いたファイルについても、fprintf(), fgets(), fputs() などのバッファリング機能付の入出力関数を使いたい時に使われる。たとえば、次のようにして使われる。

    -----------------------------------------------------------------------
    FILE -f ;
	    fd = open("file1",O_RDONLY);
	    ....
	    f = fdopen(fd, "r")
	    fgets( buff,sizeof(buff),f );
    -----------------------------------------------------------------------

これにより、open() で開いたファイルについても、バッファリング機能付の入出力関数が使えるようになる。fdopen() は、open() 以外にも、pipe() や dup() の結果にも使われる。

fdopen2() は、TCP/IPの通信用ポートに対応しているのように、入出力可能なファイル記述子について、2つの入出用と出力用の FILE - を返す。

    -----------------------------------------------------------------------
    FILE -fin, -fout ;
	    fd = initport(PORT_NAME("finger"),CLIENT,SOCK_STREAM,host);
	    ...
	    fdopen2(fd,&fin,&fout)
	    fprintf( fout, "%s\n", who );
	    fflush( fout );
	    fgets( buff, sizeof(buff), fin );
    -----------------------------------------------------------------------

fdopen2() の第1引数 fd には、initport() や connect(), accept() で作られた入出力可能(read()システム・コールもwrite() システム・コールも使える)ファイル記述子を指定する。結果として作られた FILE - は、第2、第3 引数として与えられた FILE - へのポインタで示された番地に格納される。fdopen2() は、エラーが起きた時には、0, 成功した時には、0 以外が返される。

fdopen2() のソース・プログラムは、/usr/open/classes/slab/info2/lib/fdopen2.c にある。


コンパイルとリンク

initport() を使うプログラムは、コンパイル時に "inet.h" というヘッダ・ファイルを読み込まなければならない。これは、/usr/open/classes/slab/info2/include/ に置かれている。よって、コンパイル時に、cc コマンドに次のようなオプションを与える必要がある。

	% cc -I/usr/open/classes/slab/info2/include/ -c file.c

また、initport() などの関数は、次のいずれかのファイルに格納されている。

    /usr/open/classes/slab/info2/lib/libinfo2-sun4.a	Sun用
    /usr/open/classes/slab/info2/lib/libinfo2-rs6000.a	IBM用
    (BSD/OS用は自分で作成すること)

これを利用するためには、リンク時に次のように指定する。

    % cc -o commmand file.o /usr/open/classes/slab/info2/lib/libinfo2-sun4.a

または、次のように -L, -l オプションを用いて指定する。

    % cc -o commmand file.o -L/usr/open/classes/slab/info2/lib/ -linfo2-sun4

以下の実験で使うコマンドについては、/usr/open/classes/slab/info2/2-tcp/Makefile に、コンパイルとリンクのための設定が格納されている。これを利用するとよい。


4.2.2 finger クライアントのコンパイルと実行

遠隔のfingerサーバに対して問い合わせを行うコマンド、rfinger (remote finter)のソース・プログラムを、以下のファイルに示す。

    -----------------------------------------------------------------------
    /usr/open/classes/slab/info2/2-tcp/rfinger.c:
    -----------------------------------------------------------------------
      1: 
      2: /-
      3:         rfinger.c -- Remote Finger / 遠隔専用 finger コマンド。
      4:         $Header: /home/h1/yas/slab-info2-inet/2-tcp/RCS/rfinger.c,v 1.3 1995/09/11 13:42:45 yas Exp $
      5:         Start: 1995/09/08 21:04:33
      6: -/
      7: #include <stdio.h>
      8: #include <sys/types.h>
      9: #include <sys/socket.h>
     10: #include <netinet/in.h>
     11: #include "inet.h"
     12: 
     13: main( argc,argv )
     14:     int argc ;
     15:     char -argv[] ;
     16: {
     17: /- 引数は、コマンド名を入れて3個。 -/
     18:         if( argc != 3 )
     19:         {
     20:             fprintf( stdout,"Usage: %s host \"who\"\n",argv[0] );
     21:             exit( -1 );
     22:         }
     23:         rfinter( argv[1],argv[2] );
     24: }
     25: 
     26: #define BUFFSIZE        1024
     27: 
     28: rfinter( host,who )
     29:     char -host ;        /- ホスト名 -/
     30:     char -who ;         /- 調べたい人のキー -/
     31: {
     32:     int fd ;
     33:     char buff[BUFFSIZE] ;
     34:     int count ;
     35: 
     36: /- buff で確保されているメモリよりも長い who については、エラーにする。-/
     37:         if( strlen(who) >= BUFFSIZE-2 )
     38:         {
     39:             fprintf(stderr,"search key too long: \n%s\n",who );
     40:             exit( -1 );
     41:         }
     42: /-
     43: initport() 関数により、TCP/IPのクライアント側における通信用ポートを設
     44: 定する。このとき、ポート名と接続先のホストを指定する。この関数により、
     45: TCI/IPの通信路が開設され、read(), write() により通信を行うことが可能に
     46: なる。エラーが起きたときには、-1 が返される。
     47: -/
     48:         fd = initport(PORT_NAME("finger"),CLIENT,SOCK_STREAM,host);
     49:         if( fd < 0 )
     50:         {
     51:             fprintf(stderr,"initport(,,host:%s) = %d\n",
     52:                     host, fd);
     53:             exit( -1 );
     54:         }
     55: 
     56: /-
     57: 通信内容を、バッファ buff に作る。このとき、sprintf() を使うと便利であ
     58: る。これは、printf() と同じ形式の引数をとることができる。結果は、画面
     59: に表示されるのではなく、buff (string)に保存される。最後の '\n' は、必
     60: 要である。
     61: -/
     62:         sprintf( buff,"%s\n", who );
     63:         count = strlen( buff );
     64: 
     65: /-
     66: 通信用ポートに対応しているファイル記述子を引数としてwrite() システム・
     67: コールを発行することで、TCP/IPによるプロセス間通信が行われる。これによ
     68: り、buff 番地から count バイトのデータが、通信相手に送られる。
     69: -/
     70:         if( write(fd,buff,count) != count )
     71:         {
     72:             perror("write() to socket");
     73:             exit( -1 );
     74:         }
     75: /-
     76: 通信用ポートに対応しているファイル記述子を引数として read() システム・
     77: コールを発行することにより、TCP/IPによるプロセス間通信が行われる。これ
     78: により、通信相手から送られてきたデータが、buff 番地から始まる BUFFSIZE 
     79: バイトの領域へ書き込まれる。read() システム・コールのリターン・バリュー
     80: は、実際に送られてきたデータのバイト数である。
     81: -/
     82:         while( (count=read(fd,buff,BUFFSIZE)) > 0 )
     83:         {
     84:             if( write(1,buff,count) == -1 )
     85:             {
     86:                  perror("stdout");
     87:                  exit( 1 );
     88:             }
     89:         }
     90:         close( fd );    /- TCP/IP connection を閉じる。-/
     91:         close( 1 );     /- 標準出力のファイルを閉じる。-/
     92: }
     93: /- --------------------- rfinger.c --------------------- -/
     94: static char rcsid[] =
     95: "$Header: /home/h1/yas/slab-info2-inet/2-tcp/RCS/rfinger.c,v 1.3 1995/09/11 13:42:45 yas Exp $" ;
    -----------------------------------------------------------------------


■課題8:

rfinger.c をコンパイルして、実行してみなさい。

    -----------------------------------------------------------------------
    % cp /usr/open/classes/slab/info2/2-tcp/{rfinger.c,Makefile} .
    % make rfinger
    % ./rfinger pw001 $USER
    -----------------------------------------------------------------------

報告書には、自分の情報を rfinger で調べた結果を示しなさい。ログイン名で引いた時と、finger name として設定している文字列で引いたときの結果を示しなさい。もし、finger name をまだ設定していないならば、chfn コマンドで設定しなさい。このとき、設定の前後で、表示や検索の様子がどう変わるかを調べて報告しなさい。


4.2.3 WWW クライアントの作成


■課題9:

WWWサーバからデータを得るプログラムを作りなさい。そのプログラムの名前を、wcat とする。wcat のプログラムの骨組みを/usr/open/classes/slab/info2/2-tcp/wcat.c に示す。これをコピーして、使いなさい。

    -----------------------------------------------------------------------
    % cp /usr/open/classes/slab/info2/2-tcp/wcat.c .
    -----------------------------------------------------------------------

wcat コマンドは、次のように3つの引数を与えて利用するものとする。

    -----------------------------------------------------------------------
    % ./wcat host port file
    -----------------------------------------------------------------------

ここで、host は、ホスト名、port は、TCP/IP のポート番号、file は、得るべきファイル名である。これは、URL の文法で記述すると、次のようになる。

	http://host:port/file

なお、wcat では、ポート番号の引数を省略しないものとする(省略可能なように工夫してもよい)。HTTP プロトコルで用いられる標準のポート番号は、80である。

wcat のプログラムを作る時に、4.2.2 で示した rfinger.c を参考にするとよい。initport() では、ポート名(サービス名)ではなく、ポート番号を用いる形式を利用するとよい。

    -----------------------------------------------------------------------
	    fd = initport(PORT_NUMBER(port),CLIENT,SOCK_STREAM,host);
    -----------------------------------------------------------------------

HTTP では、ヘッダ部分の行末に、キャリッジ・リターン(carrige return, CR)とライン・フィード(Line feed, LF)の両方が必要であると定められている。UNIX では、通常ライン・フィード(ニュー・ライン、New Line, NL と呼ばれることもある)だけが行末の記号として使われる。よって、画面に文字列を表示し、改行したい場合は、次のようなプログラムが使われる。

	printf("Hello,world\n");

これは、HTTP では、次のようにしなければならない。

	printf("Hello,world\r\n");

ここで、'\r' がキャリッジ・リターン、'\n' がライン・フィードである。いずれも、ソース・プログラム上では2文字に見えるが、Cコンパイラにより、1文字に変換される。それぞれ、アスキーでは、13(0x0d), 10(0x0a) である。


4.2.4 NNTP クライアントの作成


■課題10:

NNTPサーバから記事を1つ得るプログラムを作りなさい。そのプログラムの名前を、nncat とする。nncat のプログラムの骨組みを/usr/open/classes/slab/info2/2-tcp/nncat.c に示す。これをコピーして、使いなさい。

    -----------------------------------------------------------------------
    % cp /usr/open/classes/slab/info2/2-tcp/nncat.c .
    -----------------------------------------------------------------------

nncat コマンドは、次のように3つの引数を与えて利用するものとする。

    -----------------------------------------------------------------------
    % ./nncat host newsgroup number
    -----------------------------------------------------------------------

ここで、host は、NNTPサーバが動いているホストの名前newsgroup は、ニュース・グループ、number は記事番号である。HTTP プロトコルで用いられる標準のポート番号は、119(nntp) である。initport() では、PORT_NAME("nntp") の形式を利用するとよい。

報告書には、作成した nncat コマンドの動作例を示しなさい。NNTPサーバが動いているホストとしては、spn (spn.ie.u-ryukyu.ac.jp) を指定しなさい。

NNTP でやり取りされるデータの行末には、キャリッジ・リターン(carrige return, CR)とライン・フィード(Line feed, LF)の両方が必要であると定められている。UNIX では、通常ライン・フィード(ニュー・ライン、New Line, NL と呼ばれることもある)だけが行末の記号として使われる。よって、画面に文字列を表示し、改行したい場合は、次のようなプログラムが使われる。

	printf("Hello,world\n");

これは、NNTP では、次のようにしなければならない。

	printf("Hello,world\r\n");

ここで、'\r' がキャリッジ・リターン、'\n' がライン・フィードである。いずれも、ソース・プログラム上では2文字に見えるが、Cコンパイラにより、1文字に変換される。それぞれ、アスキーでは、13(0x0d), 10(0x0a) である。

余裕があれば、画面に表示する前に、UNIX に合わせて行末のキャリッジ・リターンのコードを削除するようにしなさい。


4.2.5 SMTP クライアントの作成


■課題11:

SMTPサーバに対して、VRFY 手続きを送り、受取人のアドレスを確認するプログラムを作りなさい。そのプログラムの名前を、mverify とする。mverify のプログラムの骨組みを/usr/open/classes/slab/info2/2-tcp/mverify.c に示す。これをコピーして、使いなさい。

    -----------------------------------------------------------------------
    % cp /usr/open/classes/slab/info2/2-tcp/mverify.c .
    -----------------------------------------------------------------------

mverify コマンドは、次のように2つの引数を与えて利用するものとする。

    -----------------------------------------------------------------------
    % ./mverify host who
    -----------------------------------------------------------------------

ここで、host は、SMTPサーバ(sendmailデーモン)が動いているホストの名前、who は確認するアドレスである。SMTPプロトコルで用いられる標準のポート番号は、25(smtp) である。initport() では、PORT_NAME("smtp") の形式を利用するとよい。

報告書には、作成した mverify コマンドの動作例を示しなさい。NNTPサーバが動いているホストとしては、pw001 を指定しなさい。自分のアドレスを確認してみなさい。

    -----------------------------------------------------------------------
    % ./mverify pw001 $USER
    -----------------------------------------------------------------------


4.3 WWWサーバの改良

4.2節では、HTTP, NNTP, または、SMTP のクライアントのプログラムを作成した。この節では、簡単な HTTP サーバのプログラムを解読し、機能拡張と改良を行う。


4.3.1 initport()(サーバ側)

4.2.1項では、streamライブラリのinitport()関数のうち、クライアント側の使い方を示した。ここでは、サーバ側の使い方を示す。サーバ側では、initport() は、次のような形式で使われる。

    -----------------------------------------------------------------------
    #include <sys/types.h>
    #include <sys/time.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include "inet.h"
    s = initport( PORT_NUMBER(port),SERVER,SOCK_STREAM );
    int port ;
    -----------------------------------------------------------------------

これにより、サーバ側に受け付け用のTCP/IPのポートが作られる(3.1節の図3参照)。そのポート番号は、引数portにより指定される。initport()に対するその他の引数 (SERVER, SOCK_STREAM) は、定数である。これらは、サーバ側で使う限り、必ず指定されなければならない。

結果として、1つのファイル記述子 s が返される。これは、TCP/IP のサーバ側における受け付け用のポートと対応している。受け付け用のポートとは、ポート番号を保持し、クライアントからの接続要求を受け付けるためのものである。このポートを通じて、データを送受することはできない。すなわち、このファイル・記述子に対して、read() システム・コールや write() システム・コールにより、入出力を行うことができない。実際に入出力を行うためのポートは、次の select_server_stream() によって得られる。

    -----------------------------------------------------------------------
    int client = select_server_stream(s,&fds);
    int s ;
    fd_set fds ;
    -----------------------------------------------------------------------

select_server_stream() の引数 s は、initport() により返されたファイル記述子である。fdsは、select_server_stream() 内部で複数の接続扱うための作業用変数である。fd_set とは、ファイル記述子の集合(配列)を保持するためのビット配列である。最初に FD_ZERO() により初期化されなければならない。それ以後は、select_server_stream() により管理される。

select_server_stream() は、クライアントからの接続要求が来るか、クライアントからのデータが到着するまで待つ。クライアントからの接続要求が来ると、それを受け付ける。その結果として、クライアントとの間で双方向のストリームにより通信可能なファイル記述子を返す。

select_server_stream() は、1つのサーバ・プロセスで複数のクライアントを同時に扱う事を可能にする。サーバ・プロセスは、複数のクライアントの間に、複数のTCP/IPの接続を同時に保持することができる。そして、それらの接続の中で、データが届いたものを選び、返す機能がある。

select_server_stream() により返されたファイル記述子は、クライアントからのデータが到着している、入出力可能なTCP/IPの通信ポートに対応している。サーバの処理は、この通信ポートから要求を読みだし、それに応じた処理わ行い、結果をこの通信ポートに書き出すことである。

なお、ネットワーク・プログラミングの教科書の多くは、ソケットを直接利用するように書かれている。この実験においても、stream ライブラリを使うことなく、直接ソケットを利用してプログラムを作成してもよい。


4.3.2 httpd-simple

ここでは、簡単な HTTP サーバ(WWWのサーバ)を例に、TCP/IPのサーバのプログラムの概要を示す。サーバのソース・プログラムを、次のファイルに示す。

    -----------------------------------------------------------------------
    /usr/open/classes/slab/info2/2-tcp/http-simple.c:
    -----------------------------------------------------------------------
      1: 
      2: /-
      3:         http-simple.c -- 簡単な WWW サーバ (HTTP サーバ)
      4:         $Header: /home/h1/yas/slab-info2-inet/2-tcp/RCS/httpd-simple.c,v 1.6 1995/09/11 13:35:12 yas Exp $
      5:         Start: 1995/09/03 00:27:22
      6: -/
      7: 
      8: #include <stdio.h>
      9: #include <sys/types.h>
     10: #include <sys/time.h>
     11: #include <sys/socket.h>
     12: #include <netinet/in.h>
     13: #include "inet.h"
     14: 
     15: /-
     16: fdopen2() は、標準のライブラリ関数 fdopen() を拡張したものである。これ
     17: は、TCP/IPの通信用ポートに対応しているのように、入出力可能なファイル記
     18: 述子について、2つの入出用と出力用の FILE - (fdopen() のマニュアルでは、
     19: stream)を返す。引数の fd には、initport() や connect(), accept() で作
     20: られた入出力可能(read()システム・コールもwrite() システム・コールも使
     21: える)ファイル記述子を指定する。結果として作られた FILE - は、引数とし
     22: て与えられた番地 finp, foutp にセットされる。エラーが起きた時には、0 
     23: が返される。成功した時には、1 が返される。
     24: 
     25: fdopen2() のソース・プログラムは、
     26: /usr/open/classes/slab/info2/lib/fdopen2.c にある。
     27: -/
     28: extern int fdopen2(/- int fd, FILE --finp, FILE --foutp -/); 
     29: 
     30: void
     31: usage()
     32: {
     33:         fprintf( stdout,"Usage: httpd-simple [-p port] directory\n" );
     34:         exit( -1 );
     35: }
     36: 
     37: main( argc,argv )
     38:     int argc ;
     39:     char -argv[] ;
     40: {
     41:     extern char -optarg;        /- getopt() -/
     42:     extern int optind;          /- getopt() -/
     43:     int c ;
     44:     char -dir ;
     45:     int port ;
     46: 
     47: /-
     48: 実験では、大勢の人が同時にプログラムを作る。よってね各自重ならないよう
     49: なポート番号を使わなければならない。この実験では、getuid() + 10000 を
     50: 使うことにする。もしそのポート番号が、使われていたら、initport() の中
     51: の bind() システム・コールで次のエラーが出る。
     52: ----------------------------------------------------------------------
     53: #define EADDRINUSE      48              // Address already in use
     54: ----------------------------------------------------------------------
     55: その時は、前に走らせた自分のプロセスがいないか、調べなさい。もし自分の
     56: プロセスが、そのポートを使っていれば、kill コマンドなどで殺しなさい。
     57: もし、他の人がそのポート番号を使っていたら、-p オプションで空いている
     58: ポート番号を指定してつかうか、他のホストにログインしてこのプログラムを
     59: 走らせるようにしなさい。
     60: -/
     61: 
     62:         port = getuid() + 10000 ;
     63: 
     64: /-
     65: 
     66: getopt() は、オプションを解析するためのライブラリ関数。引数 "p" は、
     67: -p オプションを受け付けるという意味である。続くコロンは、-p string の
     68: 形で、文字列を受け付けるという意味である。オプションを見つけると、リター
     69: ン・バリューとして返す。その時、受け付ける文字列は、大域変数 optarg に
     70: セットされる。
     71: -/
     72:         while( (c = getopt(argc,argv,"p:")) != EOF )
     73:         {
     74:             switch( c )
     75:             {
     76: /- atoi() は、文字列(ascii)を、整数に変えるライブラリ関数。-/
     77:             case 'p': port = atoi( optarg ); break;
     78:             default:
     79:                 usage();
     80:             }
     81:         }
     82: /-
     83: getopt() によるオプションの解析が終了した時、optind に、最初の引数(オ
     84: プションではなく必須)の添え字がセットされている。これを、argc と比べ
     85: て、引数の数が得られる。ここでは、ディレクトリ名が1つ必要である。
     86: -/
     87:         if( argc - optind != 1 )
     88:             usage();
     89:         dir = argv[optind+0] ;
     90:         httpd_simple( port, dir );
     91: }
     92: 
     93: httpd_simple( port,dir )
     94:     int port ;
     95:     char -dir ;
     96: {
     97:     int client ;        /- 通信用ポート -/
     98:     int fd;             /- 要求受付用ポート -/
     99:     fd_set fds;         /- select_server_stream() のための変数 -/
    100: 
    101: /- カレント・ワーキング・ディレクトリを、指定されたディレクトリに移す。-/
    102:         if( chdir(dir) < 0 )
    103:         {
    104:             perror( dir );
    105:             exit( -1 );
    106:         }
    107: 
    108: /- initport() で、サーバ側の要求受付用ポートを作る。そのポートには、
    109: portで指定されたポート番号が付けられる。-/
    110:         fd = initport( PORT_NUMBER(port),SERVER,SOCK_STREAM );
    111:         if( fd < 0 )
    112:         {
    113:             fprintf( stderr,"initport() returns %d\n",fd );
    114:             exit( -1 );
    115:         }
    116: /- 受付可能な URL を表示する。-/
    117:         print_my_url( port );
    118: 
    119: /- select_server_stream() のための変数の初期化 -/
    120:         FD_ZERO(&fds);
    121: 
    122: /-
    123: サーバのメイン・ループ。サーバ・プロセスは、普通、このように決して終了
    124: することがないプログラムである。サーバ・プロセスには、このように無限ルー
    125: プが1つある。これを、メイン・ループという。
    126: -/
    127:         while( 1 )
    128:         {
    129: /-
    130: select_server_stream() は、内部で自動的にクライアントからの接続要求を
    131: 受け付ける(accept())。このとき、通信用ポートが1つ作られる。
    132: select_server_stream() は、クライアントから要求データが送られてきた通
    133: 信ポート(のファイル記述子)を返す。
    134: -/
    135:             client = select_server_stream( fd,&fds );
    136: 
    137: /-1つのクライアントについて要求を処理する関数を呼ぶ。-/
    138:             perform( client );
    139: 
    140: /-TCP/IPの通信路は、perform() により切断される。HTTPでは、1つの要求の
    141: 完了ごとに通信路が切断される。-/
    142:         }
    143: }
    144: 
    145: #define BUFFSIZE        1024
    146: 
    147: /-
    148: 1つのクライアントについて要求を処理する関数。引数は、通信用ポートのファ
    149: イル記述子。read(), write() により、クライアントと通信を行うことができ
    150: る。
    151: -/
    152: perform( client )
    153:     int client ;
    154: {
    155:     FILE -fin, -fout ;
    156: 
    157:     char buff[BUFFSIZE+1] ;
    158:     char filename[BUFFSIZE] ;
    159: 
    160: /- クライアントの情報を表示。-/
    161:         print_client( client );
    162: 
    163: /-
    164: fdopen2() により、ファイル記述子から、FILE -を得る。これにより、
    165: fgets(), fprintf() といった標準入出力ライブラリ関数を利用して、プロセ
    166: ス間通信を行うことが可能になる。
    167: -/
    168:         if( fdopen2(client,&fin,&fout) == 0 )
    169:         {
    170:             fprintf( stderr,"fdopen2() faild.\n" );
    171:             fprintf( fout,"HTTP/1.0 500 Internal Server Error\r\n" );
    172:             return( -1 );
    173:         }
    174: /-
    175: クライアントからの要求の最初の行を読む。これに一番大事な情報であるファ
    176: イル名が含まれている。
    177: -/
    178:         if( fgets( buff,BUFFSIZE,fin ) == 0 )
    179:         {
    180:             fprintf( stderr,"request line read faild.\n" );
    181:             fprintf( fout,"HTTP/1.0 500 Internal Server Error\r\n" );
    182:             goto error ;
    183:         }
    184: 
    185: /- sscanf() により、ファイル名を抜き出す。-/
    186:         if( sscanf( buff,"GET %s HTTP/1.0", filename ) != 1 )
    187:         {
    188:             fprintf( stderr,"no filename in the request line\n" );
    189:             fprintf( fout, "HTTP/1.0 400 Bad Request\r\n" );
    190:             goto error;
    191:         }
    192: /-
    193: ファイル名に、".." が含まれていたり、"/" で始まっていない場合は、エラー
    194: にする。
    195: -/
    196:         if( strstr(filename,"/../" ) || filename[0] != '/' )
    197:         {
    198:             fprintf( stderr,"Dangerous file name [%s] \n",filename );
    199:             fprintf( fout, "HTTP/1.0 404 Not Found\r\n" );
    200:             goto error;
    201:         }
    202: 
    203:         fprintf( fout,"HTTP/1.0 200 Document follows\r\n" );
    204:         fprintf( fout,"Content-Type: text/plain\r\n" );
    205:         fprintf( fout,"\r\n" );
    206:         fprintf( fout,"request[%s]\n",filename );
    207: 
    208: /-
    209: もし実際にファイルの内容を返す時には、ここでそのファイルを、fopen() し
    210: て、fread() するとよい。ただし、アクセスするファイルの先頭には、"./" 
    211: を付けて、カレント・ワーキング・ディレクトリ以下のファイルしかアクセス
    212: を許さないようにしなさい。また、ファイルのによって、Content-Type: を変
    213: えること。
    214: -/
    215: 
    216: /- 2つの fclose() により通信路が切断される。-/
    217:         fclose( fin );
    218:         fclose( fout );
    219:         return( 0 );
    220: 
    221: error:
    222: /- エラーの時は、内容として、HTML でエラーメッセージを返す。-/
    223:         fprintf( fout,"Content-Type: text/html\r\n" );
    224:         fprintf( fout, "\r\n" );
    225:         fprintf( fout, "<H1>Error.</H1><ADDRESS>httpd-simple</ADDRESS>\n");
    226:         fclose( fin );
    227:         fclose( fout );
    228:         return( -1 );
    229: }
    230: 
    231: /- 自分自身がサービスできる URL を表示する関数。-/
    232: print_my_url( port )
    233:     int port ;
    234: {
    235:     char hostname[BUFFSIZE+1] ;
    236: /- gethostname() は、ホスト名を得るシステム・コール。-/
    237:         gethostname( hostname,BUFFSIZE );
    238:         hostname[BUFFSIZE] = 0 ;
    239:         printf("http://%s:%d/\n",hostname, port );
    240: }
    241: 
    242: /-
    243: クライアントの情報を返す関数。引数は、通信用ポートに対応したファイル記
    244: 述子。
    245: -/
    246: print_client( client )
    247:     int client ;
    248: {
    249:     struct sockaddr_in addr;
    250:     int addrlen;
    251: /-
    252: getpeername() システム・コールは、通信相手の名前を返す。名前とは、
    253: TCP/IP では、ホストのIPアドレスとポート番号である。それは、sockaddr_in 
    254: 構造体の sin_addr.s_addr と sin_port に設定される。
    255: -/
    256:         addrlen = sizeof(addr);
    257:         if( getpeername(client, &addr, &addrlen) < 0 )
    258:         {
    259:             perror("getpeername");
    260:             return( -1 );
    261:         }
    262:         printf("host: ");
    263:         print_ip_address( addr.sin_addr.s_addr );
    264:         printf(",  port: %d\n",addr.sin_port );
    265: }
    266: 
    267: /-IPアドレスを、点つき10進数で表示する関数。-/
    268: print_ip_address( ipaddr )
    269:     unsigned long ipaddr ;
    270: {
    271: /-
    272: この共用体を使えば、32ビットの整数を、8ビットずつ区切ってアクセスする
    273: ことができる。
    274: -/
    275:     union
    276:     {
    277:         unsigned long l ;
    278:         struct { unsigned char c1, c2, c3, c4 } b;
    279:     } x ;
    280:         x.l = ipaddr ;
    281:         printf("%d.%d.%d.%d", x.b.c1, x.b.c2, x.b.c3, x.b.c4 );
    282: }
    283: 
    284: /- --------------------- http-simple.c --------------------- -/
    285: static char rcsid[] =
    286: "$Header: /home/h1/yas/slab-info2-inet/2-tcp/RCS/httpd-simple.c,v 1.6 1995/09/11 13:35:12 yas Exp $" ;
    -----------------------------------------------------------------------


■課題12:

http-simple.c をコンパイルして、実行してみなさい。

    -----------------------------------------------------------------------
    % cp /usr/open/classes/slab/info2/2-tcp/{http-simple.c,Makefile} .
    % make http-simple
    %
    -----------------------------------------------------------------------

ウィンドウを2つ用意しなさい。1つのウィンドウで、次のように http-simple を実行しなさい。
    -----------------------------------------------------------------------
    % ./http-simple ~/public_html/
    http://pw001:13520/
    -----------------------------------------------------------------------

~/public_html/ は、このサーバが提供するデータを含んでいるディレクトリを指定する。あらかじめ作成しておくこと。ここで、 http-simple が表示した URL を記憶しなさい。実際には、ホスト名やポート番号が異なるはずである。

★注意: http-simple は、サーバ・プロセスなので、自動的に終了しない。実験が終了したら、必ず^Cやkillコマンドでこのプロセスを終了さなさい。もし、実験に使用したプロセスが残されていた場合、この減点の対象とする。

もう1つのウィンドウで、次のように Netscape を起動しなさい。(lynx でもよい。)

    -----------------------------------------------------------------------
    % netscape http://pw001:13520/Welcome.html
    -----------------------------------------------------------------------

ここで Netscape の引数は、先ほど ./http-simple が起動時に表示した URL に引き続き、何か適当な文字列(これ例では、"Welcome.html")を続けたものである。

あらかじめ Netscape を起動していた場合、Open URL を使って、URL http://pw001:13520/Welcome.html の資源を得なさい。すると、次のような画面が現われるはずである。

    -----------------------------------------------------------------------
    request[/Welcome.html]
    -----------------------------------------------------------------------

これは、http-simple.c:perform() の中で、次の fprintf() の呼び出しに対応している。
    -----------------------------------------------------------------------
	     fprintf( fout,"request[%s]\n",filename );
    -----------------------------------------------------------------------


■課題13:

http-simple.c は、要求されたURLを、そのままクライアントに返す機能しかない。これを改良して、ファイルを開き、その内容を返すようにしなさい。与えられたファイル名を使ってファイルを、fopen() で開き、fread() で読み込み、fwrite() でクライアントに返すとよい。このとき、ファイル名の拡張子が".html" の場合には、"Content-Type: text/html\r\n" を返しなさい。また、".gif" の場合には、"Content-Type: image/gif\r\n" を返しなさい。実際の HTTP サーバにならい、ファイルの大きさや日付を返すようにしてもよい。

ファイルが見つからない時には、HTTP のエラーである 404 を返しなさい。

HTTP で、ヘッダと本体を分ける空行を忘れないようにしなさい。


■課題14:

課題13では、ファイルを開き、その内容を返していた。ファイルの内容ではなく、プログラムを実行してその結果を返すようなプログラムを作りなさい。実行するプログラムとしては、次のようなものが考えられる。

(1) who (2) finger (3) ps

プログラムを実行し、その結果を取り込むためには、popen() ライブラリ関数を使うとよい。

内容の型としては、"Content-Type: text/plain\r\n" を付けるとよい。余裕があれば、"Content-Type: text/html\r\n" にして、その他の文字列とともに、プログラムの出力に <CODE>,</CODE>や <PRE>,</PRE> を付けて返すようにしなさい。


■課題15:

http-simple.c は、どのホストからの要求も受け付けている。特定のホストからの要求しか受け付けないように、改良しなさい。現在のプログラムは、クライアントのIPアドレスをprint_client() により表示している。この課題では、IPアドレスを表示するだけでなく、その内容を調べて、アクセスを許すか許さないかを決めなさい。たとえば、次のような方法が考えられる。

(1) 特定のホストだけからのアクセスを許す。(2) pw00? というホストからのアクセスを許す。(3) 133.13.30.- というパタンのIPアドレスを持つホストからのアクセスのみを許す。(4) 133.13.-.- というパタンのIPアドレスを持つホストからのアクセスのみを許す。(5) -.u-ryukyu.ac.jp というホスト名を持つホストからのアクセスのみを許す。

IP アドレスからホスト名を得るには、ライブラリ関数gethostbyaddr()を使うとよい。


5 報告書

この章の全ての実験課題について、1通の報告書を提出しなさい。それぞれの実験について、作成したプログラムとその説明(4.1を除く)、および、その実行結果を付けなさい。この実験では、プログラムの動きを説明するフローチャートを付加する必要はない。開発環境と実行環境(計算機、オペレーティング・システムのバージョン、コンパイラ)を載せなさい。

その他、この実験を通じて得たことを報告しなさい。

この実験に要した時間(単位は時間)を書きなさい。

この実験指導書の改善点を書きなさい。


一般的な注意:

報告書を、日本語で記述しなさい。プログラム、表、図、数式の羅列は、日本語ではない。それらは、必ず本文中から参照しなさい。たとえば、「○○を図1に示す」、あるいは、「○○を以下に示す。<以下、参照しているものを続ける>」といった形式を使うとよい。数学における証明のように論理的に記述する。主語と述語を明確に書くとよい。


参考資料

/usr/open/doc/inet/protocols/http-1.0.txt
	HTTP 1.0 の仕様。テキスト形式。

http://www.w3.org/hypertext/WWW/Protocols/Overview.html
	HTTPプロトコルについて。

/usr/open/doc/rfc/rfc821.txt
	SMTP (Simple Mail Transfer Protocol)

/usr/open/doc/rfc/rfc977.txt
	NNTP (Network News Transfer Protocol)


付録


streamライブラリ(sized_ioライブラリ)の README ファイル。

    -----------------------------------------------------------------------
    This package provides a quick-and-easy means of providing reliable
    and large-packet communication between processes.
    It is described further in the man page stream(3) and at length in the
    document by Don Libes entitled "Packet-Oriented Communications Using a
    Stream Protocol --or-- Making TCP/IP on Berkeley UNIX a Little More
    Pleasant to Use", NISTIR 90-4232, January 1990.
    It is especially nice because initport() does all the hard work of
    initializing TCP connections, and select_server_stream() does the
    hard work of connecting processes to each other.
    If you are running on 4.3BSD, you should add -DBSD4_3 in the Makefile.
    Otherwise, this will compile for a 4.2BSD system.
    To install, type
	    make install
    To test, type
	    make test
	    reader
	    writer	(in different window)
	    writer  (in yet another window)
	    writer  (in yet ...)
	    and so on.
    reader and writer are two programs that should communicate with
    each other.  Type things into any of the writers and reader will
    print it out prefaced by the file descriptor the data came in on.
    Bugs and problems to Don Libes
    National Bureau of Standards
    Bldg 220, Rm A-127
    Gaithersburg, MD  20899
    (301) 975-3535
    SYNOPSIS
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <inet.h>
	    cc [options] [files] sized_io.o stream.o
    DESCRIPTION
    This package implements packet or stream IO between a server process and
    a number of client processes, using the TCP/IP (stream) facilities.
    A client uses the call:
	    s = initport(PORT_NUMBER(XXX),CLIENT,SOCK_STREAM);
    s is the server's data socket and is used as a file descriptor in further
    communication.  The port may be specified by name (PORT_NAME("foo")), if it
    is registered.
    Similarly, the server uses the following call:
	    s = initport(PORT_NUMBER(XXX),SERVER,SOCK_STREAM);
    s is the server's connection socket.  To receive data or connections, the
    server calls select_server_stream().
	    client = select_server_stream(s,&fds);
    This returns a file descriptor corresponding to a client, when a client has
    sent a message to the server.  It handles initial connections as well as
    client deaths.  s is the server's connection socket that was returned by
    initport().  fds is an int used by select...() for storing a bit string
    corresponding to client sockets.  Initialize it to 0, and don't mess with it
    after that.
    To use the file descriptors in a stream-oriented manner, use read() and
    write().  To use the file descriptors in a packet-oriented manner, use
    sized_read() and sized_write().  The sized...() calls read and write one
    packet at a time, while packet boundaries are ignored in read() and write().
	    cc = sized_read(fd,buffer,maxsize)
	    cc = sized_write(fd,buffer,size)
    The arguments for sized_read() and sized_write() are very similar to read()
    and write().  The only difference is that in sized_read(), maxsize is the
    maximum size of an acceptable packet.
    -----------------------------------------------------------------------


streadm ライブラリ(sized_io ライブラリ)の入手先。

    archieで sized_io を検索するとよい。
    -------------------------------------------------------------
    ftp.ics.es.osaka-u.ac.jp
	    /UUNET91/vol8/pub
		    -rw-r--r--      34704   Jun 30 1990  sized_io.shar.Z
	    /UUNET/tape7/pub
		    -rw-rw-r--      34704   Jun 30 1990  sized_io.shar.Z
    ftp.lab.kdd.co.jp
	    /pub5/unix
		    -rw-r--r--      22851   Dec 17 1990  sized_io.shar.gz
    -------------------------------------------------------------

Shinji KONO / Tue Jan 24 12:14:39 2012