Coder Social home page Coder Social logo

sockets-api's Introduction

sockets-api

This is an initial draft intended to spark a conversation about what the actual API should ultimately be. Nothing here is set in stone. Open issues to discuss the details.

What is the goal here?

There is currently no common API for sockets (client or server) in JavaScript environments. Node.js has an API (actually, it has several), Deno has it's own API, there have been several attempts at defining a raw sockets API for browsers that really haven't gone anywhere. For Cloudflare Workers, we'd like to enable a raw sockets API but we don't want to invent Yet Another One that is incompatible with everyone else's.

Node.js' API is by far the most extensively used. We could choose to just implement that but that brings with it a tremendous amount of complexity. The Node.js API is built directly on Node.js' EventEmitter and on Node.js Streams. The API surface area is very large (EventEmitter, stream.Readable, stream.Writable, stream.Duplex, net.Socket, net.Server, tls.TLSSocket, tls.TLSServer, tls.SecureContext, a vast multitude of options -- some that we're not even sure people actually use). The current API design and implementation makes it fairly slow in comparison to raw sockets in other environments, with inefficient buffering and data flow prioritization. The existing Node.js API also ignores modern idiomatic JavaScript patterns and common APIs that exist across multiple JavaScript environments (e.g. promises, web streams, cryptokey, etc).

Other environments, such as Deno, have opted for a much more constrained approach with significantly fewer options and a greater focus on web platform API compatibility. But that API is currently designed around the Deno specific namespace.

The key goal here is to design a raw sockets API that is not specific to any single JavaScript environment, that can be implemented consistently across multiple environments, giving developers a single API surface area that will just work.

The API needs to be:

  1. Performant. It must allow for performance optimizations that improve significantly on the current state of the art in the various environments.
  2. Idiomatic. It must follow common, modern JavaScript idioms and familiar APIs that are available across multiple JavaScript runtimes.
  3. Simple. It must limit the API surface area and options to a minimal set, allowing expansion as needs progress, but focusing on immediately known requirements.
  4. Modern. It must account for newer protocols such as QUIC, and newer paradigms such as server-initiated uni and bi-directional streams.

A Conversation Starter

The API description below is intended as a conversation starter. It's meant to solicit opinions. If your knee jerk reaction is "Oh my god, what is this crap?", open an issue and express your concerns and alternative suggestions. Please don't forget the alternative suggestions as those are the only bits that are truly useful.

interface Socket : EventTarget {
  constructor(object SocketInit);

  // Provides the ability to update the socket init after it has
  // been established. This can be used, for instance, to modify
  // the timeouts, upgrade a plaintext TCP connection to TLS, or
  // to trigger a TLS renegotiation, etc.
  // The Promise is resolved once the update is considered to be
  // accepted, or rejected if the update cannot be accepted for
  // any reason.
  Promise<undefined> update(object SocketInit);

  readonly attribute ReadableStream readable;
  readonly attribute WritableStream writable;

  // Promise that is resolved when the socket connection has been
  // established. This will be dependent on the type of socket
  // and the underlying protocol. For instance, for a TLS socket,
  // ready would indicate when the TLS handshake has been completed
  // for this half of the connection.
  readonly attribute Promise<undefined> ready;
  
  // Promise that is resolved when the socket connection has closed
  // and is no longer usable.
  readonly attribute Promise<undefined> closed;

  // Request immediate and abrupt termination of the socket connection.
  // Queued inbound and outbound data will be dropped and both the
  // readable and writable sides will be transitioned into an errored
  // state. The socket's AbortSignal will also signal an abort event
  Promise<undefined> abort(optional any reason);
  readonly attribute AbortSignal signal;

  readonly attribute SocketStats stats;
  readonly attribute SocketInfo info;
}

enum SocketType{ "tcp", "udp", "tls", "quic" }

typedef USVString ALPN;
typedef (SocketType or ALPN) Type;

// As an alternative to getting a ReadableStream, a socket
// consumer can attach a SocketDataEvent that will receive
// chunks of data as they arrive with no backpressure
// or queuing applied.
interface SocketDataEvent : Event {
  readonly attributes sequence[ArrayBuffer] buffers;
}

dictionary SocketAddress {
  USVString address;
  unsigned short port;
}
 
dictionary SocketInit {
  Type type = "tcp";
  SocketAddress remote;
  SocketAddress local;

  // Optionally allows an outbound payload source to be specified when the socket
  // is constructed. Provides an alternative to using socket.writable to allow the
  // flow of data to start as soon as possible.
  SocketBody | Promise<SocketBody> body;
  
  // A signal that can be used to cancel/abort the socket.
  AbortSignal signal;

  bool allowPooling = true;
  bool noDelay = false;
  
  // Amount of time, in milliseconds, to wait for connection ready
  unsigned long readyTimeout = 0;
  
  // Amount of time, in milliseconds, a connection is allowed to remain idle
  unsigned long idleTimeout = 0;
  
  unsigned long sendBufferSize;
  unsigned long receiveBufferSize;
  unsigned short ttl;
}

typedef (string | ArrayBuffer | ArrayBufferView | ReadableStream | Blob ) SocketBody

SocketInit includes TLSSocketInit;

dictionary TLSSocketInit {
  USVString servername;
  CryptoKey key;

  // It would be great if there were a common object API for certificates like
  // there is for keys... If string is used for cert or ca, then it must be
  // in PEM format.
  ArrayBufferView or USVString cert;
  ArrayBufferView[] or USVString[] ca;
  
  // The init object is extensible such that additional properties for TLS
  // extensions can be added. E.g. a session id extension may look like:
  //
  // {
  //   servername: 'foo',
  //   session: {
  //     sessionIDContext: 'abc',
  //     session: new Uint8Array(...),
  //     ticket: new Uint8Array(...)
  //   }
  // }
  //
  // These extensions would need to be defined separately.
}

[Exposed=(Window,Worker)]
interface SocketStats {
  unsigned long bytesSent;
  unsigned long bytesReceived;
  unsigned long packetsSent;
  unsigned long packetsReceived;
}
 
[Exposed=(Window,Worker)]
interface SocketInfo {
  readonly attribute Type type;
  readonly attribute SocketAddress remote;
  readonly attribute SocketAddress local;
}
 
SocketInfo include TLSSocketInfo;
 
interface TLSSocketInfo {
  readonly attribute USVString servername;
  readonly attribute Uint8Array certificate;
  readonly attribute Uint8Array peerCertificate;
  readonly attribute USVString verificationStatus;
  readonly attribute CipherInfo cipher;
  readonly attribute EphemeralKeyInfo ephemeralKey;
  readonly attribute Uint8Array session;
  readonly attribute Uint8Array ticket;
  readonly attribute sequence<USVString> signatureAlgorithms;
}

dictionary CipherInfo {
  USVString name;
  USVString standardName;
  USVString version;
}
 
dictionary EphemeralKeyInfo {
  USVString type;
  USVstring name;
  unsigned short size;
}

// Listening-side

interface SocketListener : EventTarget {
  constructor(object SocketListenerInit);

  async iterable<OnSocketEvent>();
}

dictionary SocketListenerInit {
  Type type = "tcp";
  SocketAddress local;
  AbortSignal signal;
  // TBD
}

SocketListenerInit include TLSSocketListenerInit;

dictionary TLSSocketListenerInit {
  // TBD
}

interface SocketEvent : Event {
  readonly attribute Socket socket;
}

Examples

// Minimal TCP client
const socket = new Socket({
  remote: { address: '123.123.123.123', port: 123 },
  // Body can be string, ArrayBuffer, ArrayBufferView, Blob, ReadableStream, a Promise
  // that resolves any of these, or a sync or async Function that returns any of these. 
  body: 'hello world'
});

for await (const chunk of socket.stream())
  console.log(chunk);
// Minimal TCP client
const socket = new Socket({
  remote: { address: '123.123.123.123', port: 123 },
});

const writer = socket.writable.getWriter();
const enc = new TextEncoder();
await writer.write(enc.encode('hello world'));

for await (const chunk of socket.stream())
  console.log(chunk);
// Minimal echo server
const socketListener = new SocketListener({ local: '123.123.123.123', port: 123 });
for await (const { socket } of socketListener) {
  socket.readable.pipeTo(socket.writable);
}

// or...

const socketListener = new SocketListener({ local: '123.123.123.123', port: 123 });
socketListener.addEventListener('socket', { socket } => {
  socket.readable.pipeTo(socket.writable);
});
// Minimal UDP client
const socket = new Socket({
  type: 'udp',
  remote: { address: '123.123.123.123', port: 123 },  ttl: 20,
});

const enc = new TextEncoder();
const writer = socket.writable.getWriter();
writer.write(enc.encode('hello')); // send a datagram packet with 'hello'
writer.write(enc.encode('there')); // send a datagram packet with 'there'
writer.close();

sockets-api's People

Contributors

jasnell avatar bmeck avatar

Stargazers

Momen avatar xiaoke avatar Stephen Belanger avatar Yuki Tanaka avatar azu avatar Daiki Ihara avatar Jeff Moore avatar Robert Kawecki avatar Yuji Sugiura avatar Anthony McClosky avatar Nikita avatar Brendan Irvine-Broque avatar Andrew Chou avatar Yoshiya Hinosawa avatar Thomas Hunter II avatar Vic avatar Andrejs Agejevs avatar Whien_Liou avatar Ido Cohen avatar Benjamin Gruenbaum avatar S.A.N avatar Divy Srivastava avatar Luca Casonato avatar

Watchers

Endel Dreyer avatar  avatar James Cloos avatar Udit Vasu avatar Ido Cohen avatar Luca Casonato avatar Bartek Iwańczuk avatar Momen avatar S.A.N avatar Satya Rohith avatar  avatar Jeff Moore avatar

Forkers

bmeck

sockets-api's Issues

setting timeouts

for sockets left open for connection costs it would be good to be able to reset the timeout to a long duration when inactive and short duration when active. I think leaving it up to the user to change this duration is fine. also, this is likely a non-issue for most use cases so leaving it out is also fine if it adds complexity/perf issues for the common case.

Use `CryptoKey` for TLS keys

Currently this proposal uses a plain Uint8Array for TLS certificates and keys. This means that this spec needs to define how the keys are interpreted and parsed. There is already a different part of the web platform that specifies parsing of crypto keys: WebCrypto. What if we use the CryptoKey object from WebCrypto to represent keys?

These support RSA and ECDSA keys, which is exactly what we would need.

Go all in on WHATWG streams

Instead of providing a body on creation, writing to the socket should happen via a WritableStream. This would be achieved by implementing the GenericTransformStream mixin for Socket.

Why? GenericTransformStream is the new standard browser primitive for streaming APIs. It is used by many shipping and in progress APIs. It would be good to align.

Using WHATWG streams provide some nice benefits that some other options do not:

  • Backpressure: an event based API does not provide backpressure
  • Externally writable: an async iterator API based writer can not be written externally written to. A WritableStream has a .write method, but an async iterator can only be created by manually implementing the async iteration protocol or using a generator, both of which don't make it easy to create an iterator, and then write to it.
  • Easy piping and interop with existing APIs:
    outgoingChunks
       .pipeThrough(new TextEncoderStream())
       .pipeThrough(new CompressionStream("gzip"))
       .pipeTo(socket.writable);
    
    const incomingChunks = socket.readable
       .pipeThrough(new DecompressionStream("gzip"))
       .pipeThrough(new TextDecoderStream());

Once concern that will be brought up is that WHATWG streams are slower than some alternatives. While this can be the case, in general this is not usually a bottleneck: the Deno HTTP server builds entirely on the fetch API that uses ReadableStream and WritableStream, but still has a 5-10% lead over the Node HTTP server that builds on events. If it is decided that this is really an issue, we could always subclass ReadableStream and WritableStream and add read(buf: Uint8Array): Promise<number> and write(buf: Uint8Array): Promise<number> methods respectively for low level reading and writing. This would still support backpressure.

I'd say the benefits outweigh the downsides for using WHATWG streams.

Roadmap

Hello.
Is there an approximate timeframe for when the implementation of this interface will appear in the Node.js?

transferrability and abort

i need to think on this a bit, but if we transfer the read/writable streams to another thread it seems like we couldn't easily abort the socket from that thread?

copy/transfer SocketListener

this likely is niche and not needed but it seems like if SocketListener is able to be copied between threads that handling initial marshalling of the connection/data could be shared.

Instead of providing a `ready` promise, use an async factory function

Currently a Socket instance can be in one of three states:

  • connecting: in this case ready and closed are both pending
  • connected: in this case ready is resolved, closed is pending
  • closed: in this case ready and closed are both resolved

The API design could become a lot more straightforward if instead of providing this ready promise on the class, the constructor of the class itself would provide the functionality of the ready promise. This would also allow us to change stream from being a function that returns a Promise<ReadableStream>, to a simple readable property of type ReadableStream.

This works because a socket is always in the "connected" state initially:

  • if the connection fails, an instance of the Socket interface is never created, because the factory function rejects
  • if the connection does not fail, that means it connected
[Exposed=Window,Worker]
interface Socket {
  // Note: this interface has no constructor

  // `open` rejects if the connection can not be established. If it succeeds, the connection was successfully established.
  static Promise<Socket> open(SocketOptions options);
 
  // `stream()` gets removed - `readable` replaces it.
  readonly attribute ReadableStream readable;

  // imagine the other properties being here
};

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.