【Linux编程】TcpServer 类的设计与实现:构建高性能的 TCP 服务器(二)

TcpServer 类的设计与实现:构建高性能的 TCP 服务器

在现代网络编程中,构建一个高效、稳定的 TCP 服务器是许多网络应用的基础。本文将详细介绍一个基于 C++ 的 TcpServer 类的设计与实现,该类提供了创建 TCP 服务器、处理客户端连接、数据传输和接收等功能。通过这个类,我们可以更容易地理解和实现 TCP 通信的细节。

1. TcpServer 类概述

TcpServer 类是一个用于创建和管理 TCP 服务器的类。它封装了套接字创建、绑定、监听、客户端连接处理、数据发送和接收等操作,使得网络通信更加简洁和易于管理。

2. 类构造与析构
  • 构造函数 TcpServer::TcpServer(int _port)TcpServer::TcpServer(std::string _host, int _port) 初始化服务器的主机地址和端口,并创建套接字。
TcpServer::TcpServer(int _port) : TcpServer("0.0.0.0", _port) {
}TcpServer::TcpServer(std::string _host, int _port) : host(_host), port(_port) {std::cout << "create tcp server start." << std::endl;socket_fd = socket(AF_INET, SOCK_STREAM, 0);if (socket_fd == -1) {std::cout << "socket create error!";return;}int ret = set_epoll_mode(socket_fd, O_NONBLOCK);if (ret < 0) {std::cout << "epoll_mode failed:" << ret << std::endl;close(socket_fd);return;}server_addr.sin_family = AF_INET;inet_pton(AF_INET, host.c_str(), &server_addr.sin_addr);server_addr.sin_port = htons(port);int opt = 1;setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));isCreate = true;std::cout << "create tcp server ok." << std::endl;
}
  • 析构函数 TcpServer::~TcpServer() 虚析构函数,确保派生类的析构函数被正确调用。
TcpServer::~TcpServer() {
}
3. 服务器启动与停止
  • 启动服务器 TcpServer::Start() 绑定套接字到指定端口,并开始监听。
void TcpServer::Start() {if (running || !isCreate) {std::cout << "TcpServer start failed!" << "running=" << running << ", port=" << port << std::endl;return;}auto ret = bind(socket_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));if (ret == -1) {std::cout << "bind faild:" << ret << std::endl;close(socket_fd);return;}ret = listen(socket_fd, SOMAXCONN);if (ret == -1) {std::cout << "Listen failed:" << ret << std::endl;close(socket_fd);return;}std::cout << "server open: " << host << ":" << port << std::endl;epoll_start();
}
  • 停止服务器 TcpServer::Stop() 关闭服务器并释放资源。
void TcpServer::Stop() {this->Close();
}
4. 资源管理
  • 关闭连接 TcpServer::Close() 关闭套接字和 epoll 文件描述符,释放资源。
void TcpServer::Close() {isCreate = false;running = false;socket_event.data.fd = socket_fd;int ret = epoll_ctl(epoll_fd, EPOLL_CTL_DEL, socket_fd, &socket_event);ret = close(socket_fd);std::cout << "socket_fd已关闭:" << ret << std::endl;for (TcpClient *client : clients)client->Close();ret = close(epoll_fd);std::cout << "TcpServer epoll_fd已关闭:" << ret << std::endl;
}
5. 非阻塞模式设置
  • 设置非阻塞模式 TcpServer::set_epoll_mode() 设置套接字为非阻塞模式。
int TcpServer::set_epoll_mode(int sock_fd, int mode) {int flags = fcntl(sock_fd, F_GETFL, 0);if (flags == -1) {std::cout << "epoll_mode failed:" << sock_fd << std::endl;return -1;}return fcntl(sock_fd, F_SETFL, flags | mode);
}
6. 客户端连接处理
  • 客户端接受线程 TcpServer::client_accept_thread() 处理客户端连接和数据事件。
