Jump to content

  • Log In with Google      Sign In   
  • Create Account


to those who have read the boost::asio getting started guide


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

  • You cannot reply to this topic
3 replies to this topic

#1 breakspirit   Members   -  Reputation: 100

Like
0Likes
Like

Posted 29 September 2011 - 08:37 PM

I'm trying to modify the code in the boost::asio guide on this site http://www.gamedev.n...with-boostasio/ (namely page 10, network.h, network.cpp, and example 9a). The given code only accepts one connection at a time and then errors when that person disconnects.

I'm having a hard time understanding how to make it connect more than one person at once and continue functioning after people randomly disconnect(and not just sit in a permanent error state). The FAQ on this forum says that using tcp, there should be a socket per connection. Would that mean that I should use an array of Connection objects? What about the Hive and Acceptor? They're all kind of tied together into a big mess of classes and methods that are interdependent.

My understanding is that I want multiple sockets so that I can use them to determine who gets sent what from the server and I can tell which clients are sending me what. Is that accurate?

It's a shame that the documentation is so notoriously lacking for such a potentially useful library. If they even had an official forum, I'd post questions there, but alas they do not.

Anyway, thanks for any help you guys can give!

Sponsor:

#2 wood_brian   Banned   -  Reputation: 197

Like
1Likes
Like

Posted 29 September 2011 - 08:49 PM

I'm trying to modify the code in the boost::asio guide on this site http://www.gamedev.n...with-boostasio/ (namely page 10, network.h, network.cpp, and example 9a). The given code only accepts one connection at a time and then errors when that person disconnects.

I'm having a hard time understanding how to make it connect more than one person at once and continue functioning after people randomly disconnect(and not just sit in a permanent error state). The FAQ on this forum says that using tcp, there should be a socket per connection. Would that mean that I should use an array of Connection objects? What about the Hive and Acceptor? They're all kind of tied together into a big mess of classes and methods that are interdependent.

My understanding is that I want multiple sockets so that I can use them to determine who gets sent what from the server and I can tell which clients are sending me what. Is that accurate?

It's a shame that the documentation is so notoriously lacking for such a potentially useful library. If they even had an official forum, I'd post questions there, but alas they do not.

Anyway, thanks for any help you guys can give!


I think the Boost developer and user mailing lists are the official forum -- http://boost.org.

Brian Wood
Ebenezer Enterprises
http://webEbenezer.net

#3 Drew_Benton   Crossbones+   -  Reputation: 1713

Like
2Likes
Like

Posted 30 September 2011 - 12:34 AM

I'm having a hard time understanding how to make it connect more than one person at once and continue functioning after people randomly disconnect(and not just sit in a permanent error state).


In typical network programming, you have a listening socket that calls accept over and over to accept new incoming sockets (or just once to accept 1 connection, as most trivial examples do). Depending on the networking approach you take, whether it's blocking or non-blocking, event based or asynchronous, you will obtain a handle to the connection to work with via a socket.

The lines (in the main function):
 boost::shared_ptr< MyConnection > connection( new MyConnection( hive ) );
acceptor->Accept( connection );
represent one such instance of performing that logic.

The key difference in this approach is, rather than working with a raw socket handle, you work with a connection object that maintains the socket handle for you as well as providing you the context object to work from. So, rather than accepting a new connection and then allocating a context for it later on, a context is allocated for it initially and tied to the socket object. This is useful when you need to associate meaningful user data with each socket that you post for accepting sooner, rather than later. Some designs are better implemented this way, while others are not.

Either way, you can choose two different methods for "refilling" the pending accept queue: 1. post a lot of accepts up front to handle bursts of incoming connections (like a high use web server might) and refill when it hits a specific low threshold or 2. post one accept and then post another accept after that accept has completed (thus limiting your connection acceptance rate, but requiring less resources at any given time compared to keeping a pool of them.) You can actually mix the two, posting a lot of accepts up front and refill them as they are processed, depending on your application needs as well. Not all servers or networking projects are meant to infinity accept new connections though, so depending on your needs, you do have to tailor examples to your needs.

