WebTCP: Making TCP connections from Browser

The problem

There is no simple way to create TCP sockets in Javascript on a browser side. Although solutions like Websockets allow to create something that resemble sockets, you can use them to connect only to servers that support Websockets. Not to any random servers that know nothing about HTTP.

Why bother

If creating such connections were possible, then we could connect to any external server from browser and keep all logic in client-side Javascript without needing to implement a backend app.

For instance, it would be possible to create (or just port them from node.js) client libraries for things like: Memcache, Redis, MySQL, Riak, RabbitMQ or any other server.

While in many situations such usage would be questionable and insecure, there are cases when it could be quite useful:

  • using server-side cache from JS
  • using pub/sub servers to deliver notifications to browsers. (Redis, RabbitMQ, Apache Kafka, etc)
  • making HTTP request to any server bypassing same-origin policies :|

Solution

As an experiment I implemented this small library: WebTCP. Here is how it works.

It is impossible to make browser to initiate raw TCP connections to a server, but it is possible to use some proxy (or a “bridge” would be a better name) that will receive connection requests from the browser, create real sockets and then redirect responses back to the browser. So on a client side we will have fake socket objects that will be mapped to real socket connections on a proxy side.

How client connects to the bridge

This is when Websockets or something like Socket.IO or SockJS comes in handy. Client can use Websockets (or fall back to xhr/jsonp-polling or whatever is supported) to talk to the bridge and bridge will talk to TCP servers using real socket connections. I decided to use SockJS although Socket.IO is fine too. Here is how the entire thing looks like:

diagram

How is it different than having a backend app

The difference is in where to put logic to handle different servers’ protocols. The normal way would be to handle it on a server side. So, for instance, if browser wants to get something out of Memcache – it will make a request to some backend app that knows how to get data out of Memcache. If suddenly you want to do something with Redis then you will need to modify backend code and restart server to add support for that.

On the other hand, if browser can operate on a socket level such protocol logic could be implemented on a client side. There can be just a bridge (or easily a cluster of bridges behind HAproxy) that knows nothing about servers’ protocols and just pass data to sockets back and forth. Whatever you want to connect to from the browser just include a JS client library in the page, no need to touch bridge at all.

Examples

Here are some examples for sockets, http, memcache client and redis client. I ported memcache client easily from node.js version just by using node-browserify and by replacing net library with WebTCP connection. Redis client for node.js relies on C library, so I had to implement client from scratch, but luckily Redis protocol is pretty simple.

Socket example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//First create a SockJS tunnel. 
//Use whatever port and address your WebTCP server is on.
var net = new WebTCP('localhost', 9999)

//Now you can create sockets like this
var socket = net.createSocket("127.0.0.1", 1337)

// To send data to socket 
socket.write("hi")

// On connection callback
socket.on('connect', function(){
  console.log('connected');
})

// This gets called every time new data for this socket is received
socket.on('data', function(data) {
  console.log("received: " + data);
});

socket.on('end', function(data) {
  console.log("socket is closed ");
});

It’s also possible to specify advanced options when creating a socket connection

1
2
3
4
5
6
7
options = {
  encoding: "utf-8",
  timeout: 0,
  noDelay: true, // disable/enable Nagle algorithm
  keepAlive: false, //default is false
  initialDelay: 0 // for keepAlive. default is 0
}

And then pass those options when creating socket

1
var socket = net.createSocket("127.0.0.1", 1337, options)

HTTP example

1
2
3
4
5
6
7
8
9
10
11
12
//Create a http client
var client = net.createHTTPClient();

// GET request
client.get({ host: 'news.ycombinator.com', port: 80 }, function(res) {
  console.log(res);
});

// POST request
client.post({ host: 'news.ycombinator.com', port: 80 }, { param: 1 }, function(res) {
  console.log(res);
});

Redis example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<script src="../lib/client/webtcp-0.0.1.min.js"></script>
<script src="../lib/client/redis.js"></script>
<script>
// Redis client example
var net = new WebTCP('127.0.0.1', 9999);

var redis = new Redis(net, "127.0.0.1", 6379);

redis.send("set a 1", function(res) {
  console.log(res);
});

redis.send("incr a", function(res) {
  console.log(res);
});

redis.send("incr a", function(res) {
  console.log(res);
});

redis.send("subscribe ch1", function(res) {
  console.log(res);
});

</script>

Memcache example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<script src="../lib/client/webtcp-0.0.1.min.js"></script>
<script src="../lib/client/memcache.js"></script>
<script>

var tcp = new WebTCP('127.0.0.1', 9999);

var client = new memcache.Client(11211, "127.0.0.1");

client.connect();

client.on('connect', function(){
   console.log('connected')
});

client.on('close', function(){
   console.log('closed')
});

client.set('foo', 'some value', function(error, result){
  console.log(result);
});

client.get('foo', function(error, result){
   console.log(result);
});

client.version(function(error, result){
  console.log(result);
});

</script>

That’s about it. Although this project still need more thought to figure out good use cases, I had really fun time playing with it.

Comments