Our idea here is to create a very simple server application using the ASIO C++ library whose purpose is to accept connections, write "Hello World" and close the connection. I have choose to make use of simple organization for the code that you should not replicate on your real application -- specially nesting lambdas which can be very confusion as you code grows.
Let try to be clear one more time: this is not a guide on how you should design your application with ASIO. It's a guide for you to undertand how it works at a very basic level.
About asio
and boost::asio
asio
and boost::asio
are basically the same. They difference is that boost's version will use by default boost constructs and cannot be ran standalone like the former can.
Running standalone requires a macro to be set: ASIO_STANDALONE=1
Let's go quickly through the basics of using ASIO as a server. We first need to create a few setup objects like the io_service
, the endpoint
and the acceptor
:
asio::io_service service;
This creates a new io_service
instance that will be used to dispatch all IO async taksks -- think of it like something that looks like a thread.
A little more about asio::io_service
The io_service
is responsible for dispatching every async event you submit to the queue. You can think of it like a place where you place your tasks and, eventually they will be run. io_service
makes no assumption about multithreading, even though it is allowed to run tasks on multiple threads, you are responsible for syncronizing access to it.
io_service
is not only for networking! The io_service::post(CompletionHandler&&)
method allows you to schedule any task to be run together with IO tasks.
Having created a io_service
we now have to setup our server configuration: what should our server listen to? Any not used port would do, in this example I choose to use 2000. Feel free to use whatever port you want.
asio::ip::tcp::endpoint endpoint(asio::ip::tcp::v4(), 2000);
There's nothing fancy here, we just set a asio::ip::tcp::endpoint
that will listen to any IPv4 address on port 2000.
A little more about asio::ip::tcp::endpoint
The endpoint class is not used only in server-mode. It also provides the destination address for outgoing connections and you can query the address of clients connected on the server.
Whenever a new client tries to connect to your server you have to accept the connection and this should be done using an asio::ip::tcp::acceptor
class. We will start accepting connections in a second, but before that we need to instantiate a new acceptor
instance:
asio::ip::tcp::acceptor acceptor(service, endpoint);
Notice that asio::ip::tcp::acceptor
constructor takes two arguments: service
and endpoint
-- this is really simple to explain: tasks are going to be run on the io_service
and acceptor should bind to address pointed by the endpoint
.
Your code so far
asio::io_service service;
asio::ip::tcp::endpoint endpoint(asio::ip::tcp::v4(), 2000);
asio::ip::tcp::acceptor acceptor(service, endpoint);
Okay, now we are done with setting up. Every configuration you need is done and now you can to start accepting connections and handling them. Each connection in ASIO is represented by a asio::ip::tcp::socket
-- the socket class it where you will be doing most of your work: receiving data and writing data.
// alias to reduce verbosity
using socket_ptr = std::shared_ptr<asio::ip::tcp::socket>;
socket_ptr socket = std::make_shared<asio::ip::tcp::socket>(service);
acceptor.async_accept(*socket, [socket](std::error_code error) {
std::cout << "New connection from " << socket->remote_endpoint().address() << std::endl;
});
To simplify and reduce the need of typing the socket complete class name we setup an alias using the new C++11's using
syntax. Then we are allocating a new std::shared_ptr
for the socket. async_acept
will place a new task on io_service
io queue that will accept the first socket that tried to connect to your server. The second argument -- a lambda -- on async_accept
is a handler that will be called once a new connection has been accepted.
Error handling
You should handle errors! Just because I didn't did it here doesn't mean you shouldn't in your real-world code!
Not everything in ASIO must be async
One of the interesting points of ASIO is that it is a ascychronous IO library however, asynchronous IO is not ideal in every case scenario -- some applications might require a certain portion of the IO to be done synchronously and, gladly, ASIO supports this.
Most methods on asio::ip::tcp::acceptor
and asio::ip::tcp::socket
have both a synchronous version and a asynchronous version that can be accessed by appending async_
to the method name.
If you compile and run your code now you will be surpised that nothing has happened - the program will return immediatly. That's because io_service
was never started! To run io_service
on the main thread all we need to do is invoke run
:
service.run();
Running your program now will accept one connection to the server. After the first connection is accepted it is closed and the program terminates. This is due to an important behaviour on ASIO: io_service::run()
will only block if there are pending tasks on the queue, returning once the queue is or becomes empty.
Your code so far
asio::io_service service;
asio::ip::tcp::endpoint endpoint(asio::ip::tcp::v4(), 2000);
asio::ip::tcp::acceptor acceptor(service, endpoint);
using socket_ptr = std::shared_ptr;
socket_ptr socket = std::make_shared(service);
acceptor.async_accept(*socket, [socket](std::error_code error) {
std::cout << "New connection from " << socket->remote_endpoint().address() << std::endl;
});
service.run();
You could also use a raw pointer
In this example I am using a shared_ptr
to keep memory management issues outside the scope of this post, however if you wish you can freely use a raw pointer to the socket. Just beware that you only should release it's memory if there's no task scheduled on the socket, otherwise you application will crash. This is even more important when you start dealing with multiple threads which could be schedling tasks you don't know about.
To keep acepting more connection we need to call acceptor::async_accept
again inside our handler. To do this we need to change a certain number of things:
- The socket cannot be passed through the capture group: every connection has to have a new socket
- The lambda has to be passed to itself through the capture group
A possible implementation to this would be:
using socket_ptr = std::shared_ptr<asio::ip::tcp::socket>;
// the handler lambda takes two arguments: the socket and the error code. handler, service and acceptor are taked by the capture group and are used to schedule the next accept.
std::function<void(socket_ptr, std::error_code)> handler = [&handler, &service, &acceptor](socket_ptr socket, std::error_code error) {
std::cout << "New connection from " << socket->remote_endpoint().address() << std::endl;
// accepts the next connection -- creates a new socket and gives it to the acceptor
socket_ptr newSocket = std::make_shared<asio::ip::tcp::socket>(service);
acceptor.async_accept(*newSocket, std::bind1st(handler, newSocket));
};
socket_ptr socket = std::make_shared<asio::ip::tcp::socket>(service);
acceptor.async_accept(*socket, std::bind1st(handler, socket));
We are accepting multiple connections already the only thing that remains on our server is to send our "Hello World" message to the client connecting. Sending data is really easy all you need to do is issue a socket::async_write_some
and your done.
Let's go in more details: upon accepting the connection, inside the handler, we should issue a socket::async_write_some
call with our hello message and a write completion handler that should close the connection after sending.
std::function<void(socket_ptr, std::error_code)> handler = [&handler, &service, &acceptor](socket_ptr socket, std::error_code error) {
std::cout << "New connection from " << socket->remote_endpoint().address() << std::endl;
// writes data to the client
socket->async_write_some(asio::buffer("Hello World\n"), [socket](std::error_code error, size_t bytes) {
socket->close();
});
// accepts the next connection -- creates a new socket and gives it to the acceptor
socket_ptr newSocket = std::make_shared<asio::ip::tcp::socket>(service);
acceptor.async_accept(*newSocket, std::bind1st(handler, newSocket));
};
Note the use of the asio::buffer
class: it is a small class that will act as a wrapper around a std::string
, const char*
and many other data types.
async_write_some
might not send everything to the client
In order to keep the io_service
thread in a non-blocking manner async_write_some might not write all the data you give to the client. This is by design!
As a solution to this issue you can use asio::async_write
function that will automatically issue a new socket::async_write_some()
until all data is consumed.
If you want to see your server in action you can open a Terminal window (Application > Utilities > Terminal on the Mac or Command Prompt on Windows) and type the following:
$ telnet 127.0.0.1 2000 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. Hello World Connection closed by foreign host.
Something that I have not covered here is receiving data from the client which works in a analogous way to writing. I think it is worth mentining that socket::async_read_some
suffers from the same issues as socket::async_write_some
-- it might not fill buffer and you will have to accumulate the data you are receiving on each event.
And remember: ASIO's documentation is your friend! Good luck!
Complete code with Xcode project is on GitHub.