Networking in C++ Part #1: MMO Client/Server, ASIO & Framework Basics

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hello and welcome to the first part of a series where i'm going to explore how we can implement networking in a c-plus plus environment i have briefly touched on networking before with my hacking a logitech keyboard to display twitch chat video and in that video we introduced the idea of sockets however the problem with sockets is they're very platform dependent and they can be quite fiddly and tricky to work with instead for this series i'm going to use a technology called azio which stands for asynchronous input output and specifically i'm going to be looking at how we can develop a framework which will allow us to support a server client system in the first part of this series i'll be demonstrating some of the typical problems you encounter when you start adding networking to your applications and i'll show how azio can reduce some of those problems but on top of azio we're going to need this custom framework something very simple that we can add to future applications and allow us to have clients talking to servers or applications talking to applications very simply networking in c plus is traditionally seen as quite a tricky thing to do because there is no standard for it but by adopting a library like asio we can ensure that our programs will compile for different platforms and it's my hope that the azio library will become part of the c plus plus standard in the future however the asio library itself is written in a reasonably modern c plus way and therefore any framework we develop on top of it will also be written in a modern c plus plus way particularly by the standards of this channel but i don't want that to put you off if you're just at the beginning of your c plus learning journeys as this is a multi-part series i will be stepping through pretty much every line of code and explaining what's going on so i think there's a lot of educational value to be derived from the building blocks as we create this framework and so in part one we will be looking at how do we acquire and install azio we'll be looking at some basic examples of using it in a non-asynchronous manner and this will reveal why networking might have a bit of a tough reputation it'll also reveal some of the problems that we need to solve as part of our framework then i'll describe the overall architecture of the framework i'm trying to develop and then we'll start the development itself on some of the critical building blocks that we need so let's get started on what i hope is going to be an interesting journey to begin with we're going to need asio itself and you can download the asio files from think dash async.com asio asio started life as part of the boost framework and if you don't know boost is a bit of a playground for c-plus plus features and many of those features eventually make their way into the standard i can already hear the collective groans of people going oh no not boost well fortunately asio has a non-boost version so we don't need the entirety of boost in order to use it it can be used standalone so to get it we'll head over to the website and we'll well download it now the version at this time is asio 1.18 and in amongst all of these adverts we can see boostazio1180.zip that's the version i'm interested in the zip file contains the folder and the folder contains well just nothing really other than header files if we dip into the azio folder here we can see everything is an hpp file in fact very rarely if possibly anywhere in this directory are there any cpp files at all seo is an entirely header based framework and we're a fan of those on the one loan coder channel to install azio simply extract this folder into wherever you install your sdks and that's it because it's header-based there are no libraries to link to or anything to build next i'm going to start up visual studio and create an empty project i'm going to create a new solution we'll see throughout this series that i'll be adding several projects to this solution but for now i'm going to create a simple example project and click create as you might expect the empty project contains nothing so we're going to add a source file to it add new item and i will call it simple example.cpp and i'm going to add the absolute minimum required for a bur bones project what i would like to show first is a very simple asia program where we'll connect to something on the internet and get some data it will introduce how we get asio up and running but it'll also demonstrate some of the problems that we're going to face as part of developing our framework since we know we're going to use asio in this project we need to link to the project where we've installed the azio header files and in visual studio this is done by going to project properties and i like to make sure that we've got all configurations and all platforms selected then when we go to the vc plus plus directories we can look at where the include directories are and simply type in the path on my computer it's e drive sdks asio 1.18 include make sure this matches the path to where you installed it on linux you can either install azio exactly the same way or you can install it via the source repositories provided by your vendor the core of azio is simply to include azio.hpp now asio is a vast library with lots of modules and components to it and it doesn't only just handle networking it can handle all sorts of interesting forms of input and output to your programs but for this series i am specifically interested in it doing networking over the internet and so i'm going to include two additional header files as part of the asio library buffer which will handle the movement of memory for us and internet which prepares asio for all of the things we need to do network communication now i mentioned before that seo came out of the boost development framework but we're not interested in using boost so we can tell this asio header file that it should be used as standalone now on windows when we build it we'll see that we typically get this little warning please define win32 winnt this is very much a windows only problem and it appears because each version of windows has slightly different ways of handling networking your program will run just fine it will default to something but we can tell it specifically which version to use so we'll try that compile again good networking on the whole is a huge subject with lots of complexity to it in this series i'm not interested in the very low level nature of it i'm not going to be talking about how the tcp protocol works i'm not going to be talking about subnet masks and routing and all of that stuff that's for some of the time what i am interested in doing is shifting data from one computer to another and hopefully i'll demonstrate that you don't need to have a working knowledge of how networks work in order to achieve functional networking code and as such it's likely that i'm going to be introducing some code which is going to be quite alien but once this simple example is finished you'll see actually it's not so bad one of the nice things about azio is it's very good at handling errors and so i'll create a variable asio error code ec which we'll reuse to get 12 specific errors fundamentally asio needs a space to perform stuff and it does this via an object called an asio io context in a way you can consider this as a unique instance of asio and it's this context that hides all of the platform specific requirements next we need to construct an address of something that we want to connect to as the name implies azio can connect many different types of input to output it doesn't have to be networking but all of these things that can be connected are referred to as end points so here i've created specifically a tcp style endpoint which means the endpoint is defined by an ip address and a port i'm using the make address function to turn this string version of an ip address into something that azio can understand and i'll also pass in our error code variable just in case something goes wrong if this ip address was malformed the error code value would have something in it to tell us it has become malformed asio fully supports exceptions as a means of catching errors as well and we will use some of those later but for now we're just looking at a nice little variable instead for our purposes an endpoint is simply an address the next thing to add is called a socket in this case i specifically want a networking socket it is the hook into the operating system's network drivers and will act as the doorway to the network that we're connected to when i create the socket i associate it with the asio context we created earlier now i'm going to instruct the socket to attempt to connect to the address that we specified and here we can check the error code to see if it was successful the error code variable is quite nice because you can interrogate what its error means at runtime to display it if necessary so if i run this program it's going to attempt to connect to this ip address which is the ip address of a website called example.com very basic websites like this communicate on port 80. http you've probably seen that before now i'm just going to add this at the end of my code i know nobody likes this function but it's very easy to make videos with it and i'm going to run the program and as we can see connected press any key to continue so azio successfully connected to that location let's send it to a location where i know there isn't a working port 80 to actually connect to it's my own machine and we'll run it again we can wait it took some time but it returned no connection could be made because the target machine actively refused it asio is very nice in this regard its error messages are descriptive and useful we just observed something interesting it took time to try and connect to my machine and we'll see more of this later if the connection was successful then if i call the is open function on the socket that should return true which means i have an active and a live connection to well something at the other end the other end point since i know i'm trying to connect to a website the server at the other end is expecting an http request so i'm going to hack together a completely bur bones http request it looks like this and if you weren't aware yes the internet works by sending literal text strings around the place it's quite miraculous that we're still doing it that way the details of this string aren't actually that important for this series as we won't be doing things this way but for now it serves as a great example of how we're going to send some data to the server and see what it responds with once i've constructed this request i'm going to send it and i'm going to do that by calling the write sum function of the socket write some means please try and send as much of this data as possible when reading and writing data in asio you work with azio buffers and an azio buffer is really just a container that contains an array of bytes in this case i'm taking the bytes from my string and the size of my string to tell it how to dimension that array for each major transaction we can also interrogate the error codes although i'm not going to do that for every function else the code will become quite large once we've written some data to the server i'm going to hope that it sends me something back and i can see if there are any bytes available for me to read by calling the available function on the socket if there are some bytes then i'm going to read them from the socket and i will read those into a new vector that i've created which is sized to the amount of bytes that are available and i'll call the equivalent read sum function where again we're using an asio buffer to wrap the standard vector i will then display to the console the contents of this buffer so let's run it and see what happens oh well it connected but it said there are zero bytes available let's see why i'm going to use the debugger to step through the code line by line so i'll press ctrl and f10 to run to this location i'll construct the string i'm now going to write some data and now i'm going to check are there any bytes of oh well hang on it's a bit different this time it's saying there's well 1 631 bytes available it seems this time it's worked quite nicely i let the program run to completion we can see that the server has actually returned quite a bit of information it's returned a bit of a header here to tell me it's accepted my http request and then it's returned to the source code of the website that we connected to so that all looks fine what's the problem here well the problem stems from as soon as we wrote the request to the server we immediately tried to read the response but the server could be miles and miles away it takes time for that request to go out get processed and return so when i ran my program at full speed when i wasn't debugging it there wasn't enough time for the server to do any kind of response i'm going to brute force in a delay to see if that helps things after we have written the data i'm going to deliberately stall this main thread for 200 milliseconds this is horrific if you're ever putting hard-coded delays in your program to make it work you need to think again but let's see if it does work well it did 200 milliseconds look like adequate time for the request to go out get processed and the server to respond with data this is really bad and really inconvenient for us i don't want my program waiting 200 milliseconds every time i do any kind of network transaction fortunately asio does have some tricks up its sleeve to help so one of the things i can do is tell it to wait which will block my running program until there is some data available to read so let's take a look at that well good that's worked this time i'm going to change the ip address to a website that's going to deliver more data and run it again well this time we can see we got well a lot more information coming through but worryingly it looks like we didn't get all of the information this time let's just try running that again this time it stopped at a different point right in the middle of some code because of the time delays of the transport and the servers varying amounts of bytes might be available for us to read at any one time and since all we're doing is saying well are there any bytes to read then just try and read them we're not getting everything we're starting to see two really significant problems with networking firstly we don't know when things are going to happen as soon as it leaves our computer it's beyond our control things take time secondly and partly related to part one we don't know how much the server is going to respond with we don't know how much data to prepare in advance what size should we make our buffers i simply don't know and also putting things like weights and blocking statements in our code is a really bad thing to do it seems very wasteful well this is where we can start to exploit the asynchronous side of asio what i would like to do is prime asio with an instruction that says well if some data arrives read it and display it to the console since i don't know how large that data might be at any one time i'm going to create a reasonably large buffer and just reuse it i'm creating this outside of the scope of my main program because i'm also going to add in a function which will link to asio to handle the data reading for us and i'm going to call that function grab some data and we pass in the socket previously when we were using azio in synchronous mode we called the read sum function now i want to use it asynchronously so i'm going to call the equivalent asyncreadsum function this takes in the buffer just as before but we also need to give it a lambda function when we call the grab some data function that in turn calls the asyncreadsum function and because it's asynchronous this isn't going to do anything right away instead what it will do is prime the context with some work to do when data is available on that socket to read and the work we're going to give it is simply reading the data from the socket and putting it in our buffer and displaying it on the screen so i'll add in the lambda function here the prototype of the lambda function will tell us the error code and the amount of data that was read by the read sum function if there is no error code then i'm going to display how many bytes were actually read that time and then just loop through my buffer displaying those bytes to the console i'm then going to call the grab some data function again and you might think that this is some recursive nightmare but it isn't recall that the asyncreadsum function will prime the asio context with some work to do and then immediately return i.e asio is going to run in the background when some data arrives on that socket it's going to implement the code we put in this lambda function if in this first instance it only reads 100 bytes but the server is sent 10 000 the grab some data function will call the async read function again and it will grab a bit more which will then go and grab a bit more and a bit more until there is no data left to read and at that point the async read function will simply wait until there is some data to read so after we've written our request to the server no longer interested in reading it manually instead i'm going to call our grab some data function let's take a look well we've not been very successful here either it's connected but then immediately exited the reason for this is similar to the first time that that happened we called the grab some data function straight after issuing the request this grab some data function would have then primed the i o context to read something asynchronously but the problem is our program finished before anything could happen and this is where starting to think asynchronously can be a little tricky instead i'm going to prime the asio context with the ability to read bytes before i send some data and to stop my program exiting prematurely i'm also going to put in just a big delay let's consider for a moment what we're expecting of the context we're now lodging with it instructions with which the context has to wait for certain conditions and then it will execute code associated with those instructions that waiting part is quite interesting how does it do that we've seen already today that when we want to wait for something it invariably blocks and we don't want the i o context blocking all the time one of the things we can do with asio is run the context in its own thread this gives the context some temporal space within which it can execute these instructions without blocking our main program here i create a thread of a lambda function and all that lambda function does is called the run function of the context this run function will return as soon as the context has run out of things to do i.e there's no instructions registered with it for it to do in the future therefore it is likely by the time that our program has gotten to the line where it starts registering instructions with the context that the context has finished executing fortunately azio gives us a way to deal with this too we can create some fake work for the context to do so i know you're all thinking whoa there's loads of stuff been thrown into this already but let's just take a step back and recap on what's actually happening we're creating a context which is the space where azio can do its work but we need to prime that context with jobs for azio to do if azio doesn't have anything to do it's just going to exit immediately in the meantime we can give it some fake jobs to do which stop it from exiting immediately we allow that context to run in its own thread so if it does need to stop and wait it doesn't block the main program execution we then connect to our tcp server as before and when we're successful we prime the azio context with an instruction to read some data if it's available once it's primed we then write our http request and we wait nothing else to do this 20 second wait is deliberately long just to stop the program from exiting before we've got all the data that we needed the instruction we primed the context with was this read sum function so it'll read as much as it can and display it to the console let's take a look well we can see straight away lots of data appearing in fact the entire data of the website appeared reasonably quickly if i make this buffer much smaller let's say it's a one kilobyte buffer and if i look at the data we can see that each time it was reading 1024 bytes we had control over how much data could be read for a given read command but in a cool way when it got to the end it only needed to read 548 bytes it also didn't block or hold up our main program in any way by thinking in this asynchronous manner we've managed to overcome some of these problems regarding time delays and buffer sizes as you can see in this very simple example thinking asynchronously does provide some benefits but it's not without its own costs of complexity when you do start working with asynchronous programs you have to start thinking a little outside the box things aren't going to happen in the order you expect them to this is what makes it distinct from synchronous programming but the idea of taking a thing and priming it to do work for us in the future particularly work involving things with variables such as time delays and buffer sizes is very powerful indeed now for the framework i want to develop i think needing to think like this is perhaps a little bit too demanding of the end user that's not to patronize anybody but i think for a client server system they don't need to be thinking of it in terms of asynchronous read and write calls instead i want to provide tools and utilities that are flexible enough to have easy message passing between a client and a server or a server and many clients and it's the foundations of this framework that we'll be looking at in the rest of this video so if all of this very azio specific stuff has completely passed you by don't worry too much about it there's still a lot more interesting things to come this series is as much about developing a framework as it is the specifics of asio and in the example i've just shown we've seen how azio can handle the temporal properties of communication very well by helping us execute code when and where it is necessary to do so but the http example highlights one interesting problem we don't know in advance how much data is going to be sent and whilst that is a requirement for a lot of interesting applications such as streaming video it's actually quite inconvenient to work with now before you all start commenting yes i know that the http response header contains the number of bytes that are going to be sent so we could have tried to parse that header and prime buffers accordingly but in the framework i'm going to handle things a little differently all of our data transactions are going to involve messages and my messages are going to have two primary components the first is a header and this header is going to consist of an identifier for what is this message and secondly it's going to consist of the size of the entire message including the header in bytes the second part of the message is going to be the message body which is the payload of the message but could be zero or more bytes after all an event with no particular interesting data doesn't need a body so why transmit it using a configuration like this means we're never transmitting or trying to read data of an unknown size the header is always sent first and this is a fixed size but it contains the information required to prime our system with the requisite size of any body that we want to transmit after the header this id could be anything that can make the header distinct for example it could simply be an integer but c plus plus provides us with some more modern tools which we can use to validate whether the header is accurate or not at compile time rather than time so instead of an integer i'm going to specify an enum class an enum class is like the traditional enum structures where you would specify a bunch of symbols and they'd be associated with a value in this case there isn't really a value associated with it the symbol itself is sufficient for the compiler to use it as an identifier for something and because the enum class is strongly typed in this way the compiler can identify when we send invalid ids if we just used an integer we could have a bug in our code where we sent an id that didn't mean anything and it's very likely that both end of the systems could identify that and get rid of it but isn't it nice to have the compiler work some of that stuff out for you and tell you in advance the only problem with using an enum class like this is it's not very flexible since i want this framework to be for anybody and everybody that wants networking in their applications i've no idea what type of messages people are going to need so i can't go and provide an enum class filled with millions and millions of different types of message instead i'm going to allow the user to specify their own enum class and we'll use templates to customize the message this way our framework can behave in a predictable way but use the user's own messages when any data is transmitted it's done so serially that means i'm also going to add to our message type the ability to serialize and deserialize conveniently this should make the message type very easy for the user to use the hope being that by constraining the user in such a way as to use these defined messages for all of their data transactions from the client to the server then the client and server interfaces can be defined in such a way as to require no additional modification from the user i'm going to go back to my solution and to it i'm going to add a new project and i'm going to choose static library although really i'm just using it because it's an additional folder and i will call it net common now it'll go away and add a load of junk to it i'm just going to delete it to my net common project i'm going to add a new item and it's simply going to be net common dot h i'm going to use this header file to include all of the bits that the rest of the networking framework is going to use it saves me having to duplicate this code in multiple places and so for example i'm going to put in a bunch of the commonly used standard files and i'm going to include asio as well notice it's moaning that it can't find asio that's because i also need to specify the include directory for this project as i did before i don't know yet whether i'm going to use all of these they just happen to be the ones that i like except for chrono and now i'm going to add another header file this time netmessage and it's in this file that will include the definition of our message object and the first thing i'll do is include the common file we've just defined now it's my intention for this framework to be entirely header only in itself and that's because ultimately i hope to release it as a pixel game extension so this is totally optional if you decide to do something similar but i'm going to create mine within the namespace olc and within that the namespace net and the first thing i'll define is the structure that represents the message header so we can see i'm using the template keyword and passing in a template variable t t will represent the enum class later on whatever t happens to be i create an instance of it called id and i store the size as an unsigned 32-bit integer i'm deliberately not choosing size t in this instance firstly i believe that an unsigned int 32 will be of sufficient size for all of the data we want to transfer but secondly i can't guarantee that size t is the same on a 32-bit and 64-bit system let's say my client is using 32-bits but my server runs on 64-bits in principle the same framework code will be compiled for both platforms but in order to communicate effectively it's vitally important that the sizes of the types i use in my communication structures don't change there are often things like this to think about when working with networking and we won't really look at any others in this series but one thing to think about is the difference between x86 architectures and say arm architectures where the byte ordering of the data is different if you know that that's going to be the case then it is quite important that you put in conversion routines when reading and writing the data but like i say not worrying about that for this series i know that my clients are predominantly going to be windows and linux computers and that my server is a linux computer running on x86 compatible architectures now the message header is defined we can define the message itself now fundamentally because i have chosen an enum class to represent the id everything that relies upon the message header will also need to be a template this has a knock-on effect because in fact everything that then relies on messages will also need to be a template too if templates aren't for you you could just go back to specifying an integer but i see one of the advantages of working this way is it's actually quite suitable for a framework which is entirely comprised of header files i guess that's a subjective opinion as i mentioned in the slides the message contains a message header and it also contains a body now i'm going to use a standard vector of unsigned 8-bit integers for the body that way we're always working with bytes and as we've seen many times on the channel using a vector is a flexible way of working with arrays of data i'm going to start adding some utility functions to this the first is something that returns the size in this case it returns the size of the message header including its template so if our enum class was specified to be a 32-bit integer or an 8-bit integer that's fine and we know that our body is only going to consist of individual bytes and this is where things are going to start getting a little bit modern for the one lone coder channel whilst debugging all of this framework it's going to be useful to actually see the packets so i'm going to add an override for the bit shift left operator which in this case is tied to standard os stream this allows us to easily use our message type with standard c out so we can output things to the console in this case it's a friend because this is really going to be accessed from anywhere by anything and when this operator is called it pushes into the output stream a variety of things i want my messages to be easy to use so far to define a message we would simply create an instance of our message struct pass in the enum class as its template argument and it will go and construct the message accordingly to work with the body of the message i could work with the vector directly but i'm also going to add some convenient operator overloads that will allow us to treat the vector like it were a stack in principle let's say i have some variables float x and y i would like to simply just take my message object and pipe into it x and then pipe into it y and to get data back out of the message i do it the other way around i don't want this to just be limited to only floats i want it to work for well any trivial data type so let's work on first pushing data into the body vector perhaps confusingly i'm going to use the same operator as we did with c out but this time the types surrounding it are fundamentally different so we'll be able to know which one is being called at any given time this is the message object that contains the body that i wish to update and so i'm going to create this as a template function within the message struct this way if the user attempts to push a float in data type becomes float if you push an integer it becomes an integer if they push a struct of any description well it becomes that struct there are however certain data types in c plus plus that you can't trivially serialize these might be classes that contain static variables or complex arrangements of pointers so i'm going to use a little bit of modern c plus here and actually check that the type provided to this override is considered to be a standard layout i.e it's not too complicated i'm then going to cache the current size of the body vector now to begin with this will be zero and as we start adding more things to it the vector will increase by the size of the objects that we're pushing into it i'll need to make room for those objects by resizing the body vector and i'll do that by looking at its current size added to the size of the welt generic data type that we're pushing in the vector is now large enough to take in those additional bytes so looking at the vector's previous size we know where to start copying the memory of the new data type into the end of the body vector into that space that we've just created since the body vector has now changed in size i'm going to update the size variable in the message header that way the header always accurately reflects the size of the message now the way i'm overloading this operator is to return a reference to the message itself this allows us to chain together arbitrary objects of arbitrary type pushing them all into the vector there are advantages and disadvantages to doing it this way one of the advantages is it should handle most data types and it will automatically resize and allocate the memory of the message for us one of the disadvantages could be performance each time we add something to the messages body vector it needs to be resized however vector is smart enough to know that as it's growing it shouldn't grow linearly and in practice we'll see there's actually minimal overhead in doing it this way now looking at how we're structuring this you might think well surely it's more convenient to do this in the same order when dealing with getting data out of the body vector well let's look at the implementation of this override and see why that's not going to be the case as before i'm going to check that the data being extracted from the body vector is trivial now this time we're taking the data from the end of the vector so i want to cache a location which is from the vector's end minus the size of the data we're trying to extract then i'll physically copy that data from the vector into the user's variable we've now took data out of it so we should reduce the size of the vector and this is where we don't see a performance hit because making a vector smaller than its original size doesn't cause any reallocation if we were to take the data from the front of the vector then that data needs to be removed from the front of the vector and that will cause quite a significant reallocation each time we extract data so by treating the body vector like a stack we can ignore unnecessary vector reallocations finally just as before i want to make sure that the header size reflects the accurate size of the message and return the message itself and for the professionals out there there is a way you can do this with iterators which will allow you to extract the data from the vector in the same order that you put it there however you need to think about where you store that iterator in between extractions here i've added another project to my solution called netclient and i've configured it with asio just as before and i've added a source file called simpleclient.cppp one of the convenient things to do in visual studio is to right-click on the project and go to build dependencies and select project dependencies so i always want my net client project to depend on my net common project that way any changes to net common will also cause net client to recompile for convenience to my net common project i'm also going to add one more header file and i'm going to call it olc net.h and all this is going to do is include the other headers in this project then if i go to my netclient project go to its properties in my include directories path i'm also going to add the path to net common to this simple client and example program i can now simply include olc net.h let's put the theory into practice i'm going to create an enum class to contain my message types and for now i'll use the name custom message types i'm also going to specify that this enum class be implemented using uint32ts that way each type id is going to be four bytes i'll put in the ones i had before fire bullet and move player for example this means if i wanted to construct one of my messages i do so the following way you can see i've constructed it the with my custom message types type i go to message.header.id and it will only allow me to specify things from my custom types this is a nice thing if i specified something else the compiler will complain by adding constraints to what's allowed we're encouraging the compiler to help us write better code in the first place well let's test our message type so let's create a variable called a of type integer and we'll give that the value 1. let's create a boolean b i'll give that the value true let's create a floating point c we'll give that the value pi and let's create something less trivial perhaps so here we've got a struct of x and y and we're going to have five of them in an array and that's d and i want to put all of that into the message well it's simply a case of a b c and d once i've put those in i'm going to get rid of the original values i'm just going to set them to something completely different and then read them back out of our message we can see the compiler is happy but let's step through the code and see what's happening i'm going to run to this line first and see what happens so i'm going to press f11 in visual studio which allows me to step into a function and we can see it stepped into our operator overload it's going to push the integer a into the vector i'll now run to here and we'll have a look at what the body vector contains it says we've got a header with the id5 bullet and that the size of the whole packet is 57 bytes where's it got that 57 from because that would be a good indication as to whether it's pushed the right amount of data in well we know that the header is uint32 for the id and you in 32 for the size so that's eight bytes we've then got another four here for this integer 12 a single one for this boolean 13 another four here for this float so that's 17 then we've got 8 here times 5 so that's 40 plus the 17 we had before that's 57 bytes so that's a good indication that the vector has all of our information in it but we can see if it's there or not so let's clear out these values now by setting them to something else and then reading those back out of our message body we look at our value a is 1 which is set to b is true which is good c is pi and d is a load of undefined junk because we didn't actually define it when we pushed it in so here we've created a very flexible and quite user-friendly way of constructing a message to which we always know all of the parameters we know how big it is and we know what the message is for and where possible the compiler is going to sanity check it for us now we have an established message format so it's time to start thinking about how those messages are moved around the system in a server client framework there are always going to be two halves one is the client one is the server in a massively multiplayer online game the client is typically the thing the player uses the way you're running around the map the server fulfills a few roles one it allows the information exchange between clients but two and perhaps more importantly it can also run the game itself it's responsible for what's going on in the game world and when you're developing a game it's very difficult to shoehorn in network later on so it's good to start thinking about your game's architecture from day zero with networking in mind and we'll see a lot more of that in the next couple of episodes but for now let's just focus on how we move messages around the client application is going to run doing its thing and whilst it's running it's going to check to see if any messages have been delivered between checks multiple messages could have been delivered so these messages are going to accumulate in an input message queue the client could however at any time send a message to the server in a very similar way the server is always running too and it too is periodically going to check a queue for incoming messages the server 2 is capable of sending a message at any time conceptually i want us to imagine that the client has a connection to the server and the server has a connection to the client these two connections are connected together via asio and sockets connections accumulate a queue of outgoing messages and send them but connections also receive messages and deposit them in the queues of the client and the server and as you can probably tell already the azio and socket framework does a little bit of a switcheroo clients can only have one connection but servers can have multiple connections and so in this instance i've added another client and its connection path and the server can choose to output to clients individually or it can output to them all simultaneously but an important feature for this framework is there is only one message input queue for the server this way our server application can run on a single thread responding to messages as they are sent by the clients hopefully in the order that they're sent when we draw out the architecture like this it's useful to start to see what are the common components well one of the things that we see is we've got cues and all of these cues well they're the same thing the queues of our messages so we know we need a q structure if you look carefully at these connection objects other than being reflected in the y-axis they're identical so we're going to need to create a connection object too the clients will share the same interface so i'm proposing we create a client interface object and finally we have the server the server is unique and different to a client in that it can be connected to multiple clients via multiple connection objects we can also see that the framework sufficiently abstracts away all of the asio specific code fundamentally whatever implements the client or the server needn't concern itself with actually how the networking code works so let's now add a few more interfaces to our framework i'm going to add to the net common project another header file called net thread safe queue tsq in this case now the reason it needs to be a thread safe queue is at any given time it's being accessed by the client or it's being accessed by the connection and especially in the case of the server there's plenty of connections possibly trying to write to that queue and the server will read from it whenever it needs to all of these things are happening at various points in time probably we haven't got control of when that happens as we saw earlier on in this video data takes time to move around and given that fundamentally we're basing all of this on an asynchronous input output library as we are taking all of these parallel sporadic requests and trying to serialize them into a queue we need to make sure that the queue is thread safe implementing a thread safe queue is actually not a very difficult task but i'm choosing to implement it using locks which means as something is trying to write to the queue nothing else can read from it there are lock free thread safe cues out there but i'm keeping this simple and accessible for the video in my olc net namespace i'm going to create a class queue and you'll notice it's already a template class this is because if i'm going to the effort of creating a threadsafe queue i don't need it to just store my messages i might as well make it applicable to storing well anything at all fundamentally my queue is going to store objects in a standard double ended queue or deck and i will be using a standard mutex to protect access to that double ended queue to the queue i'm going to use a default constructor and i'm not going to allow the queue to be copied primarily because it's got mutexes in it and really implementing a thread safe cue is just a case of adding guarding to the standard functions provided to the double ended queue so if i call the front method of my queue i expect to get a reference to the object at the front of the queue there it is deck q dot front but i'm using a standard scoped lock and my mutex to protect anything else from running whilst this particular line is being actioned as well as the front we'll have the back it's a double ended cue so we have the ability to push something to the back of the queue and push to the front we can also add some convenience functions such as empty count the number of items in the queue and clear which erases all of the items in the queue i'm going to add two more functions which are a variation of those provided by the double ended queue the first is pop front because i actually want it to return the item not just remove it so i apply my scoped lock and i locally cache the object before calling the double ended cues pop front function because that will just remove it it doesn't return it and then i return the item that i've just cached in a similar way i can pop back too since i have a clear function i can also add in a destructor which will call that clear function when the queue is destroyed and that's all there is to it all we've done is put a scoped lock guarding mechanism in place for the regular routines we would see on a double ended queue type as messages are being pushed around the system when they come into the server the server application needs to know where that message came from in case it needs to respond back to the same client from the server's perspective the thing that identifies a client is the connection so i'm going to add a modified message type which also contains a pointer to the connection that message came in on we can use the same thing at the client end too though in this case it's a little redundant because the clients will only ever have one connection which invariably points to the server to my message header file i'm going to add another object called an owned message fundamentally the owned message simply encapsulates a regular message but is also tagged with a shared pointer to a connection object and we've not defined the connection object just yet just for the sake of completeness i'm also going to overload the output stream operator so we can output this we're using standard c out this connection object we haven't defined but we are using it here so i need to forward declare that this connection exists and as it is implied here the connection is also going to be a template class in fact everything that we create as part of this framework will fundamentally be a template class relying on that enum class that defines the messages on the whole any messages coming into a client or a server will be tagged with the connection that they came in via that way if necessary we can specify that connection as a target of output i'm going to add three more header files net connection net server and net client to represent the three remaining objects in our framework in this specific video we won't get around to the full implementations of these but we can start to lay out the interface both the client and the server depend on a connection so let's define that first and our connection is going to include from the common our queue and our message fundamentally the connection is a template class but it's going to inherit from a rather strange object standard enabled shared from this this will allow us to create a shared pointer internally from within this object ultimately it's similar to the this keyword we've been accustomed to on the channel before but it provides a shared pointer to this rather than a raw pointer for now i'm going to leave the constructor and destructor blank i'll add some convenient utilities such as connect to server disconnect and is connected connector server will only be called by clients disconnect can be called by both clients and servers and will shut down the connection and isconnected returns well is the connection valid and open and currently active connections allow us to send messages so we'll have a send message function and you can see it's the message parameterized with the template variable t the connection is also responsible for the azio stuff each connection will have a socket we know that azio can't run and sockets can't function without an i o context and this is where things can get a little strange a server can have multiple connections but i don't want it to have multiple asio contexts i want the azio context to behave in tandem so the connection will be provided with an asio context by the client or the server interface recall that connections only contain cues of messages going out so i'm creating a thread safe queue of my regular message type called queue messages out the connection alongside asio will interrogate this queue and send the messages when required messages coming into the server of the client are stored in a queue owned by the server of the client but the connection needs to know where that queue is so i'm also going to create a thread safe queue of owned messages in this case but it's just a reference to a queue that queue is expected to be provided by the server or the client effectively the connection object is the glue in the client header file we're going to define a client interface class the client interface is responsible for setting up azio and setting up the connection but then also acts as an access point for your application to talk to the server as i've just mentioned in the connection the server and the client must provide a physical cue to store incoming messages so we'll add that to the client interface the client interface owns the asio context and as we saw at the start of the video the context alone doesn't do very much it needs a thread now you might be thinking i thought we said we were going to have the connection handle all of the azio stuff well that's true but the client will need to set up the connection and the connection will only exist if it's valid so to begin with the client will handle the azio stuff do whatever negotiation is required with the server and then set up a connection therefore the client will start out with a socket of its own and if a connection can be established it will create it as a unique pointer and then hand over the azio stuff to the connection this is how i've decided to do it you could change this for your own implementations other functions our client interface might need would be something like connect where we can pass in a nice friendly url and a port number if we've got a connect we usually always want to disconnect too so the client can shut down the connection it may be convenient for the client application to know that the connection is still valid and finally the client application is going to need access to this queue now the queue itself is private it could be made public but i'm going to use a little accessor function finally our class needs a constructor and a destructor the constructor is simply going to associate the socket with the azio context for the client there is only one socket and one context and when the client is shut down we'll call disconnect just to make sure everything does shut down cleanly we can actually fill some of these in now just intuitively even though the implementation for most of this framework doesn't exist for example is connected all we need to do is make sure that the connection does exist and then we'll call the connections isconnected function connect is a little bit more tricky and we'll cover that significantly in the next video it's in this function that we'll call azio to physically connect to the server now instead of using the error code i'm going to catch exceptions in this instance because the nice thing about azio errors as we said at the start is that they're quite verbose and there'll be several things we want to do in here firstly create the connection object then we actually want to create the address of where we're connecting to now at the start of this video we specified it directly as an ip address however seo provides an object called a resolver which can take in a url or a string form or an ip address and convert that into something it can use to connect across a network it just produces end points like we had at the start however the resolver can fail because if you type in a url that it can't reduce to an ip address then it will throw an exception if we do remain with a valid address we'll be able to call the connect to server function of the connection and then finally create the thread that the azio context can run within there are a few bits missing up here which we'll cover in the next video disconnecting then is just a way of being courteous we'll check if the connection is actually connected and if it is disconnect either way we want to make sure that the seo context isn't running and will stop the thread that it's running within since we've disconnected we're also done with the connection object so we'll release that unique pointer now i appreciate that this is going to be a little bit frustrating usually at the end of a video i like to give an example of it all working and running however because this is part one of a series that's not going to be the case there's simply too much code to fit into one video and come up with a working result and so instead i'm going to finish off by showing what writing the client application might look like to the end user of our framework and i think you might agree that it is a real simplification once you've established your message types as we've done here we can then derive from our client interface to create a custom client for our application and it's at this point that the templates start to disappear here i've created custom client and here i have derived it from our client interface but passed along our custom message type enum class the connect disconnect and this connect functions though they're all handled for us so we can get stuck straight into actual functional stuff so let's say i wanted to tell the server that i'm firing a bullet at a particular location i can simply create a function called firebullet which takes in a float x and a y this function then goes and creates the message passes in the data and sends it and then somewhere in our main game application we'll create an instance of our custom client connect to somewhere with a specific port and whenever we need to tell the server that a bullet is being fired we just call our function as it stands this code won't compile we've not built most of it we've not provided implementations for a lot of it either but i hope at this stage you can see that actually our framework has taken a complex phenomena such as a client server relationship and reduced it into quite a flexible yet simple and easy to use interface now this is always the case with part ones you're either really frustrated that we've not gotten anywhere or it's completely put you off because it's not finished or you're really eager for part two to see where it goes and in part two we will fully implement the connection the client and the server and actually have messages moving around it's just this video has gone on long enough but what we have got here already is a nice framework with an established messaging system and cues through which we can start doing these transactions at the moment we've only been taking a really high level look at the problems that we're going to have to solve in the next couple of videos and we'll be tackling things such as timing and making sure that the connections are robust and that all of our messages are appearing in the order that we actually expect them to how do we handle things such as network failure and client disconnects when we don't expect them and if progress is going well we'll look at how we can make login systems too so i hope you look forward to all of that in part three we'll start actually implementing a bit of a game using the raycast world engine i showed in the last video so we can have lots of characters running around amaze killing each other but for now if you've enjoyed this video a big thumbs up please have a think about subscribing come and have a chat on the discord i will put the source code up for this on the github but as you know it's incomplete it won't function but you can have a look at it for sure until then see you next time and take care
Info
Channel: javidx9
Views: 316,567
Rating: undefined out of 5
Keywords: one lone coder, onelonecoder, learning, programming, tutorial, c++, beginner, olcconsolegameengine, command prompt, ascii, game, game engine, pixelgameengine, olc::pixelgameengine, networking, asio, mmo, server, client
Id: 2hNdkYInj4g
Channel Id: undefined
Length: 58min 17sec (3497 seconds)
Published: Sat Oct 03 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.