CppCon 2018: Vinnie Falco “Get rich quick! Using Boost.Beast WebSockets and Networking TS”

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments

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?

👍︎︎ 5 👤︎︎ u/tpecholt 📅︎︎ Nov 09 2018 🗫︎ replies

Good talk Vinnie!

👍︎︎ 4 👤︎︎ u/14ned 📅︎︎ Nov 09 2018 🗫︎ replies

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?

👍︎︎ 2 👤︎︎ u/mytempacc3 📅︎︎ Nov 10 2018 🗫︎ replies

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

👍︎︎ 2 👤︎︎ u/VinnieFalco 📅︎︎ Nov 12 2018 🗫︎ replies

Like this very much but without simple middleware (like js) for mainstream, c++ will not reach a lot of people i guess

👍︎︎ 2 👤︎︎ u/gocarlos 📅︎︎ Nov 08 2018 🗫︎ replies

Great presentation Vinnie!

👍︎︎ 4 👤︎︎ u/bandzaw 📅︎︎ Nov 08 2018 🗫︎ replies
Captions
- 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)
Info
Channel: CppCon
Views: 35,871
Rating: undefined out of 5
Keywords: Vinnie Falco, CppCon 2018, Computer Science (Field), + C (Programming Language), Bash Films, conference video recording services, conference recording services, nationwide conference recording services, conference videography services, conference video recording, conference filming services, conference services, conference recording, conference live streaming, event videographers, capture presentation slides, record presentation slides, event video recording
Id: 7FQwAjELMek
Channel Id: undefined
Length: 60min 44sec (3644 seconds)
Published: Thu Nov 08 2018
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.