When John Bardeen, Walter Brattain, and William Shockley invented the transistor in 1947, they changed the world in ways we are still discovering today. From their revolutionary building block, engineers could design and manufacture digital circuits far more complex than those possible earlier. Each decade that followed has seen a new generation of these devices: smaller, faster, and cheaper, often by orders of magnitude.
By the 1970s, corporations and universities could afford mainframe computers small enough to fit in a single room, and powerful enough that they could serve multiple users simultaneously. The minicomputer, a new and different kind of device, needed new and different kinds of technologies to help users get the most out of the machine. Ken Thompson and Dennis Ritchie at Bell Labs developed the operating system Unix, and the programming language C to write it. They built constructs into their system, like processes, threads, streams, and the hierarchical filesystem. Today, these constructs are so familiar, that it's hard to imagine a computer working any other way. However, they're just constructs, made up by these pioneers, with the goal of helping people like us understand the otherwise inscrutable patterns of data in memory and storage inside the machine.
C is a systems language, and it is a safe and powerful shorthand alternative for developers familiar with keying in assembly instructions. Given its familiar setting of a microprocessor, C makes low-level system tasks easy. For instance, you can search a block of memory for a byte of a specific value:
// find-byte.c
int find_byte(const char *buffer, int size, const char b) {
for (int i = 0; i < size; i++) {
if (buffer[i] == b) {
return i;
}
}
return -1;
}
By the 1990s, what we could build with transistors had evolved again. A personal computer (PC) was light and cheap enough to be found on workplace and dormitory desktops. Increased speed and capacity allowed users to boot from a character-only teletype to graphical environments, with pretty fonts and color images. And with an Ethernet card and cable, your computer got a static IP address on the internet, where network programs could connect to send and receive data with any other computer on the planet.
It was within this landscape of technology that Sir Tim Berners-Lee invented the World Wide Web, and Brendan Eich created JavaScript. Designed for coders familiar with HTML tags, JavaScript was a way to move beyond static pages of text with animation and interactivity. Given its familiar setting of a webpage, JavaScript makes high-level tasks easy. Web pages are filled with text and tags, so combining two strings is easy:
// combine-text.js
const s1 = "first string";
const s2 = "second string";
let s3 = s1 + s2;
Now, let's port each program to the other language and platform. First, from the preceding combine-text.js, let's write combine-text.c:
// combine-text.c
const char *s1 = "first string";
const char *s2 = "second string";
int size = strlen(s1) + strlen(s2);
char *buffer = (char *)malloc(size + 1); // One more for the 0x00 byte that terminates strings
strcpy(buffer, s1);
strcat(buffer, s2);
free(buffer); // Never forget to free memory!
The two string literals are easy to define, but after that, it gets a lot harder. Without automatic memory management, it's your responsibility as a developer to determine how much memory you need, allocate it from the system, write to it without overwriting the buffer, and then free it afterwards.
Secondly, let's attempt the reverse: from the find-byte.c code prior, let's write find-byte.js. Before Node, it was not possible to use JavaScript to search a block of memory for a specific byte. In the browser, JavaScript can't allocate a buffer, and doesn't even have a type for byte. But with Node, it's both possible and easy:
// find-byte.js
function find_byte(buffer, b) {
let i;
for (i = 0; i < buffer.length; i++) {
if (buffer[i] == b) {
return i;
}
}
return -1; // Not found
}
let buffer = Buffer.from("ascii A is byte value sixty-five", "utf8");
let r = find_byte(buffer, 65); // Find the first byte with value 65
console.log(r); // 6 bytes into the buffer
Emerging from generations of computing decades apart, when both computers and what people were doing with them were wildly different, there's no real reason the design, purpose, or use that drives these two languages, C and JavaScript, should necessarily come together. But they did, because in 2008 Google released Chrome, and in 2009, Ryan Dahl wrote Node.js.
Applying design principles previously only considered for operating systems. Chrome uses multiple processes to render different tabs, ensuring their isolation. Chrome was released open source and built on WebKit, but one part inside was completely new. Coding from scratch in his farmhouse in Denmark, Lars Bak's V8 used hidden class transitions, incremental garbage collection, and dynamic code generation to execute (not interpret) JavaScript faster than ever before.
With V8 under the hood, how fast can Node run JavaScript? Let's write a little program to show execution speed:
// speed-loop.js
function main() {
const cycles = 1000000000;
let start = Date.now();
for (let i = 0; i < cycles; i++) {
/* Empty loop */
}
let end = Date.now();
let duration = (end - start) / 1000;
console.log("JavaScript looped %d times in %d seconds", cycles, duration);
}
main();
The following is the output for speed-loop.js:
$ node --version
v9.3.0
$ node speed-loop.js
JavaScript looped 1000000000 times in 0.635 seconds
There's no code in the body of the for loop, but your processor is busy incrementing i, comparing it to cycles, and repeating the process. It's late 2017 as I write this, typing on a MacBook Pro with a 2.8 GHz Intel Core i7 processor. Node v9.3.0 is current, and takes less than a second to loop a billion times.
How fast is pure C? Let's see:
/* speed-loop.c */
#include <stdio.h>
#include <time.h>
int main() {
int cycles = 1000000000;
clock_t start, end;
double duration;
start = clock();
for (int i = 0; i < cycles; i++) {
/* Empty loop */
}
end = clock();
duration = ((double)(end - start)) / CLOCKS_PER_SEC;
printf("C looped %d times in %lf seconds\n", cycles,duration);
return 0;
}
The following is the output for speed-loop.c:
$ gcc --version
Apple LLVM version 8.1.0 (clang-802.0.42)
$ gcc speed-loop.c -o speed-loop
$ ./speed-loop
C looped 1000000000 times in 2.398294 seconds
For additional comparison, let's try an interpreted language, like Python:
# speed-loop.py
import time
def main():
cycles = 1000000000
start = time.perf_counter()
for i in range(0, cycles):
pass # Empty loop
end = time.perf_counter()
duration = end - start
print("Python looped %d times in %.3f seconds" % (cycles, duration))
main()
The following is the output for speed-loop.py:
$ python3 --version
Python 3.6.1
$ python3 speed-loop.py
Python looped 1000000000 times in 31.096 seconds
Node runs code fast enough so that you don't have to worry that your application might be slowed down by the execution speed. You'll still have to think about performance, of course, but constrained by factors beyond language and platform choice, such as algorithms, I/O, and external processes, services, and APIs. As V8 compiles JavaScript rather than interpreting it, Node lets you enjoy high-level language features like automatic memory management and dynamic types, without having to give up the performance of a natively-compiled binary. Earlier, you had to choose one or the other; but now, you can have both. It's great.
Computing in the 1970s was about the microprocessor, and computing in the 1990s was about the web page. Today, in 2017, another new generation of physical computing technology has once again changed our machines. The smartphone in your pocket communicates wirelessly with scalable, pay-as-you-go software services in the cloud. Those services run on virtualized instances of Unix, which in turn run on physical hardware in data centers, some of which are so large they were strategically placed to draw current from a neighboring hydroelectric dam. With such new and different machines as these, we shouldn't be surprised that what's possible for users and what's necessary for developers is also new and different, once again.
Node.js imagines JavaScript as a systems language, like C. On the page, JavaScript can manipulate headers and styles. As a systems language, JavaScript can manipulate memory buffers, processes and streams, and files and sockets. This anachronism, made possible by the performance V8 gives the language, sends it back two decades, transplanting it from the web page to the microprocessor die.
In this book, we will study the techniques professional Node developers use to tackle the software challenges of today. By mastering Node, you are learning how to build the next generation of software. In this chapter, we will explore how a Node application is designed, the shape and texture of its footprint on a server, and the powerful base set of tools and features Node provides for developers. Throughout, we will examine progressively more intricate examples demonstrating how Node's simple, comprehensive, and consistent architecture solves many difficult problems well.