共享顺风车项目总结一——第一次迭代项目架构

  • 1.项目介绍
  • 2.事件类
  • 3.短信验证码请求类
  • 4.短信验证码响应类
  • 5.事件处理类
  • 6.具体的事件处理类
  • 7.线程池
  • 8.事件服务派发类
  • 1.使用单例模式
  • 懒汉模式下的单例模式:
  • 饿汉模式下的单例模式:
  • 2.整合线程池
  • 3.处理网络数据
  • 3.实现发布者——订阅者功能
  • 9.网络接口类
  • 10.完整代码


1.项目介绍

共享顺风车,是面向用户,为用户提供共享单车的租赁、交还服务的项目。项目的服务器端架构整合了线程池与libevent,适用于高并发下的业务环境。

这篇文章主要介绍它的第一次迭代架构以及短信验证码请求与短信验证码响应业务的实现。

下面是它的第一次迭代架构图:

共享单车技术架构图 共享单车产品框架结构_共享单车技术架构图

2.事件类

iEvent类。短信验证码事件相关类的基类,实现多态的接口。其中数据成员sn_作为每个事件的标识。定义在函数内部的静态成员变量会进行sn_的初始化,保证每个事件标识符的唯一性。

u32 iEvent::generateSeqNo()
{
	static u32 seqn = 0;          //静态成员,保证每个对象sn_的唯一性
	return ++seqn;
}
MobileCodeEvent(const std::string& mobile) :iEvent(EEVENTID_GET_MOBLIE_CODE_REQ, iEvent::generateSeqNo())      //直接用类调用函数初始化父类的sn_

3.短信验证码请求类

MobileCodeEvent类,作为验证码请求事件。

4.短信验证码响应类

MobileCodeReqEvent类,作为短信验证码响应事件类,解析短信验证码请求并响应。

5.事件处理类

iEventHandler类,作为具体事件处理类的基类。实现多态的接口。

6.具体的事件处理类

UserEventHandler类。有handle成员函数。根据事件类型调用不同的函数进行处理。

调用实现发布者——订阅者功能函数。在构造函数里,订阅具体请求需要的服务,绑定具体的处理函数。在析构函数里解除绑定 。

关键结构

iEvent* UserEventHandler::handler(const iEvent* ev)        //根据事件eid调用不同的处理函数,这里只实现了短信验证码请求的处理
{
	if (!ev) {
		printf("ev is null");
	}

	u32 eid = ev->get_eid();

	if (eid == EEVENTID_GET_MOBLIE_CODE_REQ) {             //处理短信验证码请求
		return handle_mobile_code_req((MobileCodeEvent*)ev);
	}
	else if (eid == EEVENTID_GET_MOBLIE_CODE_RSP) {

	}
	else if (eid == EEVENTID_LOGIN_REQ) {

	}
	else if (eid == EEVENTID_LOGIN_RSP) {

	}
	else if (eid == EEVENTID_RECHARGE_REQ) {

	}
	else if (eid == EEVENTID_RECHARGE_RSP) {

	}

}

//短信验证码请求处理函数,接收短信验证码请求类,处理并返回短信验证码响应类
MobileCodeReqEvent* UserEventHandler::handle_mobile_code_req(MobileCodeEvent* ev)    
{
	std::string mobile_ = ev->get_mobile();
	i32 tmp = code_get();

	thread_mutex_lock(&mutex_);       //被线程池调用,锁住临界资源
	map_[mobile_] = tmp;
	thread_mutex_unlock(&mutex_);

	MobileCodeReqEvent* tmp1 = new MobileCodeReqEvent(200, tmp);
	tmp1->dump(std::cout);

	return tmp1;
}

7.线程池

值得一提的是,在这个项目中线程池调用了很多的函数,但凡是线程池调用的函数,都要注意对临界资源的互斥访问。

8.事件服务派发类

DispatchMsgService类。可以看成是一个中转站,整合了线程池与libevent库。将网络接口类接收到的数据变为具体事件请求交给线程池处理,线程池处理完后会返回相应的事件响应对象。再由该类对象处理好事件响应对象返回数据给网络接口类,网络接口类再把数据发回给客户端。

1.使用单例模式

