9. ソケット通信#

ソケット通信は、コンピュータ間の通信をプログラムするための基本的な手段である。 本章では、ソケット通信の概念と実際の動作を説明する。

9.1. TCP/IP(おさらい)#

通信プロトコル とは、通信をおこなうための約束事である。 どのような通信であれ、意味のある伝達をするためには、送り手と受け手の間であらかじめルールを決めておく必要がある。

../../_images/protocol.png

図 9.1 通信プロトコルの概念図#

ひとくちに約束事と言っても、アプリケーションごとに固有の約束事もあれば、共通して必要となる約束事もある。 そうした整理をするために、通信技術は 階層化 されている。 理論的なモデルとしては OSI参照モデル (Open System Interaction Model) が知られているが、インターネットで実際に用いられているのはより簡略化した TCP/IPモデル である。

../../_images/layer.png

図 9.2 TCP/IPモデルの階層構造#

通信をするためには、相手を識別する情報が必要である。 TCP/IPモデルで用いられる主要な識別情報を下表に示す。

表 9.1 TCP/IPモデルの階層と主な識別情報#

階層

主な識別情報

アプリケーション層

ドメイン名

www.hit-u.ac.jp

トランスポート層

ポート番号

80

インターネット層

IPアドレス

192.168.1.1

ネットワークインタフェース層

MACアドレス

02:E2:36:7A:8D:15

ここではTCP/IPの細部にまでは踏み込まないが、ざっくりした理解としては、 IPアドレスで通信相手の場所を特定し、ポート番号で通信相手の中のアプリケーションを特定する と考えれば良い。

MACアドレスは、IPアドレスが指し示す場所にいる機器の名前のようなものである。言わば、IPアドレスが住所、MACアドレスが居住者の氏名に相当する。

ドメイン名は、IPアドレスを人間にとってわかりやすい形に置き換えたものである。

../../_images/domain-ip-mac-port.png

図 9.3 IPアドレス, ポート番号, MACアドレスの関係#

練習1

自身のPCのIPアドレスとMACアドレスを確認してみよう。 ターミナルを起動し、以下のコマンドを実行すれば良い。

  • Windowsの場合: ipconfig -all

  • Macの場合: ifconfig

ネットワークインタフェースが複数ある場合、それぞれのアドレス等の情報が表示される。

練習2

PythonでIPアドレスとMACアドレスを取得してみよう。 「各自作成」となっている箇所を埋めてプログラムを完成させること。

※以下のプログラムでは、ネットワークインタフェースの指定はできない。特定のインタフェースの情報を取得したい場合は追加の工夫が必要である。

import uuid
import socket


def get_mac():
    # MACアドレスを10進整数の形で取得(例:98765432109876)
    mac_int = uuid.getnode()

    # 16進数に変換した上で、2桁ずつコロン区切りにする(例:59:D3:9E:7F:3B:34)
    # !!! 各自作成 !!!

    return mac_addr


def get_ip():
    ip = socket.gethostbyname(socket.gethostname())
    return ip


print(f'MAC Address: {get_mac()}')
print(f'IP Address: {get_ip()}')

グローバルIPとプライベートIP#

ここで、IPアドレスについて少し補足しておこう。IPアドレスには グローバルIPアドレスプライベートIPアドレス がある。

グローバルIPアドレスは、インターネット上の場所を表すために使われ、原則として同じIPアドレスが複数端末に割り当てられることは無い。 プライベートIPアドレスは、LAN (Local Area Network) 内でのみ使われるIPアドレスである。LAN内では一意であるが、他のLANには同じIPアドレスの端末が存在し得る。

プライベートIPアドレスを持つ端末が、LAN外と通信する際は、ルータがグローバルIPアドレスとプライベートIPアドレスの変換を担う(NAT, NAPT)。

