ホーム » 2024

年別アーカイブ: 2024

2025年1月
 1234
567891011
12131415161718
19202122232425
262728293031  

検索・リンク

ポート番号とファイアウォールとメール

ポート番号とソケット

サーバとなるコンピュータでは、1台のコンピュータで様々なサービスを提供することから、サービスを区別する必要がある。このためにポート番号が使われる。1台毎のコンピュータに割り当てられたIPアドレスを電話番号に例えるなら、ポート番号は内線電話番号に例えることができる。

サーバと通信する場合、サービスを提供するプログラムに応じて標準的なポート番号が決められている。サーバに届いたパケットは、ポート番号に応じてサービスプログラムを起動する。以下の表によく使われるポート番号の一例をあげる。

ポート番号 プロトコル 概要
20 ftp ファイル転送(データ)
21 ftp ファイル転送(命令)
22 ssh リモート接続(暗号対策あり)
23 telnet リモート接続(暗号化なし)
25 smtp 電子メール送信
465 smtps 電子メール送信(暗号化)
53 DNS ドメインネームサービス
80 http Web
443 https Web(暗号化)
110 pop3 メールダウンロード
995 pop3s メールダウンロード(暗号化)
143 imap メール閲覧
993 imaps メール閲覧(暗号化)
137,138,139 netbios Windows のファイル共有

 

通信パケットには、送信元IPアドレス送信元ポート番号送信先IPアドレス送信先ポート番号の情報がある。
パソコンがサーバと通信する場合は、(1)自分のIPアドレスを送信元IPアドレス、(2)その時に使われていないポート番号をランダムに選び、送信元ポート番号とする。(3)通信相手のIPアドレスと、(4)通信先のサービスのポート番号をセットして、パケットを送付する。サーバは、サービスを要求してきたクライアントの送信先ポート番号をみて、対応するサーバのプログラムが動作する。プログラムの結果を送り返す時は、送信元と送信先のIPアドレス、ポート番号を入替えてパケットを送信する。

このような、IPアドレスとポート番号でお互いにデータを送りあうデータ通信の末端という意味でソケットと呼ぶ。サーバ側は、誰からでもデータを受け入れるということでソケットを開いて待機している。クライアントは開かれたソケットに接続して情報をやり取りする。

1024未満のポート番号(ウェルノウンポート番号)は、サービスを受けとるために用途が決められているので、通常の通信プログラムでは使われない。これ以外のポート番号は、通信の送信元のポート番号として使われ、エフェメラルポート番号と呼ばれる。

 

ファイアウォール

ネットワークのサービスの中には、組織外に見せたくないものも多い。また、インターネットでは、悪意のあるプログラマが通信して攻撃を加えてくるかもしれない。基本的には個々のサーバのプログラムで、送信元のプログラムのIPアドレスを見て接続を拒否することもできるが、末端のサーバで設定がいい加減だと攻撃をうけてしまうかもしれない。そこで、組織全体でネットワークを守る必要がでてくる。そこでルータなどの機能で、パケットの送信相手のポート番号や、送信元のIPアドレスをみて、パケットを廃棄する場合がある。こういう、ネットワークからの攻撃を防ぐ装置は、ファイアウォール(防火壁)と呼ばれる。

データベースサーバの保護するためにファイアウォールを設置する例を示す。Webサービスを提供するためのデータベースだけど、インターネットから接続されると情報漏洩が発生するかもしれない。そこでデータベースサーバ(mysql)に接続するための3306ポートは、ファイアウォール(ルータ)で組織外からは接続させない。
Webサーバにリモート接続(ssh/22)されるのも危険なことから、この例ではルータで http(80),https(443)以外のパケットは通さないといった許可リスト方式で設定するのが一般的。

許可リスト方式と拒否リスト方式

ファイアウォールの設定では、信頼できる人だけを接続させる許可リスト方式と、怪しい人を除外する拒否リスト方式がある。

許可リスト方式は、接続していい相手のIPアドレスや、ポート番号だけをFireWallを通過させる方式。(以前はホワイトリスト方式と呼ぶことが多かった。) これとは逆に、攻撃をしてきそうな怪しいIPアドレスや、怪しいポート番号のパケットを捨てて接続させない方式は拒否リスト方式とよぶ。(以前はブラックリスト方式と呼ぶことが多かった。) 学校のサーバは、学内への攻撃を防ぐため、ポート番号については http, https など以外の受信は許可リスト方式となっている。

メールが届くまで

電子メールは、非常に迅速にメッセージを相手に届けることができ、そのメッセージを蓄積・加工・編集・転送できる。また、音声や画像といった情報も、複雑な文字情報に置き換えることで、転送できるようになっている。

メールは、利用者のコンピュータに直接届けられるわけではなく、多くの場合はメールを蓄積するメールサーバに送られる。利用者がメールを読む場合、メールサーバから自分の端末に蓄積されたメッセージを読み込み、メッセージを確認する。このメールのやり取りにおいて、メールを送る時、あるいはメールサーバ間でメールを中継するときには、SMTP(Simple Mail Transfer Protocol) が用いられる。一方、メールサーバからメールを読み出すときには、POP(Post Office Protocol)IMAP(Internet Message Access Protocol) と呼ばれるプロトコルが用いられる。最近では、IMAPを使ったメールの読み書きをブラウザの中で実行できる WebMail が使われることが増えている。

メールが届くまでの流れは、aさんが”foo@bar.jp“に送る場合、

  1. aさんは、自分が加入しているメールサーバに、SMTPでメールを送る。
  2. メールサーバは、メールアドレスのコンピュータ名部分”bar.jp“をDNSに問合せ、そのIPアドレスを調べ、そのコンピュータにSMTPでメールを送る。
  3. bar.jp“のメールサーバは、メールアドレスのユーザ名”foo“を取り出し、各ユーザ毎にメールを保存する。

  4. “foo”さんは、自分宛のメールを確認するために、POPまたはIMAPで自分のメールサーバ”bar.jp”に接続し、ユーザ名,パスワードで認証して自分宛のメールを受け取る。

上記の手順2で、相手のメールサーバに直接送れない場合は、コンピュータ名のMXレコードをDNSに問合せを行い、そこで得られたメールサーバに中継を依頼する。

$ nslookup -query=MX fukui-nct.ac.jp.
Non-authoritative answer:
fukui-nct.ac.jp mail exchanger = 10 fukuinct-ac-jp01c.mail.protection.outlook.com.jp

上記手順4で自分のメールを読みだす際のプロトコルで、POPは一般的に、メールサーバから自分のメール閲覧ソフトに自分宛のメールをダウンロードして削除する。このため、様々なコンピュータでメールを読む人には不便となってきた。IMAPでは、メールを読んでも、既読の目印をつけサーバに残しておく方式であり、別のコンピュータでメールを閲覧したい時にもサーバ上のメールを読むことができる。メールをフォルダに分類して保存することもできる。最近利用される Webメール では、自分が利用しているメールサーバまでは Web の機能で接続し、Webサーバとメールサーバにて IMAP を使う。

POP, IMAP, SMTPでは、暗号化されない平文が使われることから、通信内容を暗号化して通信する POPS, IMAPS, SMTPS といったプロトコルも使用される。

メールヘッダ

メールを出すときには、宛先やタイトルや本文などの情報がついている。

From: foo@bar.jp      送信元のメールアドレス
To:   hoge@piyo.jp    送り先のメールアドレス
Cc:   hogehoge@bar.jp 送信内容を確認してもらうためのコピーの送り先
Bcc:  hogefuga@bar.jp 送信相手にコピーしたことが見えないように送る時
Subject: 会議の議事録  メールのタイトル
Date: 2019年 1月 9日 12:34:56 メールを送った時間
本文
-- 
署名

送信相手に届くメールでは、上記以外にも様々な情報がつけられる。これらの情報を見ると、迷惑メールか確認することもある程度可能となる。

Received: from 送信元 by 受信サーバ
Reply-To: 返信する際の送り先
Return-Path: 送信に失敗した時に送り返す先
DKIM-Signature: メールサーバの公開鍵署名
Received-SPF: 送信元のDNS情報など

