ソケットの使用

概要

Curl 言語では、TCP ソケットと UDP ソケットを使用するための API を提供しています。両方のソケットはアドレス/ポート のペアを使用してソケットの位置を独自に識別します。サーバーと交信するクライアントは、サーバーが情報を受けるアドレスとポートを知っている必要があります。Curl のソケット API は与えられたホスト名のアドレスを検索します。
TCP ソケットはクライアントからサーバーに接続し、その後クライアントとサーバーの間に信頼性のある双方向のバイト ストリームを持つことを基本としています。接続されたソケットに使用されるクラスは DataTCPSocket です。クライアントをサーバーに接続するには、DataTCPSocket を作成した後 DataTCPSocket.connect を呼び出すか、あるいは DataTCPSocket.async-connect を呼び出します。接続しようとするサーバーを識別するポート同様に、ホスト名や IP アドレスを指定しなければなりません。これらは remote-port パラメータを DataTCPSocket.default コンストラクタ もしくは接続メソッドの1つに指定するのと同様に、remote-name または remote-address パラメータを使用して指定します。これらの呼び出しの1つが順調に完了すると、DataTCPSocket.to-InputStreamDataTCPSocket.to-OutputStream を呼び出して ByteInputStream または ByteOutputStream を取得し、これらを使用してサーバーと通信することができるようになります。ソケットが不要になったときは、DataTCPSocket.close を呼び出して、ソケットから取得した任意のストリームを閉じてください。
Curl のAPI ではクライアントの TCP ソケット接続にセキュリティ制限を課します。「ネットワーク アクセスの制限」を参照してください。
各オペレーションに対して同期のソケット呼び出しができますが、これはテストのみの使用にしてください。ソケット呼び出しがサーバーの応答を待っている間、アプレットの UI が無反応だからです。このセクションの順を追った説明では、非同期の呼び出しだけを説明しています。これらの呼び出しは、操作が完了、失敗、時間切れあるいはキャンセルされた時にイベント ループから EventHandler を呼び出すイベントを送ります。非同期の操作は、各非同期メソッドから返された AsyncWorkerAsyncWorker.cancel を呼び出すことによってキャンセルすることができます。
IDE のドキュメントでは、TCP および UDP ソケットのための Curl の API を説明する幅広いサンプルを提供しています。サンプルの使用に関する詳細は、「内容の充実したサンプル ファイル」を参照してください。
サンプルは次のファイル内にあります。
d:\automated-build-temp\build\win32-atom\docs\default\examples\dguide\sockets.zip
このアーカイブ ファイルには次のアプレット ファイルが含まれています。
これらのサンプルは特権が与えられたアプレットで実行しなければなりません。次の説明はアプレット ファイル内のコードについて言及しています。

TCP ソケット クライアント