保证只实例化一个DispatchMsgService类对象。

在这里提供实现单例模式的两种思路:

懒汉模式下的单例模式:

1.把默认的构造函数、拷贝构造函数、赋值构造函数声明为私有的,这样禁止在类的外部 创建该对象。

2.提供一个静态成员变量作为全局访问点。

3.提供一个静态成员函数返回类对象指针。

static DispatchMsgService* DMS_;       //单例模式

DispatchMsgService* DispatchMsgService::getInstance()
{
	if (DMS_ == nullptr) {    //懒汉模式,只在需要使用类对象时构造
		DMS_ = new DispatchMsgService();
	}

	return DMS_;    //如果已经存在一个类对象,则直接返回,不另外构造一个
}

饿汉模式下的单例模式:

饿汉模式实现的单例模式是线程安全的。它在类定义的时候就实例化一个类对象。

class DispatchMsgService{ 

private: 
    static DispatchMsgService* DMS_; 
protected: 
    DispatchMsgService(){}
public:
static DispatchMsgService* getInstance(){ 
       return DMS_; 
   }
};

DispatchMsgService* DispatchMsgService::instance = new DispatchMsgService();

2.整合线程池

将具体的事件交给线程池处理。

3.处理网络数据

处理网络接口类接收来的数据。

3.实现发布者——订阅者功能

订阅功能函数由UserEventHandler对象调用。

解释一下事件服务派发类对象具体的调用线程池的过程:

首先DispatchMsgService类会创建唯一的一个对象,该对象调用open()方法启动线程池。再调用enqueue(iEvent* ev)函数,他会传入一个iEvent对象,组装一个任务节点绑定任务处理函数为DispatchMsgService::svc,之后交给线程池任务队列。线程池会调用DispatchMsgService::svc函数,该函数会调用DispatchMsgService::process(const iEvent* ev)函数,如果此时该对象注册了处理函数,就创建一个UserEventHandler类对象并调用该对象的handler函数进行事件的处理。

9.网络接口类

NetworkInterface类。

1.实现网络接口

2.利用libevent库接收网络数据,解析应用层协议,重新组装数据并根据具体请求组装成不同类对象交给DispatchMsgService类处理。

3.接收处理完的响应类,将数据发送会客户端。

其中涉及到TCP分包与粘包的问题,这需要我们自己设计的应用层协议解决。
具体的应用层协议:

共享单车技术架构图 共享单车产品框架结构_c++_02


在客户端发送数据时,会把数据组织成这样的数据报形式。也就是在数据内容之上加上数据报头部(数据报头部包括数据报标识“FBEB”,事件ID与数据长度三部分)。这和计算机网络其他层的数据封装是类似的,也是加一个头部。

服务器端在接收到第一个数据后会先取出前四个字节判断是否是"FBEB" 。如果不是,则数据出错,断开该连接。如果是,则继续下面的处理,取出事件ID,保存事件ID之后根据事件ID做出具体的响应操作。取出数据长度,循化等待接收数据,直至接收到的数据长度等于数据长度。

处理TCP分包与粘包的具体代码如下:

//宏定义与数据结构
#define MESSAGE_HEADER_LEN  10
#define MESSAGE_HEADER_ID   "FBEB"


enum class SESSION_STATUS     //会话状态
{  
	SS_REQUEST,                //可以开始接收数据
	SS_RESPONSE                //接收数据完成
};

enum class MESSAGE_STATUS
{
	MS_READ_HEADER = 0,    //接收数据报头部
	MS_READ_MESSAGE = 1,   //接收数据部分
	MS_READ_DONE = 2,      //消息传输完毕
	MS_SENDING = 3         //消息传输中
};

typedef struct _ConnectSession {

	char remote_ip[32];

	SESSION_STATUS  session_stat;

	iEvent* request;
	MESSAGE_STATUS  req_stat;

	iEvent* response;
	MESSAGE_STATUS  res_stat;

	u16  eid;    //保存当前请求的事件id
	i32  fd;     //保存当前传送的文件句柄

	struct bufferevent* bev;

	char* read_buf;           //保存读消息的缓冲区
	u32  message_len;        //当前读写消息的长度
	u32  read_message_len;           //已经读取的消息长度

	char header[MESSAGE_HEADER_LEN + 1];         //保存头部,10字节+1字节
	u32 read_header_len;          //已读取的头部长度

	char* write_buf;
	u32  sent_len;           //已经发送的长度

}ConnectSession;




