九天雁翎的博客
如果你想在软件业获得成功,就使用你知道的最强大的语言,用它解决你知道的最难的问题,并且等待竞争对手的经理做出自甘平庸的选择。 -- Paul Graham

ASIO—下一代C++标准可能接纳的网络库(2)TCP网络应用

ASIO—下一代C++标准可能接纳的网络库(2)TCP网络应用

write by九天雁翎(JTianLing) – www.jtianling.com**

讨论新闻组及文件

一、   综述

本文仅仅是附着在boost::asio文档的一个简单说明和讲解,没有boost::asio文档可能你甚至都不知道我在讲什么,boost::asio的文档自然是需要从www.boost.org上去下。

基本上,网络编程领域的”Hello World”程序就是类似Echo,daytime等服务器应用了。大牛Stevens经典的《Unix Network Programming》一书更是在这两个服务器上折腾了半本书,Comer的《Internetworking With TCP/IP vol III》也不例外。boost::asio的文档也就更不例外了,全部的网络方面的例子都是以daytime服务为蓝本来讲解的。呵呵,大家这样做是有道理的,毕竟从讲解网络编程的原理来看,echo,daytime等足够的简单:)

 

二、    Tutorial

首先,因为客户端程序相对服务器程序更为简单,所以一般都从客户端开始,boost::asio也是如此,第一节,给出了一个TCP 的Daytime的实现所谓示例,这里,我不拷贝其源码了,只是列出一个用windows 下用套接字接口实现的同样程序作为对比。

1.      A synchronous TCP daytime client(一个同步的TCP daytime客户端程序)

原始的套接字实现:

#include <stdio.h>
#include <string.h>
#include "Winsock2.h"
#include "errno.h"
#include "stdlib.h"

#define MAXLINE 1000

void str_cli(SOCKET sockfd)
{
    char recvline[MAXLINE] = {0};
    while ( (recv(sockfd, recvline, MAXLINE, 0)) != NULL) 
    {
       printf("%s", recvline);
    }
    closesocket(sockfd);
}


int main(int argc, char **argv)
{
    WORD wVersionRequested = 0;
    WSADATA wsaData;
    int err;

    wVersionRequested = MAKEWORD( 2, 2 );

    // windows下此初始化为必须,实际是初始化WinsockDLL的过程
    err = WSAStartup( wVersionRequested, &wsaData );
    if ( err != 0 ) {
       return -1;
    }
    SOCKET               sockfd;
    struct sockaddr_in   servaddr;

    if (argc != 2)
    {
       printf("usage: tcpcli <IPaddress>");
       exit(1);
    }

    sockfd = socket(AF_INET, SOCK_STREAM, 0);

    ZeroMemory(&servaddr, sizeof(servaddr));
    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons(13);
    servaddr.sin_addr.s_addr = inet_addr(argv[1]);

    if( SOCKET_ERROR == connect(sockfd, (struct sockaddr *) &servaddr, sizeof(servaddr)))
    {
       printf("connet failed, Error Code: %d", WSAGetLastError());
       closesocket(sockfd);
       return -1;
    }

    str_cli(sockfd);     /* do it all */

    system("pause");
    exit(0);
}

共六十一行,并且需要处理socket创建,初始化等繁琐细节,做任何决定时基本上是通过typecode,其实相对来说也不算太难,因为除了socket的API接口属于需要额外学习的东西,没有太多除了C语言以外的东西需要学习,并且因为BSD socket是如此的出名,以至于几乎等同与事实的标准,所以这样的程序能被大部分学习过一定网络编程知识的人了解。

 

为了方便对比,我还是贴一下boost::asio示例中的代码:

#include <iostream>
#include <boost/array.hpp>
#include <boost/asio.hpp>
using boost::asio::ip::tcp;