サンプル アプレットの socket-http-client.curl は、同期または非同期のいずれかの TCP ソケット API を使用して Web サーバーに HTTP リクエストを行ってレスポンスを得る方法を示しています。
サンプルはまず、Url から接続しようとする HTTP Web サーバーを識別するホスト名とポートを取得し、その情報を使用して DataTCPSocket を作成します。この動作は SocketHttpRequest クラスの create-socket-and-command-bytes プロシージャで行われます。次のコードは DataTCPSocket を返します。
{DataTCPSocket remote-name = host, remote-port = port}
次のコード フラグメントでは、アプレットが HTTP Web サーバーに接続しようとしています。AsyncConnectSocketEvent イベントのイベント ハンドラを指定します。AsyncConnectSocketEvent は接続に関する情報を含んでいます。AsyncConnectSocketEvent.exception フィールドがその内部に例外を持っていると、接続は失敗し、アプレットはこのリクエストを中止します。AsyncConnectSocketEvent.canceled?true の場合、接続のリクエストはキャンセルされ、アプレットはそのリクエストを中止します。
リクエストが成功すると、アプレットはソケットから出力ストリームを取得し、OutputStream-of.async-write を使用して HTTP リクエストを書き込みます。
{socket.async-connect
    {on e:AsyncConnectSocketEvent do
        {if-non-null ex = e.exception then
            {cleanup-proc}
            {callback ex, null, null, false}
         elseif e.canceled? then
            {cleanup-proc}
            {callback null, null, null, true}
         else
            set output-stream = {socket.to-OutputStream}
            set worker.worker =
                {output-stream.async-write
                    bytes,
                    {on e:AsyncStreamWriteEvent do
                        ...
                    }
                }
        }
    }
}
次のコード フラグメントは、書き込みが完了、失敗、またはキャンセルされた時に呼び出され OutputStream-of.async-write に渡される EventHandler を示しています。書き込みが失敗またはキャンセルされた場合、リクエストを中止します。成功した場合、アプレットはソケットから入力ストリームを取得し、InputStream-of.async-read を使用して HTTP レスポンスを読み込みます。partial? = trueOutputStream-of.async-write に渡すと、OutputStream-of.async-write に渡された EventHandlerByteArray の各パートが書き込まれる度にイベントを受け取り、AsyncStreamWriteEvent.data-writtenAsyncStreamWriteEvent.total-data-written を調べてどのくらいデータが書き込まれたかがわかります。
{output-stream.async-write
    bytes,
    {on e:AsyncStreamWriteEvent do
        {if-non-null ex = e.exception then
            {cleanup-proc}
            {callback ex, null, null, false}
         elseif e.canceled? then
            {cleanup-proc}
            {callback null, null, null, true}
         else
            set input-stream = {socket.to-InputStream}
            set worker.worker =
                {input-stream.async-read
                    {on e:AsyncStreamReadEvent do
                        ...
                    }
                }
        }
    }
}
最後のコード フラグメントは、読み込みが成功、失敗、またはキャンセルされた時に呼び出され、 InputStream-of.async-read に渡されるイベント ハンドラを示しています。アプレットはソケットを閉じ、そのソケットのストリームを閉じ、読み込みデータまたは例外を処理します。
このアプレットはこの接続で単一の HTTP リクエストのみを行い、HTTP リクエストに含まれるヘッダーは HTTP Web サーバーに終了時に接続を閉じるよう指示し、従ってアプレットは、ソケットをクローズするサーバーから EOF を検出するまで読み込みます。接続を継続するクライアントの場合は、バイト数でどのくらいのデータが読み込まれ、n パラメータの中に渡されたかを自分で把握するか、または、partial? = true を設定しデータを受け取るたびにそれを AsyncStreamReadEvent に渡して解析する必要があります。
{on e:AsyncStreamReadEvent do
    {if-non-null ex = e.exception
     then
        {cleanup-proc}
        {callback ex, null, null, false}
     elseif e.canceled? then
        {cleanup-proc}
        {callback null, null, null, true}
     else
        {callback
            null,
            e.data asa ByteArray,
            socket.local-address.address-as-String & ":" & socket.local-port,
            false
        }
        {cleanup-proc}
    }
}

TCP ソケット サーバー