//具体函数及代码
void NetworkInterface::handle_request(struct bufferevent* bev, void* arg)
{
    ConnectSession* cs = (ConnectSession*)arg;

    if (cs->session_stat != SESSION_STATUS::SS_REQUEST)     //判断当前会话状态
    {
        LOG_WARN("NetworkInterface::handle_request - wrong session state[%d].", cs->session_stat);
        return;
    }

    if (cs->req_stat == MESSAGE_STATUS::MS_READ_HEADER)     //判断当前是否是接收数据报头部状态
    {
        i32 len = bufferevent_read(bev, cs->header + cs->read_header_len, MESSAGE_HEADER_LEN - cs->read_header_len);
        cs->read_header_len += len;

        cs->header[cs->read_header_len] = '\0';
        LOG_DEBUG("recv from client<<<< %s\n", cs->header);

        if (cs->read_header_len == MESSAGE_HEADER_LEN)      //当接收到完整的数据包头部
        {
            if (strncmp(cs->header, MESSAGE_HEADER_ID, strlen(MESSAGE_HEADER_ID)) == 0)     //比较头部标识是否是"FBEB"
            {
                cs->eid = *((u16*)(cs->header + 4));               //取出事件ID
                cs->message_len = *((i32*)(cs->header + 6));       //取出数据长度

                LOG_DEBUG("NetworkInterface::handle_request - read  %d bytes in header, message len: %d\n", cs->read_header_len, cs->message_len);

                if (cs->message_len<1 || cs->message_len > MAX_MESSAGE_LEN) {         //判断数据长度合法性,不合法就断开连接
                    LOG_ERROR("NetworkInterface::handle_request wrong message, len: %u\n", cs->message_len);
                    bufferevent_free(bev);
                    session_free(cs);
                    return;
                }

                cs->read_buf = new char[cs->message_len];          
                cs->req_stat = MESSAGE_STATUS::MS_READ_MESSAGE;        //读取数据报头部完成,修改状态为读取数据部分
                cs->read_message_len = 0;

            }
            else             //头部标识出错,断开连接
            {
                LOG_ERROR("NetworkInterface::handle_request - Invalid request from %s\n", cs->remote_ip);
                //直接关闭请求,不给予任何响应,防止客户端恶意试探
                bufferevent_free(bev);
                session_free(cs);
                return;
            }
        }
    }

    if (cs->req_stat == MESSAGE_STATUS::MS_READ_MESSAGE && evbuffer_get_length(bufferevent_get_input(bev)) > 0)        //判断当前状态是否是读取数据状态
    {
        i32 len = bufferevent_read(bev, cs->read_buf + cs->read_message_len, cs->message_len - cs->read_message_len);
        cs->read_message_len += len;
        LOG_DEBUG("NetworkInterface::handle_request - bufferevent_read: %d bytes, message len: %d read len: %d\n", len, cs->message_len, cs->read_message_len);

        if (cs->message_len == cs->read_message_len)          //当接收到完整的数据部分
        {
            cs->session_stat = SESSION_STATUS::SS_RESPONSE;         //读取全部数据完成,修改会话状态
            iEvent* ev = DispatchMsgService::getInstance()->parseEvent(cs->read_buf, cs->read_message_len, cs->eid);      //根据接收到的数据请求返回对应的事件对象

            delete[] cs->read_buf;               //释放内存
            cs->read_buf = nullptr;
            cs->read_message_len = 0;

            if (ev)
            {
                ev->set_args(cs);
                DispatchMsgService::getInstance()->enqueue(ev);
            }
            else
            {
                LOG_ERROR("NetworkInterface::handle_request - ev is null. remote ip: %s, eid: %d\n", cs->remote_ip, cs->eid);
                //直接关闭请求,不给予任何响应,防止客户端恶意试探
                bufferevent_free(bev);
                session_free(cs);
                return;
            }
        }
    }
}