Boost Asio异步TCP网络编程实例

news/2024/5/17 18:51:11 标签: Boost Asio, 异步编程, 网络编程, TCP

简介


本文主要描述TCP协议的实现,其他协议类似。

关于Boost Asio库是什么,请参考Boost Asio快速入门。这篇文章概述了Asio库的重点。

关于Boost Asio中提供的函数及使用,请参考Boost Asio 网络编程理论基础。该文可以快速预览,待到使用时再回来查询。另外,该文关于异步编程的关键注意点也有阐述。

本文关注于异步编程的实现(关于同步和异步的异同,请参考上文)。异步编程在于入门难,但一旦入门,你会发现网络编程如此简单。

场景


TCP网络编程分为客户端和服务端两个部分,一般情况下,服务端启动后等待客户端的连接到来,客户端启动后发送登录请求及其他数据。

根据协议的分类,对于服务端,又可以分为无协议的数据流接收和基于协议的数据接收,前者会连续接收,待数据完全后再解析,后者一般会先接收数据头,根据数据头计算出总体数据长度,再接收指定长度的数据。

用户需要根据项目实际需求调整相应的结节部分,总体上的程序框架是相同的。

本文示例基于协议的数据接收。

协议也非常简单,数据分为两部分:数据头(4字节)和数据体。4字节的数据头表示的是数据体的长度。数据全部接收完成后再解析处理。

其他协议可根据实际需求处理。

下面先从相对简单的客户端说起。

客户端


客户端执行的步骤如下:

  1. 向服务端建立异步连接
  2. 连接成功后开始准备接收数据
  3. 接收数据流程:先接收4个字节数据头,计算出数据体长度,再接收数据体
  4. 提供发送数据接口,并缓存不能及时发送的数据

示例代码:

// TcpMgr.h
#ifndef TCP_MGR_H_
#define TCP_MGR_H_

#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <boost/enable_shared_from_this.hpp>

#include "App.h" // 应用类,使用该TcpMgr的类,用于向应用类返回信息

using std::string;

class App;

// TcpMgr类需要一直存在,否则会发生错误。如把该实例建立在栈上,当作用域结束后,将被析构。所以类为static且返回智能指针
// 这里把构造方法设置成了私有而且不允许拷贝构造(继承自boost::noncopyable)
class TcpMgr : public boost::enable_shared_from_this<TcpMgr>, boost::noncopyable
{
public:
	// 
	static boost::shared_ptr<TcpMgr> Init(const char *ip, const unsigned short port, App *pApp);
	bool SendData(const uint8_t *pDataInfo, const size_t len, std::string errmsg); // 对外接口
	void Finit();

private:
	using error_code = boost::system::error_code;
	string m_ip;
	unsigned short m_port;
	boost::asio::io_service m_ios;
	boost::asio::io_service::work m_work;
	boost::asio::io_service::strand m_strand; // 单线程时不必要,但为了扩展,这里直接都以strand包裹了
	boost::asio::ip::tcp::socket m_sock; // 非线程安全的类,使用时务必注意
	static const int kNumOfWorkThreads = 1; // 处理tcp的线程数量
	boost::thread_group m_threads; // 用于创建线程
	bool m_socketStarted;
	enum
	{
		MSG_HEADER_LEN = 4 // 私有协议的数据头字节数
	};
	std::vector<char> m_recv_buffer; // 接收数据缓冲区
	std::list<boost::shared_ptr<std::string>> m_pending_sends; // 发送数据缓冲区,待发送数据以List管理
	App *m_pApp;
	static const int kTcpRetryInterval = 1000; //ms,自动重连的时间间隔

	TcpMgr();
	void Start(const char *ip, const unsigned short port, App *pApp);
	void OnConnect(const error_code &err); // 连接建立完成后会被调用
	void OnReadHeader(const error_code &err, size_t bytes); // 接收到数据头后会被调用
	void OnReadBody(const error_code &err, size_t bytes); // 接收到数据体后会被调用
	void OnWrite(const error_code &err); // 数据发送完成后会被调用
	void ReConnectServer();
	void RecvHeader();
	void RecvBody(int len);
	void StartRecvHeader();
	void StartRecvBody(int len);
	void SendMsg(boost::shared_ptr<string> msg);
	void StartSend();
	void Run();
	void CloseSocket();
	void StartCloseSocket();
};

