Making connections with TCP and Sockets for Workers

本日、さらに多くのTCP、UDP、QUIC-ベースのプロトコルをCloudflare Workersでサポートするための、APIおよびインフラストラクチャの開発について発表できることを非常に喜ばしく思います。リリース後、これらの新しい機能を使用すると、WorkerまたはDurable Objectに対する非-HTTPソケット接続を、HTTPおよびWebSocketsを使用するのと同じように簡単に使用することができるようになります。

難しい設定をせずに、Cloudflare Workersでは標準fetchおよびWebSocket APIの使用によってHTTPとWebSocket接続を開く機能がサポートされます。Workersで動作させるためにわずかに内部を変更した、1つのを開発しました。これは市販のドライバ(この例では、Deno-ベースのPostgresクライアントドライバ)を使用して、セキュアなCloudflare Tunnel全体でWebSocketを介しリモートPostgresサーバーと通信します。

import { Client } from './driver/postgres/postgres'

export default {
  async fetch(request: Request, env, ctx: ExecutionContext) {
    try {
      const client = new Client({
        user: 'postgres',
        database: 'postgres',
        hostname: 'https://db.example.com',
        password: '',
        port: 5432,
      })
      await client.connect()
      const result = await client.queryArray('SELECT * FROM users WHERE uuid=1;')
      ctx.waitUntil(client.end())
      return new Response(JSON.stringify(result.rows[0]))
    } catch (e) {
      return new Response((e as Error).message)
    }
  },
}

この例は、Deno-固有のTCPソケットAPIを使用するPostgresクライアントドライバのビットを、標準fetceおよびWebSocket APIに置換することによって動作します。次に、Postgresサーバーに隣接して実行されるリモートCloudflare TunnelデーモンとのWebSocket接続を確立し、事実上のTCP-over-WebSocketsを確立しました。

Connecting to a backend data center via a Cloudflare Tunnel

この例を作成して (Cloudflare Workersのランタイムを全く変更せずに) Postgresサーバーと効果的かつ効率的に通信できたという事実は印象的なことですが、そのアプローチには制限があります。1つ目の制限は、そのソリューションでは、WebSocketトンネルを確立して維持するために追加のインフラストラクチャ (この場合は、Postgresサーバーに隣接して実行されるCloudflare Tunnelデーモンのインスタンス) が必要なことです。そのデーモンをお客様に提供できることは確かに喜ばしいことですが、そのコンポーネントを全く必要としないならもっと良いでしょう。2つ目は、自身がTCP上でHTTPを介してトンネリングする、WebSockets上でのTCPのトンネリングは、あまり最適とは言えないことです。動作はするものの、もっと良い方法があります。

Cloudflare Workersから接続する

現在、JavaScriptにはソケット接続のための標準APIがありません。それを変えたいと思います。

Node.jsを使用したことがある方は、net.Socketnet.TLSSocketオブジェクトについて良く知っているでしょう。Denoを使用しているのであれば最近、Deno.connect()Deno.connectTLS() APIが導入されたことについてご存じかも知れません。これらのAPIを見るとすぐにわかりますが、全く同じことを実行しているにも関わらず、それらは互いに大きく異なっています。

Workers内からソケット接続を開いて使用する機能を追加することに決めたとき、私たちの総意として他のプラットフォームで提供されるAPIと異なる、他の非標準のプラットフォーム固有のAPIをそれ以上開発することには全く興味がありませんでした。そのため、開発するランタイムに関係なく動作する、新しい(そして最終的には標準化される) APIと連携するため、ソケット機能を必要とするすべてのJavaScriptランタイムにまで範囲を広げます。

次のものは、単純なTCPクライアント接続から開いて読み取ることを念頭に置いた荒削りの例です。

const socket = new Socket({
  remote: { address: '123.123.123.123', port: 1234 },
})
for await (const chunk of socket.readable)
  console.log(chunk)

または、この例では、UDPを使用して単純な「hello world」パケットを送信します。

