- So I'd like to thank each
and every one of you personally for attending this talk. It's called Get-rich-quick
with Beast WebSockets and networking TS. My name is Vinnie Falco
and I'm the founder and president of the C++ Alliance, it's a nonprofit that tries to
advance the interests of C++. I'm also the author of Beast
which is a Boost library that implements the HTTP
and WebSocket protocols. It uses Boost.Asio as
its networking layer, you're gonna see how that
plays into the networking TS. You can visit the repository
for documentation and examples. I'd like to say a few words about Boost. It's a wonderful collection
of peer reviewed libraries, two of its great features
are the permissive license which allows you to do almost
anything that you want, and then it has a fantastic review process where experts in the
field, review the library and they offer feedback to the author and they determine whether
the library is accepted and what needs to happen
in order for it to go in. So it results in really
high quality stuff. You've probably seen
components of Boost going to the standard library, It's great. So if you have a laptop
here or a tablet or a phone and you want to browse the source code while I'm showing it on the slide, so you can see the whole file, this is where the repository is at. Everything that I'm
gonna show you compiles and it runs, there's a
docker file in the repository if you want to deploy it to a cloud and run the server that
I'm gonna show you. I'm gonna have to move
a little bit quickly there's a lot of material to cover here so it's very unlikely
that you're gonna be able to pick up everything
from just one viewing. I encourage everyone to go to YouTube, watch the video, every time you watch it you're probably gonna
learn a little bit more, this material is can be
a little bit advanced if you want to go into the
deep functionality of it. So that said so who's ready to get rich? (laughing) So it's not just a gimmicky title, we're actually going to
look at some technologies that have been incredibly successful and I'm gonna do a case study
of two programs one of them is called Agar.io and the
other one is called slither.io. These are both heavily
multiplayer games that run in the browser and you go
up against other people and hopefully you stay alive, if not your game ends pretty quickly. Each of these programs
was individually authored by one person each ,they're free to play, anyone can open up their
browser and go to the website and they can start playing this game, there's no installation or it's very easy and accessible to get into,
you can play a quick game. So each instance of the game can host hundreds of people at once So you're going up against a
lot of people in this arena and they brought in at
their peak $100000 per day in advertising revenue. So what's the secret sauce. It's a WebSocket server written in C++ that runs in the cloud and then
you have a WebSocket client that runs in the browser. Running in the browser is a great choice it does a lot of things for you
and I think you're gonna see when I show you that stuff
that it's pretty great. So in this talk I'm going to review the two key protocols HTTP and WebSocket, then I'm going to show you some concepts in the upcoming networking TS
that are absolutely critical then we're going to look at some code which is available online and it's for a multi-user chat server. You can connect to the server and you can talk to other
people that are there, exchange messages, it looks very nice and then I'll have a few closing words. So the protocols that we need to care about are HTTP and WebSocket. HTTP powers the World Wide Web, it divides computers
up into two categories, you have clients and you have servers. The client establishes a
connection to the server and once that connection has been made then an HTTP conversation can take place. It starts when the client sends a message called an HTTP request
which has a certain format. The server receives the
message and performs some computation and
then it sends that back the result which is
called an HTTP response. So these are two types of messages and then once that happens, the client can send another request and the server sends another response and then that process continues
until either side decides that it wants to close the connection. An interesting feature of HTTP
is that it's transportable by intermediate computers
called proxies or firewalls. So you can have a computer in the middle, and it'll relay the message, it understands the HTTP protocol it can do things like enforce policy, effect security, disallow
certain activities or shape the traffic according
to corporate policies. So that's HTTP. WebSocket is developed as
an improvement of HTTP. One of the problems with HTTP is that it's inherently a half-duplex protocol. The server has to wait
for the client to send the request before it can take any action. The server can't just send
an unsolicited message to the client in HTTP. So WebSocket is developed
to ameliorate that problem and it's what we call
full duplex and symmetric. That means in a WebSocket
session either side can send a message at any
time without being prompted and they can send multiple messages they can do whatever they want. The contents of the
message are completely up to the application, there's
no predefined format you could send text, you could send Jason, it could be binary format, it's whatever the two endpoints agreed to. Like HTTP, its transportable by proxies. An interesting feature
of WebSocket is that it divides messages up
into packets called frames. Each frame has a well-defined
length and a message can have zero or more
frames associated with it. There's three special types of
messages close, ping and pong which they do what they sound like, they allow you to control the
meta attributes of the session So in order to talk WebSocket
of course you need to have a connection, but a WebSocket
connection starts out as HTTP. So the client connects to
the server and they send a special type of HTTP request
called an upgrade request. They ask the server I'd
like to talk some WebSocket. So in that request, you
can see it's over here, it's an HTTP GET request and it has the special
fields upgrade and connection and then some protocol
specific gobbledygook we don't need to get into
that, it has to be there. So that's the request,
the target the URI here it's chat.cgi allows the
client to differentiate which WebSocket service
it wants for the case where the server has more
than one WebSocket service on the same port. So the server sees that
the client wants to do a WebSocket upgrade and
then if it likes the client, if everything checks out, the server will send
back the upgrade response which is a 101 status code. It means that it going to switch protocols and as soon as the server is
done sending that response from that point onwards, all of the bytes that are transferred
between those two end points are gonna be WebSocket protocol. So now they're gonna talk WebSocket. So Beast fits into this by
providing HTTP interfaces, WebSocket interfaces, one
of the interesting features about Beast, is that it doesn't
try to reinvent the wheel, so when you make a connection
to a server using Beast you don't actually use
Beast for that you use the networking TS or
you use your boost.Asio. You use those api's to
establish the connection and then once you have
that socket connected, then you can do some
transactions with Beast. So the networking TS, you've
probably heard about it it's been 17 years in
the making in reality and it provides an
abstraction for networking so that your program can work
anywhere that networking TS is implemented in the standard library. It comes in three flavors: networking TS, reference implementation,
boost.Asio and Asio. They're all largely the
same, in fact the source code for each flavor is produced from a single source using a script. The only major difference
is where you're gonna get the header files, the
name of the header file and then the namespaces can
be a little bit different. One major difference is
that Asio and Boost.Asio have features that are not
yet in the networking TS and won't go in for the first iteration, such as support for capturing signals, I believe serial ports, not
exactly sure about that one. So I'll show you some
basics of networking TS we're all going to need to
know it 'cause it's coming. The first thing you need
to have is something called an I/O context, in order
to do any type of I/O, you have to have it. It needs to be there. We've just declared one called ioc. Now we're going to create a socket, so the socket has to be
associated with an i/o context. Here's our socket. Now once that socket is connected, we might want to write some data to it. So here you can see we're calling
this function called write I use the net name space alias to show you that it's networking TS. The actual name space might
differ depending on the flavor that you're using, you all are smart I'm sure you can figure that out. So here we're calling it write on a socket and then there's some B there, presumably that's a buffer
and then it seems to return the number of bytes that
it was able to write. So what what's the type of B? So we know we need to have
some kind of memory area with some bytes, B can't
be a simple pointer that's not enough. So I'm gonna show you two types
that networking TS provides that are suitable types for B, mutable buffer and Const buffer. As the name suggests these
are nothing more than a thin wrapper around a pointer in a size. In one version the pointers Const and the other one it's non Const. So this is really straightforward, there's no mystery to it,
it's the equivalent of like a fancy pair with the good name
and some accessor functions. So now we're gonna write
a hypothetical function. We're gonna write this
function called send message. This function accepts a
buffer from the caller and it sends it on the
socket in a special format which is a header that's
calculated from the data, it's fixed in length and then the payload which the caller provides. You can see in the function signature we have the stream which
is some templated type that we won't get into and
then we have the payload which is that Const buffer
type that you just saw. That wrapper around a pointer in size. So how would we implement that? Well, here's a one way to
implement it we call write and then we construct the header and we passed that as a buffer and then we call write again
and then we pass the payload. So we're doing it in two
steps we're writing the header then we're writing the payload. So that's not such a
great idea and the reason is because I/O operations
are very expensive. If you're writing a network
program and all that you do is try to minimize the number of I/Os, then by the time you're done it's probably gonna work pretty well, so you're gonna do pretty well with it, if you don't pay any attention to that you might find that it's not
functioning very well at all. It'll perform poorly. So it would be nice if we
could do that in a single call. So here's one way we might do it, we're creating an array of two elements and it has those two buffers in it. So wouldn't it be nice if we
could just call right once and then pass that array
which is a type of range. It turns out you can do that, here's the signature for the
networking TS write function, you can see that it's
templated on this thing called a const buffers sequence. So what's a constable for sequence? Well, it's a type of range that
has a zero or more elements where each element is an actual buffer one of those const buffer that
you saw a pointer in a size. It needs to be lightweight and copyable. It doesn't own the memory
it just refers to memory that's owned elsewhere. So some other object has to maintain the lifetime of it in
order for those operations to work correctly. Beast comes with several
different buffer sequence types optimized for various use cases. Here's the actual requirements
for a const buffer sequence. This diagram this is called
a valid expressions list. It's a table that shows all
the valid expressions on a type that meets the requirements
along with some notes. You can see that a const buffer sequence has two nested types a value
type and const iterator. Buffer sequence has to be copyable and then it has a familiar
begin and end functions which you expect to find in a range and then we have a
little special carve out where we say that Const
buffer and mutable buffer are a const buffer sequences as well. So a special case those
two types can be used wherever a buffer sequence is expected. When you dereference the iterator
you have to get something that can give you a const buffer. So whatever fancy a
range or lazy evaluation you wanna do, at the end of the day each element of that range has to has to pony up a pointer in size. Mutable buffer sequence
is another concept. It's just like the const
buffer sequence except it's writable. Every mutable buffer sequence is also a const buffer sequence. So that's that. Now how do we use it? Let's say we have a string, we're gonna construct a string
and now we want to write it. So we're constructing a Const
buffer and we're passing the pointer and size of the string. That's a little bit inconvenient, so networking TS provides a
function it's called buffer, it'll take the string and it'll construct that Const buffer for us. It's a nice convenience function. Almost every type in the standard library that can be used as a buffer has an overload in networking
TS for the buffer function that you can use to conveniently
get a buffer to that thing. So those buffer sequences
are nice but sometimes we need something a little bit different. Here's a function called read
until which we're calling. This function is a networking TS function which will read from the socket
and and it'll store the data in our buffers until it
finds a certain pattern. In this case, a double
carriage return line feed. So what's the type of B? We know that it can't be a buffer sequence because but four sequences
have no concept of resizing. It's just a range or a view
to some memory regions. So here's the signature for
networking TS read until you can see rather than a buffer sequence we have this new type
called the dynamic buffer. So this is the second important
concept in networking TS, a dynamic buffer is used very
often especially in Beast. So here's what it is, it's
a resizable buffer sequence, it's got two areas there's
a read area and a write area and as you can imagine you can
you read from the read area and you put the bytes in the write area. When you don't know the size of the input and you want to write an algorithm or you want to invoke an
algorithm that can return a variable amount of data dynamic buffer is a good choice for the signature. Now this is a concept which means that user defines dynamic
buffers are possible, again Beast comes with a nice
variety of dynamic buffers that you can use , optimize
for various scenarios. I'm gonna show you how these dynamic buffers work through code. So here we have a dynamic buffer
called a we're declaring it it starts out empty, the read and the write
areas are zero bytes. So the first thing we want to
do is get some bytes in there. We call a read sum function on our socket and then you can see we're calling prepare on the dynamic buffer with the number 128. So we're resizing the write area to have 128 bytes of possible storage. And then we're calling read some, so that reads sum is gonna
hopefully get some bytes from the socket and put
it in that right area and then the number of bytes that it was able to actually
acquire is gonna be returned. So now we have some bytes and now we wanna make them available so we call a function called
commit on those bytes. Let's say we got 60 bytes. So we had enough room
for 128, we only got 60 now we call commit and
that moves those bytes from the write area to the read area so now they're available for reading. So now we want to process that data, I have this function called
process, it processes the data and we're calling a function on the dynamic buffer called data. This returns a Const buffer sequence, let's say that process only
uses the first 40 bytes and it returns the
number 40 for bytes used. So now that we're done
with those 40 bytes we want to get rid of them, we don't
want to see them anymore. so we call a function called
consume on the dynamic buffer we had 60, we consumed
40, now we're left with 20 at this point we could read
some more and commit that or we could deal with those 20 bytes and maybe get rid of the, who knows? So you saw the valid expressions table for dynamic buffer I'm going to show you a different form of documentation, this is called an exemplar. It's a C++ declaration
that hopefully compiles or mostly compiles that
contains all the elements that are required for the type. Here we have a prototype
of dynamic buffer, you can see it's got some nested types it has some accessor
functions related to the size, various sizes and then
we have the functions that I showed you data
prepare, commit and consume. That's dynamic buffer. The next important concept
is Asynchronous I/O. Here we have a call to read some, this is a synchronous function , that means that when you call read some it's gonna block until
it fulfills its contract. That threat of execution is
going to be stuck there until it either generates an
error or return some bytes. But very often we want to
write asynchronous programs, they perform better, they scale better and they're a little more complicated but we need to do this thing. This is what that looks like. Asynchronous operations in networking TS all start with the word async underscore. Here we're calling an
asynchronous function, we're gonna read some
data into some buffers and as soon as we call async read some that function call is gonna return it's not gonna block and the operation is gonna be happening in the background and we need a way to to know
when the operation is done, so we can look at those buffers. This is that handler
parameter that's there, so what's the type of that? Well here's what one possibility is, we have a lambda expression with a particular signature and that lambda is gonna be invoked, when the
read some operation completes So async read sum will
return and then at some point the lambda expression will be called. So what are the valid
types for this handler? Well, it turns out the
next concept is called a completion Handler. Asynchronous operations
invoke a completion handler which is a concept in networking TS, it's an in vocal function object that has a particular signature. They need to be moved constructible, the asynchronous operation
will take ownership of the completion handler
by move constructing it and it'll keep it alive until
after it invokes the handler. Here are three examples
of completion handlers; the first is the lambda,
which you've already seen, the compiler will generate
the function object for you and some anonymous type with
a really long obnoxious name. And then the second example is a user-defined function object, this is no different
than the lambda except you're just writing it out by hand, it has the function call operator And then the third example
we have the return value of stood binds which
returns a call wrapper. Again, this is an in
vocal function object. In this case, the standard library, is creating that type for you. So back to our asynchronous example. We're going to call async read some, that call is going to return immediately and then sometime later our
lambda is going to be invoked. But maybe you noticed something strange there's a question mark here? What's the problem? Let me give you a hint. So that's a thread. Which thread is going to
invoke the completion Handler? It can't be the thread
that calls async read sum that would be bad. It would be undefined
behavior if the implementation just interrupted your
thread at some point. That wouldn't be very good. So networking TS provides something called the basic guarantee. A completion handler will
only be invoked from a thread that's calling a member
function called run on the i/o context associated
with the i/o object. That sounds pretty obscure, let's see what it actually looks like. Here we have an I/O text object and we're calling run on it. So what that does is, that
relinquishes the current thread to the networking implementation. And what will happen is
that call to run will block and it'll invoke completion
handlers and do things until there's no more work remaining. Now what if we don't want to
give up the current thread? Well we can give it a new thread, here you can see I'm constructing a thread and then that's right is
gonna call I/O context run but there's a little problem here because I'm creating a temporary variable and then that temporary
is going to be destroyed, when the expression is done evaluating. So this is not going to work very well. We need to detach the thread
in order for it to be able to continue and outlive that temporary. So we can have multiple threads
calling I/O context run, here's what that looks like. This is a little snippet
that will create end threads and including the current thread so it'll create n minus 1
threads and call I/O context run on that and then it'll
commandeer the current thread and call run on that one. So you'll get n threads out of it. Why do we have all this ceremony
with the i/o context run? So I like to call this
bring your own threads. Networking TS philosophy
is not to make odd choices on behalf of the user. So the threads are going
to be under your control as the author of the code, you can decide how many of them you want which determines what
synchronization model you're gonna use, you can even decide
which thread facilities you're gonna provide. It could be the standard thread, maybe it's an operating
system specific calls, you have control over
the threads entry points so you can do interesting things
like wrap it in a try-catch It's really all up to you. The code that I'm going to
show you is single threaded, it keeps things very simple. You can get rich with
single threaded code. All those applications that I
showed you are single threaded each game instance runs in one thread although multiple CPU cores are used to host multiple game
instances on the same machine, but each instance is
still single threaded. There's one piece of information that I'd like to share with you about multi-threaded Network programs, and that is the networking TS has a let's call it Executor's light. An executor in networking TS defines how a completion handler is invoked. The most important executor is the one that's associated with an
object called a strand. A strand makes sure
that completion handlers will not execute concurrently. So if you're gonna write a
multi-threaded application, strand is something you're
gonna want to look at. It involves additional
synchronization overhead but it's really essential. And in conclusion with networking TS, there's three very
common threading models. The first one which is
what I'm gonna show you is single threaded, that means only one thread calls i/o context run. We don't need to worry
about any synchronization since nothing's happening concurrently. It's easy to write, it's fast, but it only has one thread. Then we could have multiple
threads calling i/o context run. So you're gonna utilize more
of the machines resources but now you're going to be
exposed to synchronization issues which could be a good trade-off. Finally the most complex,
is multiple threads where each thread has its own I/O context. So that requires more work on your part it's more complicated and
you'll need to balance the connections among the
i/o contexts in order to make that work but it does
offer the greatest speed. It has the advantage of not
requiring synchronization in most cases and it has
the highest capacity. Okay, now I know you're
itching to see some code. Let's see that code. I'm going to show you a chat
server that's written C++ that uses Beast and it uses WebSockets and it allows people to connect and exchange messages with each other. So to keep the slides small and to not have any
particular bias towards a version of networking, I've created some handy
aliases, some namespace aliases and some type aliases, the code that's in the repository uses Boost.Asio
flavor of networking TS because Beast itself is in
boost and it uses boost Asio. So here's little shortcuts. So this servers divided up
into five major components. First we have main, hopefully
everyone knows what that is then we have a shared
state, which is information that every object in the
system needs to have access to. Then we have an object called a listener. This is responsible
for monitoring the port and accepting incoming
connections and then finally we have these two session objects an instance of these objects
represents one connected user of the corresponding protocol. So here's the shared
state, this is very simple there's two pieces of
information that we have to have on hand at all times. One is the document
root, so the chat server is also an HTTP server. It serves HTML files, it serves
images or whatever you want to put there, advertisements,
if you want to make that money The doc root is the file
system path to those files. Then we have a set of
WebSocket session pointers. This keeps track of all the users that are currently connected,
because we want to do things like send them messages
or maybe kick them off. So how do we implement
the member functions of our shared state? What we have the functions join and leave, these insert and remove
sessions from the container to maintain it as people come and go. And then we have a function called send. So this is the function
that's gonna broadcast the chat message to all
the connected users. As you can see we're taking
ownership of the string that's passed in and we're creating a shared pointer out of it. We're gonna use shared
pointers in our application even though it's single threaded because it's a very convenient way
of allowing multiple objects to refer to the same piece of data. So we construct a shared string then we loop over each
connected WebSocket session and we call send on that shared pointer, so then they can send
the message to the user. So that's our shared state, pretty simple. So here's main, there's
some command-line arguments that you need to use to
invoke the server besides the address input we
need the file system path to the document route, so
here we're going to decode those parameters and now
we have an i/o context. You gotta have that you're gonna see it in every networking program. So once we declare our i/o context, we wanna create that listening port so that we can start getting some users and making that money. So I'm calling make shared, I'm constructing a listener
object for the port and then we're invoking a
member function called run on that listener. Now notice that we're not
storing the return value of make shared, in any variable. So what's gonna happen
is, when that expression is done evaluating, that shared pointer is gonna be deleted. It's the responsibility
of the run member function of listener to make a
copy of the shared pointer in order to extend the
lifetime of the listener. If we don't do that, then the
listener is gonna be destroyed and we're not going to really have much to talk about in main. So we create the listener
then we call I/O context run. At this point the main threat of execution which thus far is the only
thread in the entire program is going to be blocked on that run and at that point networking
TS takes over the thread and it uses it to invoke
our completion handlers. So that's main. Listener here's the
declaration for listener. Now you can see we have
some networking TS types we have an acceptor, we have a socket and then we have that shared state. So the first thing we
want to do in listener is implement our run function this is what's called from main and now we're calling async
accept on that acceptor. As you know asynchronous
functions they start with the word async underscore, so this is one of them. So we're calling async accept. This call will be
outstanding until it receives an incoming connection and then it'll invoke
our completion Handler with the error code and
the socket underscore member variable will
hold that new connection. So when our completion Handler is invoked we're gonna call on accept. Now notice that in the lambda
I'm calling shared from this so we're binding a
shared pointer reference into that lambda and what that will do is that will extend the lifetime
of the listener object as we talked about in main run
has to extend the lifetime. So networking TS is going
to take ownership of that completion Handler and
as long as the operation is outstanding our listener
object is gonna exist. So once we get an incoming connection we're gonna call on accept,
we're gonna have an error code, if there was an error we'll
report it like good citizens you'll see a message otherwise we're gonna create an HTTP session. As we talked about
earlier WebSocket sessions start out as HTTP. So we create our HTTP session object, once again I'm using the same idiom. I'm calling make shared
to create the HTTP session and we're not storing
the return value anywhere we're just calling a
member function called run. So it's the responsibility
of run to make a copy of the shared pointer in order for the HTTP sessions lifetime to be extended. So now that we've accepted
our incoming connection that called async accept was good for only one connection. So we blew that operation now
we need to start another one. So we're calling async accept again, this is the same
invocation you saw earlier, the lambda does the same thing. So we've created a type of loop here. We call async accept, that
operation is outstanding when we get an incoming connection, we process it and then we
call async accept again and that keeps going until the
user terminates the server. So one last note on the listener, we want to know how to report the errors, so there's a special error
code called operation aborted. This is what will happen
if you interrupt the server like for example you press control+ C or you cancel an operation,
you're gonna get that error in the completion handler we don't want to flood
the the log with those so we just don't bother reporting it, otherwise we give them
a nice error message. So that's our listener
pretty straightforward. So as we saw the listener
constructs an HTTP session, here's that HTTP session. Now notice again we're deriving
from enable shared from this we're managing our HTTP
session with the shared pointer and now we have more variables. We have the socket
associated with the session, we have a buffer which Beast needs and then we have our shared state which has the document root
and then we have a variable that will hold the incoming HTTP request. So when we construct the session, we call run on it and now here's a Beast function called HTTP async read. So this will read an entire
HTTP message that's right. It's that simple. The message will be stored in
the req underscore variable and then once we have
the complete message, it's going to invoke
our completion Handler in this case it's a lambda. Just like before, we're
binding shared from this into the lambda, so the lifetime of our HTTP session object is gonna be extended, at least until the completion handler returns. So normally when you program with classes, you explicitly call the
constructor to create your object and you call some member functions and then you explicitly
destroy the object. Here things are a little bit different. In this code, the default
behavior is for objects managed by shared pointers to be destroyed, unless you extend their lifetime manually which is what we're doing
by calling shared from this. You're not going to see
any explicit destruction of objects in this code, instead what you're going
to see is that the lifetime is extended when it needs to be. So this model is very easy to program once you understand how it works, and then you'll never have a problem with objects not living long enough or living for too long
and having memory leaks which is a common source of problems with asynchronous programs. So that's our HTTP session
run we're going to read an HTTP message, once we get a complete HTTP request, the function on read is called. So first we check to see if
the client decided to close the connection and then here we just gonna shut down the socket. Notice how we just return. As I said before, the default
behavior is for objects to be destroyed when they're
managed by a shared pointer unless we extend the lifetime by returning from this function without making a copy of the shared pointer we're gonna guarantee
that the HTTP session is gonna be eventually destroyed when the completion Handler is destroyed. Otherwise, if there's
an error, we report it and then if you recall earlier I said that WebSocket
sessions start out with an HTTP upgrade request. So this is a Beast function is upgrade, it checks if the request
is a WebSocket upgrade if it is, then we want to
create the WebSocket session. So by now you're pretty
familiar with this idiom, we construct a shared
pointer on object managed by a shared pointer we call run on it and we give ownership of the request to the WebSocket session
so we can look at it and figure out what it wants to do, and then we return. By returning we avoid
extending the lifetime of the HTTP session object, guaranteeing that it's going
to be eventually destroyed. Now what if it's not a WebSocket upgrade. Well our chat server is
also a complete HTTP server. We wanna be able to serve
files from the document route so if it's not a WebSocket upgrade, then we want to call this
function called handle request which will handle the request
as a normal HTTP request, it'll check to see if the
file is in our document route and it'll serve it hopefully it won't allow the user to
exploit any vulnerabilities with you know putting
like dot dot in the path. Make sure you don't do that
if you're on a live server. We're not gonna go into that function. It's very long you can
look at it in the code it's in all the Beast examples. But this function returns an HTTP response and that response can have
different types depending on what's in the request. Now in C++ a function can really
only have one return type. So handle requests can't
use the return channel to return the object. Instead, it invokes a function
object which you pass in and it'll pass that response that it wants you to send in its parameter. You can see I have a lambda
which has auto as the parameter. So that's going to be a generic lambda and the lambda is
responsible for sending it. So in order to send it, we put it into a shared pointer so that we can extend its lifetime as long as the operation is active and then we call a Beasts
function async write. So async write writes
an entire HTTP message you give it to Beast and it'll send it and it'll take care of it and then when it's done you know
that the message went out. Again we're binding a
copy of the shared pointer to the HTTP session into our lambda and once that HTTP
message with the response has been sent then we're gonna
call our on write function which gets called when
the operation is complete. So at the end of the on write function we're reading another request. Once again that's that loop
that we talked about earlier, we read a request we send the response then we do the read again. So that's gonna end in one of three ways, either the clients gonna
close the connection, they're going to get an error or they're gonna do a WebSocket upgrade otherwise they're gonna
keep getting file served. So now we're down to the last class, the WebSocket session, this
is the moneymaker here. This does the thing that
we want the server to do. It's the chat. So you can see we have
some variables, a buffer this is a dynamic buffer, flatbuffer is a dynamic buffer. It's going to hold the incoming message and then we have something
called a WebSocket stream. So in the HTTP session
you may have noticed that the functions to send
and receive HTTP messages these were simple free functions that's because HTTP at its lowest level is pretty much stateless. WebSocket is more complicated, there's state information
that has to be there so we need an object
to represent that state and that's what the WebSocket stream is. Beast provides that class
for you and it manages all the boring protocol details. So that's our ws variable
then have our shared state which has our list of
connected WebSocket sessions and then we have a queue
for our outgoing messages. So one of the interesting
things about Beast the philosophy of don't make odd choices on behalf of the user, the WebSocket stream can only
send one message at a time. To allow it to send more than one message it would require making a decision. How do we queue those messages? And then we would have
to answer the question of what allocator to use,
what data structure to use and no matter what I pick
in that implementation, someone's gonna be disappointed. So rather than making
that choice, Beast pushes that responsibility onto the caller. So you're responsible for
implementing your own queue that's gonna be optimized
for your own needs. Here's our WebSocket session when we launch the session we
take that HTTP upgrade request and we just pass it to Beast. We call this function called async accept. It's an asynchronous function,
it's going to look at the upgrade request to
make sure that everything about it is kosher. It'll send back the proper
response and then once our completion handler is invoked now we know we can
start talking WebSocket. We're gonna call a
function called on accept you can see again we're binding
shared from this in there, by now you're pretty
familiar with that idiom. After we accept the WebSocket connection if there's no error we
call join on our state. So if you remember that's
gonna insert the session into our list of active sessions. We don't want to insert it
before the handshake is done because then we might
end up sending a message to a client before the handshakes complete who knows what that would do. So now we're okay to
insert them into that set. Now we wanna read a message, they're gonna send a chat
message we want to read it and here's async read, this
is a WebSockets dream function notice how the interface
to asynchronous functions they're all very similar, we pass our buffer in which is what's going to hold the result and then we have our completion Handler just like before we call shared from this to extend the lifetime
of our WebSocket session. And once we get a message, we call on read. If there is an error we report it. otherwise we want to
send that chat message to everyone who's connected. You saw the send function
in the shared state it calls send on each WebSocket session and then once we're done
sending that message we want to get rid of it in the buffer. We don't want it there anymore we want to get a new
message into the buffer. So we call async read again. This is that loop that
we were talking about. We read a message into the buffer when that completes we send
it to everyone clear out the buffer and then we call read again. It's pretty straightforward. How do we send? So this is the most complex function. When we send a message we're
sending a shared pointer if you remember our shared state puts the string into a shared pointer. The first thing we do is put it into our outgoing message queue. This will keep that string alive no matter how many people are referencing it now we make sure that
since we're looking at it we have a reference to it. It's not going to go away. We need to check if we're
already writing because as I said earlier Beast only allows one write operation at a time. So if we're already writing,
we need to just return and wait for that thing to complete, otherwise, if we're the
first item in the queue now we call async write. So this is the Beast function that sends a WebSocket message, you
can see we're calling the buffer function on
the front of the queue after it's dereferenced, so that will convert a
string into a Const buffer and then again we have
a completion handler which will be invoked after
the message is done sending. And we're gonna call on right. So this is that loop again. On write, we report the error if any, we erase the message that we just sent, if it was the last reference, if it was the last shared
point of reference, it's gona get deleted and then if there's more messages to write we call async write again that's the loop. So now we're gonna keep
on sending messages until there's nothing left in the queue. So here's the last main function fail. Here we've got two messages
that we don't want to report, operation aborted you saw earlier but now there's a new
message called closed. So as I said earlier WebSocket
has three special messages close, ping and pong. Close means that the client wants to shut down the connection, it's an orderly shutdown
and Beast will report that as a closed error. So we don't want to
report that to the console we don't want to flood the console otherwise if it's a legit
error that will print it. So finally, I know what you're thinking how do we remove the WebSocket session? Well that's easy. In the destructor, we just
call leave on our state. So when there's no more references
to the WebSocket session, the destructor will be invoked and we'll just remove it from that set. Now if you remember
there's only one thread in this program, so we're
guaranteed that nothing is executing concurrently. We don't need any luc
it's all very simple. So that's the server I know it was a lot but you can look at the program and you can tinker with it it works. Now we're gonna take a look at the client. Now the interesting thing about our client is that it's got some graphics. That's right and we
don't need a TS to do it. So here's the client you can see there's a you put in the URI
then you press connect and it'll connect to the server. You can put your name in there and then you're gonna see the chat. You can type messages and press send and you can talk to people
and then when you're done you can press disconnect. So we're gonna whip up
this interface using HTML. It's gonna be very quick
it's not gonna be like C++ we don't need 8 files. It's gonna write a few lines. this is HTML everyone
pretty much understands what that's like you've got tags that are in the angle brackets here
you can see some orange text where it says UI and app
that's an HTML comment. We're gonna insert our user interface into that part of our file. So there's only a few different controls this one is called a button I'm sorry this is an edit
box, it's the input tag. You can see that there's
some name value pairs, some of them have to do with the size and how it's drawn but then
there's one that's called ID. You can see I've put URI
as a string for the ID. So that's like the equivalent
of like a variable name. So that's the the name that
we're going to give that item if we want to refer to it in code. So here we're creating the edit box that lets you put the URI in. Now we have a couple of buttons, again we have this ID that
gives the thing a name and you can see there's
connect and disconnect. So now we also want to get the user's name so we can put it into the chat again we're using an input, that's an edit box you
can see it's got an ID. So by now you can see the pattern, you have input, you have button obviously and then finally we
need that big rectangle that shows all the text
for the chat messages. So this is called the pre
tag means pre-formatted. It won't try to rap it'll
preserve character turns, again we have an ID messages is its name and we're going to use that and we're going to refer to it. and then finally we have the the place where you actually put in
the message and send it. So that's an edit box
followed by a button again we have these IDs. Now this this is all very
nice but we've created a user interface that
doesn't do anything then when you press the
buttons nothing happens. All we've done is to find how it looks and how it's supposed to react but not what it does when
you manipulate the controls. So in order to do that we
need to write some code and I need to explain something
that's called the DOM. Which is not the popular French Champagne, it's actually the document object model. So this is a system that
allows you through code to access every element
of that HTML document if you wanna show things or hide them or if you want to change text or you wanna make something disappear or change the style, you do
that with this thing called the DOM and that happens through code. So now we're at the
coding part of the client. This is the obligatory trigger warning. The code is written in
JavaScript of course. So here's what that looks like, inside our HTML we create a script tag and it's corresponding closing tag and everything inside there is gonna be this wonderful JavaScript. Now I'm not gonna go in too much depth you can kind of look at it and
kind of guess it what it does You see new WebSocket
hopefully that should be self-explanatory. So yes in JavaScript
it only takes one line to create a WebSocket
connection to a particular URI and then return the object. So you saw what it looked like in C++, this is what it looks like in
JavaScript don't get jealous. Now when they press the connect button we want something to happen. So here what we're doing is,
we're assigning a function to the on click property
of the connect button. So all that really means it's
a fancy way of saying that when you press connect, it's gonna run that function which is gonna connect to the WebSocket at the URI that we're passing in there. Okay so ws is that WebSocket
stream in JavaScript you can see it has on open on close. These are like events and we're
attaching functions to them you can see that when the
connection is established, we are creating this
text connection opened and we're appending it to
this messages.inner text. So that's that area that
shows the chat messages all we're doing is we're
just appending a string. It's that easy to append a
string which will appear. It's pretty much painless. And then on message its get called when we receive a WebSocket message that's what the server sends the client in that broadcast that you saw. All we're doing is just
appending the contents of the WebSocket message into the control and then putting a character
turn at the end of it. So that's how you're going to
see the messages that come up if there's an error, hopefully we won't ever see
that I've never seen it. And then if you disconnect we close the WebSocket connection. Now the most complicated
part of this interface is of course when you type
your little message in and then you want to press send, so if you click the send button then we're calling send
on the WebSocket object and it's gonna send your
name and then a colon and then whatever text that typed. And then we're gonna clear the field. We don't wanna have to make
them backspace every time they send a message
that would not be cool. Finally, one last detail we
don't want to have to click the button every time you
want to press the Enter key, so this is a little snippet of code when you release the Enter
key that it will simulate a click on the send button and
that's it for our client. So believe it or not this
slide shows the entire client. So this is a full client
that's WebSocket enabled this is one HTML file that you
can find in the repository. This includes the graphics
and the user interface, it includes the edit box, it includes the JavaScript code, everything this is the client. I think that's pretty
amazing how small it is and that allows you to
rapidly create things you don't even need an IDE you just need a text editor
and then you refresh it in your browser and you
drop the file in there and you can refresh it
and you can start seeing your program come to life with your HTML. So how are you gonna use this to get rich. Well agar.io and slither.io,
when they became successful they sparked 100 clones everyone
jumped on this bandwagon, there's lots of i/o games, and there's other programs
that are not games that you can find that are out there. So what are you gonna write. Well in the program that I showed you it's a chat program
which sends text messages but you don't need to send text messages you could send chess moves or you could send the XY
coordinates of your character as they get through a maze or you could send commands to some type of textual role-playing game you can see here, you
could send scrabble moves or you could even have a game like chess where you can also chat with your opponent or you can have a lobby
where you're chatting with other people that
are looking for a game. The chat code that I showed
you would be suitable for that. So really the sky is the limit by writing a program
that runs in the browser you can get that up and
running very quickly with graphics you could even have sounds and you don't have all of
the bulk and complexities that you might get if
you try to do that in C++ and if you make your program
free then anyone can run it. You're gonna get a lot
of users very quickly. I don't know what you're gonna write. Maybe someone will write
tinder for C++ users who knows. One of the questions that
I get which is very common they asked me, " what book should I get? " Do you have a website that teaches you?" I haven't really found
anything that's great. The only advice that I can give
is to read the documentation for the networking, read the documentation for
Beasts, look at the examples write as much code as you can, the more that you write
the better you'll get it's gonna take you know
a certain number of hours of writing code and making mistakes until you're gonna be professional at it. It certainly took me
quite a number of hours. You can ask questions on
Stack Overflow in the slack that's a great place to ask questions. And if you're in a commercial setting, I think the best way to
learn is to have a mentor. If there's someone at your
job or you know someone in your community who's
an expert in networking you wanna talk to them
and have them answer your questions in a more
interactive setting. It's gonna help you get
ahead much more quickly. So that's the conclusion
of the talk I'm really glad that I made it within the time I was a little bit concerned I had to cut a lot out of this talk. There's obviously a lot more things that I could tell you I touched upon just very lightly scratched
the surface of networking, we only saw the single threading examples so here you can see
where to get the slides, you can check out Beasts
and you can also check out the rest of the Boost libraries. I highly recommend them. I think they're great. So in conclusion, I would
like to thank John Cobb and the program committee
and all the volunteers who worked tirelessly to
put this conference together so that I could bring my ideas and share them with you
thank you all for coming. That concludes the talk. I do have about five
minutes to answer questions. If you want to ask a question I'll ask. (audience clapping) Thank you. (audience clapping) I'll also be available afterwards
in case they need the room I don't know if there's another session but that's fine. Would you like to ask a question? - [Man] Yeah, so the pattern
where you're creating a shared a pointer using
enable shared from this, does it like it repeats
so often and it only works if you created the shared Pointer like that for the first time so probably does it sound a good idea to create a static create
method with the same signature as the constructor which could return you the shared pointer for all of those. - I don't understand the question. Is there a question in there? - [Man] Yeah, like basically this works because when we created the shared pointer for the first time and called run on that, alright I believe it works only if you had already a
shared pointer otherwise. (man speaks off microphone) so the question was having a create method and not have the constructor create it. - Oh you mean like an extra function? - Yeah. - I mean, sure that's possible. I mean it's just a matter of style, it's one extra function in the interface but I mean to me they're kind of the same. I was in a rush to put
the program together. - [Man] Thank you. - Thanks for asking. Next. - [Man] Hello pretty cool. I also have a question
about like shared pointers in your example and I guess
it's a quite common example you're running like single threaded and I guess like you will
have lots of atomic unless unnecessary in atomic instructions because of like shared pointer. Any thoughts on that? Is there maybe like a way to like. - Listen when those guys
are making a 100000 a day they weren't worried about
those atomic operations. (all laughing) You can get rich and waste a few cycle. But to your point, yes you're right, so atomic operations they have a cost it can be pretty considerable
in this chat server, you're probably not gonna notice, but if you have like a high
performance application you're gonna want to do
something about that. So you don't need to use shared pointer, you could use your own
smart pointer wrapper that doesn't use atomic operations and in fact I think that Peter DeMuth has if he hasn't committed already
he has some wonderful work in shared pointer in Boost which provides non atomic operations and also a shared pointer that's optimized for single threaded applications which does what you said. - [Man] SDC pass also
allows you to actually like instantiate a shared pointer
which doesn't do that but I would like to see like something maybe standardized that's why I'm asking. - I'm not aware of any standard solution that doesn't have reference counting. - [Man] A second question
is like HTTP to support is there something on the horizon? - It's not really part of Beast. HTTP one is mostly stateless, HTTP two is something else entirely. It's very complicated. There's a lot of drama and
contention around the development of the HTTP 2 standard. I know that I don't think
Google has gotten completely on board with HTTP 2, it seems like they have like
kind of like their own solution So I'm a little bit reluctant
to invest the massive amount of resources necessary to produce an HTTP 2 implementation when the benefit the benefits not really there yet. I don't really see it. And WebSocket offers a
great solution for doing the two-way messaging. Next. - [Man] So I was really taken with by the way great presentation, I was really taken with the
JavaScript websocket interface which was like make a web
socket four or five centers of callbacks and then like run. And that was after about 90 slides of C++ you're implying that we
can't have nice things but can you explain a
little bit why we can't have that particular or can we have it but we have to build it
ourselves on top of Beast or do we have to build it some other way or can we not have it for
a philosophical reason? - So I think that's a great question. So the question is why is something in JavaScript to operate
web sockets so small and simple and then in C++
it's large and complicated. You're right about that. It's definitely verbose in C++ and I attribute that to
a couple of core reasons. First of all, there's no
mistaking it but networking in C++ is late. We've needed an abstraction for networking for a very long time and
it's taken a while to get it. Every other language has abstractions, javascript has a networking abstraction you can even change the backend and use the same abstraction but another vendors implementation. They have a very robust
ecosystem of packages and libraries we don't have that yet. Networking TS is going to
be really a game changer and it's going to allow
us to start catching up and you're gonna see more
libraries like Beast, libraries that are
written on top of Beast. And to what you said, we do need people to build middleware solutions
that make it easy to do things Beasts HTTP support is very low level, there's lots that it doesn't do; it doesn't deal with proxies, it doesn't deal with authentication, it really only understands
a few of the fields. So we really need more
libraries that are gonna be built on top of it to handle things so that people don't need to
write large cumbersome programs - [Man] To check my understanding of what you said about JavaScript,
you're saying that there is something
similar to in complexity and low levelness in javascript but what I'm seeing in your five lines is a very high level bit but
they have the low level bit - That's right. - [Man] And we have the low level bit or I mean we now will have a little bit but we don't have the high level bit yet. - That's right. - [Man] What do I Google
to find out more about the low level bit in other languages? - oh I mean I think JavaScript has something called socket.IO I think I'm not really sure
I don't really know those other languages very well. - [Man] Okay, thank you. - Next. - [Man] Perfect chat. So my first chat server was
using no Js on the server using a socket I/O web socket and it
had a pub/sub interface also so when you would shoot
messages back and forth it would automatically on either side say hey I want to subscribe
to this and then route it. I assume that was in
the web socket standard. Is that something at a
higher level abstraction that is in the web socket
standard but Beast doesn't do yet or is that a JavaScript agreement
between the browser no js' - So the question is what facilities exist in Beast web sockets to do group messaging publish and subscribe. Publish and subscribe is not
part of the WebSocket protocol these are interfaces
that are that are added by the library providers. Beast does not provide
anything like that . In my opinion that goes against
the low-level philosophy. Something like that is easily written and I look forward to people developing those middleware solutions
and then battling each other to see which one's the best. Doesn't that answer your question? (man speaks off microphone) Right, right Next question. - [Man] so I had two questions. The first is I noticed
that there was a place where you moved a socket object into one of the Constructors
and then you then copied the same socket object
back into like a read async to reschedule yourself but
you didn't reinitialize the socket in between
I was curious if that's - Right okay. So the question is why did I move a socket and then use the moved from socket later. Okay that's a good question. So I want to step back and
look at move construction and move assignment in general. So we all understand
the consequences of move in the place that we're moving to, but often we don't
understand the consequences of what happens to the
object that we moved from. And the answer is that the
language doesn't specify that in general it's up to
each type that supports move to define what is the state
of the moved from object. Now in the standard library,
in most cases or all cases I think, the moved from object
has a well-defined state and in networking moved from
sockets are also well defined they behave as if they
were constructed again with the original constructor parameters. - [Man] Okay thank you. My other question was instead
of using shared pointers and shared from this when I capture you know you usually do
it like a self equals shared from this could I do like a self equals dereference this and copy this into the lambda instead. - So the question is can
we simply make copies instead of having a
shared pointer reference in the lambda? - [Man] Yes. - So the answer is no. Sockets are not really copyable, Beasts websockets stream is not copyable. So you have state that really can't be duplicated, so you need to have you
need to have a reference. You need to make sure
that only one object owns the socket or stream or else
you'll run into problems. Does that answer? - [Man] Yeah thank you. - Okay great. Next. - [Man] I think you mentioned
it quickly but just to confirm so there is no way but using
just the language and TS to do SSL or support client certificate in the networking. - Okay so the question is, I think the general
question is how do we do SSL in networking? So that's it a great question. So far my efforts to
settle on the right slides has really failed. So I think what I'm gonna do
is I'm just gonna go ahead and just pop out here. Don't look at this, don't
look at the sausage-making and I'm gonna go down to so if you notice there we go So here's our read until function. So this is a networking TS function and I didn't go into much
detail during the talk but you'll notice that the first parameter is
called sync read stream. So this is a concept that's introduced in networking TS. Oh it's over. Okay do they need the room? - [Man] Yah finish the question. - Oh okay, okay great! So this is a concept any type that meets the concept requirements
can be used there, networking TS will work with it, Beast respects these concepts as well and you can use SSL stream implementations that are provided by other
vendors Asio has an SSL stream and all of Beasts algorithms
work with SSL streams. When you construct the Beast web socket you can use an SSL stream
as its underlying transport. - [Man] Okay sounds good. - Okay great. So I think that's it. No more so if you have questions, I'll be happy to answer them outside. (audience clapping)
Maybe you would have answers to any of these questions.
Why Networking TS still * doesn't have better integration with std::future - there shouldn't be any extra arguments to the functions (like boost::asio::use_future) if you are going to use std stuff. I understand std::future is WIP but this stuff is difficult to change when it gets out to the public... * doesn't allow to specify timeouts without manually launching async timers. This is a common complain but I have never seen any reaction * boost::asio integrates with OpenSSL but what is the story with NetworkingTS? I hope it does too as without SSL support it would be a bit useless. * when looking at the buffers slide why can't it use std::span instead?
Good talk Vinnie!
That's a nice way to get rich but with Rust you will be a billionaire.
Now being serious, do you have any benchmark measuring Beast's WebSocket implementation against uWebSockets?
Great news, HTTP/3 is being announced! That means I don't have to ever implement the crappy HTTP/2 spec: https://mailarchive.ietf.org/arch/msg/quic/RLRs4nB1lwFCZ_7k0iuz0ZBa35s
Like this very much but without simple middleware (like js) for mainstream, c++ will not reach a lot of people i guess
Great presentation Vinnie!