spamとの闘い

spamとは、勝手に送られてくる迷惑メールであり、昔であれば特定の商品などの宣伝メールが送られてきた。最近では、元々、SMTPでメールを送る際には、ユーザ認証が行われていなかったことから、ウィルス(マルウェア)を拡散させるために、マルウェアをダウンロードさせるWebサイトに誘導したり、メールの添付ファイルに悪意のプログラムを混入させて送り付けてくる。 spam拡散を目的としたウィルスに感染すると、そのパソコンの利用者のメール情報を盗んだり、spam拡散の送信者(spammer)からの指令によって、spamを送信する踏み台(ボットネット/spammerに操られるパソコンのネットワーク)となってしまう。

迷惑メールのspamだが、大文字のSPAMと記載するとランチョンミートの意味となる。
spamが迷惑行為を指すようになったのは、モンティパイソンのSPAMのギャグが由来。

そこで spam 対策として、利用者が身近なメールサーバにメールの配送を依頼する際(前に掲載した図の(1)の通信)には、 SMTP送信の前にPOP/IMAP接続しユーザ認証を行った時だけメールを送ることができるPOP before SMTP(or IMAP before SMTP)や、SMTP-AUTHといった方式でユーザ認証を行うようになってきた。

一方、メールサーバからメールサーバにメールを送る際(前に掲載した図の(2)の通信)では、接続してきたメールサーバが正当なメールサーバなのかを確認する送信ドメイン認証ために、SPF, DKIM,DMARC などの機能が用いられる。SPF(Sender Policy Framework)では、DNSに登録されている正当なメールサーバの情報との比較が行われる。DKIM(DomainKeys Identified Mail)では、送信側のメールサーバが公開鍵暗号(後の講義で説明)をつかったDKIM署名をメールに付け、受信側でDKIM署名を公開鍵を使って検証を行うことで、正答なメールかを判断する。DMARCは、SPFやDKIMで検証した結果、怪しいと判断されたメールの取扱いをどうすべきかを指定できる。
(最近の google mail は、SPF,DKIM,DMARCが設定されていないとメールを受け取らない。これらの対策以前は80%がspamという時代もあったが、近年は全メールのうち50%ほどがspamらしい。)

((( SPF,DKIM,DMARCに関するDNSの設定例 )))
$ nslookup -query=TXT tsaitoh.net
tsaitoh.net  text = "v=spf1 +ip4:64.33.3.150 a mx -all"
        ### +ip4: は、このIPアドレスはメールサーバとして「正当」だよ...の意味
$ nslookup -query=TXT postfix._domainkey.tsaitoh.net
postfix._domainkey.tsaitoh.net  text = "v=DKIM1; h=sha256; k=rsa; p=...公開鍵..."
        ### p=公開鍵 は、この公開鍵で メールについているDKIM署名が確認できたら「正当」だよ...の意味
$ nslookup -query=TXT _dmarc.tsaitoh.net
_dmarc.tsaitoh.net  text = "v=DMARC1; p=quarantine; rua=mailto:report-a@tsaitoh.net; ruf=mailto:report-f@tsaitoh.net"
        ### p=quarantine は「SPF,DKIMの認証に失敗したら迷惑フォルダに分類していいよ」の意味

理解度確認

データベースの物理設計

前半はデータベースの物理設計の話を行う。後半は、レポート課題の時間とする。

データベースの物理設計

データベースの物理的設計は、データベースの格納法法や管理方法を決定する。この際には、ディスク容量の見積もりやメモリ量の見積もりが重要となる。

ディスク容量の見積もり

データベースでは、B木(以降で解説予定)などが用いられることが1つのB木のノード(データブロック)の構造をおおまかに示す。各データブロックには、そのブロックを管理するためのページ制御の情報と、実データへのポインタとなるスロット情報と、実データからなる。

実データは、すべてのデータが固定長であれば、そのデータ長とブロック毎のデータ数にページ制御の容量を加えれば良い。しかし、データ長は可変であることが多い。この場合は、データの更新でデータ長が長くなると、その後ろのデータをずらす処理が頻発すると、データ管理の効率が悪い。

そこで、実データの間には、データ長が増えた時の空き領域を設けておく。この比率がPCTFREEと呼ばれ、この領域が埋まった時にのみデータをずらす処理を行う。

また、データベースへのデータの削除を行う場合、データが1つ消える度にデータブロックの構成を変化させると効率が悪く、通常はデータ削除の目印をつけるだけとすることが多い。データ削除で空きがふえた時だけ、データブロックの構成を変えたり、データ追加の際にデータを追加する。この比率は、PCTUSEDと呼ばれる。

-- PCTFREE,PCTUSED の使い方の例 --
CREATE TABLE Person (
  id      INTEGER NOT NULL PRIMARY KEY ,
  name    VARCHAR( 20 ) ,
  address VARCHAR( 30 ) ,
)
PCTFREE 10
PCTUSED 40 ; -- PCTFREE+PCTUSED < 100 --

このため、ハードディスク容量の見積もりでは、PCTFREE,PCTUSEDを考慮する必要がある。

一般的には、容量を減らす観点であれば、PCTFREEはなるべく小さく、PCTUSEDはなるべく大きい方が望ましいが、データの更新で追加・削除・修正が頻発するのであれば、PCTFREEはある程度大きく、PCTUSEDはある程度小さい方がよい。このため、PCTFREE+PCTUSED < 100 となるようにチューニングすることが多い

例えば、ページサイズが4096バイト、ページ制御情報が32バイト、スロット制御情報が1データあたり4バイト、PCTFREEが30%、平均の1件あたりのデータ長が256バイトで、100000件を保存するとする。この場合、1ページ内でデータ用に使用できる領域は、(4096-32)✕(1-0.3) = 2844バイトとなる。この場合、1ページに保存できるデータは 2844÷(256+4) = 10.9 となり、最大で10件となる。このため、データを保存するために必要なデータ領域は 4096×(100000/10) = 40.9MB となる。単純にデータを覚えるだけであれば、本来なら 256×100000=25.6MB であるため、実際には1.6倍のデータ領域が必要であることが分かる。(教科書の説明より…)

また、実際のデータとは別に、データを高速に検索するためのインデックスファイルが作られるので、この容量も別途考慮が必要となる。

補足:残り予定:トランザクション処理, 内部構造, テスト前レポート課題

ハッシュ法

