Add Websocket Support in FrankenPHP
Describe your feature request
Is your feature request related to a problem? Please describe. The problem is that FrankenPHP does not provide a built-in solution for WebSockets. Mercure (SSE) is not full-duplex, so it cannot fully replace WebSockets for real-time, bidirectional communication.
Describe the solution you'd like A native WebSocket implementation for FrankenPHP, ideally leveraging the new ability to create Go extensions. Similar to how it can be done in Go (as described here: Implementing WebSockets in Golang ), such an integration would be very powerful. By relying on Go’s goroutines, FrankenPHP could provide a highly performant WebSocket server, seamlessly integrable with Symfony, without requiring additional frameworks like Swoole or ReactPHP.
Describe alternatives you've considered
Using Mercure (SSE), but it is not full-duplex and does not provide the same capabilities as WebSockets.
Relying on external solutions like Swoole or ReactPHP, but these add complexity and external dependencies instead of leveraging FrankenPHP’s built-in Go extension support.
WebSockets are pretty hard to scale, while SSE is much safer and easier to scale horizontally. The industry has been moving away from using WebSockets as much as possible.
The "duplex" problem is solved by using APIs to send and waiting for a response via SSE (or inline via the API response).
Technically, it should be possible to create a websockets extension the same way I created the gRPC extension.
The industry has been moving away from using WebSockets as much as possible
Supporting evidence?
I agree that in the future we will probably use WebTransport, but for now that’s not the case. The reason I made this request is that it seemed to me an extension could be implemented without reinventing the wheel via GoLang.
@withinboredom: WebSockets still have a few advantages that SSE+POST doesn’t provide:
Latency (even if SSE uses HTTP/2 when it can, that’s not always the case).
Binary format. It’s impossible to transmit gRPC or any other binary protocol through SSE.
I think we shouldn’t look at these technologies in a binary way, but rather acknowledge that each solution has its pros and cons—and it’s a pity that FrankenPHP doesn’t offer them “out of the box.”
and it’s a pity that FrankenPHP doesn’t offer them “out of the box.”
I don't see in what world it would be the webservers job to spin up a websocket server, given that nothing would be able to work with it out of the box. Working with bidirectional connections in php is barely even compatible with the "php way", where we have an external webserver running our code, rather than our code spinning up a webserver.
If you're interested in the latter, there's php-swoole with full support for websockets.
Maybe I missed something. I don’t know, Apache and Nginx can handle WebSockets, right? They are web servers. FrankenPHP replaces the whole stack, hence my question. Of course, we can spin up a Swoole or even a ReactPHP server, that’s not the point.
Even if I look at other stacks in other languages… Node.js for example, we all agree that the Node process can handle both WebSockets and serving HTML pages, right? Again, maybe I’m mistaken.
If I follow that logic, Mercure wouldn’t make much sense either, since we can very well do SSE through Swoole. :)
It seems fairly clear that the issue here is not whether it would be good/beneficial to implement a websockets solution in frankenphp (I don't think it should be done).
The real issue is that you're trying to convince volunteers to take on a task that they've expressed they aren't interested in, all while there's viable alternatives out there already (eg swoole)
If you absolutely want websockets in frankenphp, there's nothing stopping you from implementing it yourself with a caddy module (which is what Mercure is) or golang php extension, and contributing it back to the project. Both could leverage existing go websockets libraries - I was looking into this recently and https://github.com/lxzan/gws seems to be particularly impressive. I'm sure the maintainers here would be happy to point you in the right direction if this is something you'd like to do.
Maybe I missed something. I don’t know, Apache and Nginx can handle WebSockets, right? They are web servers. FrankenPHP replaces the whole stack, hence my question. Of course, we can spin up a Swoole or even a ReactPHP server, that’s not the point.
Caddy supports websocket proxies the same way that Apache does with mod_proxy_wstunnel or Nginx does out of the box. But they do not spin up a websocket server that lets you handle events in your application code - because your application code is simply not running at the time of receiving something.
Even if I look at other stacks in other languages… Node.js for example, we all agree that the Node process can handle both WebSockets and serving HTML pages, right? Again, maybe I’m mistaken.
Because every ever language moved from CGI to hosting the http server in your application code. PHP is the only language that I can think of that still defaults to (Fast-)CGI and where the only way to spin up a proper server in your application code is through the swoole extension.
If I follow that logic, Mercure wouldn’t make much sense either, since we can very well do SSE through Swoole. :)
SSE are not the equivalent of WebSockets. SSE are highly compatible with the "php way", since they're a unidirectional update mechanism from the server to the clients. WebSockets are a bidirectional communication typically used to communicate from the client to the server and back. Again, with the typical php way, there is no "running server" (read: your application) during the time a client may send data.
Of course you can also use WebSockets to replace SSE, but... why would you?
Websocket servers typically run inside an event loop. So it would be easier to support them once PHP has async built natively into the language (probably PHP 8.6+ or 9+). IMO SSE is probably preferable in most typical PHP use cases because of its simplicity.Some applications need the bidirectional protocol though, like a low-latency multiplayer game.
Some applications need the bidirectional protocol though, like a low-latency multiplayer game.
Let's pray that no stray soul gets the idea to develop one in PHP...
Let's pray that no stray soul gets the idea to develop one in PHP...
https://github.com/solcloud/Counter-Strike
I've built a google docs style live collaboration app where every keystroke get's sent to the server and then distributed to other connected clients. Currently I am distributing the live data via laravel reverb and every keystroke is a post request. i have been wondering should I consider moving to a duplex websocket system for this realtime stuff. It seems like sending keystrokes down a websocket would save a lot of resources vs a whole post request, but I'm not sure tbh. I've read some strong opinions here about not using duplex websockets and I'd love to hear more from you all about my specific use case. If you were building something like that what would you recommend? (lol but don't say node.js as I am committed to the php ecosystem ;) )
I've read some strong opinions here about not using duplex websockets and I'd love to hear more from you all about my specific use case.
Your specific use case makes a lot of sense. We don't discourage from using WebSockets in general, we (I) discourage from using them as a replacement of server sent events, which are simpler, less maintenance and automatically scale.
When you need (real-time) bidirectional communication, WebSockets are the thing to use, but for web specific scenarios, which is mostly "notify users when another user edited a resource", in which FrankenPHP would be used, SSE or long polling would do much better.
I've built a google docs style live collaboration app where every keystroke get's sent to the server and then distributed to other connected clients. Currently I am distributing the live data via laravel reverb and every keystroke is a post request. i have been wondering should I consider moving to a duplex websocket system for this realtime stuff. It seems like sending keystrokes down a websocket would save a lot of resources vs a whole post request, but I'm not sure tbh. I've read some strong opinions here about not using duplex websockets and I'd love to hear more from you all about my specific use case. If you were building something like that what would you recommend? (lol but don't say node.js as I am committed to the php ecosystem ;) )
It mainly depends on the client. If HTTP/2 is supported, you’ll gain very little in terms of performance (some improvements on the server side though).
If you want to squeeze out the maximum latency reduction, you need to switch layers — instead of TCP, use UDP. That can be achieved via WebRTC (and yes, it can be used client ↔ server, not just client ↔ client).
Otherwise, you’ll have to wait for WebTransport, which will run over UDP.
If anyone is concerned about performance with sse, it is worth checking out Datastar which is a newish framework focused on realtime, interactive hypermedia applications.
They've done a bunch of demos with interactive multi-player games/applications with extreme performance. It's htmx + alpine + much more, and smaller and simpler.
A handful of links, but there's plenty more. https://youtu.be/0K71AyAF6E4?si=ZEYQX4DVjjTmaNRI https://youtu.be/HbTFlUqELVc?si=4yfL7Y9WHzLXla65 https://data-star.dev/examples/bad_apple https://news.ycombinator.com/item?id=43971164
My point isn't necessarily that you should use datastar (though it's worth looking into), but just that sse is not your bottleneck for most applications
Hi everyone,
I’ve built a functional proof of concept of a WebSocket server for FrankenPHP, based on the existing gRPC extension.
In this PoC, I’ve implemented and exposed two PHP functions:
frankenphp_ws_getClients() – to retrieve the list of currently connected clients frankenphp_ws_send() – to send a message to a specific connected client
Both functions can be called directly from Frankenphp/Caddy. It would also be great to make them callable from a CLI script, but since it would run in a different Caddy instance, I think we’d need to expose dedicated endpoints for that.
@dunglas — would you have any idea how the worker could respond directly on Caddy’s 443/80 ports? (and not the port 5000) (I imagine Caddy can proxy the request on a internal port?)
Here’s the PoC repository: 👉 https://github.com/davidnurdin/devFrankenPhpWebsocket
You could register a caddy directive like FrankenPHP registers the php and php_server.
@davidnurdin awesome!
Here is an example of what @henderkes suggests: https://github.com/dunglas/frankenphp-grpc/pull/6
Damn , thanks u guys it working ! @henderkes @dunglas !!!
The last thing i would do is that php cli can talk with the caddy instance, do you think it is possible ? shm ? internal api caddy ? The problem is that php cli is not the same instance of caddy/franken web server
Really THANKS :) (i've update my git)
Eventually, when we get this merged: https://github.com/php/frankenphp/pull/1757
That will turn frankenphp php-cli into a complete php cli replacement but with frankenphp extensions.
Another way I could imagine would be to use the admin api to broadcast these endpoints. Then even a regular php binary could get the clients and receive/send stuff.
I've do like you tell me => i've added an endpoint in admin and it working like a charm.
I don't have any auth between cli <> admin url, All client have access to this uri ? There is a mecanism auth in caddy to the uri admin ?
Thanks
You can bind the admin API to a permissioned unix socket, or enforce an origin header, other than that I don't think so. It's by default only available on localhost, do you necessarily need auth there?
Looks like you can also secure it with TLS limited to certain public keys, meaning the client would nedd to hold the private keys to authenticate: https://caddyserver.com/docs/json/admin/remote/
I've see that private/public key is experimental, and can be changed, so u right, maybe it's not necessarily while it listen on 127.0.0.1 Thank you !
I've add the tag support (like the topics in mercure). We can now tag a client (or untag) and send a packet to a group of websockets