void TcpServer::client_accept_thread() {struct epoll_event client_events[1024];while (running) {int ret = epoll_wait(epoll_fd, client_events, 1024, -1);if (ret < 0) {if (errno == EAGAIN || errno == EWOULDBLOCK)continue;else {std::cerr << "epoll_wait failed: " << ret << " : " << errno << " : " << strerror(errno) << std::endl;break;}}for (int n = 0; n < ret; ++n) {if (client_events[n].data.fd == socket_fd) {client_connect();} else {struct epoll_event client_event = client_events[n];auto client = std::find_if(clients.begin(), clients.end(), [&client_event](const TcpClient *_client){ return (_client->client_fd == client_event.data.fd); });if (client == clients.end())continue;int ret = (*client)->data_receive(*client);if (ret == 0) {clients.erase(client);delete *client;}}}}std::cout << "服务已关闭,不再提供任何服务!" << std::endl;isDispose = true;
}
  • 客户端连接 TcpServer::client_connect() 接受客户端连接并添加到 epoll 监控。
void TcpServer::client_connect() {struct sockaddr_in client_addr;socklen_t addr_len = sizeof(client_addr);int client_fd = accept(socket_fd, (struct sockaddr *)&client_addr, &addr_len);std::cout << "accept:" << client_fd << " : " << strerror(errno) << std::endl;if (client_fd <= 0)return;TcpClient *client = new TcpClient;client->running = true;client->connected = true;client->client_fd = client_fd;client->local_addr = client_addr;int ret = set_epoll_mode(client_fd, O_NONBLOCK);if (ret == -1) {std::cout << "服务器接受客户端-set_epoll_mode failed:" << strerror(errno) << std::endl;client->Close();return;}client->add_epoll_event(client_fd, epoll_fd, EPOLLIN | EPOLLET);client->DataReceived = [this](void *sender, DataReceiveEventArgs *e){DataReceived.Invoke(this, e);};client->recv_data = new char[client->recv_data_length];clients.push_back(client);std::cout << "新的客户端已接入:" << inet_ntoa(client_addr.sin_addr) << ":" << htons(client_addr.sin_port) << std::endl;
}
7. epoll 事件处理
  • 启动 epoll TcpServer::epoll_start() 创建 epoll 实例并添加监听套接字。
void TcpServer::epoll_start() {epoll_fd = epoll_create1(0);if (epoll_fd == -1) {std::cout << "poll_create1 failed:" << epoll_fd << std::endl;close(socket_fd);return;}socket_event.events = EPOLLIN;socket_event.data.fd = socket_fd;int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, &socket_event);if (ret == -1) {std::cout << "epoll_ctl failed:" << ret << std::endl;close(epoll_fd);return;}running = true;std::thread th = std::thread(&TcpServer::client_accept_thread, this);th.detach();
}

完整的代码:
TcpServer.h 头文件

#pragma once#include <iostream>
#include <vector>
#include <list>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <sys/fcntl.h>
#include <arpa/inet.h>
#include <algorithm>
#include <unistd.h>
#include <thread>
#include "DataReceiveEventArgs.h"
#include "TcpClient.h"class TcpServer
{
public:TcpServer(int _port);TcpServer(std::string _host, int _port);~TcpServer();public:EventHandler<DataReceiveEventArgs> DataReceived;public:void Start();void Stop();void Close();bool IsDispose();private:void epoll_start();                        // epoll初始化(创建epoll)int set_epoll_mode(int sock_fd, int mode); // epoll模式--创建socket时,为非阻塞模式void client_accept_thread();void client_connect();sockaddr_in get_remote_addr(int sock);private:int socket_fd;int epoll_fd;std::string host = "0.0.0.0";int port = 0;bool running = false;bool isCreate = false;int send_buff_size = 1024 * 1024;int recv_buff_size = 1024 * 1024;struct sockaddr_in server_addr;std::list<TcpClient *> clients;// 将监听套接字添加到 epoll 中,监控 EPOLLIN (表示有数据可读)事件struct epoll_event socket_event;bool isDispose = false;
};

TcpServer.cpp

