Networking in C++ Part #2: MMO Client/Server, ASIO, Sockets & Connections

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hello and welcome to part two of my networking in c plus plus series where we're using the azio library to develop a cross-platform framework to allow us to easily integrate networking into our c plus plus applications this is a part two so if you've not seen part one i really recommend you go and see it first but those of you that have watched part one might have noticed something interesting we didn't actually do any networking but rest assured we'll certainly have that covered in this part as we will be developing a full client service system and exchanging data between a server and multiple clients so let's get started we ended the previous part by sketching out a very simple interface after we'd identified the major components of our framework i then went on to implement these thread safe cues which are all over the place and we started to craft some very basic interfaces for our client our connections but we didn't do anything with the server and as i alluded to in the introduction even though we implemented a message type we never actually sent any messages well it's time to put all of that right and i'm going to start with arguably what is the most fundamental component of this system the server the server is a program that is always running somewhere it could be on your machine it could be on a remote machine somewhere on the internet and the server will have an address for example on my machine it's 127.0.0.1 it's localhost to those that speak a bit of networking as well as the ip address it will also need a port so for example i'm using 60 000 as my port number fundamentally the server has two roles firstly it has to sit and listen for clients attempting to connect to it clients can connect at any time and this is one of the reasons why azio makes this a lot simpler when we hear the phrase anytime we may want to start thinking asynchronously and that's what azio is all about each time a client connects to our server a connection is created and as we've seen in part one these connections are going to be responsible for actually sending the messages to and from the server to the client the server also needs to be able to handle the disconnect of a client the client can disconnect at any time so again an asynchronous disconnect handler seems like a useful thing to have the server's second primary function is to handle the application logic now in this series ultimately we're going to end up with a small mmo which is a bit of an oxymoron because that stands for massively multiplayer online but i guess the word massive is subjective in this instance therefore as well as handling the connections of the clients it's also got to implement game logic as a consequence i'm proposing that we develop a server interface that can behind the scenes handle all of this networking stuff but can be inherited from by some user created subclass to handle a server-side logic we very briefly looked at a similar situation in part one where actually the client interface is subclassed by the user to provide the client-side functionality they require fundamentally the framework we're trying to develop completely abstracts away all of the networking stuff asynchronously or not in part one i introduced the concept of asio and asio contexts the asio context is the thing that allows azio to do its work but it must be primed with tasks if there is a situation such as here in this empty box that the asio context doesn't have any work to do the asia context will end and close and finish and terminate and whatever you want it becomes redundant i also showed that there was an artificial way to give the azio context some work to do to stop it from closing that was necessary to demonstrate a point in part one but for this framework is entirely unnecessary all of the asia context we create will have something to do at all times and one of the things we want the server context to do is simply sit there waiting for connections and in asia parlance this is accepting where asia will wait in the background for something to connect to it at which point this accept connection task performs its work since a client has connected a socket will have been created therefore we must create one of our connection objects to handle that socket at this point i'm going to give the server the opportunity to reject the connection for example it may be on a known band ip address or we may have too many connections already there could be various reasons if we do choose to reject i'm simply going to reprime the asio context to accept another connection should we choose to accept the connection i'm then going to establish it in the server which in this instance means i'm going to place my connection object into a container of connections and then finally i'm going to issue the task to accept the next connection again connection objects on my server will be shared pointers therefore even if we accept a connection but then go on to reject it in principle nothing will own the connection object and it will delete itself this is one of those situations where smart pointers are actually a very useful thing because stuff is happening asynchronously and i can rely on the smart pointer to keep the system nice and tidy hopefully you can see that the server's asio context will never expire it always has the job of accepting new connections but because asio is asynchronous and can do multiple things in parallel we can also give the context other work to do as well in fact once a connection is established the first thing it will do is allocate some work for itself in this case it will prime the azio context to read a header which was the header component of our message object and each connection will assign its own read header task to the context as the clients come and go and maybe nobody likes the game so there's nobody ever playing it we know that at least the asio context won't stop because it's going to sit waiting for connections therefore our server won't stop unless it crashes but it's not going to do that because you all write awesome code now let's assume that on one of these connections the read header task fires i.e one of the clients has sent enough bytes to fulfill a message header in our case it's eight bytes because recall from the previous video that it's good to always know the size of things when you're communicating them and we know that our headers are always going to be a fixed size the header may indicate the presence of a message body at which point the read header task for that connection expires but the connection itself replaces it with a read body task we know when that task is complete because the header said how big the body was therefore once the read body task has expired we have acquired a complete message from that connection and so we'll add that message to the server's thread safe incoming message queue and then the connection will re-prime the azio context for the server with read header task again the server side logic can then go and deal with these incoming messages as it needs to as you can see here we didn't get very far with our server implementation so let's flesh out the interface i know i'm going to need various bits i've already coded for my common file the threadsafe queue the message and the connection objects and because i'm making a header only framework i'm placing everything in an olc net namespace also recall that everything in our network framework depends on a customized message type so we're going to create template classes in the constructor i'm just going to take in a 16-bit integer to represent the port number we wish to establish the server to listen to and whilst i'm here i'll just throw in a destructor 2. we're going to need a way to start the server and if something can be started we should also provide a facility to stop now you may think that the mere existence of a server interface object should be enough to call the start and stop functions but as we're developing a framework we want to try and anticipate as many of the user's end needs as possible since the user will be sub typing our server object they may want initialization code before they start opening the server to the outside world or they may want a facility to simply stop the server from functioning but not have the server program closed down now i'll add some of the utilities i think we're going to need so the first and i've labeled this here as asynchronous is a task for the asio context i'm using this comment async notation so we can differentiate between the tasks that asio performs and other tasks going on in our framework we want the server to be able to send a message to a client our server is going to store the client's connections as shared pointers so this function can identify the client and the message too another convenient function is the ability to message all of the clients at once in this case we simply pass in the message but i'm also passing in the ability to ignore a specific client and we'll see why later we know that this class is going to be a base class which implies there's going to be some functions overwritten in the user's subclass for now i'm interested in delivering three different types of information to the user the first is when a client connects here we pass in the client's connection object but will allow the user to return a boolean value as to whether the client can be accepted or not so here we could put in a check for the max number of clients or we could check the client's ip address and ban it since the framework makes no assumptions about the user's server side logic i'm also going to include a function which is called when a client is disconnected for example in a game this could allow the user to remove the client from the game world so it's no longer part of the simulation finally and perhaps the most important one is on message which allows the server to deal with a message from a specific client we've established that the server will own its own thread safe queue of messages and that for asio to function we need an asio context in this instance the context is shared across all of the connected clients and in part one i showed that azio contexts need a thread one of the things that the server doesn't have is a socket of its own well it kind of does but it's hidden from us by the asio library but we do need to get the sockets of the connected clients and we'll do this through a special object called an asio acceptor and now i'm going to throw in something we've not talked about yet every client in the system will be represented by a numeric identifier the value of this identifier is irrelevant as long as it's unique for every connection this serves two purposes firstly it's a consistent id across the entire system and this information will be delivered to the clients as they connect so they know their own id and potentially they can know the id of other clients in the network and secondly i've chosen this approach because even though the clients do fundamentally have a unique ip address and port number which we could use as an identifier i'm not that comfortable with sending that information out to the other clients but we'll also see that simply working with a numeric id is much simpler than working with ip addresses anyway the acceptor object we've just created needs to be initialized from the get go we associate it with a context and this in a way is that hidden socket i was telling you about it is the address upon which the server will listen to connections and in this instance i'm specifying this is a version 4 type ip address and i simply pass along the port number passed to our constructor in the event of our server interface being destroyed i'm going to be nice and tidy and call our stop function the start function has two tasks and i'm going to put these inside a try and catch block because when azio fails it can throw exceptions and we can catch that exception and display it to the console the first thing i want to do is issue some work to the azio context so i'm going to give it the task to wait for the client connection and then and the ordering here is important i'm going to start the context by calling its run method in a thread of its own if i did this the other way around recall from part one that the context could potentially immediately run out of work and close so by issuing some work before we start the context doing its thing we've guaranteed there's a task there to keep it alive i find when i'm developing server client applications using the console to display the state of both the server and the client at different times is a fantastic way of debugging them but you do want to be careful with this too much console output will negatively affect the performance of your server application so keep it there for the big things like connects and disconnects but not for the little things that are going to happen thousands and thousands of times like message transfers if the stop function is called we can tell the asia context to attempt to try and stop it will attempt to try and close the tasks that it's got that might take a little bit of time so we'll wait on the thread by calling the join method to ensure that both azio and the thread have stopped and if anybody cares i'll also throw out the message the server has stopped now we're ready for our first asynchronous task for the asio context to perform the asio context has been associated with our acceptor object so i'm going to call the async accept function of the acceptor object and recall that with these functions you pass in a lambda function which does the work when whatever causes the async accept function to fire it'll go ahead and execute that lambda function in this instance the lambda function will receive an error code and a socket for that connection let's be sensitive to that error code if there was an error we'll display it and regardless of what path we take through this function fundamentally we don't want the azio context to have nothing to do so the last thing it will do is call the wait for client connection function again re-registering another asynchronous task if the connection attempt was successful i'm going to display that to the console and i'm going to call the remote endpoint function of the socket to give me the ip address i'm now going to temporarily create a new connection called newcom now it's a shared pointer so i need to use the make sure function to allocate the object for us and now i'm going to add something new which we've not discussed but we will in a minute i'm going to tell the connection that it is fundamentally owned by a server and this is simply because we want to tailor how the connection behaves depending on whether it is primarily owned by a server or owned by a client both the server and the client will use the exact same connection object but there is a slight difference around the edges other information i'll pass to the constructor of the connection object is the current asio context the socket that was provided by the async accept function and i'll pass by reference the incoming message queue of the server of which throughout this entire server instance there is only one instance of this queue therefore the queue becomes shared across all of the connections but it's thread safe when we're adding messages to it now going back to the concerns of our framework i want to give the user the opportunity to deny this connection so i will call the on client connect function by default it's going to return false so the user must provide some sort of override to return true if the user does deny the connection i'm simply going to display that to the server i mentioned before that the server will maintain a container of all of the connections that it is interested in so i need to add that container to our server interface class and i'm going to use a standard deck which is going to contain the shared pointer to our connection objects therefore if the user did allow the client to connect i will then push to the back of this container the new connection object we've just created up here if the connection was denied because we used a smart pointer newcom will go out of scope and the connection will be deleted valid connections will need to be assigned their identifier and i'll do that through an as yet undescribed connect to client method of our connection object where i simply pass in the id counter that we've got and increment it and then i'll display on the console the connections id and tell them that the connection has been approved a very quick recap we issue a task to the asio context to sit and wait for incoming connections when something does try and connect we'll create a new object called new connection we'll give the opportunity to the user to deny that connection but if they choose to accept it we'll add that connection to our container of connections we'll allocate it in id and we're done regardless we need to re-prime the context with more work to do so we'll just sit there waiting for the next client to connect let's now consider how we send messages to clients in principle it's very simple firstly we'll make sure that the client shared pointer is valid and that the client is indeed still connected the isconnected function on the connection will go away and check that the socket is still valid if all is good then we'll simply call the send function of our connection but there is a slight complexity one of the limitations of networking using the tcp protocol is we don't necessarily know when a client has disconnected why should we it's disconnected it's not going to send that fact it could have disconnected because somebody has yanked the cable out of the wall and so it is only when we try to manipulate the client and that manipulation fails do we have any inkling that the client is no longer there and so by testing to see if the connection socket is still valid we know if we can or can't communicate with the client in the event that we can't i've little alternative other than to assume the client has disconnected at least that's what i'm going to do for this framework so i will call the on client disconnect function allowing the user's server side code to do something about it perhaps removing the client from the game world the client is no longer valid so we'll delete it and since we can identify deleted clients we can use the erase remove idiom to entirely remove the client from our deck of connections if we had many clients connected this erasure could become quite an expensive operation therefore i want to take that into account when we message all clients in principle the message all clients function is just going to iterate through every connection in our deck of connections and as before we check that it's connected i'm also going to make sure that the client we're trying to talk to isn't the one we've specified via our ignore client parameter by default this is set to null pointer so that's handy because if the client just happened to be null pointer we wouldn't want to send the message to it anyway as before the client may no longer exist so we want to disconnect and reset but instead of calling the erase function specifically for this client i'm going to set a boolean flag which i'll declare before the start of my loop once i've finished looping through all of the clients it doesn't matter if a bunch of them have died because now i only need to call my eraser move once admittedly it's a small optimization but it does have one other very important point we don't want to change our deck of connections as we're iterating through it because if we do we may invalidate the iterators for this loop and well all hell's going to break loose we implemented three overrideable functions the third is on message so where is this called if we wanted to remain completely in the asynchronous domain we could somehow connect the client's connection to this function and when it receives a message it calls it however this is a framework decision and instead i've chosen to serialize all of those asynchronous transactions into a single queue i'm going to add an additional function update which is called by the user to explicitly process some of the messages in that queue this allows the user to decide when is the most appropriate time to actually handle incoming messages in the server side application logic as the server gets busier and busier and lots more messages start transferring it could very well be the case that this function never returns it's just constantly processing messages added to the incoming message queue and therefore the server side application logic wouldn't be updated at all so i'm providing the user with the ability to to constrain the number of messages that get processed in one go this looks a little unusual but since size t is an unsigned integer setting it to -1 sets it to the maximum number of messages this function is quite simple too i'm just going to sit in a while loop which is contingent upon firstly not exceeding the message count but also that there are indeed some messages in the queue if there are some messages i'm going to pop it off the front of the queue which will remove it in our queue implementation pass that message onto the message handler and don't forget that these queues are owned messages so there is a shared pointer to the particular client and the message contents itself we've now implemented a bur bone server interface it handles connections and it will handle storing incoming messages into the queue and gives the user a way to process these messages so let's have a go at testing it using a tool that most network programmers really love and depend upon quite frankly called putty to test if our server is generally working the first thing i'm going to do is simplify our wait for client connection simply because we've not yet really implemented the connection object but i want to see if our server is open for connections or not so i'm just going to temporarily comment out the creation of a new connection and any code that directly relies on that i've added another project to the solution and set it up in the same way as the other two and to that project i've added a single source file where i'm defining my custom message types defining a custom server class which inherits from our server interface specialized to our custom message types the constructor of this subclass takes in the port number and constructs the server interface finally all it does is provide implementations for the three functions we expect the user to override i've changed the on client connect function to true in the program's main function i create an instance of our custom server object and tell it that we're interested in using port 60 000. then i call the server to start finally i'm just going to sit in a tight loop continuously calling the update function so let's take a look now this might happen quite a bit when you're doing networking programming this is the first time i've compiled my server example and the firewall has caught out the fact that it's found an executable it's not seen before trying to access the network it can be a little bit frustrating but hey at least the firewall is doing its job in this instance it's quite safe to allow access because i've programmed it and we can see in this console window that the server has started i like to test things with servers using the putty tool and i want to try and connect to my server so i'm going to choose a raw connection and give it the location of my server which is currently on my machine on port 60 000 i recommend that you save these configurations because then you don't have to type it in each time once you've entered this information click open and we can see straight away well we got a new connection on our server you'll notice the putty window immediately closed that implies that our server well disconnected the client let's see why when the async accept function actually fired it provided us with a socket but we didn't keep hold of that socket at any point so it went out of scope and got destroyed putty picked this up and closed the connection well at least we know we can connect to the server but to do anything useful with it we need to start handling our connection object the connection object we started in the previous episode is incomplete in fact it has no implementations at all but now we have a working server we need to flesh this out and the first thing i'll introduce is an enum class called owner and this will handle that little ownership requirement i mentioned earlier the connection is wholly defined by the parameters passed in via its constructor so the first thing will be what is the owner then we pass in a reference to the asio io context that we're interested in using so this will be owned by either the server object or the client interface object thirdly connections have a socket and they have a socket unique and wholly owned by them and we know that connections must also point to an incoming message queue on either the client interface or the server interface so we'll pass that in by reference we can see in the fields related to the connection that actually the azio context and the incoming message queue are indeed references here so we must define those references at the point of construction in the constructor body i'll also store the parent now i can do that the same way as i've done it here and i'm sure people will be complaining that i haven't but let me explain the reason why in my head i consider these items to be critical infrastructure of this class indeed two of them are references that must be fulfilled however the owner of the connection is reasonably lightweight so for me this just works as a mental separation between what is really critical and what isn't since we now do have ownership i'm going to add a member variable owner type to store that whilst i'm here we also know that we're going to start allocating identifiers to clients so i'm going to store that here too and create a little getter to return that whenever we need it upon a successful connection our servo needs to call a connect to client function to issue this id but also start the process of transferring messages to and from the server and client so i'm going to add in a connect to client function here this is only relevant if the owner is a server and assuming everything went okay with the connection attempt i'm going to store the id when we're looking at things from the client's perspective that's when we're going to call connect to server so we'll look at that a little later for now i am however interested that the is connected function tells me if the socket is valid or not now that we can sensibly construct a connection i'm going to recompile our simple server program and run it then i'll use putty again to connect to our server this time we can see that the putty terminal hasn't disappeared and indeed we've connected and the server has approved the connection let's open up another putty instance and again connect to our server this time the server has again found the connection given it a unique identifier we can see that the clients are coming in from the same ip address it's my machine but they're coming in on different ports and if i close the server well putty detects that and cancels one of the things putty is useful for is actually transmitting and receiving data from servers but as it stands the connection object in our framework has no notion of the concept of reading or writing our messages so that's what we'll implement next i'm going to add four more functions and these are all going to be asynchronous tasks we'll give the azio context to perform the first is to read a header and the second is to read a body i'm sure you've already guessed what the other two are we need to write a header and also write a body as before we issue tasks by calling an asio function and passing in a lambda function to read a header i'm going to call the asyncread function and i know the size of the header already we've defined it as part of our framework and our custom message type so i will instruct azio to read from the particular socket associated with this connection an azio buffer with enough space to store a complete message header i need somewhere to actually store this data so i'm going to create a temporary message header in variable and add it to our connection class now that we've specified the asynchronous trigger we need to specify the lambda function to provide the work to do when that trigger fires as before we'll check that the error code is in our favor if it isn't for debugging purposes i'm going to output that fact and i'm going to manually force close the socket recall that closing a socket will be detected by the server or the client when it attempts to communicate with that connection in the future since it will fail to communicate it will then go and tidy up the client from our deck of connections however let's be optimistic and assume that it's read the message just fine it has read a full message header we can interrogate the message header that's just been read in to see if there is a message body which will be indicated because the size value specified in the header will be greater than zero this means we can allocate in advance enough space to store our message body in the messages body vector and then we can instruct asio to read the body i.e will register another asynchronous task with the asio context and that's what it'll do next it's very possible that we don't have bodies for all of our messages in which case at this point our temporary message is complete and we should add it to the incoming message queue of the owner of this connection and we'll come back to that in a moment now that we know something can register the read body command and we know the size of the data we're expecting to read we can call the async read command again on the same socket with a buffer and pointer to the right location if the read was successful we've now got a complete message with both header and body so we'll add it to the incoming message queue or we're in a situation we had before something has failed so we'll just close the socket both read header and read body have the potential to call add to incoming message queue so let's add that function too if the owner is a server we want to transform this message into an owned message and push it into the server's incoming message queue the owned message requires a pointer to the connection in fact it requires a shared pointer which i can extract from the shared from this function and here i simply use an initializer list to construct the own message struct however if the owner of this connection is a client then actually tagging the connection doesn't make any sense because clients only have one connection anyway so i'm just going to specify null pointer this is quite an important distinction because i want to enforce that a client can only have one connection in the client interface the connections will be stored as a single unique pointer therefore i can't use the shirt from this function to create a shared pointer for that message object regardless the add to incoming message queue function has one very more important function it is always called when we have finished reading a message so we'll use this opportunity to register another task for the asia context to perform in this case simply wait and read another header writing a message header and body is quite similar the fundamental difference being is these won't sit and wait for something to happen they'll try and execute their workloads immediately this time i'll use the async write function and construct an azio buffer large enough to send a complete message header and as before we want to do some safety checks and as before we also want to see if there is a body to send this time i'm looking just directly at the size of the body vector i could use the body's header size as well if it didn't have a body what do we do next well firstly we're done with the message in the queue so i'm going to remove it with the pop front function but secondly we can also use this opportunity to make sure that there aren't more messages to send by checking if the queue is empty if it isn't empty we may as well call the right header function again and get it to send the next message writing the message body follows a very similar pattern again if we have been successful in writing the body's bytes then we pop the message from the front of the queue and see if there are any more and if there are then we try and write the next one else there was a failure and we'll close the socket and that's it now that we have the ability to transfer messages when do we actually do it read header is one of those tasks that we want to prime the context with and so it just happens when it happens either client has sent a message we can register that task when the server calls the connector client function writing a message is a little different because we don't know when that's going to occur but it's only going to occur when the user makes it happen we have already this unimplemented send function and here we will use a slightly different azio concept the context is already happily waiting for incoming client connections and it's sat there primed waiting to read a header i need to give it another job to do and i can use the asio post function to send a job to an asio context whenever i need to as you may have come to expect already we send that job in the form of a lambda function but we need to start thinking a little bit asynchronously here it's very tempting to just say well right header but that's no good because right now the message doesn't exist in our outgoing message queue and our right header function relies on the thing at the front of the queue well that's easily enough we could push the message to our queue and then call the right header function however what happens if our asio context is already in the middle of writing a header or a body things will start to happen out of sequence and that will lead to a state we can't recover from we can make an assumption that if there are messages in our outgoing message queue already that in the background asio is busy sending them and therefore there's an intrinsic loop already of right headers and right bodies we don't need to add another task to that sequence so i'll call this right header function based upon whether the queue is empty or not and it's things like this that can make azio a little fiddly to think about basically we want to avoid adding another right header workload to an asia context that already has right header or right body workloads so by checking the state of the message out queue beforehand we can assess whether asio is already busy doing writing messages or we need to restart that right messaging process this post function is quite a powerful way of injecting work into an asia context we can also use it in our disconnect function where we can explicitly close the socket when it's appropriate for azio to do so now we have a working server and we have a working connection object where we can transfer messages about but quite deliberately our framework only allows the transmission of our message objects so i can't really sensibly use putty to connect to the server and send it information i now need to implement the client fortunately in the last episode we got quite far with the client interface because at the time most of it was quite intuitive to implement without knowing how the underlying objects really work the client interface maintains its own context and a thread for that context and here it has a unique pointer to the singular connection that it is allowed to have just like the server the client is responsible for maintaining a queue of incoming messages in fact the only part we didn't implement fully was the connect function simply because we had nothing to connect to at the time one of the things i wanted to implement was the ability to specify full domain names to connect to so you don't have to work with ip addresses each time and this is a good thing because a domain name can point to a different ip address at some point in the future this means we have to somehow turn that domain name into an ip address that the system can understand fortunately asio yet again has us covered we can create what is called an azio resolver object and associate it with the context the resolver object goes away and does some networking magic to resolve the url of the host and it could be the ip address it could just be any kind of legitimate url and our port number and resolves that into an end point and if you recall from the first video the endpoint is the the real thing that is used behind the scenes to connect the network together if we can't resolve the url supplied then an exception will be thrown and our function will return false but if it can we can then go away and create our unique pointer to our connection object in this instance we know that the owner is now a client we'll pass in a reference to the azio context and we'll create a brand new fresh out of the oven socket for things to happen with and finally the constructor requires that we pass by reference the queue to our incoming messages as with the server we don't want to start the context thread without giving the context some work to do but as of yet we're still not actually connected to the server either so we'll do both of those in one hit with our connect to server function where i'll pass in the end points that were resolved from the host that we provided let's go back to our connection object and fill this in the only thing we actually pass in are the resolved endpoints this function is only relevant to clients and i'll give azio a task to do now which is to asynchronously connect the socket to the endpoint of course we need to provide a lambda function when that event occurs and in this instance all our lambda function is going to do is issue the task to read the header the azio context is now primed waiting for messages in this instance to come from the server this is the customized client class from the first video the first thing i'll do is replace our custom message types so we have the same message types between the server and the client in practical terms it would be better to have these defined in a header file that's shared between both the server and client projects we're no longer interested in firing bullets so we'll just remove that for now so now with our very simple server and our very simple client let's launch them both and see what happens now you may find it much easier to go into the output of your project folder and launch the applications individually rather than using the debugger up here so i'm going to launch the server and now i'm going to launch a client well the client launched and immediately quit because that's what the code told it to do and what we see on the server is that the connection came in it was approved but then it failed at reading the header well that's simply because the client disconnected whilst the server was asynchronously waiting for it to receive a message header so a better approach would be to modify our client program to allow us to selectively send different types of message now i'm going to be dipping directly into a bit of windows specific code here but i'm sure the linux guys out there will be able to come up with an alternative after a successful connection in our client i'm going to sit in a loop waiting until we set a quit flag to true in that loop i'm going to check to see is the client still connected and does the client have any incoming messages on its queue if for some reason the client is no longer connected then i'm going to set my quit flag but i also want to put in some user options and i'm going to dip into some code from the console game engine here way back when where i'm going to use the function that's windows specific get async key state to tell me which key i have pressed and it will do this in a way that doesn't block so this will allow my while loop to continuously keep running so i can continuously keep checking for messages but at the same time i can grab a little bit of user input specifically i'm interested in the keys 1 2 or 3 being pressed now since get async key state returns the momentary state of a particular key i need to put in a little latch like this to make sure i only see the event of when the key is pressed so for example if the three key is pressed i can set my quit flag to true directly if my one key is pressed i'm going to call a function in my client called ping server so let's implement that we'll create the function ping server and then i want to create a specific message using one of our custom message types server ping now i'm going to do something here which will no doubt get all of the armchair experts really riled up but it makes for an interesting video i'm going to grab a time point from the chrono library yes all of this is just grabbing a particular time and i'm going to send that time point to the server in the expectation that the server will send it back to me and i can compare the two time points to see what the round trip time from client to server back to client is this is quite useful but just be cautious with doing this because it depends how system clock is implemented on your platform i happen to know in this instance that well my server and my client are running on exactly the same computer so this won't be a problem and in practice it's likely not going to be a problem either just be cautious with it since we've created a really user friendly message type i can just take that time point and shove it into the message and finally send the message hopefully the server will respond so if there is a message on my incoming message queue i'm going to pop it from the top and then simply handle a message on a case-by-case basis by examining the ids i expect the server to just send the message back to me so again i'll grab a time point which is now and i'll create another time point which will then into the then time point i will extract the time sent from the incoming message and by subtracting the two and turning them into a chrono duration and all of the other chrono rubbish you have to do in order to display this number i will display the round trip time in seconds so far we've assumed that the client will understand what a server ping message is but of course it doesn't we've not programmed it to so let's do that in our simple server implementation we get this function called when a message arrives so in a similar way we're just going to look at the header and respond to it in the case of a server ping i'm going to display to the service console that that has happened and i'm going to just simply bounce the message back to the client the onmessage function tells me which client the messages come in by and what the message contents is i'm not going to change them or interfere with them in any way because all the message body contains is the time at the point it was taken on the client so it's important to note here all we've done now is customize the server and client interfaces we don't need to handle the framework in the background so let's try this out i've compiled both of these programs so i'm going to start the server there we go and now i'll start the client and the client doesn't display anything until we press a key because it will only display something when it receives an event from the server but we can see so far that the server has received the connection so let's press one it sent a ping message we see the server has received it and it sent it back and the total round trip time in this case happened to be about 0.6 milliseconds let's press one again and each time in our client we're sending a message to the server let's open another client i'm going to have lots of windows well we can see the second connection came through it got allocated an id that was unique and i can press 1 in this client window and we get a ping press it back in this one and we get a ping and it doesn't affect the original client if i close one of the clients we see we get a read header fail message that's because the server was primed ready to read a message header for that particular connection and it couldn't so it failed now we know behind the scenes that the server has removed that connection so the server keeps on running and doesn't interfere with the existing connected clients let's add one more function message all here the client is going to send something that it wants the server to distribute to all of the other clients it's a bodyless message in this case we're just sending the header and we'll do that when we press the 2 button when we handle the messages let's be sensitive to a particular message called server message this message packet will contain the client id that sent the message all request and whilst we're here let's also be sensitive to a message called server accept so that tells us that this client has been validated by the server let's go back to our server implementation and respond to the request to message all of the other clients we'll construct a message with the id server message and into the body of that message i'm going to put in the client id that sent the request in the first place then i will call the message all clients function of the server interface specifying the message but also telling it to explicitly ignore the client that sent the message in the first place i'm also going to add some bodies to these two functions here might be quite useful to know when the server has removed a client and informing the client that it has been accepted is quite a useful thing to do that way the client can sit and wait or do something else until it is accepted so we'll create a message called server accept and we'll just send that to the client so let's try this out we'll start the server and create a client so we see that the server accepted the connection and it responded by sending a message back to the client in fact let's start a few clients and move this out the way now we know if we press the one key we get pings and we can see in the server that the pings are tagged with the ids from the unique clients if i press the 2 key this particular client is going to want to send a message to the others press 2. the server received a message all request from this client and sent that message which contained the origins client id to the other two clients do that again sends messages if i do it from this bottom client here we can see the other two clients receive the messages let's see what happens when a client disconnects so i'll close the original client we know that we got a read header fail but again it doesn't affect the other clients that are connected however it was only when we attempted to send a message from the server to a client again was the server award that it could actually remove the client that disconnected now i'm running the server and clients on my local machine you could of course run the server anywhere you have access to the ping times will increase accordingly but the behavior will be the same so what we have established here is a working framework to handle networking for us we can send message packets that we are in control of and can define and we can override a simple client and server interface with our own functionality to implement our own application logic at this stage it's robust enough for you to tinker with and start playing with and i will put the code on the github below but it's not quite ready yet to be released into the real world and we'll see why and what to do about it in part three where we start to implement this framework into a simple game until then if you've enjoyed this video a big thumbs up please have a think about subscribing come and have a chat on the discord server and i'll see you next time take care
Info
Channel: javidx9
Views: 69,449
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, client, server
Id: UbjxGvrDrbw
Channel Id: undefined
Length: 48min 13sec (2893 seconds)
Published: Sun Oct 11 2020
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.