TCP サーバーを作成するには、AcceptorTCPSocket を作成し、その後 AcceptorTCPSocket.bind を呼び出してクライアントからの接続の受信を開始するよう指示します。local-port パラメータを AcceptorTCPSocket.default コンストラクタか AcceptorTCPSocket.bind メソッドのいずれかに指定します。ポートを指定しない場合、AcceptorTCPSocket.bind を呼び出す際にシステムはランダムに受信するポートを選びます。このサーバーに接続するクライアント ソケットはサーバーに接続できるようにするためにポート番号を指定する必要があることに注意してください。その後、AcceptorTCPSocket.accept または AcceptorTCPSocket.async-accept を呼び出して外からの接続を受け入れます。これらのメソッドが成功すると、それらは接続されたクライアント ソケットと同様に使用できる DataTCPSocketになります。受け入れられたソケットが不要になったときは、DataTCPSocket.close を呼び出してソケットから取得した任意のストリームを閉じます。サーバー ソケットが不要になったときは、AcceptorTCPSocket.close を呼び出します。
サンプル アプレットの socket-http-server.curl は、同期または非同期のいずれかの TCP ソケット API を使用して簡単な HTTP 1.0 Web サーバーを実装する方法を示しています。
サンプルではまず、AcceptorTCPSocket を作成します。ローカル ポートなしで作成し、システムがひとつのポートを割り当てられるようにしますが、必要に応じてポートを要求することも可能です。その後 AcceptorTCPSocket.bind を呼び出して、クライアントがどこに接続するかを把握できるようにソケットを特定のローカル ポートにバインドします。また、AcceptorTCPSocket.bind で、ソケットが接続の受信を開始します。
次のコード フラグメントは HttpServer クラスの make-server-socket プロシージャからのものです。
def server-socket =
    {AcceptorTCPSocket
||--            local-port = 80
    }
{server-socket.bind}
{return server-socket,
    server-socket.local-port,
    "http://localhost:" & server-socket.local-port & "/"
}
次のコード フラグメントでは、アプレットが AcceptorTCPSocket.async-accept を呼び出して受信接続を終了させ、その受信接続の DataTCPSocket を取得します。このソケットはクライアントへのデータの読み込みおよび書き込みに使用されます。既定の AcceptorTCPSocket.async-accept パラメータでそれぞれの受信接続がキャンセルされるか、失敗するまで、AsyncAcceptSocketEvent とともに EventHandler の呼び出しを続けます。一度 AsyncAcceptSocketEvent が受け取られると、アプレットは接続の失敗がないか、またはキャンセルされたか、あるいは受け入れられたかどうかをチェックします。AsyncAcceptSocketEvent は受信接続の DataTCPSocket とアドレス、およびポートを持っています。アプレットは入力ストリームを作成し、HTTP リクエストの読み込みを開始します。
{server-socket.async-accept
    {on e:AsyncAcceptSocketEvent do
        {if e.exception != null or e.canceled? then
            {if e.exception != null then
                {status-callback "Failed accepting " & e.exception.message}
            }
            {server-socket.close}
            {return}
        }
        def socket = e.socket
        def remote-id =
            socket.remote-address.address-as-String & ":" &
        socket.remote-port
        {status-callback "Connection from " & remote-id}
        def input-stream =
            {socket.to-InputStream}
        def read-worker =
            {input-stream.async-read
                partial? = true,
                append? = true,
                {on e:AsyncStreamReadEvent do
                    ...
                }
            }
    }
}
サーバーは HTTP リクエストを全て取得するまで読み込みを続けますが、リクエストが届いた時にデータをチェックする以外にそのリクエストのデータ量を知る方法がありません。したがって、InputStream-of.async-read に対する呼び出しでは、データが届く時に partial? = true を渡し、AsyncStreamReadEvent の取得が必要なことを伝え 、append? = true を渡し、データがすべて同じ ByteArray に追加されたことを伝えます。イベント ハンドラは失敗またはキャンセルをチェックします。データを取得できた場合は解析を行い、すべての HTTP リクエストを受け取ったかどうかを確認します。すべての HTTP リクエストを受け取っておらず、 AsyncStreamReadEvent.done?false の場合は、単にそれを返し、次の AsyncStreamReadEvent を待ちます。しかし、すべての HTTP リクエストを受け取っていないが AsyncStreamReadEvent.done?true の場合は、すべての HTTP リクエストを受け取らずに EOF を取得します。すべての HTTP リクエストを取得した場合は、必要なデータをすべて持っているので、(リクエストに対する)応答を準備し、InputStream-of.async-read をキャンセルします。
次のコード フラグメントはこのプロセスを説明しています。
def read-worker =
    {input-stream.async-read
        partial? = true,
        append? = true,
        {on e:AsyncStreamReadEvent do
            {if e.exception != null or e.canceled? then
                {if e.exception != null then
                    {status-callback
                        remote-id & e.exception.message
                    }
                }
                {socket.close}
                {input-stream.close}
                {return}
            }
            {status-callback
                remote-id & " read " &
                (e.data asa ByteArray).size
            }
            def (http-command, request-path, http-version,
                 request-headers) =
                     {HttpServer.parse-request (e.data asa ByteArray)}
            {if request-path != null then
                {read-worker.cancel}
                {status-callback
                    remote-id & " got command " &
                    http-command & " " & request-path & " " & http-version
                }
                def response-data =
                    {HttpServer.make-response
                        http-command,
                        request-path,
                        http-version,
                        request-headers
                    }
                {status-callback
                    remote-id & " writing " &
                    response-data.size
                }
                def output-stream =
                    {socket.to-OutputStream}
                {output-stream.async-write
                    response-data,
                    {on e:AsyncStreamWriteEvent do
                        ...
                    }
                }
             elseif e.done? then
                {status-callback
                    remote-id & " got incomplete command."
                }
            }
        }
    }