#include "TcpServer.h"TcpServer::TcpServer(int _port) : TcpServer("0.0.0.0", _port)
{
}TcpServer::TcpServer(std::string _host, int _port) : host(_host), port(_port)
{std::cout << "create tcp server start." << std::endl;// AF_INET 表示使用 IPv4 协议// SOCK_STREAM 表示套接字的类型,表示 面向连接的流式套接字socket_fd = socket(AF_INET, SOCK_STREAM, 0);// std::cout << "create tcp server socket_fd:" << socket_fd << std::endl;if (socket_fd == -1){std::cout << "socket ceate error!";return;}// 文件描述符为非阻塞模式int ret = set_epoll_mode(socket_fd, O_NONBLOCK);if (ret < 0){std::cout << "epoll_mode failed:" << ret << std::endl;close(socket_fd);return;}server_addr.sin_family = AF_INET;// ip字符串转intinet_pton(AF_INET, host.c_str(), &server_addr.sin_addr);//server_addr.sin_addr.s_addr = ntohl(server_addr.sin_addr.s_addr);server_addr.sin_port = htons(port);// 以下设置表示当调用close关闭客户端时,立即释放端口,不等待// 在TCP服务端,客户端调用 close(client_fd) 关闭连接后,如果你尝试重新连接时出现端口没有完全释放的情况,通常是由于 TCP 连接的 TIME_WAIT 状态没有及时清理。这是 TCP 协议的正常行为。// 在TCP连接关闭后,端口会进入 TIME_WAIT 状态。这个状态的目的是确保最后的数据包能够正确到达。如果新的连接尝试在该端口上进行,而该端口仍然处于 TIME_WAIT 状态,就会出现端口被占用的情况。// TIME_WAIT 状态通常会持续一段时间(默认是4分钟,即240秒),这可以通过操作系统的内核参数来修改。// struct linger linger_opt;// linger_opt.l_onoff = 1;  // 启用// linger_opt.l_linger = 1; // 立即关闭// // 此方法亲测无效// // setsockopt(socket_fd, SOL_SOCKET, SO_LINGER, &linger_opt, sizeof(linger_opt));int opt = 1;setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));isCreate = true;std::cout << "create tcp server ok." << std::endl;
}TcpServer::~TcpServer()
{
}void TcpServer::Start()
{if (running || !isCreate){std::cout << "TcpServer start failed!" << "running=" << running << ", port=" << port << std::endl;// close(socket_fd);return;}// 绑定套接字到指定端口auto ret = bind(socket_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));if (ret == -1){std::cout << "bind faild:" << ret << std::endl;close(socket_fd);return;}// 开始监听ret = listen(socket_fd, SOMAXCONN);if (ret == -1){std::cout << "Listen failed:" << ret << std::endl;close(socket_fd);return;}std::cout << "server open: " << host << ":" << port << std::endl;epoll_start();
}void TcpServer::Stop()
{this->Close();
}void TcpServer::Close()
{isCreate = false;running = false;// 1. 删除socket_fd的epoll事件socket_event.data.fd = socket_fd;int ret = epoll_ctl(epoll_fd, EPOLL_CTL_DEL, socket_fd, &socket_event);// 2. 关闭监听套接字ret = close(socket_fd);std::cout << "socket_fd已关闭:" << ret << std::endl;// 3. 从 epoll 中移除所有连接的客户端文件描述符for (TcpClient *client : clients)client->Close();// 4. 关闭 epoll 文件描述符ret = close(epoll_fd);std::cout << "TcpServer epoll_fd已关闭:" << ret << std::endl;
}bool TcpServer::IsDispose()
{return isDispose;
}int TcpServer::set_epoll_mode(int sock_fd, int mode)
{/*O_NONBLOCK(非阻塞模式):如果设置了这个标志,表示该套接字(或文件)是非阻塞的,执行读写操作时不会阻塞调用进程或线程。套接字在没有数据可读或可写时不会让程序等待,而是立即返回。O_RDWR、O_WRONLY、O_RDONLY(访问模式):表示套接字的打开方式。O_APPEND(追加模式):指示文件或套接字在写操作时会追加数据。*/int flags = fcntl(sock_fd, F_GETFL, 0); // 获取当前套接字的文件状态标志if (flags == -1){std::cout << "epoll_mode failed:" << sock_fd << std::endl;return -1;}// 设置套接字为非阻塞模式return fcntl(sock_fd, F_SETFL, flags | mode);
}void TcpServer::client_accept_thread()
{struct epoll_event client_events[1024];while (running){// 阻塞等待事件int ret = epoll_wait(epoll_fd, client_events, 1024, -1);if (ret < 0){if (errno == EAGAIN || errno == EWOULDBLOCK)continue;else{std::cerr << "epoll_wait failed: " << ret << " : " << errno << " : " << strerror(errno) << std::endl;break;}}// 处理返回的事件for (int n = 0; n < ret; ++n){if (client_events[n].data.fd == socket_fd){// 如果是监听套接字的事件,有新的客户端连接client_connect();}else{struct epoll_event client_event = client_events[n];auto client = std::find_if(clients.begin(), clients.end(), [&client_event](const TcpClient *_client){ return (_client->client_fd == client_event.data.fd); });if (client == clients.end())continue;// 客户端有数据int ret = (*client)->data_receive(*client);if (ret == 0){clients.erase(client);delete *client;}}}}std::cout << "服务已关闭,不再提供任何服务!" << std::endl;isDispose = true;
}void TcpServer::client_connect()
{struct sockaddr_in client_addr;socklen_t addr_len = sizeof(client_addr);int client_fd = accept(socket_fd, (struct sockaddr *)&client_addr, &addr_len); // 接受连接std::cout << "accept:" << client_fd << " : " << strerror(errno) << std::endl;if (client_fd <= 0)return;TcpClient *client = new TcpClient;client->running = true;client->connected = true;client->client_fd = client_fd;client->local_addr = client_addr;// 设置客户端套接字为非阻塞模式int ret = set_epoll_mode(client_fd, O_NONBLOCK);if (ret == -1){std::cout << "服务器接受客户端-set_epoll_mode failed:" << strerror(errno) << std::endl;client->Close();return;}// client.SetSendBuffSize(send_buff_size);// client.SetRecvBuffSize(recv_buff_size);//  将新客户端套接字添加到 epoll 中,监听可读事件// client->create_epoll();client->add_epoll_event(client_fd, epoll_fd, EPOLLIN | EPOLLET);client->DataReceived = [this](void *sender, DataReceiveEventArgs *e){DataReceived.Invoke(this, e);};// client->start_receive();client->recv_data = new char[client->recv_data_length];clients.push_back(client);std::cout << "新的客户端已接入:" << inet_ntoa(client_addr.sin_addr) << ":" << htons(client_addr.sin_port) << std::endl;
}void TcpServer::epoll_start()
{// 创建epollepoll_fd = epoll_create1(0);if (epoll_fd == -1){std::cout << "poll_create1 failed:" << epoll_fd << std::endl;close(socket_fd);return;}// 监听可读事件socket_event.events = EPOLLIN;// 将监听套接字的文件描述符传给 epollsocket_event.data.fd = socket_fd;// 将监听套接字添加到 epoll 中,监控 EPOLLIN 事件(表示有数据可读)int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, socket_fd, &socket_event);if (ret == -1){std::cout << "epoll_ctl failed:" << ret << std::endl;close(epoll_fd);return;}running = true;std::thread th = std::thread(&TcpServer::client_accept_thread, this);th.detach();
}
8. 总结

本文详细介绍了 TcpServer 类的设计与实现,包括构造与析构、服务器启动与停止、资源管理、非阻塞模式设置、客户端连接处理以及 epoll 事件处理。通过这个类,我们可以更容易地理解和实现 TCP 通信的细节。这个类提供了一个简洁的接口来管理 TCP 服务器,使得网络编程更加高效和易于维护。

TcpServer 类的设计注重模块化和可扩展性,允许开发者根据具体需求进行定制和扩展。通过使用 epoll 事件模型,TcpServer 类能够支持高并发的客户端连接,适用于需要处理大量并发连接的网络应用。此外,类中的非阻塞模式设置和资源管理机制确保了服务器的稳定性和高效性。

总的来说,TcpServer 类为构建高性能的 TCP 服务器提供了一个强大的基础。通过这个类,开发者可以快速构建和部署 TCP 服务器,满足各种网络应用的需求。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/497558.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Linux:线程的概念

线程&#xff1a;进程内的一个执行分支&#xff0c;他的执行粒度比进程要细 一、通过进程引入线程 以前我们想要一个执行流&#xff0c;我们需要fork一个子进程&#xff0c;然后子进程需要拷贝 take_struct结构体进程地址空间页表文件描述符表…… 而当我们只创建一个task_st…

跟着逻辑先生学习FPGA-实战篇第二课 6-2 LED灯流水灯实验

** 硬件平台&#xff1a;征战Pro开发板 软件平台&#xff1a;Vivado2018.3 仿真软件&#xff1a;Modelsim10.6d 文本编译器&#xff1a;Notepad** 征战Pro开发板资料 链接:https://pan.baidu.com/s/1AIcnaGBpNLgFT8GG1yC-cA?pwdx3u8 提取码:x3u8 1 知识背景 我们在《LED 灯…

Kafka数据迁移全解析:同集群和跨集群

文章目录 一、同集群迁移二、跨集群迁移 Kafka两种迁移场景&#xff0c;分别是同集群数据迁移、跨集群数据迁移。 一、同集群迁移 应用场景&#xff1a; broker 迁移 主要使用的场景是broker 上线,下线,或者扩容等.基于同一套zookeeper的操作。 实践&#xff1a; 将需要新添加…

Ashy的考研游记

文章目录 摘要12.1112.2012.21 DAY1&#xff08;政治/英语&#xff09;政治英语 12.22 DAY2&#xff08;数学/专业课&#xff09;数学专业课 结束估分 摘要 在24年的12月里&#xff0c;Ashy完成了他的考研冲刺&#xff0c;顺利的结束了他本年度的考研之旅。 在十二月里&#…

【Sentinel】流控效果与热点参数限流

目录 1.流控效果 1.1.warm up 2.2.排队等待 1.3.总结 2.热点参数限流 2.1.全局参数限流 2.2.热点参数限流 2.3.案例 1.流控效果 在流控的高级选项中&#xff0c;还有一个流控效果选项&#xff1a; 流控效果是指请求达到流控阈值时应该采取的措施&#xff0c;包括三种&…

【Unity3D】ECS入门学习(十二)IJob、IJobFor、IJobParallelFor

IJob&#xff1a;开启单个线程进行计算&#xff0c;线程内不允许对同一个数据进行操作&#xff0c;也就是如果你想用多个IJob分别计算&#xff0c;将其结果存储到同一个NativeArray<int>数组是不允许的&#xff0c;所以不要这样做&#xff0c;如下例子就是反面教材&#…

SELECT的使用

目录 1、SQL的查询命令 1.1 SELECT语句 1.2 投影查询 1.3 选择查询 1.4 聚合函数查询 1.5 简单分组查询(GROUP BY) 1.6 内连接查询 1.7 外连接查询 1.8 子查询 1. 无关子查询 2. 相关子查询 带exists的相关子查询&#xff1a; 1.9 集合查询 1. UNION(并) 2. INT…

电池均衡系统

一、电池概况 常见的电池分类&#xff1a;铅酸电池、锂电池、钠离子电池、碱性干电池、纽扣电池等&#xff0c;其中钠离子电池暂未大规模商用。 例如&#xff1a;电动车电池电压通常48v、60v、72v等&#xff0c;汽车启动电池电压&#xff1a;12v 而构成高电压的电池&#xf…

Windows系统下载、部署Node.js与npm环境的方法

本文介绍在Windows电脑中&#xff0c;下载、安装并配置Node.js环境与npm包管理工具的方法。 Node.js是一个基于Chrome V8引擎的JavaScript运行时环境&#xff0c;其允许开发者使用JavaScript编写命令行工具和服务器端脚本。而npm&#xff08;Node Package Manager&#xff09;则…

使用arduino从零做一辆ROS2Jazzy的阿克曼小车---电机驱动篇

本项目采用 Arduino Mega2560 Pro 作为主控开发板&#xff0c;电机驱动器选用 TB6612FNG&#xff0c;并配备了 12V 电源、两个直流减速电机和一个舵机。未来计划通过嘉立创将各模块集成到一个 PCB 板上&#xff0c;提升系统的集成度和稳定性。 本文将聚焦于电机驱动部分&#x…

华为麦芒5(安卓6)termux记录 使用ddns-go,alist

下载0.119bate1 安卓5和6版本,不能换源,其他源似乎都用不了,如果root可以直接用面具模块 https://github.com/termux/termux-app/releases/download/v0.119.0-beta.1/termux-app_v0.119.0-beta.1apt-android-5-github-debug_arm64-v8a.apk 安装ssh(非必要) pkg install open…

图片转成oled使用的字模数据

目录 oled尺寸 如何生成用到的图片 图片转字模 1.首先用Img2Lcd转成bmp单色图片 2.然后用PCtoLCD2002把单色图片转字模 oled尺寸 我使用0.96寸oled模块&#xff0c;对应着的分辨率是128*64&#xff0c;对应着宽高像素比128*64。所以不是随意一张图片就能用的&#xff0c;…

【通信网络】二层基础:03 二层转发基础

1. 二层转发概述 数据链路层&#xff0c;位于OSI模型中的第二层&#xff0c;所以称之为二层。本文我们讨论的转发过程&#xff0c;就是在数据链路层上的转发过程&#xff0c;即二层转发。 1.1 MAC地址 为了唯一的表示一台网络设备&#xff0c;网络设备都有自己的MAC地址。IE…

从0到100:基于Java的大学选修课选课小程序开发笔记(上)

背景 为学生提供便捷的课程选择方式&#xff0c;并帮助学校进行课程管理和资源调配&#xff1b;主要功能包括&#xff1a;课程展示&#xff0c;自主选课&#xff0c;取消选课&#xff0c;后台录入课程&#xff0c;统计每门课程报名情况&#xff0c;导出数据&#xff0c;用户管…

基于Springboot + vue实现的火锅店管理系统

&#x1f942;(❁◡❁)您的点赞&#x1f44d;➕评论&#x1f4dd;➕收藏⭐是作者创作的最大动力&#x1f91e; &#x1f496;&#x1f4d5;&#x1f389;&#x1f525; 支持我&#xff1a;点赞&#x1f44d;收藏⭐️留言&#x1f4dd;欢迎留言讨论 &#x1f525;&#x1f525;&…

基于Svelte 5的体检查询系统前端设计与实现探究

一、引言 1.1 研究背景与意义 在当今数字化时代&#xff0c;医疗信息化已成为医疗行业发展的重要趋势。随着人们对健康的重视程度不断提高&#xff0c;体检作为预防疾病、保障健康的重要手段&#xff0c;其相关信息的管理和查询需求也日益增长。传统的体检查询系统前端往往存…

科大讯飞在线语音合成(流式版)python版

1、进入自己的项目 复制APPID、APISecret、APIKey 2、添加好听发音人 复制vcn参数 3、需要替换代码部分&#xff1a; 换自己喜欢的发声人的参数 换上自己的APPID、APISecret、APIKey 4、完整代码&#xff1a; # -*- coding:utf-8 -*- import _thread as thread import base…

TCP 为什么采用三次握手和四次挥手以及 TCP 和 UDP 的区别

1. TCP 为什么采用三次握手和四次挥手 采用三次握手的原因&#xff1a; 确认双方的收发能力。第一次握手&#xff0c;客户端发送 SYN 报文&#xff0c;告诉服务器自身具备发送数据的能力&#xff0c;第二次握手&#xff0c;服务器回应 SYN ACK 报文&#xff0c;表名自己既能…

python-Flask:SQLite数据库路径不正确但是成功访问到了数据库,并对表进行了操作

出现了这个问题&#xff0c;就好像是我要去找在南方的人&#xff0c;然后我刚好不分南北&#xff0c;我认为的方向错了&#xff0c;实则方向对了。 在我针对复盘解决&#xff1a;sqlite3.OperationalError: unrecognized token: “{“-CSDN博客这个内容的时候&#xff0c;又出现…

2024-12-29-sklearn学习(25)无监督学习-神经网络模型(无监督) 烟笼寒水月笼沙,夜泊秦淮近酒家。

文章目录 sklearn学习(25) 无监督学习-神经网络模型&#xff08;无监督&#xff09;25.1 限制波尔兹曼机25.1.1 图形模型和参数化25.1.2 伯努利限制玻尔兹曼机25.1.3 随机最大似然学习 sklearn学习(25) 无监督学习-神经网络模型&#xff08;无监督&#xff09; 文章参考网站&a…