プライベートIPアドレスを使うことで、有限なグローバルIPアドレスの浪費を抑えることができる。 また、LAN内の端末が直接インターネット上にさらされることを防ぐセキュリティ面の利点もある。

IPアドレス(v4)は32bitであり、各byteを10進数で表記してピリオドで区切るのが一般的な表記方法である。 例:192.168.1.1 ( = 11000000.10101000.00000001.00000001)

プライベートIPアドレスとして使えるのは以下の範囲と定められている。

  • 10.0.0.0 ~ 10.255.255.255

  • 172.16.0.0 ~ 172.31.255.255

  • 192.168.0.0 ~ 192.168.255.255

Domain Name System#

IPアドレスは人間にとってはとっつきづらいため、代替表現として ドメイン名 が用いられる。 たとえば、一橋大学公式ウェブサイトをホストしているサーバ(ウェブサーバ)のドメイン名は www.hit-u.ac.jp である。 より正確には、www がホスト名、 hit-u.ac.jp がドメイン名、 www.hit-u.ac.jp はFQDN (Fully Qualified Domain Name) という。

ドメイン名とIPアドレスの対応関係を管理する仕組みを DNS (Domain Name System) という。 DNSサーバに問い合わせることで、あるドメイン名と対応するIPアドレスを調べたり、逆にIPアドレスと対応するドメイン名を調べたりすることができる。

練習3

ターミナルを起動し、以下のコマンドを使って、任意のドメイン名と対応するIPアドレスを調べてみよう。

$ nslookup [FQDN]

ドメイン名 (FQDN) の例: www.amazon.co.jp, www.google.co.jp, www.cao.go.jp

練習4

Pythonでは、socket.gethostbyname 関数を用いることでドメイン名からIPアドレスを取得できる。以下のプログラムの get_ip 関数を各自完成させて、実際に nslookup と同じように動作することを確認してみよう。

import socket


def get_ip(domain):
    # !!! 各自作成 !!!
    pass

if __name__ == '__main__':
    domain = input('Enter FQDN: ')
    print(f'IP Address: {get_ip(domain)}')

ポート番号#

前述のとおり、ポート番号は通信相手の中のアプリケーションを特定するために用いられる。 16bitの整数値であり、0~65535までの番号がある。 このうち、0~1023はWell Known Ports / System Portsと呼ばれ、対応するアプリケーション層プロトコルがあらかじめ定められている。

表 9.2 主なWell Known Portsの一覧#

ポート番号

アプリケーション層プロトコル

トランスポート層プロトコル

21

FTP (ファイル転送)

TCP

22

SSH (リモートログイン)

TCP

25

SMTP (メール送信)

TCP

53

DNS

UDP

80

HTTP

TCP

110

POP3 (メール受信)

TCP

143

IMAP (メール受信)

TCP

443

HTTPS

TCP

ウェブ閲覧では、多くの場合、ウェブサーバの443番ポートにアクセスしていることになる。 なお、本来、ウェブサイトのURLには https//:<host>:<port>/<path> のような形式でポート番号が付記されている。

しかし、プロトコルのデフォルトポート番号である場合には省略することができ(ブラウザが補完してくれる)、ウェブ閲覧時にはほとんどのケースで省略されている。 試しに、HTTPS接続のウェブサイトURLに、ポート番号443を付け足してブラウザで読み込んでみよう。 問題なく表示されるはずである。

【例】 https://www.hit-u.ac.jp:443/

実際の通信の流れ#

ここまでのおさらいを兼ねて、ウェブアクセス時の通信の流れを考えてみよう。