#endif

// TcpMgr.cpp
#include "TcpMgr.h"

#include <thread>
#include <chrono>
#include "flatbuffers/AppDataType_generated.h" // 使用了FlatBuffers对数据编码

using namespace AppDataType;
using namespace std;

TcpMgr::TcpMgr()
    : m_work(m_ios),
      m_strand(m_ios),
      m_sock(m_ios),
      m_socketStarted(false)
{
}

boost::shared_ptr<TcpMgr> TcpMgr::Init(const char *ip, const unsigned short port, App *pApp)
{
    boost::shared_ptr<TcpMgr> newTcpMgr(new TcpMgr());
    newTcpMgr->Start(ip, port, pApp);
    return newTcpMgr;
}

void TcpMgr::Run()
{
    m_ios.run();
}

void TcpMgr::Finit()
{
    CloseSocket();
    m_ios.stop();
    m_threads.join_all();
}

void TcpMgr::Start(const char *ip, const unsigned short port, App *pApp)
{
    m_ip = ip;
    m_port = port;
    m_pApp = pApp;
    boost::asio::ip::tcp::endpoint ep(boost::asio::ip::address::from_string(m_ip), m_port);
    m_sock.async_connect(ep, m_strand.wrap(boost::bind(&TcpMgr::OnConnect, shared_from_this(), _1))); // 异步连接
    for (auto i = 0; i < kNumOfWorkThreads; ++i)
    {
        m_threads.create_thread(boost::bind(&TcpMgr::Run, this));
    }
}

void TcpMgr::OnConnect(const error_code &err)
{
    if (!err && m_sock.is_open()) // 连接成功
    {
        m_pApp->OnServerConnected();
        m_socketStarted = true;
        RecvHeader(); // 开始接收数据头
    }
    else
    {
        m_pApp->OnServerDisConnected(err.message());
        ReConnectServer();
    }
}

void TcpMgr::ReConnectServer()
{
    std::this_thread::sleep_for(std::chrono::milliseconds(kTcpRetryInterval));
    boost::asio::ip::tcp::endpoint ep(boost::asio::ip::address::from_string(m_ip), m_port);
    m_sock.async_connect(ep, m_strand.wrap(boost::bind(&TcpMgr::OnConnect, shared_from_this(), _1)));
}

void TcpMgr::OnReadHeader(const error_code &err, size_t bytes)
{
    if (!err)
    {
        size_t bodyLen = flatbuffers::ReadScalar<flatbuffers::uoffset_t>(m_recv_buffer.data()); // 解析实际数据长度
        RecvBody(bodyLen); // 开始接收数据体
    }
    else
    {
        m_pApp->OnServerDisConnected(err.message());
        CloseSocket();
        ReConnectServer();
    }
}

void TcpMgr::StartRecvHeader()
{
    m_recv_buffer.resize(MSG_HEADER_LEN);
    async_read(m_sock, boost::asio::buffer(m_recv_buffer, MSG_HEADER_LEN), m_strand.wrap(boost::bind(&TcpMgr::OnReadHeader, shared_from_this(), _1, _2)));
}

void TcpMgr::RecvHeader()
{
    m_strand.post(boost::bind(&TcpMgr::StartRecvHeader, shared_from_this()));
}

void TcpMgr::OnReadBody(const boost::system::error_code &ec, size_t bytes_transferred)
{
    if (!ec)
    {
        // 处理接收到的数据
        {
            // 数据交由子类处理,注意子类在处理中不能进行耗时操作,否则可能导致数据接收的阻塞
            m_pApp->OnRecvedData(&m_recv_buffer[0], bytes_transferred + MSG_HEADER_LEN);
        }
        // 继续读取
        RecvHeader();
    }
    else
    {
        m_pApp->OnServerDisConnected(ec.message());
        CloseSocket();
        ReConnectServer();
    }
}