ここまでの授業では、配列(データ検索は、登録順保存ならO(N)2分探索ならO(log N)となる、2分探索ができるのは配列がランダムアクセスができるからこそ)、単純リスト(データ検索(シーケンシャルアクセスしかできないのでO(N)となる)、2分探索木( O(log N) ) といった手法を説明してきた。しかし、もっと高速なデータ検索はできないのであろうか?

究極のシンプルなやり方(メモリの無駄)

最も簡単なアルゴリズムは、電話番号から名前を求めるようなデータベースであれば、電話番号自身を配列添え字番号とする方法がある。しかしながら、この方法は大量のメモリを必要とする。

import java.util.*;

class PhoneName {
    int    phone ;  // (例) 27-2925
    String name ;
    
    PhoneName( int ph , String nm ) {
        this.phone = ph ;
        this.name  = nm ;
    }
}

public class Main {
    public static PhoneName[] table ;
    public static void entry( int ph , String nm ) {
        table[ ph ] = new PhoneName( ph , nm ) ;
    }
    public static String find( int ph ) {
        return table[ ph ].name ;
    } 
    public static void main(String[] args) throws Exception {
        table = new PhoneName[ 1000000 ] ; // 無駄にでかい
        entry( 272925 , "tsaitoh" ) ;
        entry( 621111 , "nit-fukui") ;
        entry( 123456 , "forger" ) ;
        System.out.println( find( 621111 ) ) ;
    }
}

しかし、50人程度のデータであれば、電話番号の末尾2桁を取り出した場合、同じ数値の人がいることは少ないであろう。であれば、電話番号の末尾2桁の値を配列の添え字番号として、データを保存すれば、配列サイズは100件となり、メモリの無駄を減らすことができる。

ハッシュ法

先に述べたように、データの一部を取り出して、それを配列の添え字番号として保存することで、高速にデータを読み書きできるようにするアルゴリズムはハッシュ法と呼ばれる。データを格納する表をハッシュ表、データの一部を取り出した添え字番号はハッシュ値、ハッシュ値を得るための関数がハッシュ関数と呼ばれる。

import java.util.*;

class PhoneName {
    int    phone ;  // 27-2925
    String name ;
    
    PhoneName( int ph , String nm ) {
        this.phone = ph ;
        this.name  = nm ;
    }
}

public class Main {
    public static PhoneName[] table ;
    public static void entry( int ph , String nm ) {
        table[ ph ] = new PhoneName( ph , nm ) ;
    }
    public static String find( int ph ) {
        return table[ ph ].name ;
    } 
    public static void main(String[] args) throws Exception {
        table = new PhoneName[ 1000000 ] ;
        entry( 272925 , "tsaitoh" ) ;
        entry( 621111 , "nit-fukui") ;
        entry( 123456 , "forger" ) ;
        System.out.println( find( 621111 ) ) ;
    }
}

ただし、上記のプログラムでは、電話番号の末尾2桁が偶然他の人と同じになることを考慮していない。
例えば、データ件数が100件あれば、同じ値の人も出てくるであろう。このように、異なるデータなのに同じハッシュ値が求まることを、ハッシュ衝突と呼ぶ。

ハッシュ関数に求められる特性

ハッシュ関数は、できる限り同じような値が求まるものは、ハッシュ衝突が多発するので、避けなければならない。例えば、6桁の電話番号の先頭2桁であれば、電話番号の局番であり、同じ学校の人でデータを覚えたら、同じ地域の人でハッシュ衝突が発生してしまう。また、ハッシュ値を計算するのに、配列の空き場所を一つ一つ探すような方式では、データ件数に比例した時間がかかり、高速なアルゴリズムとは言えない。このことから、ハッシュ関数には以下のような特徴が必要となる。

  • 同じハッシュ値が発生しづらい(一見してデタラメのように見える値)
  • 簡単な計算で求まること。
  • 同じデータに対し常に、同じハッシュ値が求まること。

ここで改めて、異なるデータでも同じハッシュ値が求まった場合、どうすれば良いのだろうか?

ハッシュ法を簡単なイメージで説明すると、100個の椅子(ハッシュ表)が用意されていて、1クラスの学生が自分の電話番号の末尾2桁(ハッシュ関数)の場所(ハッシュ値)に座るようなもの。自分のイスに座ろうとしたら、同じハッシュ値の人が先に座っていたら、どこに座るべきだろうか?

オープンアドレス法

先の椅子取りゲームの例え話であれば、先に座っている人がいた場合、最も簡単な椅子に座る方法は、隣が空いているか確認して空いていたらそこに座ればいい。

これをプログラムにしてみると、以下のようになる。このハッシュ法は、求まったアドレスの場所にこだわらない方式でオープンアドレス法と呼ばれる。

import java.util.*;

class PhoneName {
    int    phone ;  // 27-2925
    String name ;
    
    PhoneName( int ph , String nm ) {
        this.phone = ph ;
        this.name  = nm ;
    }
}

public class Main {
    public static PhoneName[] table ;
    public static int hash_func( int ph ) {
        return ph % 100 ;
    }
    public static void entry( int ph , String nm ) {
        int idx = hash_func( ph ) ;
        while( table[ idx ] != null )
            idx = (idx + 1) % 100 ;
        table[ idx ] = new PhoneName( ph , nm ) ;
    }
    public static String find( int ph ) {
        int idx = hash_func( ph ) ;
        for( ; table[ idx ] != null ; idx = (idx + 1) % 100 )
            if ( table[ idx ].phone == ph )
                return table[ idx ].name ;
        return null ;
    }
    public static void main(String[] args) throws Exception {
        table = new PhoneName[ 100 ] ;
        entry( 272925 , "tsaitoh" ) ;
        entry( 621111 , "nit-fukui") ;
        entry( 123425 , "forger" ) ;
        System.out.println( find( 272925 ) ) ;
        System.out.println( find( 123425 ) ) ;
    }
}

注意:このプログラムは、ハッシュ表すべてにデータが埋まった場合、無限ループとなるので、実際にはもう少し改良が必要である。

この実装方法であれば、ハッシュ表にデータが少ない場合は、ハッシュ値を計算すれば終わり。よって、処理時間のオーダはO(1)となる。しかし、ハッシュ表がほぼ埋まっている状態だと、残りわずかな空き場所を探すようなもの。

文字列のハッシュ値

ここまでで説明した事例は、電話番号をキーとするものであり、余りを求めるだけといったような簡単な計算で、ハッシュ値が求められた。しかし、一般的には文字列といったような名前から、ハッシュ値が欲しいことが普通だろう。

ハッシュ値は、簡単な計算で、見た目デタラメな値が求まればいい。 (ただしく言えば、ハッシュ値の出現確率ができるだけ一様であること)。一見規則性が解らない値として、文字であれば文字コードが考えられる。複数の文字で、これらの文字コードを加えるなどの計算をすれば、 偏りの少ない値を取り出すことができる。

public static int hash_func( String nm ) {
    int s = 0 ;
    for( int i = 0 ; i < nm.length() ; i++ )
        s += nm.charAt( i ) ;
    return s % 100 ;
}

文字列順で異なる値となるように

前述のハッシュ関数は、”ABC”さんと”CBA”さんでは、同じハッシュ値が求まってしまう。文字列順で異なる値が求まるように改良してみる。

public static int hash_func( String nm ) {
    int s = 0 ;
    for( int i = 0 ; i < nm.length() ; i++ )
        s += (nm.charAt( i ) + s * 小さい素数) % 大きい素数 ;
    return s % 100 ;
}

以下の方法は、繰り返しの度に s に小さい素数を掛けることで、数値全体に文字の影響がでるようにしている。これだけだと計算途中の s の値が最終的な100個に収めるための “% 100” で下2桁に影響がでないことから、大きい素数で余りを求めてみた。この計算方法は、疑似乱数を生み出す線形合同法の考え方を参考にした。

チェイン法

前に述べたオープンアドレス法は、ハッシュ衝突が発生した場合、別のハッシュ値を求めそこに格納する。配列で実装した場合であれば、ハッシュ表のサイズ以上の データ件数を保存することはできない。

チェイン法は、同じハッシュ値のデータをグループ化して保存する方法。 同じハッシュ値のデータは、リスト構造とするのが一般的。ハッシュ値を求めたら、そのリスト構造の中からひとつづつ目的のデータを探す処理となる。

この処理にかかる時間は、データ件数が少なければ、O(1) となる。しかし、ハッシュ表のサイズよりかなり多いデータ件数が保存されているのであれば、ハッシュ表の先に平均「N/ハッシュ表サイズ」件のデータがリスト構造で並んでいることになるので、O(N) となってしまう。

import java.util.*;

class PhoneNameNode {
    int             phone ;  // 27-2925
    String          name ;
    PhoneNameNode   next ;
    
    PhoneNameNode( int ph , String nm , PhoneNameNode nx ) {
        this.phone = ph ;
        this.name  = nm ;
        this.next  = nx ;
    }
}

public class Main {
    public final static int table_size = 100 ;
    public static PhoneNameNode[] table ;
    public static int hash_func( int ph ) {
        return ph % table_size ;
    }
    public static void entry( int ph , String nm ) {
        int idx = hash_func( ph ) ;
        table[ idx ] = new PhoneNameNode( ph , nm , table[ idx ] ) ;
    }
    public static String find( int ph ) {
        int idx = hash_func( ph ) ;
        for( PhoneNameNode p = table[ idx ] ; p != null ; p = p.next )
            if ( p.phone == ph )
                return p.name ;
        return null ;
    }
    public static void main(String[] args) throws Exception {
        table = new PhoneNameNode[ table_size ] ;
        for( int i = 0 ; i < table_size ; i++ )
            table[ i ] = null ;
        entry( 521125 , "tomoko" ) ;
        entry( 272925 , "saitoh" ) ;
        entry( 621160 , "mike"   ) ;
        System.out.println( find( 272925 ) ) ;
        System.out.println( find( 521125 ) ) ;
    }
}

理解度確認

毎年、冬休み期間中の自主的な理解度確認として、CBT を用いた理解度確認を行っています。今年も実施しますので、下記のシステムにログインし情報構造論では「ソフトウェア」(50分) を受講して下さい。

  • https://cbt.kosen-ac.jp/
  • 認証には、MS-365 のアカウントとパスワードでログインしてください。

データベースとB木

2分探索木の考え方を拡張したものでB木があり、データベースシステムではB木を基本としたデータ構造が活用されている。

B木の構造

2分木では、データの増減で木の組換えの発生頻度が高い。そこで、1つのノード内に複数のデータを一定数覚える方法をとる。B木では、位数=Nに対し、最大2N個のデータ d0, … , d2N-1 と、2N+1本のポインタ p0, … , p2N から構成される。pの先には、di-1< x < di を満たすデータが入った B木のノードを配置する。ただし、データの充填率を下げないようにするため、データは最小でもN個、最大で2Nを保存する。下図は位数2のB木の例を示す。

B木からデータの検索

データを探す場合は、ノード内のデータ di の中から探し、見つからない場合は、ポインタの先のデータを探す。位数がある程度大きい場合、ノード内の検索は2分探索法が使用できる。また、1つのノード内の検索が終われば、探索するデータ件数は、1/N〜1/2Nとなることから、指数的に対象件数が減っていく。よって、検索時間のオーダは、O( log ) となる。

B木へのデータの追加

B木にデータを追加する場合は、ノード内に空きがあれば、単純にデータの追加を行う。ノード内のデータが2N個を越える場合は、以下のような処理を行う。

ノード内のデータと追加データを並べ、その中央値を選ぶ。この中央値より大きいデータは、新たにつくられたノードに移す。中央値のデータは上のノードに追加処理を行う。このような方法を取ることで、2分木のような木の偏りが作られにくい構造となるようにする。

データを削除する場合も同様に、データ件数がN個を下回る場合は、隣接するノードからデータを取ってくることで、N個を下回らないようにする。

B木とデータベース

このB木の構造は、一般的にデータベースのデータを保存するために広く利用されている。

データベースシステムでは、データを効率よく保存するだけでなく、データの一貫性が保たれるように作られている。
例えば、データベースのシステムが途中でクラッシュした場合でも、データ更新履歴の情報を元にデータを元に戻し、データを再投入して復旧できなければならない。データを複数の所からアクセスした場合に、その順序から変な値にならないように、排他制御も行ってくれる。

データベースで最も使われているシステムは、データすべてを表形式で扱うリレーショナル・データベースである。

((リレーショナル・データベースの例))
STUDENT[]                           RESULT[]
ID   | name     | grade | course    ID   | subject | point
-----+----------+-------+--------   -----+---------+-------
1001 | t-saitoh |  5    | EI        1001 | math    | 83
1002 | sakamoto |  4    | E         1001 | english | 65
1003 | aoyama   |  4    | EI        1002 | english | 90
                                   外部キー
((SQLの例 2つの表の串刺し))
-- 60点以上の学生名,科目名,点数を出力 --
select STUDENT.name, RESULT.subject, RESULT.point --射影--
   from STUDENT , RESULT                          --結合--
   where STUDENT.ID = RESULT.ID     -- 串刺し --   --選択--
         and RESULT.point >= 60 ;

((上記SQLを Java で書いた場合))
STUDENT[] student = { ... } ;
RESULT[]  result  = { ... } ;
for( int st = 0 ; st < student.length ; st++ )           // 結合(from)
   for( int re = 0 ; re < result.length ; re++ )
      if ( student[ st ].ID == result[ re ].ID           // 選択(where)
           && result[ re ].point >= 60 )
           System.out.println( student[ st ].name + " "  // 射影(select)
                               + result[ re ].subject + " "
                               + result[ re ].point ) ;

B+木

データベースの処理では、目的のデータを O(log N) で見つける以外にも、全データに対する処理も重要である。この場合、全てのデータに対する処理では、単純なB木では再帰呼び出しが必要となる。しかし、他の表でも再帰処理を伴うと、プログラムは複雑になってしまう。

そこで、B木のデータを横方向に並べて処理を行う場合に、その処理が簡単になるように B+木が用いられる。
この方法では、末端のノードは、隣接するノードへのポインタを持つ。下図で示すB+木では、青で示す検索用のB木の部分と、赤で示す順次処理を行うためのシーケンスセットの部分から構成される。

ドメイン名とDNS

ドメイン名とDNS

インターネットでの通信では、IPプロトコルでコンピュータを指定するが、IPアドレスは無機質で覚えるのが大変であり、コンピュータに名前をつけて利用する。この際に、コンピュータの所属などが分かるようにしたものをドメイン名と呼ぶ。

例えば、電子情報工学科のドメイン名 www.ei.fukui-nct.ac.jp は、ピリオド部分で区切られ、以下のような意味を持つ。

  • .jp – 国ドメイン(.uk イギリス,.ch 中国,アメリカは無し)
  • .ac – 種別ドメイン(.co.jp,.com:会社,.ne.jp,net:ネットワーク系)
  • fukui-nct – 組織ドメイン
  • .ei. – サブドメイン(組織内が細分化されている場合)
  • www. – ホスト名

このような省略されていない、対象となるコンピュータを指定するためのドメイン名は、FQDN(Fully Qualified Domain Name)と呼ばれる。FQDNでの名前を ホスト名 と呼ぶことも多い。

ただしアメリカでは、国ドメインを一般的に使わない。また最近では、世界的な企業では国ドメインが意味をなさないので、アメリカ以外でも .com や .net といった、汎用トップレベルドメイン(gTLD)が使われる。様々なサービスを展開している企業では、組織種別が意味をなさないため、toyota.jp といった種別ドメインがない.jpドメイン名も増えてきた。高専機構のドメイン名 kosen-ac.jp も、”kosen-ac” が高専機構の組織ドメイン名なので注意。”-ac”は種別ドメインではない。

以下に、主要な組織ドメイン・国ドメインをあげる。

国ドメイン 国名
.jp 日本
.us アメリカ
.uk イギリス
.fr フランス
.de ドイツ
.cn 中国
.to トンガ
.tv ツバル
.gl グリーンランド
種別ドメイン(日本) 種別ドメイン 国名
.ac.jp .edu 教育機関
.go.jp .gov 政府機関
.co.jp .com 企業
.ne.jp .net ネットワーク組織
.or.jp .org 公益法人
.biz ビジネス用途
.info 情報関係用途
.name 名前

はgTLD

 

DNSのしくみ

DNSは、Domain Name Service であり、コンピュータ名(ドメイン名)から、IPアドレスを調べるサービスで、ポート番号53,UDPを使っている。

インターネットに接続する際には、最も身近なDNSの情報が与えられ、ユーザがコンピュータ名を問い合わせると、身近なDNSがコンピュータのIPアドレスを返してくれる。この際に、検索結果はキャッシュとして一定期間保存される。身近なDNSがそのコンピュータ名を知らない場合は、上位のDNSに問い合わせを行い、DNSルートサーバもコンピュータ名をキャッシュしていない場合は、管理元の組織のDNSに問い合わせが行われる。このようにすることで特定のDNSサーバに問い合わせが集中しないようになっている(負荷分散)。 DNSサーバの情報は DHCP サーバからIPアドレスなどと一緒に取得することができる。

以前の説明で DHCP(IPアドレス,サブネットマスク,ゲートウェイなどのネットワーク設定のサービス)を紹介しているが、IPアドレス以外にも、DHCPはそのネットワークで使える最寄りの DNS サーバの情報を得ることができる。


DNSと正引きと逆引き

DNSの使い方としては、一般的な使い方は、ドメイン名からIPアドレスを調べる正引きが多い。ブラウザは http://www.fukui-nct.ac.jp/ というURLが与えられたら、DNSに www.fukui-nct.ac.jp を問い合わせ、104.215.53.205 の結果が得られることで、http://104.215.53.205/ のコンピュータに接続を試みる。

これとは逆に、サーバ側では接続してきた相手のコンピュータが信頼できる相手か調べたい時がある。この時には IPアドレスからドメイン名を調べる逆引きを行う。これにより、IP アドレスをきちんと管理している組織であれば、ドメイン名が分かるのでどの組織から接続されているのか確認ができる。

DNSの情報を調べるためのコマンドは、nslookup を用いる。

DNSと様々な情報

DNS では、様々な情報が取得できる。IPアドレス以外にも、メールを送ってもらうサーバのIPアドレス(MXレコード)なども取得できる。

((( 正引きの例 )))
$ nslookup www.google.com
Server:         172.31.208.1
Address:        172.31.208.1#53

Non-authoritative answer:
Name:   www.google.com
Address: 142.250.206.228                                          # 調べる度に異なる値が返ってくるかも
Name:   www.google.com
Address: 2404:6800:400a:804::2004

((( 逆引きの例 )))
$ nslookup 142.250.206.228
228.206.250.142.in-addr.arpa    name = kix06s10-in-f4.1e100.net.  # 正引きと逆引きが一致していない例

Authoritative answers can be found from:

((( MX レコードを調べる例 )))
$ nslookup -query=MX fukui-nct.ac.jp   # MXレコード = そのドメイン宛のメールはどのコンピュータに送ればいい?
Non-authoritative answer:
fukui-nct.ac.jp mail exchanger = 10 fukuinct-ac-jp01c.mail.protection.outlook.com.

((( AAAA レコードを調べる例 )))
$ nslookup -query=AAAA www.google.com  # AAAAレコード = IPv6アドレスを指定した正引き
Non-authoritative answer:
Name:   www.google.com
Address: 2404:6800:400a:813::2004

((( 正引きと逆引きの異なる例 )))
$ nslookup tsaitoh.net
Name:   tsaitoh.net
Address: 64.33.3.150
$ nslookup 64.33.3.150
150.3.33.64.in-addr.arpa  name = ttn64-33-3-150.ttn.ne.jp.

DNSとセキュリティ

DNSは、コンピュータ名とIPアドレスを対応付けるものであり、これには正引き(コンピュータ名からIPアドレスを求める)と、逆引き(IPアドレスからコンピュータ名を求める)がある。セキュリティ対策が厳しい場所では、

  • 正引きを使うことで、特定の組織のドメイン名を持つコンピュータからのアクセスを許可/禁止する。(例:国ドメイン.xxからは接続拒否)
  • 正引きで、コンピュータ名が登録されている所からのみ許可する。(例:組織ドメイン.fukui-nct.ac.jpからは接続許可)
  • IPアドレスから逆引きして求めたコンピュータ名をさらに正引きして同じIPアドレスが求まるかを確認

といった対策を行う。

  • DNSのドメイン名は、当初は最初に申請した人に割り当てられる。このため、nintendo.com といったドメイン名を、関係ない人が取得するといったトラブルがあった。(サイバースクワッティング)
  • DNSを用いたクラッキングでは、ウィルスに感染させたパソコンに偽物のIPアドレスを教えることで、偽装した別コンピュータに誘導し個人情報を盗む手口がある。(DNSポイズニング/スプーフィング)
  • 他にもウィルスに感染させた大量のパソコンから、同時にルートサーバに大量のDNSの問合せを送ることで、処理能力を低下させると、インターネット全体でDNS参照ができなくなる攻撃もある。(DNSルートサーバへの分散DoSアタック)
  • DNSは、他のコンピュータに接続するための重要な情報だが、独裁国家などでは国にとって不都合な情報が得られるドメイン名のIPアドレスを改ざんしアクセスできないようにすることもある。このため、Google 社では 覚えやすい 8.8.8.8 という IPアドレスの DNS サーバを提供している。この 8.8.8.8 は、DNS の返答速度も速いことから、ブラウザの表示速度を高速化するために自分のPCに設定する人も多い。
  • 暗号化されていない通信は、同一ネットワーク上の機器がパケット解析ソフトなどを実行すると、相手がどういうDNS参照をしているか見られてしまう。この対策として最近は DNS over HTTPS という方式も出てきている。

ドメイン名と罠

データべースの設計と正規形

テスト問題の返却および解答の説明を行い、その後、データベースの設計において、重要な正規形についての説明の導入。

正規形

データベースにおいて、様々な不整合を防ぐために正しい設計が必要であることを 改めて説明し、それには正規形としての条件を満たしている必要があることを説明する。一般的に不整合が発生しないためには、以下の第1正規形第2正規形第3正規形を満たすように表を分ければよい。

第一正規形は、すべての要素が原子値である条件を満たせばいい。 要素の中が複数の項目であったり表形式のデータがあると、 表構造のリレーショナルデータベースにはできない。


キーの説明超キー(スーパーキー)とは、データベースで1つのデータを 選び出すために必要なデータ項目であり、複数の項目で1データを指定 できる場合もある。

候補キーとは、必要最小限の項目となっているものを指す。 1項目が抜けても選別できなくなるようであれば、候補キーとは言わない。 主キーとは、候補キーのなかで管理の都合上便利なもの。

データ項目の値が決まると、他のデータ項目が自動的に決まるものは、 従属関係があるという。

第1正規化 第2正規化

第二正規形は、部分従属がなく、すべての非キーデータ項目が、候補キーに 完全従属する場合をいう。

  • 完全従属とは、候補キーを構成する全てのデータ項目に、非キーデータ項目が従属していること。
  • 部分従属とは、候補キーを構成するデータ項目の一部のデータ項目に、非キー項目が従属していること。

この例において、単価は商品が決まれば自動的に求まる情報。 (単価が日々変化することはないという条件で…) これは、部分従属となる。他に部分従属となっている属性は何か?

  • 推移従属性とは、データ項目でA→B→Cと、次々と値が求められる関係を指す。

第三正規形とは、 候補キー以外の非キーデータ項目は、候補キーに完全従属し、 かつどの候補キーにも推移従属しない関係をいう。

第3正規化

上記の例では、単価と個数が決まれば、金額が求まる推移従属の関係が含まれている。

おまけ:BC正規形,第4,5正規形

この他にも、 さらに「非キーからキーに関数従属性がある場合にそれを取り除く」、 ボイスコッド正規形(BC正規化)。 「対称性のある多値従属性(キーを決めると複数データが該当)を分解」して得られる第4正規形や、 「元になるテーブルの結合従属性を維持して分解」することにより得られる第5正規形などがある。

トップダウン設計・ボトムアップ設計

データベースの設計にあたって、実際の設計手順の説明を行う。

トップダウン設計では、対象業務を記述し、その中から名詞となっている実体を抽出する。 さらに動詞や形容詞のように記載されている関連を抽出する。 抽出した実体・関連では、あいまいであったり冗長であったりするので、整理したうえで、 その実体・関連をER図に表す。

ボトムアップ設計では、対象業務で実際に使われている入力帳票や結果の出力などを 見ながら、第1正規形を満たすように表を作っていく作業からおこなう。

トップダウン設計やボトムアップ設計で、 ER図や第一正規形を満たすような表が出来上がったら、 その属性の中で従属性を確認しながら、第2正規形・第3正規形へと整理していく。

データベース後半課題

データベース後半の課題は「卒業研究の対象をデータベースとして設計」とする。

情報系の卒研テーマであれば、処理対象のデータの中にはデータベースで管理するのがふさわしい対象について設計せよ。実験系の卒研テーマであれば、実験結果の表をデータベースで管理するとした場合の設計を行うこと。どちらでもない卒研で、卒研のテーマの中にデータベース化すべき対象が無い場合は、身の回りの帳票(例えばコンビニのレシートなど)をデータベース化することを検討すること。

レポートで記載する内容は、以下の通りとする。

  • 卒業研究におけるデータベース化する対象の説明
  • データベースをトップダウン設計する際の
    • 実体と関連を抽出するまでの説明
    • 正規化を行う経過の説明
    • 上記を踏まえたトップダウン設計でのER図
  • データベースをボトムアップ設計する際の
    • 対象とする帳票に相当するデータの一例と説明
    • レベル分けや正規化を行う経過の説明
    • 上記を踏まえたボトムアップ設計でのER図
  • 考察
    • トップダウン設計とボトムアップ設計に違いがあれば、設計の見直しの過程の説明
    • 両設計方法から分かったこと

Node-REDのインストール

卒研学生用に、古いPCに Ubuntu 24 をインストールし、Node-RED 環境を構築

Ubuntu のセットアップ後に以下の作業を行う。

nodejs のインストール

新しい nodejs 22 を入れたいので apt パッケージは使わない。

#--- nodejs 22 install
$ sudo apt install curl
$ curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
$ sudo apt install -y nodejs

Node-REDのインストール

#--- node-red install
$ sudo npm install -g npm@10.9.1
$ sudo npm install -g -unsafe-perm node-red node-red-admin

Node-REDのsystemd登録

OS起動と共に Node-RED を起動したいので、Systemd に登録

Systemd のファイルを作成

#--- node-red systemd setup
$ sudo vi /etc/systemd/system/node-red.service

[Unit]
After=syslog.target network.target
Documentation=http://nodered.org/

[Service]
Environment="NODE_OPTIONS=--max-old-space-size=128"
Environment="NODE_RED_OPTIONS=-v"
ExecStart=/usr/bin/node-red $NODE_OPTIONS $NODE_RED_OPTIONS
WorkingDirectory=/root/
User=root
Group=root
Nice=10
SyslogIdentifier=Node-RED
StandardOutput=syslog
Restart=on-failure
KillSignal=SIGINT

[Install]
WantedBy=multi-user.target

Systemd の有効化と起動と確認

$ sudo systemctl enable node-red
$ sudo systemctl start node-red
$ sudo systemctl status node-red
● node-red.service
     Loaded: loaded (/etc/systemd/system/node-red.service; enabled; preset: enabled)
     Active: active (running) since Wed 2024-12-04 14:25:50 JST; 10min ago
       Docs: http://nodered.org/
   Main PID: 23602 (node-red)
      Tasks: 11 (limit: 9334)
     Memory: 58.2M (peak: 68.4M)
        CPU: 2.243s
     CGroup: /system.slice/node-red.service
             └─23602 node-red

12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: 復元することはできません。その場合、ファイルを削除してクレデンシャルを
12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: 再入力しなければなりません。
12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: 設定ファイル内で 'credentialSecret' オプションを使って独自キーを設定
12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: します。変更を次にデプロイする際、Node-REDは選択したキーを用いてクレ
12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: デンシャルを再暗号化します。
12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: ---------------------------------------------------------------------
12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: 4 Dec 14:25:51 - [warn] 暗号化されたクレデンシャルが存在しません
12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: 4 Dec 14:25:51 - [info] サーバは http://127.0.0.1:1880/ で実行中です
12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: 4 Dec 14:25:51 - [info] フローを開始します
12月 04 14:25:51 tsaitoh-lab Node-RED[23602]: 4 Dec 14:25:51 - [info] フローを開始しました

後は、ブラウザを起動して、http://127.0.0.1:1880/ を開くだけ。

トランスポート層・TCPとUDP

前回の授業でブロードキャストなどの説明をしたことを踏まえ、DHCP について改めて説明

DHCP

前回の IP では、異なるサブネットを繋ぐ役割としての Internet Protocol (IP) について説明をした。IP での通信では、IPアドレスが必要だが、正しく接続ができるためには、(1)IPアドレス、(2)サブネットマスク、(3)ゲートウェイアドレス が必要となる。しかし、この情報は初心者には設定が難しいし、IPアドレスが他の利用者と重複させないためには、きちんとした管理が必要となる。

この時に使われるのが DHCP(Dynamic Host Configuration Protocol) であり、通常のパソコンは、IPの自動設定としておけば、DHCP を用いて前述の(1),(2),(3) の情報が自動で設定される。DHCP機能は、一般的にルータや WiFi の AP(アクセスポイント)の中に組み込まれている。

DHCPサーバには、使用可能なIPアドレスを登録しておく。利用者が DHCP クライアントとして接続する際には、ブロードキャストパケットを使い、同じサブネット内に DHCP リクエストを送る。このリクエストを DHCPサーバが受信したら、サーバはクライアントに向かって、貸し出し用の IP アドレスの1つをクライアントに提供する(サブネットマスクやゲートウェイなども提供)。


データ通信ではノイズなどの影響で通信に失敗することがある。これらを補うためのTCPがある。またTCPの通信の欠点を補うUDPがある。この授業では、TCPとUDPによるトランスポート層の説明を行う。

TCP

TCP(Transmission Control Protocol/トランスミッションコントロールプロトコル)では、分割されたパケットを元の順序に組み上げたり、パケットが途中で消えた場合の再送などの処理を行う。この機能により確実に相手に送る機能が実現されている。

3way ハンドシェーク

TCPの通信では、最初に相互に通信が可能かを確認するハンドシェークが行われる。パケットには、SYN,ACK,FINといった種別を表すフラグがついており、SYNは接続確立の要求を表す。ACKは了解を表す。FINは切断要求を表す。通信開始の時には、(1)通信OK?、(2)OKだよ,そっちもOK?、(3)OKだよ! といった3つの通信パケットで確認してから通信を行う。この最初のやり取りを3way ハンドシェークという。

  • SYN flood攻撃 – 3wayハンドシェークは、この後に送られてくるパケットを並び替えるためのメモリを準備などが必要となる。このため通信ルールを無視して相手にSYNパケットだけを大量に送ると相手はムダな準備作業により他の通信が困難になる場合がある。

SEQ番号,ACK番号

通信パケットには、SEQ番号(シーケンス番号/32bit)ACK番号(アクノリッジ番号/32bit)という情報がついており、送信元はACK番号を確認することで、どのパケットが正しく届いたのかを認識できる。3wayハンドシェーク時には、相手のSEQ番号に1を加えたACK番号をつけてパケットを返送することで通信が始められることが確認できる。実際のデータを送信する際には、受け取ったデータ長をSEQ番号に加えた値を、ACK番号にして受信に成功したことを相手に伝える。これにより、小分けにされたパケットで次に何を送れば良いのか判別できる。
(Acknowledge = 承認する)

通信で、パケット分割して送って、その一つ毎の返答を待つと、通信の待ち時間が増えてしまう。このため、相手が受け取り可能であれば、一度に前回の2倍のパケットを返信を待たずに送る。(ウィンドウサイズの拡大)

チェックサムとタイムアウト

通信では、送る途中でデータにノイズが混入したり、パケットが消失することがある。このため、パケットにはパケットのチェックサム(バイトデータを加算した値)を付けて送り、受信時に比較してノイズ混入でデータが壊れていないかを確認する。こういった際には、パケットが正しく届かない。パケットが消失したりして、通信相手からの返送が届かないで一定の待ち時間が経過することをタイムアウトと呼ぶ。この時、返信パケットにはデータのSEQ番号とACK番号の情報があるため、受け取りに失敗したパケットが判別できるので、送り側は失敗したパケットを再送する。

受け取り側は、同じくSEQ番号やACK番号を元にパケットの順番を正しく並べ戻すことができる。

TCP FINパケット

通信を切断する場合には、相互に切断して良いか確認する4回の通信で終了する。

UDP

TCPによる通信は、相手側からの受け取った返事を待ちながら通信を行う。このため、通信にかかる時間を要する。また、複数の利用者に一斉にデータをばらまくブロードキャスト通信では、個別のパケット欠落を修復しようとすると、処理が複雑になる。

これらの対応策として、UDP(User Datagram Protocol)がある。これは、TCP通信でのパケット分割や再送処理を行わない極めて単純な送信方法である。このため、相手側に正しくデータが送られる保証はない。確実に相手に送る必要があれば、確認や再送は上位プロトコルの責任となる。

UDP通信は返信を待つ必要がないので、動画・音声配信などのリアルタイム性が求められる通信でよく使われる。UDPは正しく通信ができずパケットが途絶えても、一時的に動画が止まるなり音声が止まるといったように、問題が少ないような場合に有用となる。

ICMP/ping

IPプロトコルのプロトコルの1つとして ICMP (Internet Control Message Protocol) がある。このプロトコルは、ネットワーク機器(ノード)の間で、通信の確認をするためのもので、ping コマンドや traceroute コマンドで使われる。

基本的に、ICMPパケットを相手コンピュータに送り、返事が返ってくるかを確認する。ping や traceroute は、返事が返ってくるまでの時間や、パケットをいくつ送っていくつ返ってきた…などの情報を表示することができ、相手コンピュータまでの通信路が正常かどうかが判断できる。

$ ping www.google.co.jp
PING www.google.co.jp (172.217.25.163) 56(84) バイトのデータ
64 バイト応答 送信元 syd09s13-in-f3.1e100.net (172.217.25.163): icmp_seq=1 ttl=115 時間=7.85ミリ秒
64 バイト応答 送信元 syd09s13-in-f3.1e100.net (172.217.25.163): icmp_seq=2 ttl=115 時間=8.02ミリ秒
^C   # 途中で強制終了させるために Ctrl-C で止める
]$ traceroute www.google.co.jp
traceroute to www.google.co.jp (172.217.25.163), 30 hops max, 60 byte packets
 1  airstation.ei.fukui-nct.ac.jp (192.168.2.1)  0.355 ms  0.529 ms  0.549 ms
