{"id":15788325,"url":"https://github.com/mevdschee/ws2api","last_synced_at":"2025-03-31T18:24:10.066Z","repository":{"id":237328855,"uuid":"794262869","full_name":"mevdschee/ws2api","owner":"mevdschee","description":null,"archived":false,"fork":false,"pushed_at":"2024-05-01T01:32:46.000Z","size":18,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2024-05-02T14:43:13.626Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/mevdschee.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-04-30T19:15:33.000Z","updated_at":"2024-06-19T09:13:45.155Z","dependencies_parsed_at":"2024-04-30T22:32:01.544Z","dependency_job_id":"7b440764-d76d-4db1-83de-d2bd1fcb8dcb","html_url":"https://github.com/mevdschee/ws2api","commit_stats":null,"previous_names":["mevdschee/ws2api"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mevdschee%2Fws2api","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mevdschee%2Fws2api/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mevdschee%2Fws2api/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/mevdschee%2Fws2api/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/mevdschee","download_url":"https://codeload.github.com/mevdschee/ws2api/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":246515988,"owners_count":20790150,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-04T21:41:49.696Z","updated_at":"2025-03-31T18:24:10.042Z","avatar_url":"https://github.com/mevdschee.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# WS to API\n\nProxy messages from Websockets to a RoadRunner PHP API server (see [blog post](https://tqdev.com/2024-scaling-to-1-million-websockets)).\n\n    WS client --[ws upgrade]--\u003e WS server --[http get request]--\u003e API server\n\n    WS client \u003c--[ws connect]-- WS server \u003c--[http response \"ok\"]-- API server\n\n    WS client --[message]--\u003e WS server --[http post request]--\u003e API server\n\n    WS client \u003c--[message]-- WS server \u003c--[http response]-- API server\n\n    WS client --[ws close]--\u003e WS server --[http delete request]--\u003e API server\n\n    WS client \u003c--[ws disconnect]-- WS server \u003c--[http response \"ok\"]-- API server\n\nAnd also:\n\n    API server --[http post request]--\u003e WS server --[message]--\u003e WS client\n\nNote that responses to server-to-client requests are handled as client-to-server\nrequests.\n\nNB: Use HAproxy with Origin header and disabled Keep-Alive to go from WSS to WS.\n\n### Websocket\n\nA WebSocket (WS) can send an HTTP upgrade to the server and after that they can\nsend messages in either direction.\n\n### WS upgrade\n\nA connect from a websocket client may look like this:\n\n    GET /\u003cClientId\u003e HTTP/1.1\n    Host: WS server\n    Upgrade: websocket\n    Connection: Upgrade\n\nThe websocket upgrade is converted to a HTTP request with the following content:\n\n    GET /\u003cClientId\u003e\n    Host: API server\n\nAnd the connection upgrade is made when the response to this message is:\n\n    ok\n\nOther strings are treated as error messages.\n\n### WS to API\n\nThe websocket messages that are received are sent using a HTTP request to the\nAPI server:\n\n    POST /\u003cClientId\u003e\n    Host: API server\n\n    \u003cRequestMessage\u003e\n\nAdn the HTTP request may have a response:\n\n    \u003cResponseMessage\u003e\n\nIf the response is non-empty, then it is sent back on the (right) websocket as a\nmessage in the reverse direction.\n\n### API to WS\n\nA websocket message can be also be sent using a HTTP request to the websocket\nserver:\n\n    POST /\u003cClientId\u003e\n    Host: WS server\n\n    \u003cRequestMessage\u003e\n\nThe response that the WS client may send needs to be filtered from the incomming\nrequest messages.\n\n### Profiling\n\nThe proxy application suppports the standard \"-cpuprofile=\" and \"-memprofile=\"\nflags to create pprof profiles.\n\n### Performance results\n\nThe proxy application was benchmarked to build up and hold 250k connections each\ndoing one message per 10 seconds in 60 seconds (from 0 to 250k connections) ending\nat 25k messages per second within 32GB RAM.\n\n### Scaling\n\nYou can scale the application by load balancing using HAproxy with \"uri\"\nload-balancing algorithm (with depth = 1). This will ensure that messages for\none `\u003cClientId\u003e` will always end up on the same server. On Nginx you need to\nuse:\n\n    hash $request_uri consistent;\n\nin order to ensure that the `\u003cClientId\u003e` will always end up on the same server.\n\n### Tuning\n\nIf you dont't want the parallism to run completely wild you can limit the number\nof HTTP connections from the proxy to the web server using the following \nconfigurable values in the HTTP client's Transport:\n\n    MaxConnsPerHost:     10000, // c10k I guess\n    MaxIdleConnsPerHost: 1000,  // just guessing\n    Timeout:             60 * time.Second,\n\nYou may also have to set the nf_conntrack_max a little higher using:\n\n    sudo sysctl -w net.netfilter.nf_conntrack_max=2621440\n\nNext to that I suggest that you increase the max number of open files using:\n\n    sudo sysctl -w fs.file-max=1073741816\n\nNote that you may also need to change `/etc/security/limits.conf` file. Use\nthe 'ulimit -n' command to check the effective maximum number of open files.\n\nNote that the performance was never tested in Docker containers. Also Docker\nnetworking may have significant overhead. I suggest bare metal when possible.\n\n### Statistics\n\nYou can let Prometheus (or another OpenMetrics compatible) scraper scrape the\nmetrics of the proxy. Amongst other variables it keeps track of are:\n\n- connections_opened\n- connections_closed\n- requests_started\n- requests_failed\n- requests_succeeded\n\nYou can find the number of open connections by calculating: \n\n    active_connections = requests_started - (requests_succeeded + requests_failed)\n\nYou can do this within Grafana using PromQL or another query language.\n\n### Other implementations\n\n- Go with GWS (this repo)\n- PHP with OpenSwoole ([source](https://github.com/mevdschee/ws2api-php))\n- PHP with Swow ([source](https://github.com/mevdschee/ws2api-php-alt))\n- JS with Deno ([source](https://github.com/mevdschee/ws2api-js))\n\nNote that the performance of the non-Go implementations may vary.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmevdschee%2Fws2api","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmevdschee%2Fws2api","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmevdschee%2Fws2api/lists"}