void TcpMgr::StartRecvBody(int len)
{
    m_recv_buffer.resize(MSG_HEADER_LEN + len);
    async_read(m_sock, boost::asio::buffer(&m_recv_buffer[MSG_HEADER_LEN], len), m_strand.wrap(boost::bind(&TcpMgr::OnReadBody, shared_from_this(), _1, _2)));
}

void TcpMgr::RecvBody(int len)
{
    m_strand.post(boost::bind(&TcpMgr::StartRecvBody, shared_from_this(), len));
}

void TcpMgr::OnWrite(const error_code &err)
{
    if (err)
    {
        m_pApp->OnServerDisConnected(err.message());
        CloseSocket();
        ReConnectServer();
    }
    else
    {
        m_pending_sends.pop_front();
        StartSend();
    }
}

void TcpMgr::StartSend()
{
    if (!m_pending_sends.empty())
    {
        boost::asio::async_write(m_sock, boost::asio::buffer(*m_pending_sends.front().get()), m_strand.wrap(boost::bind(&TcpMgr::OnWrite, shared_from_this(), _1)));
    }
}

void TcpMgr::SendMsg(boost::shared_ptr<string> pmsg)
{
    bool should_start_send = m_pending_sends.empty();
    m_pending_sends.emplace_back(pmsg);
    if (should_start_send)
    {
        StartSend();
    }
}

bool TcpMgr::SendData(const uint8_t *pDataInfo, const size_t len, std::string errmsg)
{
    try
    {
        auto pmsg(boost::make_shared<string>((const char *)(pDataInfo), len));
        m_strand.post(boost::bind(&TcpMgr::SendMsg, shared_from_this(), pmsg));
    }
    catch (const std::exception &e)
    {
        errmsg.assign("发送数据失败:" + string(e.what()));
        return false;
    }

    return true;
}

void TcpMgr::CloseSocket()
{
    m_strand.post(boost::bind(&TcpMgr::StartCloseSocket, shared_from_this()));
}

void TcpMgr::StartCloseSocket()
{
    if (!m_socketStarted)
    {
        return;
    }
    boost::system::error_code ec;
    m_sock.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
    m_sock.close(ec);
    m_socketStarted = false;
}

几点说明:

  • strand是不必要的,因为只使用了一个线程,不会有并发。如果使用了多线程,则是必要的
  • 数据的接收是顺序的,不需要缓冲区暂存。但如果应用处理较慢,会有TCP接收缓冲区数据堆积
  • 发送数据是要管理缓冲区的,因为应用层可能要发送大量数据,而socket不是一直可供使用
  • 数据的接收需要根据协议修改,应用层也根据需要修改

服务端


服务端比较复杂,执行的步骤如下:

  1. 在指定端口上异步接收连接请求,并为连接绑定TcpSession
  2. 连接到来后,需要做两件事:一是继续异步接收连接请求,二是让TcpSession开始接收数据
  3. 接收数据流程同客户端
  4. 发送数据流程同客户端

示例代码如下:

// ClientMgrBase.h
#ifndef CLIENT_MGR_BASE_H_
#define CLIENT_MGR_BASE_H_

// #define BOOST_ASIO_ENABLE_HANDLER_TRACKING
#include <string>
#include <boost/thread.hpp>
#include <boost/asio.hpp>

using std::string;

class AsioTcp; // 接收tcp连接请求的类
class TcpSession; // 用于跟踪每个已经连接的类,包括socket和数据缓冲区

// 子类从该类派生,即可使用服务
class ClientMgrBase
{
public:
    ClientMgrBase();
    virtual ~ClientMgrBase();

    // 启动服务,开始在指定的端口监听连接,在程序启动时调用。如果返回值非0,则err存储为错误信息
    int Start(const unsigned short port, std::string &err);
    // 停止监听,释放所有资源,一般在程序退出时调用。如果返回值非0,则err存储为错误信息
    int Release(std::string &err);
    // 发送数据。如果返回值非0,则err存储为错误信息
    int SendData(boost::shared_ptr<TcpSession> handler, const char *pData, const size_t nDataSize, std::string &err);
    // 主动关闭会话。如果返回值非0,则err存储为错误信息
    int CloseSession(boost::shared_ptr<TcpSession> handler, std::string &err);