(略)
 9  108.170.243.33 (108.170.243.33)  8.245 ms 108.170.243.65 (108.170.243.65)  6.893 ms  6.936 ms
10  72.14.239.25 (72.14.239.25)  6.899 ms  7.125 ms 72.14.238.23 (72.14.238.23)  7.140 ms
11  syd09s13-in-f163.1e100.net (172.217.25.163)  7.014 ms  7.007 ms  6.961 ms

トランスポート層

OSI参照モデルでは、TCPプロトコルとUDPプロトコルをあわせてトランスポート層と呼び、TCP+UDPとネットワーク層のIPプロトコルでの通信が、今日のインターネット通信の基本プロトコルとなっており、総称して TCP/IPとかインターネット・プロトコル・スイート と呼ぶ。(suite=”一連のものや一緒に機能するものの集まり”/sweetじゃない)

木構造の探索・深さ優先探索・幅優先探索

深さ優先探索(前順)

以前紹介した2分木の表示では、再帰呼び出しで木の処理を記述していた。

しかし、スタックを使って未処理のノードを保存しながらノードに対する処理を行えば、ループ処理で「すべての木のノード」に対する処理を記述できる。このような木に対する処理は、末端方向に処理が進むことが優先されるため深さ優先探索と呼ぶ。

以下のプログラムで示す深さ優先探索では、ノードに対する処理を行ってから左の枝葉の処理が行われる。このような深さ優先探索は前順(pre-order)と呼ぶ。

