服务端基础使用说明

1. 服务代码框架

假设将要开发的服务名为 Demo,因此,假设服务程序名为 DemoServer,服务的核心类名称为 DemoQuestProcessor

服务器框架一般会被分为三个文件:

  • DemoServer.cpp
  • DemoQuestProcessor.h
  • DemoQuestProcessor.cpp

如果服务很简单,也可以将三个文件合并为一个文件。

文件如下:

DemoQuestProcessor.h

#ifndef Demo_Processor_h
#define Demo_Processor_h
 
#include "IQuestProcessor.h"
 
using namespace fpnn;
 
class DemoQuestProcessor: public IQuestProcessor
{
    QuestProcessorClassPrivateFields(DemoQuestProcessor)
public:
    QuestProcessorClassBasicPublicFuncs
};
 
#endif

DemoQuestProcessor.cpp

#include "DemoQuestProcessor.h"
using namespace fpnn;

DemoServer.cpp

#include <iostream>
#include "TCPEpollServer.h"
#include "DemoQuestProcessor.h"
#include "Setting.h"
 
int main(int argc, char* argv[])
{
    if (argc != 2)
    {
        std::cout<<"Usage: "<<argv[0]<<" config"<<std::endl;
        return 0;
    }
    if(!Setting::load(argv[1])){
        std::cout<<"Config file error:"<< argv[1]<<std::endl;
        return 1;
    }
 
    ServerPtr server = TCPEpollServer::create();
    server->setQuestProcessor(std::make_shared<DemoQuestProcessor>());
    if (server->startup())
        server->run();
 
    return 0;
}

此时,以上已经是一个可以运行的FPNN 服务的所有代码。

一般实际开发中,如果没有特殊需求,服务的三个框架文件直接拷贝使用以上代码,替换 DemoQuestProcessor 为实际服务对应的名称即可。

从现在开始,本教程不再修改 DemoServer.cpp 中的代码,仅对 DemoQuestProcessor.h & DemoQuestProcessor.cpp 进行修改。

2. 添加接口方法

  1. 基本概念

    在开始添加服务接口前,需要明白几个基本概念:

    • FPQuestPtr & FPAnswerPtr

      FPQuestPtr 为服务请求数据的 shared_ptr 指针。
      FPAnswerPtr 为服务应答数据的 shared_ptr 指针。

      FPQuestPtr 所指向的对象中包含有客户端向服务器发送的所有请求信息。
      包含:

      • 请求的方法名
      • 方法参数
      • 请求序号
      • 链接性质(TCP or HTTP)
      • 方法参数的编码协议(msgPack or Json)

      一般仅需要关心方法参数即可。

      FPAnswerPtr 所指向的对象中包含有服务器向客户端发送的所有应答信息。
      包含:

      • 应答状态(正常、异常)
      • 对应的请求序号
      • 回复的应答数据

      FPQuest & FPAnswer 请参见:/proto/FPMessage.h。

    • FPReaderPtr

      FPReaderPtr 为FPNN 数据提取器的 shared_ptr 指针。
      通过对应的 FPReader 或 FPQReader 或 FPAReader 可以读取/提取 对应的 FPQuest 或 FPAnswer 中包含的全部数据。

      FPReader & FPQReader & FPAReader 请参见:/proto/FPReader.h。

    • twoway & oneway

      请求一般分为两种类型,一种是一问一答。
      即一个请求,一个回应。这种类型称为 twoway。
      另外一种只有请求,没有应答。换句话说,只有通知,没有响应。这种类型称为 oneway。

    • ConnectionInfo

      ConnectionInfo 包含了当前的链接状态。
      包含:

      • 客户端 ip(IPv4 or IPv6)
      • 客户端 port
      • 连接是否加密
      • 是否来自于IPv4内网/局域网
      • 是否是 WebSocket

      ConnectionInfo 请参见:/core/IQuestProcessor.h。

  2. 添加接口

    1. 接口的格式

      FPNN 无需 IDL 文件,因此,所有的接口格式均统一为

       FPAnswerPtr method_name(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci)
      

      用实际接口对应的函数名称替换 method_name 即可。

      假设 Demo 服务有两个接口,一个为 echo,是 twoway 类型;一个为 notify,是 oneway 类型。
      修改后的 DemoQuestProcessor.h & DemoQuestProcessor.cpp 如下:

      DemoQuestProcessor.h

       #ifndef Demo_Processor_h
       #define Demo_Processor_h
      
       #include "IQuestProcessor.h"
      
       using namespace fpnn;
      
       class DemoQuestProcessor: public IQuestProcessor
       {
           QuestProcessorClassPrivateFields(DemoQuestProcessor)
       public:
           FPAnswerPtr echo(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci);
           FPAnswerPtr notify(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci);
           QuestProcessorClassBasicPublicFuncs
       };
      
       #endif
      

      DemoQuestProcessor.cpp

       #include "DemoQuestProcessor.h"
      
       using namespace fpnn;
      
       FPAnswerPtr DemoQuestProcessor::echo(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci)
       {
           //-- need to add some codes
       }
      
       FPAnswerPtr DemoQuestProcessor::notify(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci)
       {
           //-- need to add some codes
       }
      
    2. 请求参数的获取

      请求参数的获取,一般使用 FPReaderPtr->wantXXX() 或 FPReaderPtr->getXXX()。

      比如,要获取一个名为 paramInt 的整型参数(int64_t, int32_t, int, int16_t, int8_t, …),可以使用

       int a = args->wantInt("paramInt")
       //-- or
       int a = args->getInt("paramInt")
      

      对于字符串,假设参数名为 paramString

       std::string str = args->wantString("paramString")
       //-- or
       std::string str = args->getString("paramString")
      

      对于数组,假设参数名为 IntArray 和 StringArray

       std::vector<int> arr = args->want("IntArray", std::vector<int>())
       std::vector<std::string> arr = args->want("IntArray", std::vector<std::string>())
       //-- or
       std::vector<int> arr = args->get("IntArray", std::vector<int>())
       std::vector<std::string> arr = args->get("IntArray", std::vector<std::string>())
      

      更多更详细的接口,请参见:/proto/FPReader.h。
      更多实例请参见 /proto/test/。

      至于使用 want 还是 get,取决于要获取的参数,是否是可选参数。
      want 系列方法,要求参数必须存在,且类型匹配。否则会抛出异常。
      如果业务没有处理该异常,则 FPNN 框架将自动捕获处理。对于 twoway 类型的请求,FPNN 框架还将自动返回参数缺失,或者参数类型错误的异常状态应答。

      对于 get 系列方法,对应于可选参数。不要求参数必须存在。如果目标参数不存在,则返回默认值。
      默认值可以指定,比如

       int a = args->getInt("paramInt", 100)
       std::string str = args->getString("paramString", "default")
      

      get 系列接口的默认值如下:

      类型 默认值
      整型 | 0 |
      浮点型 | 0.0 |
      bool | false |
      字符串 | "" |

      具体请参见:/proto/FPReader.h。
      更多实例请参见 /proto/test/。

      假设 echo 接口有一个字符类型必选参数,叫做 feedback;notify 有两个参数,一个字符类型必选参数:type,一个浮点型可选参数:value。
      则修改后的 DemoQuestProcessor.cpp 如下:

       #include "DemoQuestProcessor.h"
      
       using namespace fpnn;
      
       FPAnswerPtr DemoQuestProcessor::echo(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci)
       {
           std::string feedback = args->wantString("feedback");
           std::cout<<"Echo interface received: "<<feedback<<std::endl;
      
           //-- need to add some codes
       }
      
       FPAnswerPtr DemoQuestProcessor::notify(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci)
       {
           std::string type = args->wantString("type");
           double value = args->getDouble("value");
      
           std::cout<<"Notify interface received: type: "<<type<<", value: "<<value<<std::endl;
           //-- need to add some codes
       }			
      
    3. 接口返回

      对于 oneway 类型的请求,因为不用回应,所以接口直接返回 nullptr 即可。
      对于 twoway 类型的请求,如果没有采用 “提前返回”(参见高级部分) 或者 “异步返回”(参见高级部分),则须返回一个有效的 FPAnswerPtr。
      该 FPAnswerPtr 指向的 FPAnswer 对象,包含了需要返回给客户端的应答。

      假设 echo 接口,将 feedback 原样返回,返回的参数名为 got,则修改后的 DemoQuestProcessor.cpp 如下:

       #include "DemoQuestProcessor.h"
      
       using namespace fpnn;
      
       FPAnswerPtr DemoQuestProcessor::echo(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci)
       {
           std::string feedback = args->wantString("feedback");
           std::cout<<"Echo interface received: "<<feedback<<std::endl;
      
           FPAWriter aw(1, quest);
           aw.param("got", feedback);
           return aw.take();
       }
      
       FPAnswerPtr DemoQuestProcessor::notify(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci)
       {
           std::string type = args->wantString("type");
           double value = args->getDouble("value");
      
           std::cout<<"Notify interface received: type: "<<type<<", value: "<<value<<std::endl;
           return nullptr;
       }
      

      其中:

      • FPAWriter 是 FPAnswer 的生成器。需要将最表层的参数个数,和收到的 FPQuestPtr 对象作为参数传入。
      • aw.param() 函数将自动识别参数类型。
      • take() 操作,将会自动生成 FPAnswerPtr 对象。
      • 一个 FPAWriter 对象,只能 take() 一次。

      具体请参见:/proto/FPWriter.h。
      更多实例请参见 /proto/test/。

    4. 接口注册

      接口已经编写完毕,但如果不进行接口注册,则外界仍然无法访问。
      接口注册一般是在 IQuestProcessor 子类实例的构造函数注册。
      接口注册函数为:

       /*
           attributes:
               EncryptOnly: 只有加密链接可以调用该接口
               PrivateIPOnly: 只有内网地址可以调用该接口(IPv4 内网地址,或者IPv4/IPv6 本地环路地址)
       */
       inline void registerMethod(const std::string& method_name, MethodFunc func, uint32_t attributes = 0);
      

      注册的接口名可以和函数名不同,只要是合法字符串即可。可以包含空格。但不得以 * 号开头。

      “*” 号开头的接口,保留为框架内置接口。目前可用的内置接口请参考 FPNN 内置接口

      一般情况下,为了便于维护,注册的接口名建议与接口函数名保持一致。

      修改后的 DemoQuestProcessor.h 如下:

       #ifndef Demo_Processor_h
       #define Demo_Processor_h
      
       #include "IQuestProcessor.h"
      
       using namespace fpnn;
      
       class DemoQuestProcessor: public IQuestProcessor
       {
           QuestProcessorClassPrivateFields(DemoQuestProcessor)
       public:
           FPAnswerPtr echo(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci);
           FPAnswerPtr notify(const FPReaderPtr args, const FPQuestPtr quest, const ConnectionInfo& ci);
      
           DemoQuestProcessor()
           {
               registerMethod("echo", &DemoQuestProcessor::echo);
               registerMethod("notify", &DemoQuestProcessor::notify);
           }
           QuestProcessorClassBasicPublicFuncs
       };
      
       #endif
      

3. 编辑配置文件

必须的配置文件就只有一行

FPNN.server.listening.port = 6789 

该行指定了服务器运行所需监听的 IPv4 端口。

完整的配置文件模版请参见 FPNN 标准配置模版

假设 demo 服务的配置文件名为 demo.conf,则基本配置如下:

FPNN.server.listening.ip = 
FPNN.server.listening.port = 6789
FPNN.server.name = DemoServer
 
FPNN.server.log.level = DEBUG
FPNN.server.log.endpoint = std::cout
FPNN.server.log.route = DemoServer

FPNN.server.log.endpoint 指定了日志输出的形式。
std::cout 代表了日志将直接输出至终端屏幕。
如果同一台机上还运行着 logAgent,则可配置日志输出到 logAgent。
参照 logAgent 的配置文件配置 FPNN.server.log.endpoint 即可。
logAgent 采用默认配置时,FPNN.server.log.endpoint 可配置为

FPNN.server.log.endpoint = unix:///tmp/fplog.sock

如果日志配置为输出到 logAgent,还需找运维人员,在 logAgent & logServer 中配置对应的 FPNN.server.log.route

4. 编译

建立文件夹 DemoServer,并将 DemoServer.cpp、DemoQuestProcessor.h、DemoQuestProcessor.cpp 拷贝至 DemoServer 文件夹下。
假设 FPNN-Release 的目录为 infra-fpnn-release-path,创建文件 Makefile,内容如下:

EXES_SERVER = DemoServer
FPNN_DIR = infra-fpnn-release-path
 
CFLAGS +=
CXXFLAGS +=
CPPFLAGS += -I$(FPNN_DIR)/extends -I$(FPNN_DIR)/core -I$(FPNN_DIR)/proto -I$(FPNN_DIR)/base -I$(FPNN_DIR)/proto/msgpack -I$(FPNN_DIR)/proto/rapidjson
LIBS += -L$(FPNN_DIR)/extends -L$(FPNN_DIR)/core -L$(FPNN_DIR)/proto -L$(FPNN_DIR)/base -lextends -lfpnn
 
OBJS_SERVER = DemoServer.o DemoQuestProcessor.o
 
all: $(EXES_SERVER)
 
clean:
    $(RM) *.o $(EXES_SERVER)
 
include $(FPNN_DIR)/def.mk

然后执行 make 编译即可。

实际项目中,将 Makefile 中的 DemoServer、DemoServer.o、DemoQuestProcessor.o 替换为对应的实际名称即可。

5. 运行

执行 ./DemoServer demo.conf 即可。

测试 echo 接口:
运行 FPNN cmd 命令:./cmd localhost 6789 echo '{"feedback":"Hello, FPNN!"}' 1 1

测试 notify 接口:
运行 FPNN cmd 命令:./cmd localhost 6789 notify '{"type":"normal", "value":2.3}' 0 1

FPNN cmd 命令,请参考 FPNN 内置工具 相关条目。