Once a connection is accepted, MyAcceptor::OnAccept is called for you to verify if the connection is allowed (think in terms of software application specific firewall). If it is allowed, the MyConnection::OnAccept function is then invoked. Alas, a new connection is never posted again for accepting, so you cannot accept more connections! To remedy this behavior, you simply create a new MyConnection object and pass it to Accept of the acceptor. The follow code is the new MyAcceptor::OnAccept function that accepts more than one connection:


bool OnAccept( boost::shared_ptr< Connection > connection, const std::string & host, uint16_t port )
{
	boost::shared_ptr< MyConnection > new_connection( new MyConnection( GetHive() ) );
	this->Accept( new_connection );

	global_stream_lock.lock();
	std::cout << "[" << __FUNCTION__ << "] " << host << ":" << port << std::endl;
	global_stream_lock.unlock();

	return true;
}


Each time a connection is accepted, a new connection is posted so you can handle more than one connection. When you use this approach, you have to ensure you post the new connection for acceptance first, as if you accidently skip that logic, then no more connections will be accepted.

If instead you wanted to signal the another thread based on an event, the logic in the main function would be used instead:

boost::shared_ptr< MyConnection > connection( new MyConnection( hive ) );
acceptor->Accept( connection );

Putting implementation specifics aside, the key thing to understand here is that for every connection you wish to accept, you have to post another accept for the next connection. The server is not in an error state; it's simply not accepting any more connections! This was by design for that simple example (as is pretty standard, see accept on MSDN for reference). In terms of other boost::asio examples that show this, the chat server example does a good job. Bolded below are the lines of interest (hm, seems bold tags don't work correctly, look at the b and /b in square brackets):
 class chat_server
{
public:
  chat_server(boost::asio::io_service& io_service,
      const tcp::endpoint& endpoint)
    : io_service_(io_service),
      acceptor_(io_service, endpoint)
  {
      [b]start_accept();[/b]
  }

  [b]void start_accept()
  {
    chat_session_ptr new_session(new chat_session(io_service_, room_));
    acceptor_.async_accept(new_session->socket(),
        boost::bind( [b] &chat_server::handle_accept [/b], this, new_session,
          boost::asio::placeholders::error));
  }[/b]

  void [b]handle_accept[/b](chat_session_ptr session,
      const boost::system::error_code& error)
  {
    if (!error)
    {
      session->start();
    }

    [b]start_accept(); [/b]// NOTE: Can be an issue if start() throws!
  }

private:
  boost::asio::io_service& io_service_;
  tcp::acceptor acceptor_;
  chat_room room_;
};


From that code, an accept is first posted upon construction, then after each subsequent incoming connection is accepted, a new accept is posted. The danger of posting the accept at the end of the handler is what I was mentioning before.

They're all kind of tied together into a big mess of classes and methods that are interdependent.


It's only a few hundred lines of code! If you think that's a big mess, then heaven forbid you actually look through the boost::asio library code! ;) Most of the mess you see is related to the look and feel of boost using the C++ language and not the logic at hand. The interdependency of the code is by design and pretty much unavoidable since it wraps up the boost::asio library. For more information on this, check out the following page: The Proactor Design Pattern: Concurrency Without Threads.

The code is organized as follows:

Hive - Wraps up the boost::asio io_service object and the work object into one discrete object you can control the 'master' system from. Any objects that are constructed using the Hive object can be serviced through that Hive object.

Acceptor - Wraps up an boost::asio acceptor object to allow you to accept incoming connections. Note: An acceptor is a general purpose name, do not think of it as a "server" as the concept of a server is far more specific in functionality while an Acceptor simply accepts/maintains new connections. You can however, turn an Acceptor into a server using the wrapper, since that's the way the code is designed, to make things easier to work with,

Connection - Wraps up a boost::asio socket object that represents a connection to a remote host or an incoming connection.

To implement your own custom logic upon events and context specific data, you derive your own classes as the examples do and then get to work. While it might seem trivial, if you look through all my examples before the wrapper and imagine doing that for every project, hopefully you can see how tiresome and messy it'd be.