このような探索は、ゲームですべての戦略の先読みを行う処理で用いられる。

import java.util.*;

class BTreeNode {
    int       data ;
    BTreeNode left ;
    BTreeNode right ;
    
    BTreeNode( int x , BTreeNode l , BTreeNode r ) {
        this.data  = x ;
        this.left  = l ;
        this.right = r ;
    }
}

public class Main {
    // 再帰呼び出しによる木の操作 = 深さ優先探索(前順)
    public static void print_rec_depth_first_pre_order( BTreeNode p ) {
        if ( p != null ) {
            System.out.print( p.data + " " ) ;
            print_rec_depth_first_pre_order( p.left ) ;
            print_rec_depth_first_pre_order( p.right ) ;
        }
    }
    // 深さ優先探索(前順)
    public static void print_depth_first_pre_order( BTreeNode p ) {
        // 未処理ノードを保存するスタック
        LinkedList stack = new LinkedList<>() ;
        stack.addFirst( p ) ; // push
        while( !stack.isEmpty() ) {
            // スタックトップを取り出す
            p = stack.removeFirst() ; // pop
            if ( p != null ) {
                System.out.print( p.data + " " ) ;
                // 枝を未処理スタックに保存
                stack.addFirst( p.right ) ; // 右を後回し
                stack.addFirst( p.left ) ;
            }    
        }
    }
    public static void main(String[] args) throws Exception {
        BTreeNode top =
            new BTreeNode( 42 ,
                    new BTreeNode( 27 ,
                            new BTreeNode( 13 , null , null ) ,
                            new BTreeNode( 38 , null , null ) ) ,
                    new BTreeNode( 72 ,
                            new BTreeNode( 64 , null , null ) ,
                            new BTreeNode( 81 , null , null ) ) ) ;
        // 再帰による深さ優先探索(前順)
        print_rec_depth_first_pre_order( top ) ;
        System.out.println() ;
        // スタックを使った深さ優先探索(前順)
        print_depth_first_pre_order( top ) ;
        System.out.println() ;
    }
}