int main(int argc, char* argv[])
{
    try
    {
       if (argc != 2)
       {
           std::cerr << "Usage: client <host>" << std::endl;
           return 1;
       }
       boost::asio::io_service io_service;
       tcp::resolver resolver(io_service);
       tcp::resolver::query query(argv[1], "daytime");
       tcp::resolver::iterator endpoint_iterator = resolver.resolve(query);
       tcp::resolver::iterator end;
       tcp::socket socket(io_service);
       boost::system::error_code error = boost::asio::error::host_not_found;
       while (error && endpoint_iterator != end)
       {
           socket.close();
           socket.connect(*endpoint_iterator++, error);
       }
       if (error)
           throw boost::system::system_error(error);
       for (;;)
       {
           boost::array<char, 128> buf;
           boost::system::error_code error;
           size_t len = socket.read_some(boost::asio::buffer(buf), error);
           if (error == boost::asio::error::eof)
              break; // Connection closed cleanly by peer.
           else if (error)
              throw boost::system::system_error(error); // Some other error.
           std::cout.write(buf.data(), len);
       }
    }
    catch (std::exception& e)
    {
       std::cerr << e.what() << std::endl;
    }
    return 0;
}

boost::asio的文档中的实现也有47行,用了多个try,catch来处理异常,因为其实现的原因,引入了较多的额外复杂度,除了boost::asio以外,即便你很熟悉C++,你也得进一步的了解诸如boost::array, boost:system等知识,(虽然其实很简单)并且,从使用上来说,感觉并没有比普通的socket API简单,虽然如此,boost::asio此例子还是有其优势的,比如ipv4,ipv6的自适应(原socket API仅仅支持ipv4),出错时更人性化的提示(此点由C++异常特性支持,相对比C语言中常常只能有个error code)。

当然,此例子过于简单,而asio是为了较大规模程序的实现而设计的,假如这么小规模的程序用原始的套接字就足够了。这点是需要说明的。

 

2.      Daytime.2 - A synchronous TCP daytime server(同步的TCP daytime服务器)

有了客户端没有服务器,那客户端有什么用呢?^^所以,接下来boost::asio适时的给出了一个daytime的服务器实现,这里还是先给出使用一个原始套接字的例子:

#include <time.h>
#include "Winsock2.h"
#include "errno.h"
#include "stdlib.h"

#define MAXLINE 1000
int main(int argc, char **argv)
{
    WORD wVersionRequested = 0;
    WSADATA wsaData;
    int err;

    wVersionRequested = MAKEWORD( 2, 2 );

    // windows下此初始化为必须,实际是初始化WinsockDLL的过程
    err = WSAStartup( wVersionRequested, &wsaData );
    if ( err != 0 ) {
       return -1;
    }

    SOCKET               listenfd, connfd;
    struct sockaddr_in   servaddr;
    char              buff[MAXLINE];
    time_t            ticks;

    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    ZeroMemory(&servaddr, sizeof(servaddr));
    servaddr.sin_family      = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port        = htons(13);  /* daytime server */

    if( bind(listenfd, (struct sockaddr *) &servaddr, sizeof(servaddr))
        == SOCKET_ERROR) 
    {
           printf("bind failed: %d/n", WSAGetLastError());
           closesocket(listenfd);
           WSACleanup();
           return 1;
    }

    if (listen( listenfd, SOMAXCONN ) == SOCKET_ERROR)
    {
       printf("Error listening on socket./n");
       WSACleanup();
       return 1;
    }

    for ( ; ; ) 
    {
       connfd = accept(listenfd, (struct sockaddr*) NULL, NULL);
       if (connfd == INVALID_SOCKET) 
       {
           printf("accept failed: %d/n", WSAGetLastError());
           closesocket(listenfd);
           WSACleanup();
           return 1;
       } 

       ticks = time(NULL);
       _snprintf(buff, sizeof(buff), "%.24s/r/n", ctime(&ticks));
       if( SOCKET_ERROR == send(connfd, buff, strlen(buff), 0))
       {
           printf("send failed: %d/n", WSAGetLastError());
           closesocket(connfd);
           WSACleanup();
           return 1;
       }
       closesocket(connfd);
    }

    WSACleanup();
    return 0;
}

全程序75行,大部分用于socket的初始化,及其状态的转换,直到真正的进入监听状态并开始接受连接,每个socket API的调用都需要判断返回值,这也算是C语言程序共同的特点。

另外,看看boost::asio的实现。

#include <ctime>
#include <iostream>
#include <string>
#include <boost/asio.hpp>
using boost::asio::ip::tcp;