クライアント側 (HTTPリクエストの送信)

  1. ウェブブラウザ等でURLを指定してアクセス (例: https://www.hit-u.ac.jp)

  2. URLからドメイン名を抽出し、DNSサーバに問い合わせてIPアドレスを取得 (例: 153.127.164.128)

  3. 上記IPアドレス宛にHTTPリクエストを送信

ポイント

クライアントから送信されるHTTPリクエストには、宛先IPアドレスの他に、 宛先ポート番号, 送信元IPアドレス, 送信元ポート番号 が含まれる。

  • 宛先ポート番号: URLで指定されている番号。省略されている場合はデフォルトの番号(HTTPSであれば 443

  • 送信元IPアドレス: サーバがレスポンスを送る際の宛先情報となる (上記の例では 153.127.164.128)

  • 送信元ポート番号: サーバからのレスポンスを受け取るクライアント側のアプリケーション (ブラウザ等) を示す番号 (例: 49152 など)

サーバ側 (HTTPレスポンスの返送)

  1. クライアントからHTTPリクエストメッセージを受信

  2. 宛先ポート番号(443)から、サーバ内の該当するプログラムを特定 (例:Apache, nginx等のウェブサーバ)

  3. 当該プログラムがメッセージを解釈し、HTTPレスポンスを生成

  4. 生成したレスポンスを、クライアント宛に返送

クライアント側 (HTTPレスポンスの受信)

  1. ウェブサーバからHTTPレスポンスメッセージを受信

  2. レスポンスに含まれるコンテンツ(HTMLファイル等)をレンダリングしてブラウザ上に表示

重要な点は、双方向の通信のために

  • 宛先IPアドレス

  • 宛先ポート番号

  • 送信元IPアドレス

  • 送信元ポート番号

の4つの情報が必要ということである。

../../_images/example-http.png

図 9.4 HTTP通信に必要な情報#

9.2. ソケット#

ソケット (Socket) とは、TCP/IP通信をおこなうプログラムを簡単に作成できるようにするために、OSが提供する仕組みである。

TCP/IP通信では、様々な処理が必要であり、例えば、トランスポート層にTCPプロトコルを用いる場合、再送制御(※1)や輻輳制御(※2)を行う必要がある。 このような複雑な処理をあらかじめ実装し、通信プログラムの開発を手軽におこなえるようにしたものがソケットであり多くのOSがソケットを提供している。

  • ※1: 通信パケットが欠落した場合にリカバリーする仕組み

  • ※2: 通信経路が混雑している場合に通信量を抑える仕組み

ソケット通信では、IPアドレスとポート番号を指定することで、 通信相手に対するデータの投げ込み口(インタフェース)が形成 され、これを用いて簡単にTCP/IP通信を行うことができる。

下図のように「自身のIPアドレス」「自身のポート番号」「相手のIPアドレス」「相手のポート番号」の組み合わせごとにひとつのソケットが形成される。

../../_images/socket.png

図 9.5 ソケットの概念図#

なお、ソケット通信においては、その実態に関係なく、接続要求を送り出す側を クライアント 、待ち受ける側を サーバ と呼ぶことが多い。

echoサーバの実装#

ソケットを用いたプログラミングを理解するためにまずは、簡単なソケット通信プログラムを動かしてみよう。

ここでは、クライアントがサーバへテキストメッセージを送り、サーバは受け取ったメッセージをそのままクライアントへ送り返すという通信を考える。このような振る舞いをするサーバを echoサーバ と呼ぶ。

Pythonでは socket モジュールを用いてソケット通信を行うことができる。以下はクライアントのプログラムである。

import socket


def send_echo(host, port):
    # ソケットを生成
    # NOTE:
    # AF_INET: IPv4を用いたソケットでホストのIPアドレスとポート番号を用いる
    # SOCK_STREAM: TCP通信を用いるソケット
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # サーバへ接続(ソケットを形成)
    client.connect((host, port))
    print(f'Connected to {host}:{port}')

    # メッセージを送信
    # NOTE: メッセージはバイト列で送信する必要がある (-> UTF-8でエンコード)
    message = input('Enter message to send: ')
    client.sendall(message.encode('utf-8'))

    # サーバからの応答を受信 (2048バイトまで受信)
    data = client.recv(2048)
    print(f'Received: {data.decode('utf-8')}')

    client.close()
    print('Connection closed')


if __name__ == '__main__':
    # サーバのIPアドレスあるいはドメイン
    host = 'localhost'

    # サーバのポート番号
    port = 50000

    send_echo(host, port)

なお、 localhost は自分自身を表す特殊なホスト名である。 同様に、IPアドレス 127.0.0.1 も自分自身を表す。(localhostを127.0.0.1で置き換えても同じ動作となる) 上記プログラムでは、ひとまず同じコンピュータ内でサーバプログラムとクライアントプログラムを動作させることを想定し、通信相手としてlocalhostを指定している。

クライアント側のポート番号は、ソケット形成時に自動で選択される。 特定のポート番号を使うよう、プログラム内で指定することも可能である。

続いて、以下はサーバ側のプログラムである。

import socket


def start_echo_server(host, port):
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # 指定したIP・ポートで待ち受け(リッスン)開始
    server.bind((host, port))
    server.listen()
    print(f'Server started at {host}:{port}')
    
    # クライアントからの接続を受け付け
    client_socket, client_address = server.accept()
    print(f'Connected by {client_address}')

    # クライアントからのメッセージを受信
    data = client_socket.recv(2048)
    print(f'Received: {data.decode('utf-8')}')

    client_socket.sendall(data)
    client_socket.close()        

    print(f'Disconnected from {client_address}')
    
    server.close()
    print('Server stopped')


if __name__ == '__main__':
    # メッセージを待ち受けるサーバのIPアドレスあるいはドメイン
    host = 'localhost'

    # メッセージを待ち受けるサーバのポート番号
    port = 50000

    start_echo_server(host, port)

では、実際にプログラムを動かしてみよう。ターミナルを2つ立ち上げたら、先に echo_server.py を動かし、その後 echo_client.py を実行すれば良い。

練習5

上記のechoサーバとクライアントのプログラムは、1往復のメッセージで終了してしまうので、連続して複数のメッセージをやりとりできるよう、作り変えてみよう。

双方のプログラムを無限ループとしておき、特定のメッセージ(たとえば「exit」)を入力するとループを抜けるようにすれば良い。

注意

受信待ち状態のプログラムをターミナルから強制終了する場合は、キーボードからCtrl-Cを入力する。 ただし、本章執筆時点において、少なくともWindowsの場合、受信待ち状態ではCtrl-Cによる強制終了ができない場合がある。 以下のように、 settimeout で受信待ちを一定時間ごとにタイムアウトさせる(その後 continue で再び受信待ちする)ようにすれば、Ctrl-Cが効くようになる。

server.settimeout(1)

while True:
    try:
        client_socket, client_address = server.accept()
    except socket.timeout:
        continue

練習6

サーバ側について、あるクライアントとの通信が終了したあとも、次のクライアントからの接続を引き続き待ち受けることができるように、プログラムをさらに改良してみよう。

ソケット通信の状態遷移#

ここで、ソケット通信の動作フローを確認しておこう。以下の 図 9.6 はPythonのsocketモジュールを前提としたフローである。

クライアントは socket(...) でソケットを作成し、 connect() でサーバへ接続要求を行う。

一方、サーバ側は socket(...) でソケットを作成した後、 bind(...) で通信を待ち受けるIPアドレス・ポート番号にソケットを紐づけ、 listen() で待受を開始する。そして、クライアントからの接続要求を accept() で受け付ける。

../../_images/socket-flow.png

図 9.6 ソケット通信の状態遷移#

関数

主な引数

引数の説明

socket()

family

接続先情報の形式。デフォルト値は AF_INET(IPv4)

socket()

type

ソケットの種類。使いたいトランスポート層プロトコルに合わせる。
デフォルト値は SOCK_STREAM(TCP)。
UDPの場合はSOCK_DGRAMを指定

connect()

address

接続先情報(IPアドレス+ポート番号)

bind()

address

接続先情報(IPアドレス+ポート番号)

send()/sendall()

bytes

送信するデータのバイト列

recv()

bufsize

バッファサイズ(一度に受信するデータの最大サイズ)

関数およびその引数の詳細については公式ドキュメントを参照のこと。

HTTP GET#

第3回で学んだHTTPリクエストを、ソケット通信を使って実装してみよう。

ソケット通信を行うためには、少なくとも 通信相手のアドレス (FQDN, IPアドレス等)と ポート番号 が必要である。HTTPリクエストの場合、ポート番号はデフォルトで80と決まっているので、後はアドレスが分かれば良い。

ポート番号の節でも触れたように、URLはいくつかの部品から構成されている。<protocol>//:<host>:<port>/<path>?<query-strings> のようなフォーマットで記載される。

表 9.3 URLの構成要素#

URLの例

プロトコル

ホスト

ポート

パス

クエリ文字列

http://www.example.com/foo/index.html

http

www.example.com

(80)

/foo/index.html

無し

https://www.google.co.jp/search?q=foo

https

www.google.co.jp

(443)

/search

q=foo

URLからホスト部分を取り出せば、ソケット通信の送信先アドレスとして使うことができる。

URLを分解する処理は urllib.parse.urlparse 関数で簡単に行うことができる。

from urllib.parse import urlparse


url = 'http://www.example.com/foo/index.html'
parsed_url = urlparse(url)
host = parsed_url.netloc
path = parsed_url.path

print(f'  Address: {host}')
print(f'File path: {path}')
  Address: www.example.com
File path: /foo/index.html

これでソケット通信を開始するために必要な情報は得ることができた。 後はHTTPリクエストメッセージを作成してウェブサーバに送信すれば、レスポンスとしてHTML等が返ってくるはずである。

HTTP GETメッセージの(ミニマムな)フォーマットは、以下のとおりである。半角スペースの有無に注意してほしい。

GET <パス> HTTP/1.1\r\n
Host: <ホスト>\r\n
\r\n

各行の行末には、改行コードとして CR (Carriage Return) と LF (Line Feed) を入れる必要がある (通常、CRは\r、LFは\n)。また、メッセージの末尾には空行 (\r\n だけの行) を設ける必要があり、この空行によってHTTPメッセージのヘッダとボディの境目を区別する。ただし、GETメッセージには通常はボディが存在しないので、空行で終わる形となる。

HTTPのフォーマットに関するより詳細な情報については、IETFが定めたHTTP/1.1の仕様書(RFC2616)や、各種参考書等を参考にされたい。

練習7

HTTP GETを行う以下のプログラムを完成させて、動作を確認しよう。 message = に続く箇所を、上記のミニマムなフォーマットを参考にして埋めれば良い。

import socket
from urllib.parse import urlparse


def http_get(url):
    parsed_url = urlparse(url)
    host = parsed_url.netloc
    path = parsed_url.path
    if path == '':
        path = '/'

    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect((host, 80))
    client.settimeout(3)

    # 以下のmessageに適当なHTTP GETメッセージを作成する
    message = '!!! 各自作成 !!!'

    client.sendall(message.encode('utf-8'))

    response = b''
    while True:
        try:
            data = client.recv(2048)
        except socket.timeout:
            break

        response += data
        if len(data) < 1:
            break

    client.close()

    return response.decode('utf-8')


if __name__ == '__main__':
    response = http_get('http://www.example.com/')
    print(response)

TCPとUDP#

ここまでのソケット通信プログラムは、いずれも トランスポート層プロトコル として TCP を用いるものであったが、トランスポート層の代表的なプロトコルとしては他に UDP も存在する。以下では、これら2つのプロトコルの違いについて見てみよう。

まず、 TCP はコネクション型のプロトコルであり、通信を開始する前に 接続の確立 を行う必要がある。

すなわち、通信開始時には 3-wayハンドシェイク と呼ばれる仕組みによりまず接続可否を確認し、 通信する2者間でコネクション(セッション)を確立 してからデータのやりとりを行う。

また、ネットワーク上でパケットが紛失(パケットロス)した場合に 再送 を行ったり、通信経路が混雑している場合には 輻輳制御 を、受信者の受信処理が追いつかない場合には フロー制御 を行う。

../../_images/tcp.png

図 9.7 TCPによるデータの送受信#

一方で、 UDP はコネクションレス型であり、接続可否の確認などはなく、 いきなりデータを相手に送信する 。また、TCPのような再送や通信制御などは行われない。そのため、UDPは、映像や音声等、リアルタイム性が求められ、かつ(ごく)一部のデータが欠損しても大きな問題とならない通信で用いられる。

この両者を比較すると、TCPのメリットは 信頼性の高さ であり、UDPのメリットは処理がシンプルであるがゆえの 高速性 であるといえる。

なお、データの欠損が許容されない場合にはTCPを用いることが一般的であるものの、UDPを用いた上で独自の再送制御等を行うQUICなどのプロトコルも存在する。

../../_images/image-text.png

図 9.8 UDPでは一部のデータが欠損する#

以下に、UDPを用いるechoサーバとクライアントのプログラムを示す。

UDPを用いる場合、 socket() 関数呼び出し時にSOCK_STREAMの代わりにSOCK_DGRAMを指定する。 また、UDPはコネクションレス型であるので、connect()listen()は使用せず、データの送受信にも sendto()recvfrom() を用いる点に注意してほしい。

サーバ

import socket


def start_echo_server(host, port):
    server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    server.bind((host, port))
    
    data, addr = server.recvfrom(2048)
    print(f'Received: {data.decode('utf-8')} from {addr}')

    server.sendto(data, addr)
    server.close()


if __name__ == '__main__':
    host = 'localhost'
    port = 50000
    start_echo_server(host, port)

クライアント

import socket


def send_echo(host, port):
    client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

    message = input('Enter message to send: ')
    client.sendto(message.encode('utf-8'), (host, port))

    data, addr = client.recvfrom(2048)
    print(f'Received: {data.decode('utf-8')} from {addr}')

    client.close()


if __name__ == '__main__':
    host = 'localhost'
    port = 50000
    send_echo(host, port)

9.3. マルチスレッド・プログラミング#

もう少し本格的なサーバを作成する前に、ソケット通信と併用される技術として マルチスレッド・プログラミング についても説明しておこう。

マルチスレッドとは、複数の処理を「見かけ上」同時に実行する仕組みである。

通常のプログラムでは、命令文の記述順序に沿って逐次的に処理が実行されるので、当然ながら、前の処理が終わらなければ、次の処理も実行されない。

recv() のように、イベント(ここではメッセージの受信)が発生するまで待つような処理があると、それ以降の処理も待たされることになる。このような処理の中断を ブロッキング と呼ぶ。

サーバのプログラムは、通常、複数のクライアントからの接続を同時に処理する必要があるため、とあるクラアントからのメッセージの受信 (= recv())を待つ間、他のクライアントに対する処理も中断してしまうのは好ましくない。

このようなブロッキングを避けるために、処理の流れを複数に分岐させる仕組みがマルチスレッドである。 スレッドとは、ある一連の処理(コンピュータに対する命令)の流れを指す。

../../_images/multi-threading.png

図 9.9 シングルスレッドとマルチスレッドの比較#

上図は人間にとってわかりやすいイメージであるが、コンピュータの内部ではどうなっているかというと、下図のように CPUの利用枠(CPU時間)を細切れにして各処理に割り当てる ようなことが行われる(時分割処理)。

../../_images/time-division.png

図 9.10 CPU時間の分割によるマルチスレッドの実現#

これにより、処理Aが終わっていなくとも、処理Bが進むことになり、「見かけ上」同時に実行されているように映る。また、CPUのコアが物理的に複数ある場合には、実際に同時に実行されるケースもある。

このようなマルチスレッドを利用したプログラムをPythonで実現するには threading モジュールを用いる。以下はサンプルプログラムである。

このプログラムでは、2つのスレッドを使って、「5秒間隔で整数値をインクリメントしながら表示」する処理を行いつつ、「テキストを入力すると小文字を大文字に変換したものを表示」する処理を行う。また 'exit' と入力するとプログラムを終了する。(タイミングによっては5秒弱待たされる)

import time
import threading


flag = True

def upper_case():
    global flag
    while flag:
        text = input('')
        if text == 'exit':
            flag = False
            break

        print(text.upper())

def count_up():
    count = 0
    while flag:
        print(count)
        count += 1
        time.sleep(5)


thread1 = threading.Thread(target=upper_case)
thread1.start()

thread2 = threading.Thread(target=count_up)
thread2.start()

thread1.join()
thread2.join()

実行例

../../_images/multi-threading-sample.png

PythonとGIL

Pythonには、並列処理を単純化するための仕組みとして、複数スレッドが同時実行されることを避ける仕組み(Global Interpreter Lock)が採用されている。そのため、仮にCPUに複数のコアがあって2つ以上のスレッドを作成できるとしても、その中でPythonのコードを実行できる計算単位は一つに制限されている。

従って、マルチスレッド・プログラミングを行ったとしても、前述の「見かけ上の同時実行」が基本となる。

ただし、Python 3.14以降では、GILを持たない「フリースレッド版」のPythonが導入されており、将来的にはこちらがデフォルトになる予定である。

スレッドとプロセス

マルチスレッドのように複数の処理を同時に実行する仕組みとしては マルチプロセス も存在する。

プロセスというのは、OS上で動作するプログラムの実行単位のことで、各プロセスは独立したメモリ空間を持っている。

一方、スレッドは各プロセス内で動作する処理の単位であり、同一プロセス内の複数のスレッドはメモリ空間を共有している。

一例として、とあるPC上で文書作成ソフトとウェブブラウザを起動している場合、文書作成ソフトとウェブブラウザは、それぞれ独立したプロセスとして動作している。

一方、文書作成ソフトで開いている複数の文書や、Webブラウザで開いている複数のタブは、同一プロセス内の複数のスレッドによって処理されている。

9.4. 練習問題#

TCP通信を用いたチャットサーバを作成し、以下の仕様を満たすように改良しなさい。

  • 複数のクライアントから接続が可能

  • 各クライアントから受信したメッセージは、以下のように処理

    • 初回のメッセージは、そのクライアントのユーザ名として扱う

    • 2回目以降のメッセージは、冒頭に「<ユーザ名>: 」を付け加えた上で、接続している全クライアントに転送(送信元クライアントを含む)

また、以下の仕様を満たすチャットクライアントを実装しなさい。

  • プログラム起動後、ユーザ名を入力

  • サーバに接続し、上記ユーザ名を送信

  • それ以降は、以下の動作

    • テキストを入力するとサーバに送信

    • サーバからテキストを受信したら画面に表示

    • exit と入力したら切断

ヒント

サーバにおいては「複数クライアントからの接続」と「メッセージの送信」を別のスレッドで処理し、クライアントにおいては「メッセージの入力」と「メッセージの表示」を別のスレッド処理すると良い。

../../_images/chat.png

図 9.11 練習問題の完成イメージ#

9.5. 参考文献#

  • 『マスタリングTCP/IP入門編 第6版』井上直也ほか著, オーム社, ISBN: 978-4-274-22447-8

  • 『ポートとソケットがわかればインターネットがわかる』小川晃通著, 技術評論社, ISBN: 978-4-7741-8570-5