The "purpose" of the wrapper is to show a practical OOP example of what you might want to do with the core functionality the boost::asio library gives you. Most of the time, you will duplicate network code project after project and after a while, you will want to write a wrapper to avoid it. The network.cpp/.h was a simplification of my wrapper. While the namespace expansion is quite annoying, I can't say I'd want to go back and change anything about it conceptually. You won't be able to implement the generality or flexibility the wrapper provides in significantly less code. You could merge the OnAccept/OnConnect and pass an enum to save some space, but not much. The implementation specifics of the atomic_cas32 is really iffy and would be the only thing I'd consider looking into updating, but simply using a synchronized lock would be a lot of extra overhead, but might be needed on some platforms.

The intent of the design is also very specific to a recurring problem I had come across with network code. That is, the separation of "client" and "server" objects made for far messier code and actual communication between different objects greatly increased code complexity. With this proactor design that boost::asio uses, certain tasks are made a lot easier since communication between "client" and "server" objects are seemless. The biggest example is a proxy that requires accepting incoming connections than connecting to new remote destinations.

My understanding is that I want multiple sockets so that I can use them to determine who gets sent what from the server and I can tell which clients are sending me what. Is that accurate?


Once you add in the code to accept more connections, your network events will then execute in the context of the connection that receives them (each context has its own strand so in the context of the connection, everything is thread safe internally). One of the biggest networking challenges comes about when you need to multiplex these events into a "simulation" of some sorts. For example, if you were writing a web server, each connection does not need to know of the others, so the code as-is would just have the http processing added to work with. There is very little global state to worry about, so you don't have much to do.

If you were instead writing a game server, then you would have to come up with a way to pass the objects created from packets to your "simulator" in a fashion that you could associate the object from that connection and then be able to easily send objects to send back. This is a lot more complex and is not easily shown in simple examples. One such "easy" way, would be to give each connection a GUID, keep a mapping of GUIDs to Connection objects, then lock a global event queue and post events to that queue for a main thread to process. That in itself is another discussion though and outside the scope of the guide.

If you are going to use the network wrapper for any testing and stuff, line 122 of network.cpp should read: "connection->StartError( error );" and not "StartError( error );". It's a minor error that should rarely trigger, but if it does, it might mess something up that shouldn't be. The wrapper code is simply the lowest level code your networking logic uses for getting the raw data and connection management. For any real project, you would have to add additiona layers on top to handle your specific network protocol, message serialization/deserialization, and everything on up in terms of program logic.

Lastly, the most important thing to understand about that code are the sacrifices you make if you use it. Performance hits are a key issue as the overhead of the code will be at some factor that has to be measured and taken into consideration. For example, boost::bind does carry quite an overhead, but the trade-off is the unique functionality it provides and simplification of a lot of things that otherwise would not be possible as easily. Relying on strand for synchronization makes life a lot easier as well, at the expense of the overhead. boost::asio also has specific allocation strategies (official example) you must be aware of if you plan on using it in production code. The use of vectors and lists also carries quite an overhead, but all these issues are only issues if you measure performance and determine it is not suitable for your code. When trying to write generic, simple, wrapper code, certain sacrifices do have to be made. Once you know exactly what needs your project requires networking wise, you can write your own code and take what you need and get rid of the rest.

If you are new to networking, there's a lot of concepts and specific implementation strategies to wrap your head around. Boost::asio and C++ are by no means "simple", so you should discuss what you are looking for and why you think boost::asio can help you on your project so you can be sure you are on the right path. I'm not trying to say anything against boost::asio, but everyone's situation is different so if you are here on the forums, it'll be a good idea to make use of all the resources here. Hopefully that clears up some of your questions!

"But I, being poor, have only my dreams. I have spread my dreams under your feet; tread softly, because you tread on my dreams." - William Butler Yeats

#4 breakspirit   Members   -  Reputation: 100

Like
0Likes
Like

Posted 30 September 2011 - 09:16 PM

Drew, that was an excellent response. Thank you very much for taking the time to type it all out.




Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS