【项目总结】c++与go语言基于HTTP 协议的服务注册中心【c++与golang】

文章目录

  • 前言
  • 一、注册中心整体框架
    • 1、成员变量以及单例模式
    • 2、服务列表的维护
    • 3、HTTP请求处理回调函数
    • 4、启动服务注册中心
  • 二、线程安全
  • 三、支持负载均衡
  • 四、服务注册以及心跳机制
    • 1、更新心跳
    • 2、结束心跳
  • 五、服务发现


前言

服务注册中心是一种用于实现微服务架构的基础设施,用于管理和维护服务的注册、发现、负载均衡等功能。它可以让不同的服务通过一个中心化的方式进行管理,降低了服务之间的耦合性,提高了服务之间的可扩展性和可维护性。使用HTTP通信可以简化注册中心的实现,因为HTTP是一种开放的、标准的协议,很容易被不同的语言和框架支持。自定义Header字段可以传递更多信息,扩展性非常强,可以根据不同的需求设计不同的字段。此外,使用HTTP通信也具有较好的跨平台和跨语言特性,使得不同的服务可以在不同的服务器上运行,提高了应用的可移植性和灵活性。
使用HTTP协议实现服务注册中心具有以下优点:
通用性强:HTTP协议是广泛应用的标准协议,简单易用,可以跨平台、跨语言使用。
易于部署和管理:基于HTTP协议的服务注册中心可以很方便地部署在云端或本地服务器上,并通过监控和管理工具来维护和管理服务。
支持多种传输协议:服务注册中心可以支持多种传输协议,包括HTTP、gRPC等,从而满足不同场景下的需求。

【项目总结】c++与go语言基于HTTP 协议的服务注册中心【c++与golang】_第1张图片
go语言的基于HTTP 协议的服务注册中心实现已经在之前的文章实现了。
go语言实现的rpc框架
本文章用c++语言实现注册中心,利用go语言作为服务注册者,c++语言最为服务发现者,实现的功能有服务注册,服务发现,心跳机制以及负载均衡。

一、注册中心整体框架

服务注册者可以向注册中心注册服务发送心跳获取可用的服务地址并注销服务。其中,注册和注销操作会修改 ServerList 中的数据,而获取服务地址则只是读取该数据。同时,由于多个线程可能同时访问 ServerList,因此该程序使用了互斥锁来保证数据的安全性。HTTP服务使用httplib库来实现。

1、成员变量以及单例模式

注册中心类Registry使用单例模式来保证只有一个 Registry 对象实例存在,用于管理服务的注册和发现。因为在多个地方需要使用该类时,如果每次都创建一个新的对象,则会导致数据不一致或者资源浪费的问题。
首先,包含了三个私有成员变量 ServerList、ServerFreMap、Mu 和Timeout。其中,ServerList 是一个类似于 Map 的容器,用于存储各个服务名称对应的服务器列表;ServerFreMap 也是一个 Map 类型的变量,用于记录每个服务被调用的次数;Mu 是一个互斥锁。Timeout表示服务超时时间,也就是服务经过这么久没有发送心跳就默认该服务以及宕机了。

  private:
   std::chrono::duration<int, std::milli> Timeout;
   std::mutex Mu;
   std::map<std::string, int> ServerFreMap;
   std::map<std::string, std::vector<ServerItem>> ServerList;
   SelectMode selectmodel;
   explicit Registry(std::chrono::duration<int, std::milli> timeout,
                     SelectMode model)
       : Timeout(timeout),
         selectmodel(model){
             // std::cout << "timeout:" << std::ctime(timeout) << std::endl;
         };

  public:
   // 获取 Registry 实例的静态方法
   static Registry& instance(std::chrono::duration<int, std::milli> timeout,
                             SelectMode model) {
      static Registry registry(timeout, model);  // 静态局部变量
      return registry;
   }

Instance() 方法返回一个静态的 Registry 对象实例。它定义了一个静态局部变量 instance,该变量在第一次调用 Instance() 方法时被创建,并且只被创建一次,之后每次调用 Instance() 方法都返回该静态变量的引用。

由于构造函数是私有的,所以无法通过外部创建 Registry 类的对象,从而保证只有一个 Registry 实例存在。同时,使用 delete 关键字禁止了拷贝构造函数和赋值运算符的使用,防止非预期的对象复制。

因此,该程序使用单例模式来确保只有一个 Registry 对象实例存在,从而避免了数据不一致或者资源浪费的问题。

2、服务列表的维护

接下来,程序实现了三个公共方法:
PutServer() 方法:用于将一个服务注册到注册中心。在该方法中,程序首先获取该服务对应的服务器列表,如果列表中已经存在相同 IP 地址的服务器,则更新其活跃时间;否则,将新服务器添加到列表中。最后,输出一条日志表示新服务器已经成功注册。


void Registry::PutServer(const std::string& serverName,
                         const std::shared_ptr<ServerItem>& serverItem) {
   std::lock_guard<std::mutex> lock(Mu);
   auto& serverList = ServerList[serverName];
   for (auto& item : serverList) {
      if (item.Addr == serverItem->Addr) {
         item.ActiveTime =
             std::chrono::system_clock::now();  // if exists, update start time
                                                // to keep alive
         return;
      }
   }
   serverItem->ActiveTime =
       std::chrono::system_clock::now();  // if exists, update start time
                                          // to keep alive
   std::cout << "##############server Regist############## " << std::endl;
   std::cout << serverItem->Name << ":" << serverItem->Addr << std::endl;
   serverList.push_back(*serverItem);
}

DestructServer() 方法:用于从注册中心删除某个服务对应的服务器。在该方法中,程序首先获取该服务对应的服务器列表,然后删除相应的服务器信息。如果该服务对应的服务器列表为空,则从 ServerList 中删除该服务。

void Registry::DestructServer(const std::string& serverName,
                              const std::string& addr) {
   std::cout << "##############server destruct############## " << std::endl;
   std::cout << serverName << ":" << addr << std::endl;

   auto& serverItemList = ServerList[serverName];
   for (auto it = serverItemList.begin(); it != serverItemList.end();) {
      if (it->Addr == addr) {
         it = serverItemList.erase(it);  // timeout, remove from server list
         if (serverItemList.empty()) {
            ServerList.erase(serverName);
         }
      }
   }
}

UpdateAllServer方法:用于更新所有服务实例的存活情况,首先打印了一条日志以提示当前服务实例数量,然后遍历ServerList中的所有服务实例,检查其是否超时并移除超时的实例。如果某个服务实例所对应的vector为空,则从ServerList中移除该服务

void Registry::UpdataAllServer() {
   std::lock_guard<std::mutex> lock(Mu);
   std::cout << "======= UpdataAllServer:" << std::endl;
   std::cout << "number of server" << ServerList.size() << std::endl;

   std::vector<std::string> deleteserver;
   for (auto& [serverName, serverItemList] : ServerList) {
      更新存活的 ServerItem
      std::cout << serverName << "  usetime:" << ServerFreMap[serverName]
                << std::endl;
      for (auto it = serverItemList.begin(); it != serverItemList.end();) {
         if (std::chrono::system_clock::now() - it->ActiveTime < Timeout) {
            std::cout << "  " << it->Addr << std::endl;
            ++it;
         } else {
            std::cout << "one server timeout  " << it->Name << ":" << it->Addr
                      << std::endl;
            // //z注意

            it = serverItemList.erase(it);  // timeout, remove from server list
            if (serverItemList.empty()) {
               deleteserver.push_back(serverName);
            }
         }
      }
   }
   for (int i = 0; i < deleteserver.size(); i++) {
      ServerList.erase(deleteserver[i]);
   }
}

DiscoverServer() 方法:用于从注册中心中查找指定服务名称对应的服务器地址。在该方法中,程序首先使用 UpdataAllServer() 方法更新所有服务列表中的存活状态,然后根据负载均衡算法选择一个可用的服务器。最后,输出一条日志表示已经成功找到一个可用的服务器。


std::string Registry::DiscoverServer(const std::string& serverName) {
   // //z=锁必须放在这个位置就会出错
   // std::lock_guard lock(Mu);
   UpdataAllServer();
   if (!ServerList.count(serverName)) {
      return "";
   }
   // //z=锁必须放在这个位置
   std::lock_guard<std::mutex> lock(Mu);
   std::vector<ServerItem>& ServerItemList = ServerList[serverName];
   ServerFreMap[serverName]++;

   int chooseIndex = 0;
   if (selectmodel == RoundRobinSelect) {
      // 负载均衡轮询算法 取这个服务被调用的次数&数组大小
      chooseIndex = ServerFreMap[serverName] % ServerItemList.size();
   } else {
      // 负载均衡随机选择
      std::random_device rd;  // 获取随机设备种子
      std::mt19937 gen(rd());  // 使用 Mersenne Twister 算法生成随机数引擎
      std::uniform_int_distribution<> distrib(
          0, ServerItemList.size());  // 定义均匀分布
      chooseIndex = distrib(gen);     // 生成随机数
   }
   std::cout << "/select model:" << selectmodel
             << " get addr:" << ServerItemList[chooseIndex].Addr << std::endl;

   return ServerItemList[chooseIndex].Addr;
}

3、HTTP请求处理回调函数

定义了一个名为 RegisterHandler() 的方法,用于处理请求。该方法使用 HTTP 协议实现,主要包含三个接口:
/registerCentor/register:注册服务。
/registerCentor/GetServer:获取可用的服务地址。
/registerCentor/deregister:注销服务。


void Registry::RegisterHandler(httplib::Server& server) {
   // 心跳机制
   server.Post("/registerCentor/register", [&](const httplib::Request& req,
                                               httplib::Response& res) {
      auto addrAndServiceName = req.get_header_value("Server-Information");
      if (addrAndServiceName.empty()) {
         res.status = 500;
         return;
      }
      auto pos = addrAndServiceName.find('#');
      if (pos == std::string::npos) {
         res.status = 500;
         return;
      }
      auto serviceName = addrAndServiceName.substr(0, pos);
      auto addr = addrAndServiceName.substr(pos + 1);
      auto serverItem = std::make_shared<ServerItem>();
      serverItem->Name = serviceName;
      serverItem->Addr = addr;
      PutServer(serviceName, serverItem);
      res.status = 200;
   });
   // 获取
   server.Get("/registerCentor/getServer",
              [&](const httplib::Request& req, httplib::Response& res) {
                 std::cout << "a client GetServer" << std::endl;
                 auto serverName = req.get_header_value("servername");
                 if (serverName.empty()) {
                    res.status = 500;
                    return;
                 }
                 auto retServerAddr = DiscoverServer(serverName);
                 res.set_header("ServerList", retServerAddr.c_str());
              });
   server.Post("/registerCentor/deregister", [&](const httplib::Request& req,
                                                 httplib::Response& res) {
      std::cout << "get a server destruct" << std::endl;
      auto addrAndServiceName = req.get_header_value("Server-Deregister");
      if (addrAndServiceName.empty()) {
         res.status = 500;
         return;
      }
      auto pos = addrAndServiceName.find('#');
      if (pos == std::string::npos) {
         res.status = 500;
         return;
      }
      auto serviceName = addrAndServiceName.substr(0, pos);
      auto addr = addrAndServiceName.substr(pos + 1);
      DestructServer(serviceName, addr);
      res.status = 201;
   });
}

4、启动服务注册中心

创建了一个线程,每隔 5 秒钟执行一次 UpdataAllServer() 方法,用于更新所有服务器的状态。该方法会检查每个服务对应的服务器列表,并标记哪些服务器处于活跃状态。如果某个服务器超过了设定的超时时间,则将其从服务器列表中删除。
然后,调用 Registry 类的 RegisterHandler() 方法,将 HTTP 请求处理程序注册到 httplib::Server 对象上,并启动服务器监听。在客户端需要进行服务注册、发现或注销时,可以向该服务器发送相应的 HTTP 请求。

   Registry& registry =
       Registry::instance(std::chrono::minutes(1), RoundRobinSelect);

   // 更新状态

   std::thread([&]() {
      while (true) {
         // 每隔5秒钟执行一次 UpdataAllServer 函数
         std::this_thread::sleep_for(std::chrono::seconds(5));
         registry.UpdataAllServer();
      }
   }).detach();

   httplib::Server server;
   registry.RegisterHandler(server);
   server.listen("127.0.0.1", 8080);  // 启动服务器
   return 0;

二、线程安全

使用了互斥锁(std::mutex)来保证多线程下的数据访问安全性。在 Registry 类中,将 Mu 变量定义为一个互斥锁,在需要修改 ServerList 的方法中,如 PutServer()、DiscoverServer()、DestructServer() 和 UpdataAllServer() 等方法中,都使用了 std::lock_guard 对象来锁定 Mu 变量
std::lock_guard 是一个 RAII(资源获取即初始化)对象,用于封装互斥锁的自动加锁和解锁。它的底层原理是:在创建 std::lock_guard 对象时,会自动调用互斥锁的 lock() 方法,将锁定该互斥锁;当 std::lock_guard 对象离开作用域时,会自动调用互斥锁的 unlock() 方法,将互斥锁解锁。

void Registry::PutServer(const std::string& serverName,
                         const std::shared_ptr<ServerItem>& serverItem) {
   std::lock_guard<std::mutex> lock(Mu);  // 锁定互斥锁
   // ...
}

std::string Registry::DiscoverServer(const std::string& serverName) {
   std::lock_guard<std::mutex> lock(Mu);  // 锁定互斥锁
   // ...
}

void Registry::DestructServer(const std::string& serverName,
                              const std::string& addr) {
   std::lock_guard<std::mutex> lock(Mu);  // 锁定互斥锁
   // ...
}

void Registry::UpdataAllServer() {
   std::lock_guard<std::mutex> lock(Mu);  // 锁定互斥锁
   // ...
}

这样,在多线程环境下,当一个线程对 ServerList 进行修改时,其他线程就无法访问该变量,以避免数据竞争和数据不一致的问题。

三、支持负载均衡

本注册中心实现了两种负载均衡算法,第一种是随机算法,第二种是轮询算法。
轮询算法是一种基本的负载均衡算法,它会依次将请求分配给每个服务器,并记录每个服务器已经处理的请求数量。当有新的请求到来时,选择已经处理请求数最少的服务器进行处理。该算法适用于服务器处理能力相近且不需要复杂的负载均衡策略的情况。
随机选择算法是一种简单的负载均衡算法,它会随机选择一个服务器进行处理。该算法适用于服务器处理能力相近且请求较为均匀的情况。
首先定义枚举类型:

enum SelectMode { RandomSelect, RoundRobinSelect };

然后再DiscoverServer服务发现函数中实现负载均衡。

 int chooseIndex = 0;
   if (selectmodel == RoundRobinSelect) {
      // 负载均衡轮询算法 取这个服务被调用的次数&数组大小
      chooseIndex = ServerFreMap[serverName] % ServerItemList.size();
   } else {
      // 负载均衡随机选择
      std::random_device rd;  // 获取随机设备种子
      std::mt19937 gen(rd());  // 使用 Mersenne Twister 算法生成随机数引擎
      std::uniform_int_distribution<> distrib(
          0, ServerItemList.size());  // 定义均匀分布
      chooseIndex = distrib(gen);     // 生成随机数
   }

四、服务注册以及心跳机制

服务注册者需要实现简单的服务注册和心跳检测功能,并通过 HTTP 协议与注册中心进行通信。它将当前服务器的信息发送到注册中心,以便其他客户端可以发现并使用该服务器提供的服务;同时还通过定时发送心跳信号来告知注册中心当前服务器的存活情况,从而保证了注册中心的可靠性和数据的准确性。
首先定义好结构体以及默认发送时间间隔。

const(
	defaultTimeout = time.Second*30 
)
type Register struct{
	registryAddr string
	serverName string
	httpClient *http.Client
	addr string 
	duration time.Duration
}
func NewRegister(registryAddr string,serverName,addr string,duration time.Duration)*Register  {
	register:=&Register{
		registryAddr:registryAddr,
		serverName:serverName,
		httpClient :&http.Client{},
		addr:addr,
		duration:duration,
	}
	return register
}

1、更新心跳

为当前服务器开启一个新的心跳检测器。在函数中,首先判断心跳检测的时间间隔是否为 0,如果是则设置为默认值(30 秒);然后调用 SendHeartbeat() 函数向注册中心发送心跳信号,并使用定时器每隔一段时间重复发送心跳信号。
调用 SendHeartbeat向注册中心发送心跳信号,告知注册中心当前服务器仍然存活。在函数中,首先构造一个 POST 请求并设置头文格式,然后调用 HTTP 客户端的 Do() 方法发送请求。

func (r *Register)NewHeartbeat() {
	if r.duration == 0 {
		// make sure there is enough time to send heart beat
		// before it's removed from registryAddr
		// 比默认的少一分钟
		r.duration = defaultTimeout
	}
	fmt.Println("NewHeartbeat duration:",r.duration)
	var err error
	err = r.SendHeartbeat()
	go func() {
		t := time.NewTicker(r.duration)
		// 定时器 一旦时间到了 channel就会收到消息 变为不阻塞状态
		for err == nil {
			<-t.C
			err = r.SendHeartbeat()
		}
	}()
}
// 发送心跳信号 
func (r *Register)SendHeartbeat() error {
	log.Println(r.serverName, "send heart beat to registr center", r.registryAddr)
	req, _ := http.NewRequest("POST", r.registryAddr+"/registerCentor/register", nil)
	req.Header.Set("Server-Information", r.serverName+"#"+r.addr)
	if _, err := r.httpClient.Do(req); err != nil {
		log.Println("rpc server: heart beat err:", err)
		return err
	}
	return nil
}

2、结束心跳

用于向注册中心发送注销请求,将当前服务器从注册中心移除。在函数中,首先构造一个 POST 请求并设置头文格式,然后调用 HTTP 客户端的 Do() 方法发送请求。最后,打印响应状态码表示请求结果

func (r *Register)Cleanup() {
    // 构造请求
	fmt.Println("deregister......   ",r.registryAddr)
    req, err := http.NewRequest("POST", r.registryAddr+"/registerCentor/deregister", nil)
    if err != nil {
        fmt.Println("构造请求错误:", err)
        return
    }

    // 设置头文格式 Server-destion
    req.Header.Set("Server-Deregister", r.serverName+"#"+r.addr)

    // 发送请求
    client := &http.Client{}
    resp, err := client.Do(req)
    if err != nil {
        fmt.Println("发送请求错误:", err)
        return
    }
    defer resp.Body.Close()
    // 打印响应信息
    fmt.Println("响应状态码:", resp.StatusCode)
}

之后创建了一个 channel(信道) interrupt,用于接收操作系统的信号,调用 signal.Notify(interrupt, os.Interrupt) 监听操作系统的 os.Interrupt 信号,当程序收到该信号时,会向 interrupt 信道发送一个元素。这个信号通常是通过按下 Ctrl+C 生成的。

之后,开启一个新的 goroutine 来等待接收信号。在 goroutine 内部使用 select 语句来从 interrupt 信道中读取数据,如果成功读取到数据则执行相应的操作。

在这里,由于只监听了 os.Interrupt 信号,所以仅会处理 Ctrl+C 终止进程的情况。当接收到该信号时,会调用 r.Cleanup() 函数来进行清理工作

	 // 创建 channel 以接收操作系统信号
	 interrupt := make(chan os.Signal, 1)
	 signal.Notify(interrupt, os.Interrupt)
		go func() {
			// 等待接收信号
			for {
				select {
				case <-interrupt:
					r.Cleanup()
					fmt.Println("Program interrupted.")
					// 执行需要的代码
					os.Exit(1)
				}
			}
		}()

五、服务发现

使用 httplib 库提供的 httplib::Client 类发送 POST 请求,并在响应头部中查找指定 key 的值。如果响应头部中确实包含了名为 “ServerAddr” 的键值对

   httplib::Client cli("127.0.0.1", 8080);
   std::string data = R"({"key": "value"})";
   std::string header_key = "servername";
   std::string header_value = "test";
   httplib::Headers headers = {{header_key, header_value}};
   auto res =
       cli.Post("/registerCentor/getServer", headers, data, "application/json");
//    // 获取响应头部中指定 key 的值
   std::string serverAddr;
   for (const auto& header : res->headers) {
      if (header.first == "ServerAddr") {
         serverAddr = header.second;
         break;
      }
   }
   std::cout << "Server addr: " <<serverAddr << std::endl;

你可能感兴趣的:(c++与golang,项目总结,http,分布式,服务发现,c++,golang)