幅優先探索

同じように、未処理のノードを待ち行列に保存しながら、ノードに対する処理を行うことでも、すべての木に対する処理が記述できる。この場合、根本の処理をすべて終えてから、末端に対する処理が行われる。このような処理は幅優先探索と呼ぶ。

幅優先探索の処理と、前順の深さ優先探索を比べると、ノードを覚えるデータ構造に待ち行列を使う(幅優先探索)か、スタックを使う(深さ優先探索)の違いしかない。

幅優先探索は、ゲームの戦略で先読みを行う場合に用いられるが、チェス・将棋・囲碁といったすべての先読みを行うと手数が爆発的に増加するため、不利な戦略の先読みを途中で打ち切る必要がある場合に有用である。

public class Main {
    // 幅優先探索
    public static void print_breadth_first( BTreeNode p ) {
        // 未処理ノードを保存する待ち行列
        LinkedList queue = new LinkedList<>() ;
        queue.addFirst( p ) ; // put
        while( !queue.isEmpty() ) {
            // 待ち行列の先頭を取り出す
            p = queue.removeLast() ; // get
            if ( p != null ) {
                System.out.print( p.data + " " ) ;
                // 枝を未処理待ち行列に追加
                queue.addFirst( p.left ) ; // 左から処理
                queue.addFirst( p.right ) ;
            }    
        }
    }
    public static void main(String[] args) throws Exception {
        BTreeNode top = (略) ;
        // 幅優先探索で表示
        print_breadth_first( top ) ;
        System.out.println() ;
    }
}