    // 回调函数,由子类实现
    // 新连接建立
    virtual void OnNewConnection(boost::asio::ip::tcp::socket &newSocket) = 0;
    // 连接断开
    virtual void OnDisConnection(boost::shared_ptr<TcpSession> handler) = 0;
    // 接收到数据
    virtual void OnRecvData(boost::shared_ptr<TcpSession> handler, const char *pData, const size_t nDataSize) = 0;
    // 发送数据的结果,ec为0时发送成功,非0时发送失败。无论成功与否,均返回发送的数据,以便处理和调试
    virtual void OnSendComplete(boost::shared_ptr<TcpSession> handler, const int ec, const std::string &em, const char *buf, const size_t len) = 0;
    // 发生异常时, 向子类返回错误信息
    virtual void OnRtnErrMsg(boost::shared_ptr<TcpSession> handler, const std::string &err) = 0;

private:
    boost::shared_ptr<AsioTcp> m_pAsioTcp;
};

// 与每个已经连接的客户端关联的类,包括socket和数据缓冲区
class TcpSession : public boost::enable_shared_from_this<TcpSession>, boost::noncopyable
{
public:
    TcpSession(boost::asio::io_service &ioService, ClientMgrBase *pClientMgrBase);
    virtual ~TcpSession();
    void ReadHeader();
    bool SendData(const char *pData, const size_t nDataSize);
    void CloseSocket();
    boost::asio::ip::tcp::socket &GetSocket();
    boost::asio::strand &GetStrand();

private:
    enum
    {
        MSG_HEADER_LEN = 4
    };
    std::vector<char> m_recv_buffer;
    std::list<boost::shared_ptr<std::string>> m_pending_sends;
    boost::asio::ip::tcp::socket m_socket;
    boost::asio::strand m_strand;
    ClientMgrBase *m_pClientMgrBase;
    bool m_started;

    void HandleReadHeader(const boost::system::error_code &ec, size_t bytes_transferred);
    void HandleReadBody(const boost::system::error_code &ec, size_t bytes_transferred);
    void HandleWrite(const boost::system::error_code &ec, size_t bytes_transferred);
    void StartCloseSocket();
    void StartRecvHeader();
    void RecvBody(int len);
    void StartRecvBody(int len);
    void SendMsg(boost::shared_ptr<string> msg);
	void StartSend();
};

// 负责启动并准备接收新连接
class AsioTcp
{
public:
    AsioTcp();
    virtual ~AsioTcp();

    void Start(ClientMgrBase *pClientMgrBase, const unsigned short port);
    void SendMsg(boost::shared_ptr<TcpSession> handler, const char *pData, const size_t nDataSize);
    int CloseConnection(boost::shared_ptr<TcpSession> handler, std::string &err);
    void Stop();

private:
    using TcpSessionPtr = boost::shared_ptr<TcpSession>;
    using acceptor = boost::asio::ip::tcp::acceptor;
    boost::asio::io_service m_ioservice;
    boost::shared_ptr<boost::asio::io_service::work> m_work;
    boost::shared_ptr<acceptor> m_pAcceptor;
    ClientMgrBase *m_pClientMgrBase;
    int m_numOfWorkThreads;
    boost::thread_group m_workThreads;

    void Run();
    void StartAccept();
    void HandleAccept(const boost::system::error_code &ec, TcpSessionPtr pTcpSession);
};

#endif

// ClientMgrBase.cpp
#include "ClientMgrBase.h"
#include <iostream>
#include <thread>
#include <string>
#include <boost/bind.hpp>
#include "AppDataType_generated.h"

using namespace AppDataType;
using namespace boost::asio;
using namespace std;

ClientMgrBase::ClientMgrBase()
{
}

ClientMgrBase::~ClientMgrBase()
{
}

int ClientMgrBase::Start(const unsigned short port, string &err)
{
    try
    {
        m_pAsioTcp = boost::make_shared<AsioTcp>();
        m_pAsioTcp->Start(this, port);
    }
    catch (const std::exception &e)
    {
        err.assign(e.what());
        return -1;
    }
    return 0;
}

