-
Notifications
You must be signed in to change notification settings - Fork 983
std.socket API proposal #405
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
I am interested by the feature. Here are some suggestions:
|
90f098c
to
15a2ff3
Compare
Okay here is an update.
os.setsockopt(sock_srv, os.SO_REUSEADDR, new Uint32Array([1]).buffer)
ret = send (sockfd, buf, len?)
ret = sendto (sockfd, buf, len,saddr)
ret = recv (sockfd, buf, len?)
[ret,saddr] = recvfrom(sockfd, buf, len?)
In addition to the given http_server example, what client demo would be nice ? |
Having a socket API for quickjs is great, however it won't be much useful (for me at least) if only blocking functions are available.
And a wrapper for the select() function. |
For now yes, because all Note that existing Adding *Async variant to all
agree, we could either go for:
I have a preference for the last one but I'll let @bellard have the last word. I'll also add a poll({fd:number,events:number}[], timeoutMs=-1): number[] // returned events masks SOCK_NONBLOCK http_client example#!/usr/bin/env qjs
///@ts-check
/// <reference path="../doc/globals.d.ts" />
/// <reference path="../doc/os.d.ts" />
/// <reference path="../doc/std.d.ts" />
import * as os from "os";
import * as std from "std";
/** @template T @param {os.Result<T>} result @returns {T} */
function must(result) {
if (typeof result === "number" && result < 0) throw result;
return /** @type {T} */ (result)
}
/** @param {os.FileDescriptor} fd @param {string[]} lines */
function sendLines(fd, lines) {
const buf = Uint8Array.from(lines.join('\r\n'), c => c.charCodeAt(0));
const written = os.send(fd, buf.buffer, buf.byteLength);
if (written != buf.byteLength) throw `send:${written} : ${std.strerror(-written)}`;
}
const [host = "example.com", port = "80"] = scriptArgs.slice(1);
const ai = os.getaddrinfo(host, port).filter(ai => ai.family == os.AF_INET && ai.port); // TODO too much/invalid result
if (!ai.length) throw `Unable to getaddrinfo(${host}, ${port})`;
const sockfd = must(os.socket(os.AF_INET, os.SOCK_STREAM | os.SOCK_NONBLOCK));
must(os.connect(sockfd, ai[0]) == -std.Error.EINPROGRESS);
must(os.poll([{ fd: sockfd, events: os.POLLOUT }])?.[0] == os.POLLOUT);
sendLines(sockfd, ["GET / HTTP/1.0", `Host: ${host}`, "Connection: close", "", ""]);
const chunk = new Uint8Array(4096);
while (os.poll([{ fd: sockfd, events: os.POLLIN }])?.[0] == os.POLLIN) {
const got = os.recv(sockfd, chunk.buffer, chunk.byteLength);
if (got <= 0) break;
console.log(String.fromCharCode(...chunk));
}
os.close(sockfd); |
Kind of true, but filesystem functions are perfectly usable in a synchronous way, whereas a socket API with only synchronous functions is little more than a toy API. Also look at os.sleepAsync()
Oh, sorry, missed that. Well, then I think it covers all my use cases (together with your proposed SOCK_NONBLOCK). If you can also use os.setReadHandler() after listen() and os.setWriteHandler() after a connect() call, then I think everything is covered. Have you tested that?
I also like SOCK_NONBLOCK fwiw. It seems supported by all the BSDs as well as Linux.
I don't think it's the right approach, because once again your poll() function will be synchronous and block other events. But in any case, this is moot because if SOCK_NONBLOCK can be set to a socket FD and os.setReadHandler() and os.setWriteHandler() can be attached to sockets FDs, then I believe everything can be built from these primitives. A connectAsync() that returns a Promise like sleepAsync() would certainly be nice, but as you said, that can be added later. |
I tested it (both on sync/async socket), It kinda works, but for some reason it also call the handler when there is nothing to recv/send.
Yes, we either
|
If the handler is called when there is "nothing to recv", then I don't understand, unless the socket is in error (if I remember correctly, a socket in error or has reached EOF will be reported as "readable" because a read() call will not block). However, I don't know what you mean when you say the handler is called when there is nothing to send: a write handler should be called when there is room in the socket buffer for a write, in other words when a write() call will not block. so after a successful connect, it is normal that the write handler is called. In the case of an async socket connection, the connect() call will return EINPROGRESS and operate in the background, then you can register the FD for select(write), and the write handler will be notified when the connection is done (or has failed) which is the time a write call to the socket will succeeds.
It probably translate differently for the different functions, but the easiest and most important to start with are probably connect and accept. For connect, I guess we want to be able to write something like:
To do that, you can follow the general sequence described in the first answer here: https://stackoverflow.com/questions/17769964/linux-sockets-non-blocking-connect So:
Does that make sense? |
Similarly, I guess we want to be able to write something like
or
So we need something like:
|
Finally, for the send/recv calls, I think the os.setReadHandler and os.setWriteHandler functionality is enough for real world applications. Or maybe sendAsync() and recvAsync() could be added exactly like accept(): first trying a normal send / recv with the socket in NONBLOCK mode, and repeat in the quickjs event loop as long as EWOULDBLOCK/EAGAIN is received. In any case I think a good high level test that the API is good enough would be to rewrite your two nice http client + server examples in such a way that they work both together in the same quickjs instance (start the server on localhost 8080, then connect to the same port with the client) :) I will stop here but if you want I could try to write one of these functions. Thanks you. |
const chunk = new Uint8Array(16);
os.setReadHandler(sockfd, ()=> {
const ret = os.recv(sockfd, chunk.buffer);
if(!ret) os.setReadHandler(sockfd, null);
else console.log(String.fromCharCode(...chunk.slice(0, ret)));
}) All your JS async acceptAsync(); // no await
await acceptAsync()
I gave you access to my branch if you want to push an eventloop-based accept/listen/connect prototype Since my initial goal was to create an light and portable GUI-based JS webapp (cilent+server) I'll
|
Good questions.
Ok, thank you, I will try to see if I can do something. I hope I won't mess anything with the branch because I'm not very good with git... |
Here some remarks:
|
@yne: if you want, after you provide the nonblocking sockets, I can write the async wrappers in Javascript, let me know. |
I'm now more confident with the internal poll loop of quickjs so I would prefer to offer native async socket API than forcing everybody to wrap with setReadHandler / setWriteHandler. I'm planning on offering this API (os.connect is now async): bind (sockfd: FileDescriptor, addr: SocketAddr ): Result<Success>;
listen (sockfd: FileDescriptor, backlog?: number ): Result<Success>;
shutdown(sockfd: FileDescriptor, type: SocketShutOpt): Result<Success>;
connect (sockfd: FileDescriptor, addr: SocketAddr ): Promise<Result<Success>>;
accept (sockfd: FileDescriptor ): Promise<[remotefd: FileDescriptor, remoteaddr: SocketAddr]>;
recv (sockfd: FileDescriptor, buffer: ArrayBuffer, length?: number ): Promise<Result<number>>;
send (sockfd: FileDescriptor, buffer: ArrayBuffer, length?: number ): Promise<Result<number>>;
recvfrom(sockfd: FileDescriptor, buffer: ArrayBuffer, length?: number ): Promise<[total: Result<number>, from: SocketAddr]>;
sendto (sockfd: FileDescriptor, buffer: ArrayBuffer, length : number, addr: SocketAddr): Promise<Result<number>>;
//with type Result<T> = T | NegativeErrno; The changes to provide a native async API (as I plan it):
NB: each reject()/resolve() also list_del() @ceedriic non-blocking socket (via O_NONBLOCK flag) are available since c087718 if you want to try |
As far as I'm concerned, an API like that would be fantastic.
Great (you also need to poll for write for the connect+EAGAIN case, but I suppose it's your plan)
Well, If you can deliver the above API and it is accepted by the project, I don't think I need to spend any time on a javascript implementation, this will be better 😄. When you've something ready to test, let me know and I'll try your code. The only possible improvement I see (which again can - and should - be done later or in javascript) that would be nice to have would be to provide an optional
Because sometimes you want short reads or short writes, but often (in the case of send for example) you would prefer to have all the bytes written on the sockets buffer before the promise is resolved (same for read if you're implementing a binary protocol and know in advance the exact number of bytes you want to read) So it would simplify user code if you can write something as simple as:
Without having to loop on short writes. |
This PR aim to start a discussion about the futur socket API.
My goal is to use QuickJS as sing portable (via cosmo) graphic app via webapp+CGI server.
I took inspiration from other std syscall wrapper to do this proposal
fd = std.socket(domain=AF_INET, type=SOCK_STREAM, protocol=0)
err = std.bind(sock, {addr:XXX,port:XXX})
err = std.listen(sock)
fd = std.accept(sock)
err = std.connect(sock, {addr:XXX,port:XXX})
I'm confident about the return value API, but I'm open to feedback about:
socket()
currently return a TCP fd,listen()
default to a backlog of 10)SockAddr()
constructor (with .parse() and .toString()) instead of the current plain{address:number,port:number}
object used inbind()
andconnect()
os
related thanstd
SO_REUSEADDR
I added, and add a setsockopt wrapper (but just for this usecase ?)Once agreed, I'll add required unit tests/docs ...