网络编程(22)——通过beast库快速实现websocket服务器
目录
二十二、day22
1. websocket简述
2. 基于TCP长连接实现sebsocket
a. Connection
b. ConnectionMgr
c. WebServer
d. 编译的小问题
3. 测试
4. 基于http实现的websocket
二十二、day22
因为http受限于请求-响应模式,客户端发起请求,服务器响应后连接立即关闭,每次通信都要重新建立连接,如果我们想要服务器与客户端之间可以随时互相发送数据,那么http只有多次重新建立客户端与服务器的连接才能满足我们的需求,但开销太大。
websocket有两种实现方式,第一种是基于tcp长连接进行升级,第二种是基于http升级,今天主要学习第一种。
如果是在tcp长连接进行升级,实际就是在已有的TCP连接之上,直接切换到WebSocket协议。它的核心思路是:在已经建立的TCP连接上,通过WebSocket协议的握手过程,达成协议切换,从而实现全双工的长连接通信。
如果在http的基础上进行实现websocket,那么websocket其实就是基于http进行初始握手,然后升级为websocket协程,从短连接升级为长连接,WebSocket则成为一个独立的、全双工的长连接协议,不再受HTTP的请求-响应模式限制。
参考视频:
C++ 网络编程(23) beast网络库实现websocket服务器_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1Mu411b7qV/?spm_id_from=333.337.search-card.all.click
1. websocket简述
websocket有两种升级方式,第一种是基于tcp进行升级,第二种基于http进行升级,前者只能接受websocket请求,而后者既可以接受http请求,也可以接受websocket请求,今天主要学习第一种。
WebSocket协议需要先通过HTTP协议进行初始握手。客户端发送一个HTTP请求,其中包含了Upgrade头,表明希望将连接从HTTP升级为WebSocket。服务器同意后,会返回HTTP 101 Switching Protocols状态码,确认连接升级为WebSocket。
1)HTTP握手请求:客户端发送的HTTP请求头中包含以下内容:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
这里,协议由1.0升级为1.1,keep_alive置为true,升级为长连接。
请求头已经被beast库封装好了,我们只需要调用就行,不需要像上面这样构造一个请求头。步骤如下:
stream<tcp_stream> ws(ioc);
net::ip::tcp::resolver resolver(ioc);
get_lowest_layer(ws).connect(resolver.resolve("www.example.com", "ws"));// Do the websocket handshake in the client role, on the connected stream.
// The implementation only uses the Host parameter to set the HTTP "Host" field,
// it does not perform any DNS lookup. That must be done first, as shown above.ws.handshake("www.example.com", // The Host field"/" // The request-target
);
首先,初始化一个websoket对象ws,然后定义一个解析器,解析对端的地址并连接。
连接成功后,websocket调用handshake函数进行一次握手,用于升级协议。如果想判断握手是否成功,可初始化一个response_type 对象
// This variable will receive the HTTP response from the server
response_type res;
ws.handshake(res, // Receives the HTTP response"www.example.com", // The Host field"/" // The request-target
);
通过解析res的内容,即可判断握手是否成功
2)服务器响应:如果服务器支持WebSocket并同意升级,会返回:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
在此之后,连接便从HTTP切换为WebSocket协议,开始全双工通信。
而beast同样也已经将其封装好了,我们只需要调用ws.accept()或者async_accept()函数即可升级协程为websocket.
ws.accept();
ws.async_accept()
该函数首先会接收客户端的握手协议,并解析校验,如果没有问题,就向对端回一个响应,内容就是上面那部分内容。
除此之外,如果服务器先从数据流中读取数据,之后再升级为websocket,beast同样提供了这样的函数:
// This buffer will hold the HTTP request as raw characters
std::string s;// Read into our buffer until we reach the end of the HTTP request.
// No parsing takes place here, we are just accumulating data.net::read_until(sock, net::dynamic_buffer(s), "\r\n\r\n");// Now accept the connection, using the buffered data.
ws.accept(net::buffer(s));
通过调用read_until函数读取对端请求,直至读取到‘\r\n\r\n’,也就是直至将整个请求头读取出来,请求头的末尾是‘\r\n\r\n’。然后再调用accept接收升级。
3)在http服务器的基础上使用websocket协议
// This buffer is required for reading HTTP messages
flat_buffer buffer;// Read the HTTP request ourselves
http::request<http::string_body> req;
http::read(sock, buffer, req);// See if its a WebSocket upgrade request
if(websocket::is_upgrade(req))
{// Construct the stream, transferring ownership of the socketstream<tcp_stream> ws(std::move(sock));// Clients SHOULD NOT begin sending WebSocket// frames until the server has provided a response.BOOST_ASSERT(buffer.size() == 0);// Accept the upgrade requestws.accept(req);
}
else
{// Its not a WebSocket upgrade, so// handle it like a normal HTTP request.
}
首先,和昨天学的一样,先定义一个请求头req用于将读到的请求内容存放到请求头req中,然后调用websocket::is_upgrade(req)函数判断是否要升级为websocket,如果是就调用accept升级。
2. 基于TCP长连接实现sebsocket
在已经建立的TCP连接上,通过WebSocket协议的握手过程,达成协议切换,从而实现全双工的长连接通信。
实现步骤如下:
1)建立TCP连接:客户端和服务器首先通过TCP协议建立一个长连接(通常是在特定的端口上,比如80或443)。
2)协议升级:
- 在这个TCP连接上,客户端发起一个WebSocket握手请求,携带必要的信息(如Sec-WebSocket-Key等),告知服务器希望将TCP连接升级为WebSocket协议。
- 服务器收到请求后,同意并返回相应的信息(Sec-WebSocket-Accept),确认连接升级。
3)通信切换:升级完成后,TCP连接保持不变,但通信协议切换为WebSocket协议。这时,客户端和服务器可以在这个TCP长连接上,进行实时的全双工数据交换。
代码实现如下:
a. Connection
该类用于管理一个Websocket连接(多个connection通过ConnectionMgr类进行管理),并负责客户端与服务器间的通信。
因为beast库为websocket单独封装了一个超时定时器,所以在这里不需要向上一节http服务器的搭建一样自定义一个超时器函数。
#pragma once
#include <iostream>
#include <boost/beast.hpp>
#include <boost/asio.hpp>
#include <memory>
#include <boost/uuid/uuid.hpp>
#include <boost/uuid/uuid_io.hpp>
#include <queue>
#include <mutex>
#include <boost/uuid/uuid_generators.hpp>namespace net = boost::asio;
namespace beast = boost::beast;
namespace websocket = boost::beast::websocket;
using namespace boost::beast;class ConnectionMgr;class Connection : public std::enable_shared_from_this<Connection> {
private:std::unique_ptr<websocket::stream<tcp_stream>> _ws_ptr;std::string _uuid;net::io_context& _ioc;flat_buffer _recv_buffer;std::queue<std::string> _send_que;std::mutex _send_mtx;
public:Connection(net::io_context& ioc);std::string GetUid();net::ip::tcp::socket& GetSocket();void AsyncAccept();void Start();void AsyncSend(std::string msg);
};
首先,为了实现伪闭包,Connection类需要继承std::enable_shared_from_this<T>,便于调用shared_from_this()函数。
成员变量的介绍如下:
- std::unique_ptr<websocket::stream<tcp_stream>> _ws_ptr:初始化一个WebSocket流指针,用于管理WebSocket连接。其中,websocket::stream<T>是Beast 库中用于表示一个 WebSocket 流的模板类,这里将一个底层的TCP连接(tcp_stream)包装为一个WebSocket,通过这个类,程序可以在一个 TCP 连接上实现 WebSocket 协议,进行双向的实时通信。
通过 websocket::stream<tcp_stream> 简单写一个客户端:
net::io_context ioc;tcp::resolver resolver(ioc);auto const results = resolver.resolve("example.com", "80");// 创建 TCP 流并连接到服务器tcp::socket socket(ioc);net::connect(socket, results.begin(), results.end());// 创建 WebSocket 流并进行 WebSocket 握手websocket::stream<tcp::socket> ws(std::move(socket));ws.handshake("example.com", "/");// 发送一条消息ws.write(net::buffer(std::string("Hello WebSocket!")));// 接收服务器返回的消息beast::flat_buffer buffer;ws.read(buffer);std::cout << beast::make_printable(buffer.data()) << std::endl;// 关闭 WebSocket 连接ws.close(websocket::close_code::normal);
- _uuid:用于存储每个connection的名称,每个名称都是唯一的
- _ioc:上下文,注意,这里是&而不是初始化一个io_context
- flat_buffer _recv_buffer:用于接收WebSocket消息的缓冲区,flat_buffer 是 Boost.Beast 库中用于管理内存缓冲区的一个类,它的主要作用是为网络通信中的数据接收和发送提供一个高效的缓冲区,一般用于WebSocket、HTTP 协议的数据收发。该缓存区可以和beast的解析器配合使用,以解析数据。
- _send_que:存储发送消息的队列
- _send_mtx:互斥锁
Connection类的函数实现如下:
1)构造函数
Connection::Connection(net::io_context& ioc) : _ioc(ioc), _ws_ptr(std::make_unique<websocket::stream<tcp_stream>>(make_strand(ioc))){// 生成uuidboost::uuids::random_generator generator;boost::uuids::uuid uuid = generator();_uuid = boost::uuids::to_string(uuid);
}
首先,构造函数接受一个io_context引用的ioc作为参数,并将ioc赋值给_ioc,用于管理异步操作。然后,创建一个 websocket::stream<tcp_stream> 对象,并赋值给_ws_ptr(这里要么在初始化列表中进行赋值,要么通过右值进行赋移动值,因为_ws_ptr是一个unique_ptr不允许被复制拷贝或者赋值)。
然后,通过boost自带的函数(雪花算法)生成一个唯一的uuid。每个Connection的uuid可以代表这个独立的连接。
2)GetUid()
std::string Connection::GetUid() {return _uuid;
}
3)GetSocket()
net::ip::tcp::socket& Connection::GetSocket() {// 获取最底层的socketreturn boost::beast::get_lowest_layer(*_ws_ptr).socket();
}
返回一个指向底层使用的 TCP socket,在 WebSocket 的实现中,它本质上是基于一个底层的 TCP 连接来进行数据传输的。
返回局部变量的引用会有问题吗?在这里不会有问题,因为socket是由_ws_ptr生成的,只有_ws_ptr不被释放,那么socket就一直存在。
4)AsyncAccept()
void Connection::AsyncAccept() {auto self = shared_from_this();_ws_ptr->async_accept([self](boost::system::error_code err) {try {if (!err) {ConnectionMgr::GetInstance().AddConnection(self);self->Start();}else {std::cout << "websocket accept failed, err is " << err.what() << std::endl;}}catch (std::exception& e) {std::cerr << "websocket async accept exception is " << e.what() << std::endl;}});
}
该函数用于异步接收WebSocket 连接,通过 async_accept() 处理传入的 WebSocket 握手请求。
注意,这里WebSocket 对象的 async_accept 和 acceptor对象的 async_accept 不一样。
1)WebSocket 对象的 async_accept主要用于处理WebSocket 协议的握手(handshake),即在建立 TCP 连接后,客户端与服务器需要通过 WebSocket 协议进行握手以升级该连接为 WebSocket 连接。
- 首先,客户端通过 HTTP 发出一个特殊的握手请求,要求升级到 WebSocket 协议。
- 服务端使用 WebSocket 的 async_accept() 方法来异步处理这个请求,并返回相应的 WebSocket 握手响应。
- 握手成功后,连接正式变为 WebSocket 连接,双方可以开始传输 WebSocket 帧
2)而acceptor对象的 async_accept 用于异步接受一个新的 TCP 连接(socket),即监听某个端口,并等待客户端的连接请求,这里的 async_accept() 是处理纯 TCP 连接的接受,并不涉及 WebSocket 协议。
Websocker的流程如下:
1. TCP 层:- 服务端:TCP Acceptor `async_accept()`(等待并接受 TCP 连接)- 客户端:发起 TCP 连接2. WebSocket 层:- 服务端:WebSocket `async_accept()`(处理 WebSocket 握手请求)- 客户端:发送 WebSocket 协议升级请求3. WebSocket 数据传输:- 服务端和客户端通过 WebSocket 协议进行数据传输
如果WebSocket 握手成功,调用lambda函数,将当前连接对象加入ConnectionMgr 的管理中,并调用该连接对象的Start()函数开始处理WebSocket 的数据传输,进入连接的业务逻辑。
5)Start()
void Connection::Start() {auto self = shared_from_this();_ws_ptr->async_read(_recv_buffer, [self](boost::system::error_code ec, std::size_t buffer_bytes) {try {if (!ec) {self->_ws_ptr->text(self->_ws_ptr->got_text());std::string recv_data = boost::beast::buffers_to_string(self->_recv_buffer.data());self->_recv_buffer.consume(self->_recv_buffer.size());std::cout << "websocket receive msg is " << recv_data << std::endl;self->AsyncSend(std::move(recv_data));self->Start();}else {std::cout << "websocket async read error is " << ec.what() << std::endl;return;}}catch (std::exception& e) {std::cerr << "exception is " << e.what() << std::endl;ConnectionMgr::GetInstance().RmvConnection(self->GetUid());}});
}
该函数用于读取 WebSocket 消息,并在读取后进行处理和响应。最后,递归调用Start() 实现持续读取,因为是异步读,所以并不会超出栈的上限,除此之外还可以通过协程实现。
在lambda函数内部:
- 首先通过 got_text() 函数检查接收的 WebSocket 消息是否是文本数据,并将读取到的帧标记为文本类型;
- 将读取在缓存区的数据转换为string类型赋值给变量recv_data,然后清空缓存区_recv_buffer为下一次读取做准备;
- 将读取到的消息打印出来,然后将接受到的数据转发到 AsyncSend() 函数,处理发送响应;
- 递归调用Start(),等待下一条消息。
6)AsyncSend
void Connection::AsyncSend(std::string msg) {std::unique_lock<std::mutex> lck_guard(_send_mtx);int que_len = _send_que.size();_send_que.push(msg);if (que_len > 0) {return;}lck_guard.unlock();auto self = shared_from_this();_ws_ptr->async_write(boost::asio::buffer(msg.c_str(), msg.size()),[self](boost::system::error_code ec, std::size_t msize) {try {if (!ec) {std::string send_msg;{std::lock_guard<std::mutex> lck_guard(self->_send_mtx);self->_send_que.pop();if (self->_send_que.empty()) {return;}send_msg = self->_send_que.front();}self->AsyncSend(std::move(send_msg));}else {std::cout << "async send err is " << ec.what() << std::endl;ConnectionMgr::GetInstance().RmvConnection(self->GetUid());return;}}catch (std::exception& e) {std::cerr << "async read exception is " << e.what() << std::endl;ConnectionMgr::GetInstance().RmvConnection(self->GetUid());}});
}
该函数用于服务器的回传,该函数通过队列和互斥锁来管理并发情况下的消息发送,确保不会出现多次并发写入的情况。如果消息正在发送中,则新消息会加入队列,待当前消息发送完毕后,队列中的下一个消息再被发送。
首先,加锁然后将消息插入队列,插入队列后立马解锁(我们只需要在使用队列时加锁,不使用队列是解锁,避免不相干的逻辑占用锁)。
然后,调用websocket的异步发送async_write()函数发送websocket消息,如果发送成功,调用lambda函数,释放已发送的上一条消息,然后加锁从队列中获取消息,获取完立刻解锁,最后递归调用 AsyncSend继续发送。
有没有发现,我这里使用了两种方法进行了解锁
第一种:
std::unique_lock<std::mutex> lck_guard(_send_mtx);int que_len = _send_que.size();_send_que.push(msg);if (que_len > 0) {return;}lck_guard.unlock();
第二种:
{std::lock_guard<std::mutex> lck_guard(_send_mtx);int que_len = _send_que.size();_send_que.push(msg);if (que_len > 0) {return;}}
这两种方法都可以,一种通过unique_lock手动加解锁,另一种通过‘{}’自动解锁。
b. ConnectionMgr
该类用于管理Connection连接,通过将Connection连接添加到map中,手动的通过uuid删除或增加Connection连接。ConnectionMgr类是一个单例模式类,我这里使用了C++11新特性,用最简便的方法实现单例,还有另外一种方法可以实现单例模式,这种方法可以人为定义删除器,防止单例类的析构函数被意外主动调用。
另一种单例的实现可参考文章:
爱吃土豆:网络编程(19)——C++使用asio协程实现并发服务器3 赞同 · 0 评论文章https://zhuanlan.zhihu.com/p/957175334
#pragma once
#include "Connection.h"
#include <boost/unordered_map.hpp>class ConnectionMgr
{
private:ConnectionMgr(const ConnectionMgr&) = delete;ConnectionMgr& operator=(const ConnectionMgr&) = delete;ConnectionMgr();boost::unordered_map<std::string, std::shared_ptr<Connection>> _map_cons;
public:static ConnectionMgr& GetInstance();void AddConnection(std::shared_ptr<Connection> conptr);void RmvConnection(std::string);
};
单例的实现就不再叙述了,直接介绍成员变量:
- _map_cons:使用boost库的无序关联容器,类似于STL 的 std::unordered_map ,该容器通过哈希表实现,比有序容器的查找效率更快,但是不会主动排序。键是string类型的值,也就是connection的uuid;值是connection的智能指针。
成员函数的实现:
ConnectionMgr::ConnectionMgr() {}void ConnectionMgr::AddConnection(std::shared_ptr<Connection> conptr) {_map_cons[conptr->GetUid()] = conptr;
}void ConnectionMgr::RmvConnection(std::string uuid) {_map_cons.erase(uuid);
}ConnectionMgr& ConnectionMgr::GetInstance() {static ConnectionMgr instance;return instance;
}
无需显示定义构造函数,AddConnection函数用于加入一个新的connection连接,RmvConnection用于移除一个connection连接,二者都是根据键进行操作。GetInstance静态成员函数用于返回ConnectionMgr类的唯一实例,因为从C++11及之后开始,返回局部静态变量是线程安全的。
c. WebServer
该类用于发起TCP层客户端与服务器的TCP连接。
#pragma once
#include "ConnectionMgr.h"class WebServer
{
private:net::ip::tcp::acceptor _acceptor;net::io_context& _ioc;
public:WebServer(const WebServer&) = delete;WebServer& operator=(const WebServer&) = delete;WebServer(net::io_context& ioc, unsigned short port);void StartAccept();};
WebServer不做成一个单例类,但我们同样不允许它被拷贝被赋值,所以将赋值运算符和复制构造函数均delete。然后定义StartAccept()函数用于服务器接收客户端的连接。
WebServer::WebServer(net::io_context& ioc, unsigned short port) : _ioc(ioc), _acceptor(ioc, net::ip::tcp::endpoint(net::ip::tcp::v4(), port)){std::cout << "Server start on port: " << port << std::endl;
}void WebServer::StartAccept() {auto con_ptr = std::make_shared<Connection>(_ioc);_acceptor.async_accept(con_ptr->GetSocket(), [this, con_ptr](boost::system::error_code ec) {try {if (!ec) {con_ptr->AsyncAccept();}else {std::cerr << "acceptor async accept error is " << ec.what() << std::endl;}StartAccept();}catch (std::exception& e) {std::cerr << "async accept error is " << e.what() << std::endl;}});
}
WebServer类的构造函数和之前将的一样,将io_context的引用赋值给_ioc,将acceptor与ioc和服务器端点绑定,acceptor将从该端点监听客户端的连接。
StartAccept()于异步接受传入的连接,并启动connection连接的start函数。
再提一遍,WebSocket 对象的 async_accept 和 acceptor对象的 async_accept 不一样。
d. 编译的小问题
如果直接编译的话会报错,显示生成的对象文件过大,超出默认限制。我们这里这里打开项目的属性->C/C++->命令行->输入‘/bigobj’。
3. 测试
通过该网站进行在线测试:
websocket在线测试www.websocket-test.com/编辑https://link.zhihu.com/?target=http%3A//www.websocket-test.com/
启动服务器,
打开测试网站,输入服务器域名,点击连接并发送“hello world”
ws://127.0.0.1:10086
网站显示:
程序显示:
一个简单的websocket服务器搭建成功。
4. 基于http实现的websocket
除了通过TCP长连接可以实现websocket服务器外,也可以通过http服务器升级为websocket,代码可参考博主恋恋风辰的代码仓库:
secondtonone1/boostasio-learngitee.com/secondtonone1/boostasio-learn编辑https://link.zhihu.com/?target=https%3A//gitee.com/secondtonone1/boostasio-learn
相关文章:

网络编程(22)——通过beast库快速实现websocket服务器
目录 二十二、day22 1. websocket简述 2. 基于TCP长连接实现sebsocket a. Connection b. ConnectionMgr c. WebServer d. 编译的小问题 3. 测试 4. 基于http实现的websocket 二十二、day22 因为http受限于请求-响应模式,客户端发起请求,服务器…...

从视频截取每一帧作为图像
查看视频有多少帧 import cv2def count_frames_per_second(video_path):cap cv2.VideoCapture(video_path)if not cap.isOpened():print("Error: Could not open video")return None# Get frames per secondfps cap.get(cv2.CAP_PROP_FPS)# Get total number of f…...

终端 数据表格
// // Created by HongDaYu on 17 十月 2024. //#ifndef HDYSDK_UTIL_H #define HDYSDK_UTIL_H#include <cstdint> #include <string> #include <list> #include <iomanip> #include <memory>class dataGrid { private:std::list<const char*…...

2.4.ReactOS系统运行级别降低IRQL级别KfLowerIrql 函数
2.4.ReactOS系统运行级别降低IRQL级别KfLowerIrql 函数 2.4.ReactOS系统运行级别降低IRQL级别KfLowerIrql 函数 文章目录 2.4.ReactOS系统运行级别降低IRQL级别KfLowerIrql 函数KfLowerIrql 函数 KfLowerIrql 函数 /*******************************************************…...

数字后端实现静态时序分析STA Timing Signoff之min period violation
今天给大家分享一个在高性能数字IC后端实现timing signoff阶段经常遇到的min period violation。大部分时候出现memory min period问题基本上都是需要返工重新生成memory的。这是非常致命的错误,希望大家在做静态时序分析时一定要查看min period violation。 什么是…...

phpstorm+phpstudy 配置xdebug(无需开启浏览器扩展)
今天又被xdebug折磨了,忘记了以前咋配置了现在百度发现好多都是各种浏览器扩展而且也没有真正的用到项目上的都是测试的地址怎么样的 我就简单写一下自己实战吧 不支持workerman swoole hyperf等这种服务框架 如果你会请教教我 工具版本phpstudy8.1.xphpstorm2021.x…...

AI赋能安全运营 | 赛宁网安深度参与四川省网络安全沙龙
为促进四川省、市网络安全公共服务领域的经验交流与深入探讨,打通网络安全供需上下游,加速汇聚省、市优质网络安全设备和服务资源,提升巴中市乃至四川省网络安全防护水平,共同推动四川省网络安全事业的蓬勃发展。 2024年10月15日…...

R语言中,.RData 和 .rds 的区别
.RData 和 .rds 是 R 语言中两种不同的数据保存格式,二者有一些关键的区别: 1. 存储内容的类型: .RData 文件:可以同时保存多个对象(如数据框、向量、列表等),当你加载 .RData 文件时…...

python实现录屏功能
python实现录屏功能 将生成的avi文件转为mp4格式后删掉avi文件 参考感谢:https://www.cnblogs.com/peachh/p/16549254.html import os import cv2 import time import threading import numpy as np from PIL import ImageGrab from pynput import keyboard from da…...

酷克数据出席2024金融业数据库技术大会
10月16日,由中国金融电子化集团指导,北京金融信息化研究所主办的“2024金融业数据库技术大会”在京顺利召开。本次大会以“大模型时代下的数据库创新发展”为主题,汇聚了来自管理部门、金融机构、科技企业以及学术研究机构的众多专家学者&…...

find_library、pkg_check_modules、pkg_search_module的区别
在CMake中,find_library、pkg_check_modules和pkg_search_module是用于查找和使用库的三种不同命令。以下是具体介绍: find_library 功能:find_library用于查找指定的库文件(动态库或静态库),不依赖于库提供…...

多jdk版本环境下,jenkins系统设置需指定JAVA_HOME环境变量
一、背景 由于不同项目对jdk版本的要求不同,有些是要求jdk11,有些只需要jdk8即可。 而linux机器上安装jdk的方式又多种多样,最后导致jenkins打包到底使用的是哪个jdk,比较混乱。 1、java在哪 > whereis java java: /usr/bin/…...

Java mybatis day1015
ok了家人们,今天学习了mybatis这个框架,我们一起去看看吧 一.Mybatis简介 1.1 Mybatis概述 MyBatis 最初是 Apache 的一个开源项目 iBatis, 2010 年 6 月 这个项目由 Apache Software Foundation 迁移到了 Google Code 。随着开发团队转投 Google Cod…...

音乐播放器项目专栏介绍
1.简介 本专栏使用Qt QWidget作为显示界面,你将会学习到以下内容: 1.大量ui美化的实例。 2.各种复杂ui布局。 3.常见显示效果实现。 4.大量QSS实例。 5.Qt音频播放,音乐歌词文件加载,展示。 6.播放器界面换肤。 相信学习了本专栏…...

如何修改SpringBoot内置容器默认上下文
引言 默认情况下,Spring boot 应用程序通过上下文路径“/”访问,这是嵌入式服务器的默认设置,即我们可以直接通过http://localhost:8080/访问该应用程序。 但是在生产环境中部署 Spring Boot 应用程序时,指定上下文路径是一个常…...

R语言详解predict函数
R语言中predict函数在建立模型,研究关系时常用。但是不同type得到的结果常常被混为一谈,接下来,探讨predict得到的不同结果。 #数据 set.seed(123) n<-1000 age<-rnorm(n,mean50,sd10) gender<-rbinom(n,1,0.5) disease<-rbinom…...

QT 实现随机码验证
1.界面实现效果 以下是具体的项目需要用到的效果展示,用于验证字母。 2.简介 自定义CaptchaMovableLabel,继承自QLabel类: 中间的4个字母,就是CaptchaMovableLabel类来实例化的对象。 主要功能如下: 1.显示字母&am…...

集合框架12:Set集合概述、Set接口使用
视频链接:13.24 Set接口使用_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1zD4y1Q7Fw?spm_id_from333.788.videopod.episodes&vd_sourceb5775c3a4ea16a5306db9c7c1c1486b5&p24 1、Set集合概述 特点:无序、无下标,元素不可…...

如何打开荣耀手机的调试模式?
问题描述: 最近用荣耀手机进行测试,打开开发者选项,打开USB调试,在选择USB配置时,发现仅有选择USB以太网才可以连接Android Studio,也就是打开ADB调试模式。 但是,打开USB以太网后,…...

Meta新模型Dualformer:融合快慢思维,推理能力媲美人脑
Meta 的 FAIR 团队最近推出了一款名为 Dualformer 的全新 Transformer 模型,该模型模仿人类的双重认知系统,能够无缝整合快速和慢速推理模式,在推理能力和计算效率上取得了显著突破。 人类的思维过程通常被认为是由两种系统控制的:系统1快速…...

CDGA|数据治理:如何让传统行业实现数据智能
在当今这个数字化时代,数据已成为推动各行各业转型升级的关键力量。对于传统行业而言,如何从海量、复杂的数据中挖掘价值,实现“数据智能”,成为了提升竞争力、优化运营效率、创新业务模式的重要途径。本文将探讨数据治理如何助力…...

Spring源码5.2.9 编译踩坑
源码编译踩坑 拉取源码 我这块以5.2.9版本为例 spring-projects/spring-framework at v5.2.9.RELEASE (github.com) 版本分析 确定版本 这块将Gradle升级到了5.6.4 ,我们去官网下载即可 Gradle安装 Gradle | Releases 解压 将其解压到你想存放的文件夹 配置环…...

【前端】如何制作一个自己的网页(5)
上节课我们学习了以下知识: 1、网页中常见的文本元素,如标题元素与段落元素; 2、两个通用属性id与class; 3、元素的两种类型——块级元素与行内元素。 其实除了文本内容外,网页还可以包含图片、超链接等各类信息&a…...

Unity实战案例全解析 类宝可梦回合制的初级案例 源码分析(加了注释和流程图)
这是一个老教程了,但是对于没有写过回合制的初级程序同学来讲是比较适合的,也可以直接看源码,半小时内可以解决战斗 当然,我也没写过回合制系统所以就到处找,思路明白了就能自己修改了 视频教程 - 油管链接 Turn-Bas…...

AI绘图大模型 Stable Diffusion 使用详解
近年来,生成式 AI 技术,特别是 AI 绘图模型的进展令人瞩目。Stable Diffusion 是其中一款开源的大规模图像生成模型,它能够根据文本描述生成高质量的图像,支持从写实风格到卡通、幻想等各种不同的视觉效果。本文将深入介绍如何使用…...

es索引库操作和使用RestHignLevelClient客户端操作es
目录 es索引库操作 mapping映射操作 索引库的CURD操作 1.创建索引库和映射 编辑 2.查询索引库 3.删除索引库 4.修改索引库 5.总结 文档的CURD操作 1.新增文档 2.查询文档 3.删除文档 4.修改文档 全量修改 增量修改 5.总结 RestAPI 使用API例子 需要的数…...

安卓数据共享
在 Android 中,数据共享是指不同应用之间共享数据或同一应用不同组件之间共享数据的机制。SQLite 数据库、内容提供者(Content Provider)、共享偏好(Shared Preferences)和文件存储等方式可以实现数据共享。下面将详细…...

Gin框架操作指南02:JSON渲染
官方文档地址(中文):https://gin-gonic.com/zh-cn/docs/ 注:本教程采用工作区机制,所以一个项目下载了Gin框架,其余项目就无需重复下载,想了解的读者可阅读第一节:Gin操作指南&#…...

【随手记】MySQL单表访问方法
在MySQL查询优化器中,单表访问方法(Access Method)指的是查询时数据库如何从一个表中访问所需的数据。不同的访问方法适用于不同的查询场景,主要包括 const、ref、ref_or_null、range、index 和 all。这些方法从效率上依次递减&am…...

机器学习:情感分析的原理、应用场景及优缺点介绍
一、情感分析算法概述 情感分析是自然语言处理中的一个重要任务,主要用于判断文本中所包含的情感倾向,如正面、负面或中性。 二、基于词典的情感分析算法 原理 词典构建:首先需要构建一个情感词典。这个词典包含了一系列带有情感倾向的词汇…...