int ClientMgrBase::Release(string &err)
{
    try
    {
        m_pAsioTcp->Stop();
    }
    catch (const std::exception &e)
    {
        err.assign(e.what());
        return -1;
    }
    return 0;
}

int ClientMgrBase::SendData(boost::shared_ptr<TcpSession> handler, const char *pData, const size_t nDataSize, string &err)
{
    try
    {
        m_pAsioTcp->SendMsg(handler, pData, nDataSize);
    }
    catch (const std::exception &e)
    {
        err.assign(e.what());
        return -1;
    }
    return 0;
}

int ClientMgrBase::CloseSession(boost::shared_ptr<TcpSession> handler, string &err)
{
    return m_pAsioTcp->CloseConnection(handler, err);
}

AsioTcp::AsioTcp() : m_work(new boost::asio::io_service::work(m_ioservice))
{
    m_numOfWorkThreads = std::thread::hardware_concurrency();
}

AsioTcp::~AsioTcp(void)
{
}

void AsioTcp::Run()
{
    m_ioservice.run();
}

void AsioTcp::Start(ClientMgrBase *pClientMgrBase, const unsigned short port)
{
    m_pClientMgrBase = pClientMgrBase;
    m_pAcceptor = boost::make_shared<acceptor>(m_ioservice, ip::tcp::endpoint(ip::tcp::v4(), port));
    StartAccept();
    // 启动任务处理线程
    for (auto i = 0; i < m_numOfWorkThreads; i++)
    {
        m_workThreads.create_thread(boost::bind(&AsioTcp::Run, this));
    }
}

void AsioTcp::StartAccept()
{
    TcpSessionPtr pTcpSession = boost::make_shared<TcpSession>(m_ioservice, m_pClientMgrBase);
    m_pAcceptor->async_accept(pTcpSession->GetSocket(), pTcpSession->GetStrand().wrap(boost::bind(&AsioTcp::HandleAccept, this, _1, pTcpSession)));
}

void AsioTcp::HandleAccept(const boost::system::error_code &ec, TcpSessionPtr pTcpSession)
{
    if (ec)
    {
        string err = "AsioTcp处理连接请求失败:" + ec.message();
        m_pClientMgrBase->OnRtnErrMsg(pTcpSession, err);
        StartAccept();
        return;
    }

    if (m_pClientMgrBase != NULL)
    {
        m_pClientMgrBase->OnNewConnection(pTcpSession->GetSocket());
    }

    try
    {
        StartAccept();
        pTcpSession->ReadHeader();
    }
    catch (const std::exception &e)
    {
        string err = "AsioTcp重启连接失败:" + string(e.what());
        m_pClientMgrBase->OnRtnErrMsg(pTcpSession, err);
    }
}

void AsioTcp::SendMsg(boost::shared_ptr<TcpSession> handler, const char *pData, const size_t nDataSize)
{
    handler->SendData(pData, nDataSize);
}

void AsioTcp::Stop()
{
    m_ioservice.stop(); // 此操作会立即结束所有任务
    // m_work.reset(); // 此操作会等待ios处理完所有任务后返回,有可能阻塞
    m_workThreads.join_all();
}

int AsioTcp::CloseConnection(boost::shared_ptr<TcpSession> handler, string &err)
{
    handler->CloseSocket();
    (void)err;
    return 0;
}

TcpSession::TcpSession(boost::asio::io_service &ioService, ClientMgrBase *pClientMgrBase)
    : m_socket(ioService),
      m_strand(ioService),
      m_pClientMgrBase(pClientMgrBase)
{
    m_started = true;
}

TcpSession::~TcpSession()
{
}

boost::asio::ip::tcp::socket &TcpSession::GetSocket()
{
    return m_socket;
}

boost::asio::strand &TcpSession::GetStrand()
{
    return m_strand;
}

void TcpSession::ReadHeader()
{
    m_strand.post(boost::bind(&TcpSession::StartRecvHeader, shared_from_this()));
}

void TcpSession::StartRecvHeader()
{
    m_recv_buffer.resize(MSG_HEADER_LEN);
    async_read(m_socket, boost::asio::buffer(m_recv_buffer, MSG_HEADER_LEN), m_strand.wrap(boost::bind(&TcpSession::HandleReadHeader, shared_from_this(), _1, _2)));
}

