<menu id="w8yyk"><menu id="w8yyk"></menu></menu>
  • <dd id="w8yyk"><nav id="w8yyk"></nav></dd>
    <menu id="w8yyk"></menu>
    <menu id="w8yyk"><code id="w8yyk"></code></menu>
    <menu id="w8yyk"></menu>
    <xmp id="w8yyk">
    <xmp id="w8yyk"><nav id="w8yyk"></nav>
  • 網站首頁 > 物聯資訊 > 技術分享

    使用Boost.Asio編寫通信程序

    2016-09-28 00:00:00 廣州睿豐德信息科技有限公司 閱讀
    睿豐德科技 專注RFID識別技術和條碼識別技術與管理軟件的集成項目。質量追溯系統、MES系統、金蝶與條碼系統對接、用友與條碼系統對接

    摘要:本文通過形像而活潑的語言簡單地介紹了Boost::asio庫的使用,作為asio的一個入門介紹是非常合適的,可以給人一種新鮮的感覺,同時也能讓體驗到asio的主要內容。

     

    Boost.Asio是一個跨平臺的網絡及底層IO的C++編程庫,它使用現代C++手法實現了統一的異步調用模型。

    ASIO的同步方式

    ASIO庫能夠使用TCP、UDP、ICMP、串口來發送/接收數據,下面先介紹TCP協議的讀寫操作。對于讀寫方式,ASIO支持同步和異步兩種方式,首先登場的是同步方式,下面請同步方式自我介紹一下。

    自我介紹

    大家好!我是同步方式!

    我的主要特點就是執著!所有的操作都要完成或出錯才會返回,不過偶的執著被大家稱之為阻塞,實在是郁悶~~(場下一片噓聲),其實這樣 也是有好處的,比如邏輯清晰,編程比較容易。

    在服務器端,我會做個socket交給acceptor對象,讓它一直等客戶端連進來,連上以后再通過這個socket與客戶端通信, 而所有的通信都是以阻塞方式進行的,讀完或寫完才會返回。

    在客戶端也一樣,這時我會拿著socket去連接服務器,當然也是連上或出錯了才返回,最后也是以阻塞的方式和服務器通信。

    有人認為同步方式沒有異步方式高效,其實這是片面的理解。在單線程的情況下可能確實如此,我不能利用耗時的網絡操作這段時間做別的事 情,不是好的統籌方法。不過這個問題可以通過多線程來避免,比如在服務器端讓其中一個線程負責等待客戶端連接,連接進來后把socket交給另外的線程去 和客戶端通信,這樣與一個客戶端通信的同時也能接受其它客戶端的連接,主線程也完全被解放了出來。

    我的介紹就有這里,謝謝大家!

    示例代碼

    好,感謝同步方式的自我介紹,現在放出同步方式的演示代碼(起立鼓掌!)。

    服務器端

    #include <iostream>
    #include <boost/asio.hpp>
    int main(int argc, char* argv[])
    {
    using namespace boost::asio;
    // 所有asio類都需要io_service對象
    io_service iosev;
    ip::tcp::acceptor acceptor(iosev,ip::tcp::endpoint(ip::tcp::v4(), 1000));
    for(;;)
    {
    // socket對象
    ip::tcp::socket socket(iosev);

    // 等待直到客戶端連接進來
    acceptor.accept(socket);

    // 顯示連接進來的客戶端
    std::cout << socket.remote_endpoint().address() << std::endl;

    // 向客戶端發送hello world!
    boost::system::error_code ec;
    socket.write_some(buffer("hello world!"), ec);
    // 如果出錯,打印出錯信息
    if(ec)
    {
    std::cout << boost::system::system_error(ec).what() << std::endl;
    break;
    }
    // 與當前客戶交互完成后循環繼續等待下一客戶連接
    }
    return 0;
    }

    客戶端

    #include <iostream>
    #include <boost/asio.hpp>

    int main(int argc, char* argv[])
    {
    using namespace boost::asio;

    // 所有asio類都需要io_service對象
    io_service iosev;
    // socket對象
    ip::tcp::socket socket(iosev);
    // 連接端點,這里使用了本機連接,可以修改IP地址測試遠程連接
    ip::tcp::endpoint ep(ip::address_v4::from_string("127.0.0.1"), 1000);
    // 連接服務器
    boost::system::error_code ec;
    socket.connect(ep,ec);
    // 如果出錯,打印出錯信息
    if(ec)
    {
    std::cout << boost::system::system_error(ec).what() << std::endl;
    return -1;
    }
    // 接收數據
    char buf[100];
    size_t len=socket.read_some(buffer(buf), ec);
    std::cout.write(buf, len);

    return 0;
    }
    小結

    從演示代碼可以得知

    • ASIO的TCP協議通過boost::asio::ip名 空間下的tcp類進行通信。
    • IP地址(address,address_v4,address_v6)、 端口號和協議版本組成一個端點(tcp:: endpoint)。用于在服務器端生成tcp::acceptor對 象,并在指定端口上等待連接;或者在客戶端連接到指定地址的服務器上。
    • socket是 服務器與客戶端通信的橋梁,連接成功后所有的讀寫都是通過socket對 象實現的,當socket析 構后,連接自動斷 開。
    • ASIO讀寫所用的緩沖區用buffer函 數生成,這個函數生成的是一個ASIO內部使用的緩沖區類,它能把數組、指針(同時指定大 小)、std::vector、std::string、boost::array包裝成緩沖區類。
    • ASIO中的函數、類方法都接受一個boost::system::error_code類 型的數據,用于提供出錯碼。它可以轉換成bool測試是否出錯,并通過boost::system::system_error類 獲得詳細的出錯信息。另外,也可以不向ASIO的函數或方法提供 boost::system::error_code,這時如果出錯的話就會直 接拋出異常,異常類型就是boost::system:: system_error(它是從std::runtime_error繼承的)。

    ASIO的異步方式

    嗯?異步方式好像有點坐不住了,那就請異步方式上場,大家歡迎...

    自我介紹

    大家好,我是異步方式

    和同步方式不同,我從來不花時間去等那些龜速的IO操作,我只是向系統說一聲要做什么,然后就可以做其它事去了。如果系統完成了操作, 系統就會通過我之前給它的回調對象來通知我。

    在ASIO庫中,異步方式的函數或方法名稱前面都有“async_” 前綴,函數參數里會要求放一個回調函數(或仿函數)。異步操作執行 后不管有沒有完成都會立即返回,這時可以做一些其它事,直到回調函數(或仿函數)被調用,說明異步操作已經完成。

    在ASIO中很多回調函數都只接受一個boost::system::error_code參數,在實際使用時肯定是不夠的,所以一般 使用仿函數攜帶一堆相關數據作為回調,或者使用boost::bind來綁定一堆數據。

    另外要注意的是,只有io_service類的run()方法運行之后回調對象才會被調用,否則即使系統已經完成了異步操作也不會有任 務動作。

    示例代碼

    好了,就介紹到這里,下面是我帶來的異步方式TCP Helloworld 服務器端

    #include <iostream>
    #include <string>
    #include <boost/asio.hpp>
    #include <boost/bind.hpp>
    #include <boost/smart_ptr.hpp>

    using namespace boost::asio;
    using boost::system::error_code;
    using ip::tcp;

    struct CHelloWorld_Service
    {
    CHelloWorld_Service(io_service &iosev)
    :m_iosev(iosev),m_acceptor(iosev, tcp::endpoint(tcp::v4(), 1000))
    {}

    void start()
    {
    // 開始等待連接(非阻塞)
    boost::shared_ptr<tcp::socket> psocket(new tcp::socket(m_iosev));
    // 觸發的事件只有error_code參數,所以用boost::bind把socket綁定進去
    m_acceptor.async_accept(*psocket,
    boost::bind(&CHelloWorld_Service::accept_handler, this, psocket, _1));
    }

    // 有客戶端連接時accept_handler觸發
    void accept_handler(boost::shared_ptr<tcp::socket> psocket, error_code ec)
    {
    if(ec) return;
    // 繼續等待連接
    start();
    // 顯示遠程IP
    std::cout << psocket->remote_endpoint().address() << std::endl;
    // 發送信息(非阻塞)
    boost::shared_ptr<std::string> pstr(new std::string("hello async world!"));
    psocket->async_write_some(buffer(*pstr),
    boost::bind(&CHelloWorld_Service::write_handler, this, pstr, _1, _2));
    }

    // 異步寫操作完成后write_handler觸發
    void write_handler(boost::shared_ptr<std::string> pstr, error_code ec,
    size_t bytes_transferred)
    {
    if(ec)
    std::cout<< "發送失敗!" << std::endl;
    else
    std::cout<< *pstr << " 已發送" << std::endl;
    }

    private:
    io_service &m_iosev;
    ip::tcp::acceptor m_acceptor;
    };

    int main(int argc, char* argv[])
    {
    io_service iosev;
    CHelloWorld_Service sev(iosev);
    // 開始等待連接
    sev.start();
    iosev.run();

    return 0;
    }
    小結

    在這個例子中,首先調用sev.start()開 始接受客戶端連接。由于async_accept調 用后立即返回,start()方 法 也就馬上完成了。sev.start()在 瞬間返回后iosev.run()開 始執行,iosev.run()方法是一個循環,負責分發異步回調事件,只 有所有異步操作全部完成才會返回。

    這里有個問題,就是要保證start()方法中m_acceptor.async_accept操 作所用的tcp::socket對 象 在整個異步操作期間保持有效(不 然系統底層異步操作了一半突然發現tcp::socket沒了,不是拿人家開涮嘛-_-!!!),而且客戶端連接進來后這個tcp::socket對象還 有用呢。這里的解決辦法是使用一個帶計數的智能指針boost::shared_ptr,并把這個指針作為參數綁定到回調函數上。

    一旦有客戶連接,我們在start()里給的回調函數accept_handler就會被 調用,首先調用start()繼續異步等待其 它客戶端的連接,然后使用綁定進來的tcp::socket對象與當前客戶端通信。

    發送數據也使用了異步方式(async_write_some), 同樣要保證在整個異步發送期間緩沖區的有效性,所以也用boost::bind綁定了boost::shared_ptr。

    對于客戶端也一樣,在connect和read_some方法前加一個async_前綴,然后加入回調即可,大家自己練習寫一寫。

    ASIO的“便民措施”

    asio中提供一些便利功能,如此可以實現許多方便的操作。

    端點

    回到前面的客戶端代碼,客戶端的連接很簡單,主要代碼就是兩行:

    ...
    // 連接
    socket.connect(endpoint,ec);
    ...
    // 通信
    socket.read_some(buffer(buf), ec);

     

    不過連接之前我們必須得到連接端點endpoint,也就是服務器地址、端口號以及所用的協議版本。

    前面的客戶端代碼假設了服務器使用IPv4協議,服務器IP地址為127.0.0.1,端口號為1000。實際使用的情況是,我們經常只能知道服務器網絡ID,提供的服務類型,這時我們就得使用ASIO提供的tcp::resolver類來取得服務器的端點了。

    比如我們要取得163網站的首頁,首先就要得到“www.163.com”服務器的HTTP端點:

    io_service iosev;
    ip::tcp::resolver res(iosev);
    ip::tcp::resolver::query query("www.163.com","80"); //www.163.com 80端口
    ip::tcp::resolver::iterator itr_endpoint = res.resolve(query);

    這里的itr_endpoint是一個endpoint的迭代器,服務器的同一端口上可能不止一個端點,比如同時有IPv4和IPv6 兩種。現在,遍歷這些端點,找到可用的:

    // 接上面代碼
    ip::tcp::resolver::iterator itr_end; //無參數構造生成end迭代器
    ip::tcp::socket socket(iosev);
    boost::system::error_code ec = error::host_not_found;
    for(;ec && itr_endpoint!=itr_end;++itr_endpoint)
    {
    socket.close();
    socket.connect(*itr_endpoint, ec);
    }
     
    如果連接上,錯誤碼ec被清空,我們就可以與服務器通信了:
    if(ec)
    {
    std::cout << boost::system::system_error(ec).what() << std::endl;
    return -1;
    }
    // HTTP協議,取根路徑HTTP源碼
    socket.write_some(buffer("GET <a href="http://www.163.com" title="http://www.163.com">http://www.163.com</a> HTTP/1.0 "));
    for(;;)
    {
    char buf[128];
    boost::system::error_code error;
    size_t len = socket.read_some(buffer(buf), error);
    // 循環取數據,直到取完為止
    if(error == error::eof)
    break;
    else if(error)
    {
    std::cout << boost::system::system_error(error).what() << std::endl;
    return -1;
    }

    std::cout.write(buf, len);
    }

    當所有HTTP源碼下載了以后,服務器會主動斷開連接,這時客戶端的錯誤碼得到boost::asio::error::eof,我們 要根據它來判定是否跳出循環。

    ip::tcp::resolver::query的構造函數接受服務器名和服務名。前面的服務名我們直接使用了端口號"80",有時 我們也可以使用別名,用記事本打開%windir%\system32\drivers\etc\services文件(Windows環境),可以看到 一堆別名及對應的端口,如:

    echo            7/tcp                # Echo
    ftp 21/tcp # File Transfer Protocol (Control)
    telnet 23/tcp # Virtual Terminal Protocol
    smtp 25/tcp # Simple Mail Transfer Protocol
    time 37/tcp timeserver # Time

    比如要連接163網站的telnet端口(如果有的話),可以這樣寫:

    ip::tcp::resolver::query query("www.163.com","telnet");
    ip::tcp::resolver::iterator itr_endpoint = res.resolve(query);
    超時

    在網絡應用里,常常要考慮超時的問題,不然連接后半天沒反應誰也受不了。

    ASIO庫提供了deadline_timer類來支持定時觸發,它的用法是:

     

    // 定義定時回調
    void print(const boost::system::error_code& /*e*/)
    {
    std::cout << "Hello, world! ";
    }
    deadline_timer timer;
    // 設置5秒后觸發回調
    timer.expires_from_now(boost::posix_time::seconds(5));
    timer.async_wait(print);

    這段代碼執行后5秒鐘時打印Hello World!

    我們可以利用這種定時機制和異步連接方式來實現超時取消:

    deadline_timer timer;
    // 異步連接
    socket.async_connect(my_endpoint, connect_handler/*連接回調*/);
    // 設置超時
    timer.expires_from_now(boost::posix_time::seconds(5));
    timer.async_wait(timer_handler);
    ...
    // 超時發生時關閉socket
    void timer_handler()
    {
    socket.close();
    }

    最后不要忘了io_service的run()方法。

    統一讀寫接口

    除了前面例子所用的tcp::socket讀寫方法(read_some, write_some等)以外,ASIO也提供了幾個讀寫函數,主要有這么幾個:

    read、write、read_until、write_until

    當然還有異步版本的

    async_read、async_write、async_read_until、async_write_until

    這些函數可以以統一的方式讀寫TCP、串口、HANDLE等類型的數據流。

    我們前面的HTTP客戶端代碼可以這樣改寫:

    ...
    //socket.write_some(buffer("GET <a title="http://www.163.com">http://www.163.com</a> HTTP/1.0 "));
    write(socket,buffer("GET <a href="http://www.163.com" title="http://www.163.com">http://www.163.com</a> HTTP/1.0 "));
    ...
    //size_t len = socket.read_some(buffer(buf), error);
    size_t len = read(socket, buffer(buf), transfer_all() ,error);
    if(len) std::cout.write(buf, len);

    這個read和write有多個重載,同樣,有錯誤碼參數的不會拋出異常而無錯誤碼參數的若出錯則拋出異常。

    本例中read函數里的transfer_all()是一個稱為CompletionCondition的對象,表示讀取/寫入直接緩 沖區裝滿或出錯為止。另一個可選的是transfer_at_least(size_t),表示至少要讀取/寫入多少個字符。

    read_until和write_until用于讀取直到某個條件滿足為止,它接受的參數不再是buffer,而是boost::asio:: streambuf。

    比如我們可以把我們的HTTP客戶端代碼改成這樣:

    boost::asio::streambuf strmbuf;
    size_t len = read_until(socket,strmbuf," ",error);
    std::istream is(&strmbuf);
    is.unsetf(std::ios_base::skipws);
    // 顯示is流里的內容
    std::copy(std::istream_iterator<char>(is),
    std::istream_iterator<char>(),
    std::ostream_iterator<char>(std::cout));
    基于流的操作

    對于TCP協議來說,ASIO還提供了一個tcp::iostream。用它可以更簡單地實現我們的HTTP客戶端:

    ip::tcp::iostream stream("www.163.com", "80");
    if(stream)
    {
    // 發送數據
    stream << "GET <a href="http://www.163.com" title="http://www.163.com">http://www.163.com</a> HTTP/1.0 ";
    // 不要忽略空白字符
    stream.unsetf(std::ios_base::skipws);
    // 顯示stream流里的內容
    std::copy(std::istream_iterator<char>(stream),
    std::istream_iterator<char>(),
    std::ostream_iterator<char>(std::cout));
    }

    用ASIO編寫UDP通信程序

    ASIO的TCP協議通過boost::asio::ip名空間下的tcp類進行通信,舉一返三:ASIO的UDP協議通過boost::asio::ip名空間下的udp類進行通信。

    我們知道UDP是基于數據報模式的,所以事先不需要建立連接。就象寄信一樣,要寄給誰只要寫上地址往門口的郵箱一丟,其它的事各級郵局 包辦;要收信用只要看看自家信箱里有沒有信件就行(或問門口傳達室老大爺)。在ASIO里,就是udp::socket的send_to和receive_from方法(異步版本是async_send_to和asnync_receive_from)。

    下面的示例代碼是從ASIO官方文檔里拿來的(實在想不出更好的例子了:-P):

    服務器端代碼

    //
    // server.cpp
    // ~~~~~~~~~~
    //
    // Copyright (c) 2003-2008 Christopher M. Kohlhoff
    // (chris at kohlhoff dot com)
    //
    // Distributed under the Boost Software License, Version 1.0.
    // (See accompanying
    // file LICENSE_1_0.txt or
    // copy at <a title="http://www.boost.org/LICENSE_1_0.txt">http://www.boost.org/LICENSE_1_0.txt</a>)
    //
    #include <ctime>
    #include <iostream>
    #include <string>
    #include <boost/array.hpp>
    #include <boost/asio.hpp>
    using boost::asio::ip::udp;
    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;
    // 在本機13端口建立一個socket
    udp::socket socket(io_service, udp::endpoint(udp::v4(), 13));
    for (;;)
    {
    boost::array<char, 1> recv_buf;
    udp::endpoint remote_endpoint;
    boost::system::error_code error;
    // 接收一個字符,這樣就得到了遠程端點(remote_endpoint)
    socket.receive_from(boost::asio::buffer(recv_buf),
    remote_endpoint, 0, error);
    if (error && error != boost::asio::error::message_size)
    throw boost::system::system_error(error);
    std::string message = make_daytime_string();
    // 向遠程端點發送字符串message(當前時間)
    boost::system::error_code ignored_error;
    socket.send_to(boost::asio::buffer(message),
    remote_endpoint, 0, ignored_error);
    }
    }
    catch (std::exception& e)
    {
    std::cerr << e.what() << std::endl;
    }
    return 0;
    }

    客戶端代碼

    //
    // client.cpp
    // ~~~~~~~~~~
    //
    // Copyright (c) 2003-2008 Christopher M. Kohlhoff
    // (chris at kohlhoff dot com)
    //
    // Distributed under the Boost Software License, Version 1.0.
    // (See accompanying file LICENSE_1_0.txt or
    // copy at <a title="http://www.boost.org/LICENSE_1_0.txt">http://www.boost.org/LICENSE_1_0.txt</a>)
    //
    #include <iostream>
    #include <boost/array.hpp>
    #include <boost/asio.hpp>
    using boost::asio::ip::udp;
    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;
    // 取得命令行參數對應的服務器端點
    udp::resolver resolver(io_service);
    udp::resolver::query query(udp::v4(), argv[1], "daytime");
    udp::endpoint receiver_endpoint = *resolver.resolve(query);
    udp::socket socket(io_service);
    socket.open(udp::v4());
    // 發送一個字節給服務器,讓服務器知道我們的地址
    boost::array<char, 1> send_buf = { 0 };
    socket.send_to(boost::asio::buffer(send_buf), receiver_endpoint);
    // 接收服務器發來的數據
    boost::array<char, 128> recv_buf;
    udp::endpoint sender_endpoint;
    size_t len = socket.receive_from(
    boost::asio::buffer(recv_buf), sender_endpoint);
    std::cout.write(recv_buf.data(), len);
    }
    catch (std::exception& e)
    {
    std::cerr << e.what() << std::endl;
    }
    return 0;
    }

    用ASIO讀寫串行口

    ASIO不僅支持網絡通信,還能支持串口通信。要讓兩個設備使用串口通信,關鍵是要設置好正確的參數,這些參數是:波特率、奇偶校驗 位、停止位、字符大小和流量控制。兩個串口設備只有設置了相同的參數才能互相交談。

    ASIO提供了boost::asio::serial_port類,它有一個set_option(const SettableSerialPortOption& option)方法就是用于設置上面列舉的這些參數的,其中的option可以是:

    • serial_port::baud_rate 波特率,構造參數為unsigned int
    • serial_port::parity 奇偶校驗,構造參數為serial_port::parity::type,enum類型,可以是none, odd, even。
    • serial_port::flow_control 流量控制,構造參數為serial_port::flow_control::type,enum類型,可以是none software hardware
    • serial_port::stop_bits 停止位,構造參數為serial_port::stop_bits::type,enum類型,可以是one onepointfive two
    • serial_port::character_size 字符大小,構造參數為unsigned int
    演示代碼
    #include <iostream>
    #include <boost/asio.hpp>
    #include <boost/bind.hpp>
    using namespace std;
    using namespace boost::asio;
    int main(int argc, char* argv[])
    {
    io_service iosev;
    // 串口COM1, Linux下為“/dev/ttyS0”
    serial_port sp(iosev, "COM1");
    // 設置參數
    sp.set_option(serial_port::baud_rate(19200));
    sp.set_option(serial_port::flow_control(serial_port::flow_control::none));
    sp.set_option(serial_port::parity(serial_port::parity::none));
    sp.set_option(serial_port::stop_bits(serial_port::stop_bits::one));
    sp.set_option(serial_port::character_size(8));
    // 向串口寫數據
    write(sp, buffer("Hello world", 12));
    // 向串口讀數據
    char buf[100];
    read(sp, buffer(buf));
    iosev.run();
    return 0;
    }

    上面這段代碼有個問題,read(sp, buffer(buf))非得讀滿100個字符才會返回,串口通信有時我們確實能知道對方發過來的字符長度,有時候是不能的。

    如果知道對方發過來的數據里有分隔符的話(比如空格作為分隔),可以使用read_until來讀,比如:

    boost::asio::streambuf buf;
    // 一直讀到遇到空格為止
    read_until(sp, buf, ' ');
    copy(istream_iterator<char>(istream(&buf)>>noskipws),
    istream_iterator<char>(),
    ostream_iterator<char>(cout));

    另外一個方法是使用前面說過的異步讀寫+超時的方式,代碼如下:

    #include <iostream>
    #include <boost/asio.hpp>
    #include <boost/bind.hpp>
    using namespace std;
    using namespace boost::asio;
    void handle_read(char *buf,boost::system::error_code ec,
    std::size_t bytes_transferred)
    {
    cout.write(buf, bytes_transferred);
    }
    int main(int argc, char* argv[])
    {
    io_service iosev;
    serial_port sp(iosev, "COM1");
    sp.set_option(serial_port::baud_rate(19200));
    sp.set_option(serial_port::flow_control());
    sp.set_option(serial_port::parity());
    sp.set_option(serial_port::stop_bits());
    sp.set_option(serial_port::character_size(8));
    write(sp, buffer("Hello world", 12));
    // 異步讀
    char buf[100];
    async_read(sp, buffer(buf), boost::bind(handle_read, buf, _1, _2));
    // 100ms后超時
    deadline_timer timer(iosev);
    timer.expires_from_now(boost::posix_time::millisec(100));
    // 超時后調用sp的cancel()方法放棄讀取更多字符
    timer.async_wait(boost::bind(&serial_port::cancel, boost::ref(sp)));
    iosev.run();
    return 0;
    }
    RFID管理系統集成商 RFID中間件 條碼系統中間層 物聯網軟件集成
    最近免费观看高清韩国日本大全