std::string make_daytime_string()
{
    using namespace std; // For time_t, time and ctime;
    time_t now = time(0);
    return ctime(&now);
}
int main()
{
    try
    {
       boost::asio::io_service io_service;
       tcp::acceptor acceptor(io_service, tcp::endpoint(tcp::v4(), 13));
       for (;;)
       {
           tcp::socket socket(io_service);
           acceptor.accept(socket);
           std::string message = make_daytime_string();
           boost::system::error_code ignored_error;
           boost::asio::write(socket, boost::asio::buffer(message),
              boost::asio::transfer_all(), ignored_error);
       }
    }
    catch (std::exception& e)
    {
       std::cerr << e.what() << std::endl;
    }
    return 0;
}

全程序35行,比使用原始套接字的版本省略了一半,并且还是保持着可移植性(我的例子只能在windows下运行)。

从其文档和实现来看,实现上将很多函数转化为类了,使用上也没有简化一些。。。做出这样的结论还是当我处于对socket API的熟悉程度要远大于boost::asio的情况。也许对于纯粹的初学者,要学习asio会比socket API简单更多一些。毕竟相当多的细节,比如各种情况下的错误返回,各类接口需要传入的适当的参数,甚至套接字初始化,状态的转换等等在boost::asio中都简化了太多。此例中的例子就是accept函数在boost::asio中实现为了acceptor类。

另外,这里值得说明一下,虽然BSD socket套接字属于事实上的标准,但是其实同一套程序不经过一定的更改要放在Linux,Windows上同时运行是不可能的,因为其中总有些细微的差别,总记得刚开始工作的时候,拿着《Unix Network Programming》在Windows下去学习,结果一个小程序都用不了。。。结果是完全不知道Windows下特有的WSAStartup初始化-_-!但是boost::asio就彻底的消除了这样的差别。这也应该算是boost::asio的一个优势吧。

 

3.      An asynchronous TCP daytime server(异步TCP daytime服务器)

与原有asio的简单应用一样,从第三个例子开始就已经是有点意思了的程序了,程序的复杂性上来了,异步相对同步来说效率更高是不争的事实,并且其不会阻塞的特性使得应用范围更广,并且异步也是大部分高性能服务器实际上使用的方式,比如Windows下的完成端口,Linux下的Epoll等,asio的底层就是用这些方式实现的,只不过将其封装起来,使得使用更加简单了。这里提供异步的例子就不是那么简单了-_-!呵呵,偷懒的我暂时就不提供了。其实用select也是可以模拟出异步的特性的,asio在操作系统没有很好的支持异步特性的API时,就是利用select模拟出异步的。但是作为select的例子,可以参考我以前学习时写的《服务器 Select模型的实现**

例子中用tcp_server类处理accept事件,用tcp_connection类来处理连接后的写入事件,并且用shared_ptr来保存tcp_connection类的对象。

 

总结:

boost::asio的确在某种程度上简化了网络客户端/服务器程序的编写,并且易于编写出效率较高的网络应用,(效率能高到什么程度没有实测)但是,作为与C程序员一脉相承的C++程序员,在完全不了解诸如asio:: async_write,asio:: async_accept等函数的实现时有多大的胆量去放心使用,这是个问题。说要去真的理解其实现吧。。。那么就将陷入真正的boost精密C++技巧使用的泥潭,因为boost::asio与其他boost库结合的是如此的紧密,特别是boost::bind,而boost::bind现在的实现实在不是那么优美,并且在下一版的C++标准中variadic templates的加入,是会使其实现简化很多的,这样说来,用boost::asio还是不用。。。是个问题。也许真正能让人下定决心在项目中使用boost::asio的时候,就是在下一代C++标准中其变成了std::asio的时候吧^^

 

write by九天雁翎(JTianLing) – www.jtianling.com

分类:  网络技术 
标签:  ASIO  Boost  C++ 

Posted By 九天雁翎 at 九天雁翎的博客 on 2009年06月07日

前一篇: ASIO—下一代C++标准可能接纳的网络库(1)简单的应用 后一篇: ASIO—下一代C++标准可能接纳的网络库(3)UDP网络应用