深さ優先探索(中順)

2分探索木のようなデータで、昇順で並べて表示する必要がある場合、再帰呼び出しのプログラムでは、「左枝の処理、ノードへの処理、右枝の処理」の順で記述する必要がある。このような深さ優先探索は中順(in-order)と呼ばれる。しかし、先に説明した再帰による深さ優先探索(前順/pre-order)では、「ノードへの処理、左枝の処理、右の枝の処理」で記述されている。

スタックを用いた深さ優先探索で、2分探索木の昇順のように中順で処理を行うには、未処理のノードの保存順序に工夫が必要となる。

import java.util.*;
public class Main {
    // 再帰呼び出しによる木の操作 = 深さ優先探索(中順)
    public static void print_rec_depth_first_in_order( BTreeNode p ) {
        if ( p != null ) {
            print_rec_depth_first_in_order( p.left ) ;
            System.out.print( p.data + " " ) ;
            print_rec_depth_first_in_order( p.right ) ;
        }
    }
    // 深さ優先探索(中順)
    public static void print_depth_first_in_order( BTreeNode p ) {
        // 未処理ノードを保存するスタック
        LinkedList stack = new LinkedList<>() ;
        for( ;; ) {
            // ノードを保存しながら左末端まで進む
            while( p != null ) {
                stack.addFirst( p ) ;
                p = p.left ;
            }
            // 未処理が残っていないなら終了
            if ( stack.isEmpty() )
                break ;
            // スタックトップを取り出す
            p = stack.removeFirst() ; // pop
            System.out.print( p.data + " " ) ;
            p = p.right ;
        }
    }
    public static void main(String[] args) throws Exception {
        BTreeNode top = (略) ;

        // 再帰による深さ優先探索(中順)
        print_rec_depth_first_in_order( top ) ;
        System.out.println() ;
        // スタックを使った深さ優先探索(中順)
        print_depth_first_in_order( top ) ;
        System.out.println() ;
    }
}