const socket = new Socket({
  type: 'udp',
  remote: { address: '123.123.123.123', port: 1234 },
});
const enc = new TextEncoder();
const writer = socket.writable.getWriter();
await writer.write(enc.encode('hello world'));
await writer.close();

APIは一般的に、TLSの有無にかかわらず、TCP、UDP、QUICに対して、クライアントおよびサーバー側の両方で十分動作するように設計されており、単一のJavaScriptランタイム固有のどのメカニズムにも依存しません。それはEventTargetReadableStreamWritableStreamAbortSignalpromisesなど、既存の広くサポートされるWebプラットフォーム標準を基準にしています。既にfetch() API、サービスWorkers、およびasync/awitを使用するpromisesに精通した開発者に、良く知られたものとなるでしょう。

interface Socket : EventTarget {
  constructor(object SocketInit);

  Promise<undefined> update(object SocketInit);

  readonly attribute ReadableStream readable;
  readonly attribute WritableStream writable;
  
  readonly attribute Promise<undefined> ready;
  readonly attribute Promise<undefined> closed;

  Promise<undefined> abort(optional any reason);
  readonly attribute AbortSignal signal;
 
  readonly attribute SocketStats stats;
  readonly attribute SocketInfo info;
}

これは現時点では単なる提案に過ぎず、機能がWorkersで提供される頃には詳細が上記の例から変更される可能性があります。開発者がコードを実行する場所に関係なく構築するための一貫した基盤を持つことができるように、他のプラットフォームがこの新しいAPIの開発とサポートの取り組みに加わることを願っています。

Socket Workersの導入

ソケットクライアント接続の機能についてはまだこのストーリーの中間地点にすぎません。

最初にこれらの機能を追加することについて話し始めたとき、以下のような明確な質問が寄せられました。非HTTPプロトコルを使用したWorkersへの接続はどうですか? Workerをいくつかの他のバックエンドデータベースに接続する機能を持つようにするだけでなく、データベース全体をedge上に実装する場合は、Workers内部に、それに接続する非HTTPクライアントを配置することはできますか? さらに言えば、SMTPサーバーをWorkers内に実装することはできますか? または、MQTTメッセージキューはどうですか? または完全なVoIPプラットフォームはどうですか? またはパケットフィルタ、変換、インスペクタ、またはプロトコルトランスコーダの実装はどうですか?

WorkersはHTTPとWebSocketsだけに留めておくにはあまりにも強力なので、まもなくSocket Workersを導入します。これは、HTTPを使用せず、未加工のTCP、UDP、またはQUICプロトコルを直接使用して接続することができるWorkersです。

この新しいWorkers機能はどのようなものですか?多くの詳細はまだ未定ですが、その発想は現在"fetch"イベントが動作するのとほぼ同じ方法で"connect"イベントを理解し応答するWorkersスクリプトをデプロイすることです。重要なこととして、これは以下のようにクライアント接続に対して開発されたものと同じ共通ソケットAPI上に基づいて構築されます。

addEventListener('connect', (event) => {
  const enc = new TextEncoder();
  const writer = event.socket.writable.getWriter();
  writer.write(enc.encode('Hello World'));
  writer.close();
});

次の手順(および実施要請)

JavaScriptおよびSocket Workers用の新しいソケットAPIの開発は活発に進められており、まずWorkersがバックエンド上のデータベースにさらに効率的な方法で接続できるようにすることに焦点が当てられています — ユーザーはこちらからサインアップしてDatabase ConnectorsとSocket Workersへのアクセスの待機リストに参加することができます。初期からのユーザー、および技術パートナーと共に作業して、これらの新しい機能を開発、修正、およびテストできることを嬉しく思います。

リリース後は、Socket Workersによって、Cloudflareネットワークエッジにデプロイ可能なタイプのインテリジェントな分散アプリケーションの門が広く開かれることを期待しており、あなたがそれを使って作成するものを見ることを楽しみにしています。