void TcpSession::HandleReadHeader(const boost::system::error_code &ec, size_t bytes_transferred)
{
    (void)bytes_transferred;
    if (!ec)
    {
        size_t bodyLen = flatbuffers::ReadScalar<flatbuffers::uoffset_t>(m_recv_buffer.data()); // 解析实际数据长度
        RecvBody(bodyLen);
    }
    else
    {
        // 发生错误
        m_pClientMgrBase->OnRtnErrMsg(shared_from_this(), ec.message());
        CloseSocket();
    }
}

void TcpSession::RecvBody(int len)
{
    m_strand.post(boost::bind(&TcpSession::StartRecvBody, shared_from_this(), len));
}

void TcpSession::StartRecvBody(int len)
{
    m_recv_buffer.resize(MSG_HEADER_LEN + len);
    async_read(m_socket, boost::asio::buffer(&m_recv_buffer[MSG_HEADER_LEN], len), m_strand.wrap(boost::bind(&TcpSession::HandleReadBody, shared_from_this(), _1, _2)));
}

void TcpSession::HandleReadBody(const boost::system::error_code &ec, size_t bytes_transferred)
{
    if (!ec)
    {
        // 处理接收到的数据
        if (m_pClientMgrBase != NULL)
        {
            // 数据交由子类处理,注意子类在处理中不能进行耗时操作,否则可能导致数据接收的阻塞
            m_pClientMgrBase->OnRecvData(shared_from_this(), &m_recv_buffer[0], bytes_transferred + MSG_HEADER_LEN);
        }
        // 继续读取
        ReadHeader();
    }
    else
    {
        m_pClientMgrBase->OnRtnErrMsg(shared_from_this(), ec.message());
        CloseSocket();
    }
}

bool TcpSession::SendData(const char *data, const size_t size)
{
    try
    {
        auto pmsg(boost::make_shared<string>(data, size));
        m_strand.post(boost::bind(&TcpSession::SendMsg, shared_from_this(), pmsg));
    }
    catch (const std::exception &e)
    {
        return false;
    }

    return true;
}

void TcpSession::SendMsg(boost::shared_ptr<string> pmsg)
{
    bool should_start_send = m_pending_sends.empty();
    m_pending_sends.emplace_back(pmsg);
    if (should_start_send)
    {
        StartSend();
    }
}

void TcpSession::StartSend()
{
    if (!m_pending_sends.empty())
    {
        boost::asio::async_write(m_socket, boost::asio::buffer(*m_pending_sends.front().get()), m_strand.wrap(boost::bind(&TcpSession::HandleWrite, shared_from_this(), _1, _2)));
    }
}

void TcpSession::HandleWrite(const boost::system::error_code &ec, size_t bytes_transferred)
{
    if (!ec)
    {
        // 数据发送完成
        m_pending_sends.pop_front();
        StartSend();
    }
    else
    {
        // 发生错误
        string err = "数据发送失败:" + ec.message();
        m_pClientMgrBase->OnSendComplete(shared_from_this(), -1, err, m_pending_sends.front()->c_str(), bytes_transferred);
        CloseSocket();
    }
}

void TcpSession::CloseSocket()
{
    m_strand.post(boost::bind(&TcpSession::StartCloseSocket, shared_from_this()));
}

void TcpSession::StartCloseSocket()
{
    if (!m_started)
    {
        return;
    }
    boost::system::error_code ec;
    m_pClientMgrBase->OnDisConnection(shared_from_this());
    m_socket.shutdown(boost::asio::ip::tcp::socket::shutdown_both, ec);
    m_socket.close(ec);
    m_started = false;
}