2分木による構文木

コンパイラの処理の流れ

構文の構造を表すために、2分木を使うという話をこの後に行うが、その前にコンパイラが機械語を生成するまでの処理の流れについて説明をする。

Cコンパイラのソース
  ↓
プリプロセッサ (#define,#includeなどの処理)
  ↓
コンパイラ
・字句解析(ソースコードをトークンに切り分ける)
・構文解析(トークンから構文木を生成)
・最適化(命令を効率よく動かすために命令を早い命令に書き換え)
・コード生成(構文木から中間コードを生成)
  |
  | リンカでライブラリと結合
 (+)←---ライブラリ
  ↓ 
 機械語

バイトコードインタプリタ

通常、コンパイラとかインタプリタの説明をすると、Java がコンパイラとか、JavaScript はインタプリタといった説明となる。しかし、最近のこういった言語がどのように処理されるのかは、微妙である。

(( Java の場合 ))
foo.java (ソースコード)
 ↓       Java コンパイラ
foo.class (中間コード)
 ↓
JRE(Java Runtime Engine)の上で
中間コードをインタプリタ方式で実行

あらかじめコンパイルされた中間コードを、JREの上でインタプリタ的に実行するものは、バイトコードインタプリタ方式と呼ぶ。

ただし、JRE でのインタプリタ実行では遅いため、最近では JIT コンパイラ(Just-In-Time Compiler)により、中間コードを機械語に変換してから実行する。

また、JavaScriptなどは(というか最近のインタプリタの殆どPython,PHP,Perl,…は)、一般的にはインタプリタに分類されるが、実行開始時に高級言語でかかれたコードから中間コードを生成し、そのバイトコードをインタプリタ的に動かしている。

しかし、インタプリタは、ソースコードがユーザの所に配布されて実行するので、プログラムの内容が見られてしまう。プログラムの考え方が盗まれてしまう。このため、変数名を短くしたり、空白を除去したり(…部分的に暗号化したり)といった難読化を行うのが一般的である。

字句解析と構文解析

トークンと正規表現(字句解析)

字句解析でトークンを表現するために、規定されたパターンの文字列を表現する方法として、正規表現(regular expression)が用いられる。

((正規表現の書き方))
選言     「abd|acd」は、abd または acd にマッチする。
グループ化 「a(b|c)d」は、真ん中の c|b をグループ化
量化    パターンの後ろに、繰り返し何回を指定
      ? 直前パターンが0個か1個
       「colou?r」
      * 直前パターンが0個以上繰り返す
       「go*gle」は、ggle,gogle,google
      + 直前パターンが1個以上繰り返す
       「go+gle」は、gogle,google,gooogle

正規表現は、sed,awk,Perl,PHPといった文字列処理の得意なプログラム言語でも利用できる。こういった言語では、以下のようなパターンを記述できる。

[文字1-文字2...] 文字コード1以上、文字コード2以下
      「[0-9]+」012,31415,...数字の列
^     行頭にマッチ
$     行末にマッチ
((例))
[a-zA-Z_][a-zA-Z_0-9]* C言語の変数名にマッチする正規表現

構文とバッカス記法

言語の文法(構文)を表現する時、バッカス記法(BNF)が良く使われる。

((バッカス記法))
<表現> ::= <表現1...> | <表現2...> | <表現3...> | ... ;

例えば、加減乗除記号と数字だけの式の場合、以下の様なBNFとなる。

((加減乗除式のバッカス記法))
<加算式> ::= <乗算式> '+' <乗算式>    【要注意】わざと間違っている部分あり
          | <乗算式> '-' <乗算式>
          | <乗算式>
          ;
<乗算式> ::= <数字> '*' <乗算式>
          | <数字> '/' <乗算式>
          | <数字>
          ;
<数字>   ::= [0-9]+
          ;

上記のバッカス記法には、間違いがある。”1+2+3″を正しく認識できない。どこが間違っているだろうか?

このような構文が与えられた時、”1+23*456″と入力されたものを、“1,+,23,*,456”と区切る処理が、字句解析である。

また、バッカス記法での文法に合わせ、以下のような構文木を生成するのが構文解析である。

  +
 / \
1   *
   / \
  23   456

2項演算と構文木

演算子を含む式が与えられたとして、古いコンパイラではそれを逆ポーランド変換して計算命令を生成していた。しかし最近の複雑な言語では、計算式や命令を処理する場合、その式(または文)の構造を表す2分木(構文木)を生成する。。

   +
  / \
 1   *
    / \
   2   3

演算子の木のノードで、末端は数値であることに注目し、右枝・左枝がNULLなら数値(data部にはその数値)、それ以外は演算子(data部には演算子の文字コード)として扱うとして、上記の構文木のデータを作る処理と、その構文木の値を計算するプログラムを示す。

import java.util.*;

class ExpTreeNode {
    String      op_val ;
    ExpTreeNode left ;
    ExpTreeNode right ;
    
    ExpTreeNode( String op , ExpTreeNode l , ExpTreeNode r ) {
        this.op_val = op ;
        this.left   = l ;
        this.right  = r ;
    }
    ExpTreeNode( String val ) {
        this.op_val = val ;
        this.left   = null ;
        this.right  = null ;
    }
}

public class Main {
    public static int eval( ExpTreeNode p ) {
        if ( p.left == null && p.right == null ) {
            return Integer.parseInt( p.op_val ) ;
        } else {
            int l_val = eval( p.left) ;
            int r_val = eval( p.right ) ;
            switch( p.op_val ) {
            case "+" : return l_val + r_val ;
            case "*" : return l_val * r_val ;
            }
            return -1 ; // error
        }
    }
    public static void main(String[] args) throws Exception {
        ExpTreeNode top =
            new ExpTreeNode( "+" ,
                             new ExpTreeNode( "1" ) ,
                             new ExpTreeNode( "*" ,
                                              new ExpTreeNode( "2" ) ,
                                              new ExpTreeNode( "3" ) ) ) ;
        
        System.out.println( eval( top ) ) ;
    }
}

オブジェクト指向を使った構文木(テスト範囲外)

前述の2分木を用いた構文木では、式を構成する「整数値」の式、「式 演算子 式」の演算式の二つのパターンを、無理やり1つのクラスで扱っていた。整数値と演算子の文字列の違いがあるため、数値も文字列で保存したものを parseInt() で数値に変換している。

こういった、大きくは式と分類できるけど、数値を表す式、演算子からなる式と異なるデータの場合、Java では、オブジェクト指向プログラミングの抽象クラス仮想関数のテクニックを用いる。式を表現する基底クラス ExpNode から、数値を表す ExpNodeInteger , 演算子からなる式を表す ExpNodeOperator を派生させ、式の値を求める際には、各クラスに応じた計算処理を行う仮想関数 eval() を使って処理を行っている。

import java.util.* ;

// 抽象基底クラス
abstract class ExpNode {
    abstract public int eval() ;
}

// 整数で具体化
class ExpNodeInteger extends ExpNode {
    int  value ;
    
    ExpNodeInteger( int i ) {
        this.value = i ;
    }
    // 仮想関数
    public int eval() {
        return this.value ;
    }
}

// 演算子で具体化
class ExpNodeOperator extends ExpNode {
    String  op ;
    ExpNode left ;
    ExpNode right ;
    
    ExpNodeOperator( String s , ExpNode l , ExpNode r ) {
        this.op    = s ;
        this.left  = l ;
        this.right = r ;
    }
    // 仮想関数
    public int eval() {
        int l_val = this.left.eval() ;
        int r_val = this.right.eval() ;
        switch( this.op ) {
        case "+" : return l_val + r_val ;
        case "*" : return l_val * r_val ;
        }
        return -1 ;
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        ExpNode top =
            new ExpNodeOperator( "+" ,
                                 new ExpNodeInteger( 1 ) ,
                                 new ExpNodeOperator( "*" ,
                                                      new ExpNodeInteger( 2 ) ,
                                                      new ExpNodeInteger( 3 ) ) ) ;

        System.out.println( top.eval() ) ;
    }
}

システム

最新の投稿(電子情報)

アーカイブ

カテゴリー