次のコード フラグメントはアプレットが、ソケットからの ByteOutputStream に最終的にレスポンスを書き込む方法と、それが完了して EventHandler が呼び出された時に、すべてのストリームとソケットを閉じる方法を示しています。このサーバーが同じクライアント ソケットから、さらにコマンドを読み込み、レスポンスを送る必要がある場合は、ストリームとソケットを閉じず、別の InputStream-of.async-read、または別の OutputStream-of.async-write を開始します。また、InputStream-of.async-read または OutputStream-of.async-write が進行中の間、AcceptorTCPSocket.async-accept はまだアクティブで、さらに受信する接続を受け入れ EventHandler を呼び出します。したがって、このサーバーは待たせることなく同時に多くのクライアントを処理し、サーバーを実行するコンピュータによってのみ制限されます。
def output-stream =
    {socket.to-OutputStream}
{output-stream.async-write
    response-data,
    {on e:AsyncStreamWriteEvent do
        {if e.exception != null then
            {status-callback
                remote-id &
                e.exception.message
            }
         elseif e.canceled? then
        }
        {socket.close}
        {input-stream.close}
        {output-stream.close}
        {status-callback remote-id & " done."}
    }
}

UDP ソケット

UDP ソケットは、ある UDP ソケットから任意の別の UDP ソケットにパケットを送ることができますが、信頼性の保証がなく、パケットのサイズの制限があります。まず、UDPSocket を作成します。local-portUDPSocket.default コンストラクタに指定するか、またはそれを UDPSocket.bind に提供します。ローカル ポートを指定しない場合、システムは UDPSocket.bind 呼び出しの際にランダムに受信するポートを選びます。次に UDPSocket.read-packet または UDPSocket.async-read-packet を呼び出し、受信するパケットを読み込むか、あるいは UDPSocket.write-packet または UDPSocket.async-write-packet を呼び出してパケットの書き込みを開始します。パケットを書き込み、指定された remote-nameremote-portだけが見えるように読み込まれるパケットにフィルタをかけるための既定の送り先として使われる UDPSocket.defaultremote-portremote-name を指定することができますが、UDP ソケットは任意の特定のサーバーやクライアントに接続できるわけではありません。いずれの場合にも、アドレスとポートはパケットが読み込まれる際、常に返されます。ソケットが不要になったときは、UDPSocket.close を呼び出します。
UDP ソケットがパケットの書き込みまたは読み込みをするにはセキュリティ上の制限があります。「ネットワークのアクセス制限」を参照してください。
サンプル アプレットの udp-server.curl では、同期および非同期の両方の API を使用してパケットを読み込むサーバーを実装する方法を示し、パケット内のものをすべて大文字に変え、送信元に送り返します。udp-client.curl と呼ばれるクライアントのサンプルもありますが、これはサーバーにパケットを送りレスポンスを待つと言うこと以外は、サーバーのサンプルと似ています。
次のコード フラグメントは udp-server.curl からのものです。最初のフラグメントが示すように、サンプルはまず、UDPSocket を作成します。ローカル ポートなしに作成し、システムがそれを割り当てられるようにしますが、必要に応じてポートをリクエストすることもできます。その後 UDPSocket.bind を呼び出して、クライアントにパケットの送り先を指示できるようにあとで返すことができる、特定のローカル ポートにソケットをバインドします。また、UDPSocket.bind はソケットに対しパケットの受信を開始させます。UDP ソケットは基本的に送られてくるパケットを待つ必要がないので、UDPSocket.bind をスキップしパケットの書き込みを開始することができます。
def socket = {UDPSocket}
{socket.bind}
次のフラグメントはUDPSocket.async-read-packet を呼び出すことで、サンプルがどこか別の場所から届くパケットを待つ方法を示しています。既定の UDPSocket.async-read-packet パラメータは AsyncReadPacketSocketEvent とともにパケットの受信のたびにそれがキャンセル、または失敗するまで EventHandler を呼び出し続けます。一度 AsyncReadPacketSocketEvent が受け取られると、コードは失敗がないか、キャンセルされていないかをチェックし、失敗もキャンセルもない場合、パケット データに対して必要な動作をし、パケットの送信元のアドレスとポートにレスポンスを送ります。これはパケット、アドレスおよびポートを送る準備ができた配列に追加することによって行われ、その後、その配列からのパケットが送信中であることを確認するコードを呼び出します。
def read-worker =
    {socket.async-read-packet
        {on e:AsyncReadPacketSocketEvent do
            {if e.exception != null or e.canceled? then
                {socket.close}
             else
                def packet = {non-null e.packet-data}
                {UDPServer.upcase-packet packet}

                {packet-array.append packet}
                {address-array.append {non-null e.remote-address}}
                {port-array.append e.remote-port}
                {status
                    e.remote-address.address-as-String & ":" &
                    e.remote-port & " read " & packet.size & " bytes"
                }
                {write-proc}
            }
        }
    }