说明:

  • 异步操作的精髓与客户端类似,因为不知道具体的异步操作何时发生,所以要保证异步操作发生时所有相关的实例是活动的
  • 正因为上述原因,所以在bind函数中,使用的都是shared_from_this()智能指针,如果使用普通指针,在相关回调函数中可能实例已经释放,就会崩溃
  • 异步发送之前,要把数据缓存,不然可能会造成数据丢失
  • 启动的线程数量为std::thread::hardware_concurrency();,它获取了操作系统的核心数,实际根据需要选择
  • #define BOOST_ASIO_ENABLE_HANDLER_TRACKING宏可以打开Boost调试,当心,这会输出较多日志,方便调试
  • 封装的类可以根据需要简化

小结


Boost Asio异步网络编程强健、跨平台,但在使用时要特别注意一些事项:

  • 要保证异步操作发生时,相关的实例是活动的
  • 要注意socket类非线程安全,在多线程中要使用strand
  • 接收和发送缓冲区要注意自行管理
  • 注意异常处理

把握住了以上几点,先写个简单的应用,测试一下,改下代码,知道什么情况下会有问题,什么情况下是安全的,就差不多掌握了。


http://www.niftyadmin.cn/n/1630145.html

相关文章

实例:用C#.NET手把手教你做微信公众号开发(6)--普通消息处理之视频、小视频

本篇讲解微信客户端向公众号发送视频和小视频的处理方式。 视频消息常见应用&#xff1a; 在线教学&#xff1b; 基于公众号的定向类型小视频应用&#xff0c;类似于抖音、快手&#xff0c;但不用再安装app&#xff1b; 视频剪辑、特效添加&#xff0c;比如美颜&#xff1b…

hdu1059

题意&#xff1a;把价值为1&#xff0c;2&#xff0c;3&#xff0c;4&#xff0c;5&#xff0c;6的宝石平均分成两份&#xff0c;不能切割&#xff0c;有没有办法分开。 分析&#xff1a;多重背包。之前直接用01背包的方法做78ms&#xff0c;然后想试试用二进制优化&#xff0c…

启动eclipse出现错误Java was started but returned exit =一个数字

昨天因为Tomcat问题装了JDK1.6&#xff0c;原来的jdk是1.8的&#xff0c;然后今天启动eclipse出现异常&#xff0c;Java was started but returned exit 1。 异常中已经说明java is started 说明java运行是正常的&#xff08;不放心可以去命令行dos下测试java环境&#xff09;&…

Linux前后台程序的切换与控制

本文主要介绍在Linux下运行程序&#xff0c;及在前台和后台之间切换与控制的方法。 前台运行程序 通常的运行方法是&#xff1a;./a.out&#xff0c;表示运行当前目录下的可执行程序&#xff0c;并运行在前台。 运行在前台指的是当前终端窗口会暂停接受其他指令&#xff0c;而…

linux下查看用户登录历史

linux last 功能说明&#xff1a;列出目前与过去登入系统的用户相关信息。语  法&#xff1a;last [-adRx][-f <记录文件>][-n <显示列数>][帐号名称...][终端机编号...]补充说明&#xff1a;单独执行last指令&#xff0c;它会读取位于/var/log目录下&#xff0c…

实例:用C#.NET手把手教你做微信公众号开发(7)--普通消息处理之位置消息

今天我们来讲一下一个非常重要的消息&#xff1a;GPS位置消息。 一、应用举例 基于位置的应用太多太多了&#xff0c;比如&#xff1a; 查找附近的人&#xff1b; 查找附近的商家&#xff1b; 计算与指定的人或商家的距离&#xff1b; 使用百度地图、腾讯地图、阿里地图的…

Spark2.1.0完全分布式环境搭建

1.选取三台服务器&#xff08;CentOS系统64位&#xff09; 114.55.246.88 主节点 114.55.246.77 从节点 114.55.246.93 从节点 之后的操作如果是用普通用户操作的话也必须知道root用户的密码&#xff0c;因为有些操作是得用root用户操作。如果是用root用户操作的话就不存在以上…

Eclipse上GIT插件EGIT使用手册之十二_重置功能

GIT中有三种重置功能&#xff0c;分别是soft、mixed、hard&#xff0c;区别如下&#xff1a; l Soft - 当前分支重置到指定commit记录位置&#xff0c;索引和工作树不变&#xff1b; l Mixed - 当前分支重置到指定commit记录位置&#xff0c;索引被更新&#xff0c;工作树不变…