最後は、送信準備のできたパケットの1つの書き込みを開始する必要があるかどうかをみるコードを示します。既に送られたものがない場合、送る必要のあるパケット、アドレス、ポートを指定して UDPSocket.async-write-packet を呼び出します。書き込みが完了、失敗、またはキャンセルされたときは、次の書き込みを再開するか、キャンセルされたエラーがあれば、終了します。この書き込みが進行中に、追加の受信パケットも UDPSocket.async-read-packet 呼び出しを続けて書き込んだり、処理されたり、送信される応答パケットのリストに加えたりします。UDP ソケットでは、パケットは様々なネットワーク レイヤによってドロップされたり、送信先に届かない場合があるので注意してください。
def write-proc =
    {proc {}:void
        {if packet-array.size > 0 and socket.open? and
            write-worker == null
         then
            def packet-size = packet-array[0].size
            def remote-address = address-array[0]
            def remote-port = port-array[0]
            set write-worker =
                {socket.async-write-packet
                    packet-array[0],
                    remote-address = remote-address,
                    remote-port = remote-port,
                    {on e:AsyncWritePacketSocketEvent do
                        {status
                            remote-address.address-as-String & ":" &
                            remote-port & " wrote " & packet-size & " bytes"
                        }
                        set write-worker = null
                        {if e.exception != null or e.canceled? then
                            {socket.close}
                         else
                            {write-proc}
                        }
                    }
                }
            {packet-array.remove 0}
            {address-array.remove 0}
            {port-array.remove 0}
        }
    }

IPv6 サポート

バージョン8.0のAPIから、ソケット、HTTP、およびセキュリティ システムに対して IPv6 のサポートが追加されました。以前のAPIは今までと同様に動作しますが、それらに加えほとんどのコールが IPv6 アドレスを受け付けるようになりました。オペレーティング システムが IPv6 をサポートしている場合は、IPv6 アドレスはオペレーティング システムから String または Url の形式での受け渡しが可能になっています。IPv6 をアプリケーションで使用する場合は新しいバージョンの API が必要になりますが、ホスト名やURLを受け渡しするだけのアプリケーションであれば特別な作業は必要ありません。
8.0 の API は SocketInetAddress クラスを拡張し、新規のメソッドとゲッターを追加しています。それらを使うことにより、IPv4 と IPv6 のアドレスを ホスト オペレーティング システム がサポートしているアドレス ファミリに応じて利用することができます。また、 DataTCPSocket.start-connectUDPSocket.write-packet はアドレスをどのアドレスファミリでも動作するように変換しようと試みます。ただし、オペレーティング システムがサポートしていないアドレスが渡された場合はその他のコールは失敗します。
ソケット API は下記のフォーマットで IPv6 アドレスを受け付けます。 また Url API は '[' ']' でアドレスが括られているフォーマットを受け付けます。
ホスト名は数値のアドレスよりも汎用的であり、アプリケーションがホスト名もしくは文字列のみをアドレスに使用している場合は特別な修正を加えることなく IPv4/IPv6 どちらのプロトコルでもサーバ/クライアントで動作いたします。(ただし、適切なアドレス ファミリを両方がサポートしており、その設定が適切である必要があります。)