当前位置: 首页 > news >正文

深入理解WebSocket协议

WebSocket

“ 一直以来对WebSocket仅停留在使用阶段,也没有深入理解其背后的原理。当看到 x x x was not upgraded to websocket,我是彻底蒙了,等我镇定下来,打开百度输入这行报错信息,随即看到的就是大家说的跨域,或者Origin导致,其实webSocket本身不存在跨域问题,由于目前在Flutter项目中遇到这个错误,接下来我也将从Flutter源码中寻找问题所在。在此,特别感谢公司后台同事在此问题上的指导和帮助。”

什么是跨域

既然提到了跨域,我们还是先弄清什么是跨域。在了解什么是跨域的时候,我们首先要了解一个概念,叫同源策略,什么是同源策略呢,就是我我们的浏览器出于安全考虑,只允许与本域下的接口交互。不同源的客户端脚本在没有明确授权的情况下,不能读写对方的资源。

是什么意思呢,就比如你刚刚登录了淘宝买了东西,但是你现在又点进去了另外一个网站,那么你现在的淘宝账户是属于登录状态,而并没有登出,所以你现在点进去的这个网站可以看到你的账户信息,并操作你的账户信息,这样子就很危险。
我们再来了解一个概念,就是本域,什么是本域呢?就是同协议,同域名,同端口就叫本域。从下面的图片我们可以清楚的了解到,什么是本域,以及什么时候跨域。再结合前面WebSocket问题,就明白了为什么不存在跨域问题,因为我们通过WebSocket的API new出的都是当前的单一实例,要访问其他域则需要单独new出实例。

跨域

为什么需要WebSocket

通常我们客户端都是通过http向服务端发送request请求,然后得到response响应,也就是说http是一问一答的模式,这种模式对数据资源、数据加载足够够用,但是需要数据推送的场景就不合适了。我们知道Http2有Server push概念,那只是推资源用的,比如我们浏览器请求了html,服务端可以连带css资源一起推给浏览器,浏览器可以选择接不接收。对即时通讯要求较高的场景就需要用到WebSocket了。所以WebSocket本质上一种计算机网络协议,用来弥补Http协议在持久通信能力上的不足。它的最大特点就是,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。

WebSocket使用

以Flutter平台为例,其它平台都大同小异,仅作为参考


// Copyright (c) 2018, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/status.dart' as status;void main() {final channel = IOWebSocketChannel.connect('ws://localhost:1234');channel.stream.listen((message) {channel.sink.add('received!');channel.sink.close(status.goingAway);});
}

WebSocket connect发生了什么

在上面的例子当中,当我们调用了WebSocket的connect方法后,究竟会经过什么流程呢?为了直观,我先截我们公司前端ws链接情况,如下:Response Headers
Request Headers

WebSocket严格意义上和http没什么关系,从上面的流程可以看出,首先是发出了get请求,然后返回101进行了一次协议切换,如下图:
协议切换
切换的过程是这样的:
从上面截图中可以清楚的看到,请求的Request Headers携带有以下值:

Connection: Upgrade
Upgrade: websocket
Cookie: user=emh1aHVpdGFvX3N0cnVnZ2xlQDE2My5jb20%3D; password=emh1aHVpdGFvMTIz; JSESSIONID=node01l5dg6m63bgvvbu8il1jvkylo0.node0
Sec-WebSocket-Key: I3BB/MVp6YrFg65CFIndTg==
Sec-WebSocket-Version: 13
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/109.0.0.0 Safari/537.36

前面两个就是升级到Websocket协议的意思,第三个是Cookie,携带的是用户的账号密码以及SessionId等,针对这个属性后面单独展开来说,因为这里不注意就会遇到坑,第三个header是保证安全的一个key,Sec-WebSocket-Version就是版本的意思,根据查看flutter WebSocket部分源码,使用Postman以及结合浏览器发现这个应该是写死的都是13。

服务端返回的Response Headers:


Connection: upgrade
Date: Tue, 28 Feb 2023 16:07:47 GMT
Sec-WebSocket-Accept: 1VYpY/UEy4+X03+a8zMw05SjCtw=
Sec-WebSocket-Extensions: permessage-deflate
Server: nginx/1.21.5
Upgrade: websocket 

上面只是复制了部分Headers,更详细的可以参考上面的截图,我们发现无论是Request Headers还是Response Headers,有重合相同的部分:


请求:
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key : I3BB/MVp6YrFg65CFIndTg==响应:
Connection: upgrade
Upgrade: websocket
Sec-WebSocket-Accept: 1VYpY/UEy4+X0+a8zMw05SjCtw

Sec-WebSocket-Accept是对请求带过来的Sec-WebSocket-Key值处理后的结果,加入这个header就是为了确定对方一定有WebSocket能力,不然建立了链接,对方一直不回消息,那不就白等了么。
Sec-WebSocket-Key经过什么处理得到了Sec-WebSocket-Accept?其实很简单,就是客户端传过来的key,加上一个固定的字符串,经过sha1加密之后转成base64的结果,这个固定的字符串为:

258EAFA5-E914-47DA-95CA-C5AB0DC85B11

如果你不信,可以自己动手实现一下,或者在网上搜下这个字符串:

固定字符串
如果你到这里还不信,我们稍后看看Flutter源码是怎么实现的,你就会真的明白它就是这样。

前面提到WebSocket链接先通过get请求,服务端返回状态码101进行协议切换,然后也提到Headers的处理过程,下面我们通过阅读Flutter WebSocket源码证实我们上面分析的是不是正确的。

进入connect方法,跳转到io.dart文件

factory IOWebSocketChannel.connect(
Object url, {
Iterable<String>? protocols,
Map<String, dynamic>? headers,
Duration? pingInterval,
Duration? connectTimeout,
}) {
late IOWebSocketChannel channel;
final sinkCompleter = WebSocketSinkCompleter();
var future = WebSocket.connect(
url.toString(),
headers: headers,
protocols: protocols,
);
if (connectTimeout != null) {
future = future.timeout(connectTimeout);
}
final stream = StreamCompleter.fromFuture(future.then((webSocket) {
webSocket.pingInterval = pingInterval;
channel._webSocket = webSocket;
channel._readyCompleter.complete();
sinkCompleter.setDestinationSink(_IOWebSocketSink(webSocket));
return webSocket;
}).catchError((Object error, StackTrace stackTrace) {
channel._readyCompleter.completeError(error, stackTrace);
throw WebSocketChannelException.from(error);
}));
return channel =
IOWebSocketChannel._withoutSocket(stream, sinkCompleter.sink);}     

url就是我们要链接的WebSocket url,是必须传的,其它都是配置选项,Headers就是我们上面提到的Request Headers,我在项目中的配置大概是下面这样,可以和后台协商配置。

wss() async {
Map<String, String> headers = {};
///升级到WebSocket协议headers['Connection'] = 'Upgrade';headers['Upgrade'] = 'websocket';headers['Sec-WebSocket-Extensions'] =
'permessage-deflate; client_max_window_bits';///验证headers['Cookie'] = 'JSESSIONID=node0j43bo45tn9e81wkdqf9dau7bx1877.node0';print(headers);_channel =
await IOWebSocketChannel.connect(ApiConfig.wss, headers: headers);_channel?.stream.listen((message) {print(message);print(json.decode(message));
Map<String, dynamic> map = json.decode(message);
var result = map['positions'];
if (result != null) {
for (var value in (result as List)) {maps[value['deviceId'].toString()] = value;}}});
}

再看看WebSocket的connect方法中,直接调用了WebSocket实现类_WebSocketImp中的connect方法。

static Future<WebSocket> connect(String url, Iterable<String>? protocols, Map<String, dynamic>? headers,{CompressionOptions compression = CompressionOptions.compressionDefault,HttpClient? customClient}) {Uri uri = Uri.parse(url);
if (!uri.isScheme("ws") && !uri.isScheme("wss")) {
throw WebSocketException("Unsupported URL scheme '${uri.scheme}'");}Random random = Random();
// Generate 16 random bytes.Uint8List nonceData = Uint8List(16);
for (int i = 0; i < 16; i++) {nonceData[i] = random.nextInt(256);}String nonce = base64Encode(nonceData);final callerStackTrace = StackTrace.current;uri = Uri(scheme: uri.isScheme("wss") ? "https" : "http",userInfo: uri.userInfo,host: uri.host,port: uri.port,path: uri.path,query: uri.query,fragment: uri.fragment);
return (customClient ?? _httpClient).openUrl("GET", uri).then((request) {
if (uri.userInfo != null && uri.userInfo.isNotEmpty) {
// If the URL contains user information use that for basic
// authorization.String auth = base64Encode(utf8.encode(uri.userInfo));request.headers.set(HttpHeaders.authorizationHeader, "Basic $auth");}
if (headers != null) {headers.forEach((field, value) => request.headers.add(field, value));}
// Setup the initial handshake.///headers处理request.headers..set(HttpHeaders.connectionHeader, "Upgrade")..set(HttpHeaders.upgradeHeader, "websocket")..set("Sec-WebSocket-Key", nonce)..set("Cache-Control", "no-cache")..set("Sec-WebSocket-Version", "13");
if (protocols != null) {request.headers.add("Sec-WebSocket-Protocol", protocols.toList());}if (compression.enabled) {request.headers.add("Sec-WebSocket-Extensions", compression._createHeader());}return request.close();}).then((response) {
Future<WebSocket> error(String message) {
// Flush data.response.detachSocket().then((socket) {socket.destroy();});
return Future<WebSocket>.error(WebSocketException(message), callerStackTrace);}var connectionHeader = response.headers[HttpHeaders.connectionHeader];
if (response.statusCode != HttpStatus.switchingProtocols ||connectionHeader == null ||!connectionHeader.any((value) => value.toLowerCase() == "upgrade") ||response.headers.value(HttpHeaders.upgradeHeader)!.toLowerCase() !=
"websocket") {
return error("Connection to '$uri' was not upgraded to websocket");}String? accept = response.headers.value("Sec-WebSocket-Accept");
if (accept == null) {
return error(
"Response did not contain a 'Sec-WebSocket-Accept' header");}_SHA1 sha1 = _SHA1();sha1.add("$nonce$_webSocketGUID".codeUnits);List<int> expectedAccept = sha1.close();List<int> receivedAccept = base64Decode(accept);
if (expectedAccept.length != receivedAccept.length) {
return error(
"Response header 'Sec-WebSocket-Accept' is the wrong length");}
for (int i = 0; i < expectedAccept.length; i++) {
if (expectedAccept[i] != receivedAccept[i]) {
return error("Bad response 'Sec-WebSocket-Accept' header");}}
var protocol = response.headers.value('Sec-WebSocket-Protocol');_WebSocketPerMessageDeflate? deflate =negotiateClientCompression(response, compression);return response.detachSocket().then<WebSocket>((socket) =>_WebSocketImpl._fromSocket(socket, protocol, compression, false, deflate));});}

上面的代码很长也很多,但是真的很重要,也很容易理解,接下来逐一分析上面每一行的功能和作用。

1,判断传入的url是否满足WebSocket url格式,如果不满足就会抛出异常。


Uri uri = Uri.parse(url);
if (!uri.isScheme("ws") && !uri.isScheme("wss")) {
throw WebSocketException("Unsupported URL scheme '${uri.scheme}'");}

2,在前面我们一直强调的请求Headers中的key,也就是Sec-WebSocket_key是怎么生成的呢,过程也很简单。


Random random = Random();
// Generate 16 random bytes.
Uint8List nonceData = Uint8List(16);
for (int i = 0; i < 16; i++) {
nonceData[i] = random.nextInt(256);
}
String nonce = base64Encode(nonceData);final callerStackTrace = StackTrace.current;

最后会把这个nonce,赋值给Sec-WebSocket-key。

3,Get请求,并携带Headers。

return (customClient ?? _httpClient).openUrl("GET", uri).then((request) {
if (uri.userInfo != null && uri.userInfo.isNotEmpty) {
// If the URL contains user information use that for basic
// authorization.String auth = base64Encode(utf8.encode(uri.userInfo));request.headers.set(HttpHeaders.authorizationHeader, "Basic $auth");}
if (headers != null) {headers.forEach((field, value) => request.headers.add(field, value));}
// Setup the initial handshake.request.headers..set(HttpHeaders.connectionHeader, "Upgrade")..set(HttpHeaders.upgradeHeader, "websocket")..set("Sec-WebSocket-Key", nonce)..set("Cache-Control", "no-cache")..set("Sec-WebSocket-Version", "13");
if (protocols != null) {request.headers.add("Sec-WebSocket-Protocol", protocols.toList());}if (compression.enabled) {request.headers.add("Sec-WebSocket-Extensions", compression._createHeader());}return request.close();}).

在前面提到配置Headers,比如升级协议Upgrade,安全校验的Sec-WebSocket-key等,其实我们只需要配置与服务端协商好的key,其它别的是不用配置的,也就是底层会自动帮我们加上这些信息,前面也提到版本,这里可以看到就是写死的13。

4,协议切换,前面分析过程中提到了通过Get请求返回状态码为101进行WebSocket协议切换,这个101在哪出现然后判断的呢,我们且看下面的代码。


var connectionHeader = response.headers[HttpHeaders.connectionHeader];
if (response.statusCode != HttpStatus.switchingProtocols ||connectionHeader == null ||!connectionHeader.any((value) => value.toLowerCase() == "upgrade") ||response.headers.value(HttpHeaders.upgradeHeader)!.toLowerCase() !=
"websocket") {
return error("Connection to '$uri' was not upgraded to websocket");}

这里出现了response.statusCode和HttpStatus.switchingProtocols,HttpStatus.switchingProtocols这个值是什么呢,跟进去看发现就是常量101。


static const int switchingProtocols = 101;

看到这里,是不是和我们前面分析的一模一样。还有重要的一点,这里出现了Connection to ‘$uri’ was not upgraded to websocket。也就是我们最开始提到的异常,其实这里太笼统。只要返回的状态码不为101或者接收的connectionHeader为null,都直接抛出了这个头疼的not upgraded to websocket,所以不了解的时候,就像我开头说的根本无从下手,针对此问题在后面我也会分析会出现这个异常的原因。其实看到这里,我们心里或许也知道是什么导致的了。

5,Sec-WebSocket-Accept的值是怎么来呢?答案就是Sec-WebSocket-key加上固定的字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11,通过Sha1加密转Base64的来的。

_SHA1 sha1 = _SHA1();sha1.add("$nonce$_webSocketGUID".codeUnits);List<int> expectedAccept = sha1.close();List<int> receivedAccept = base64Decode(accept);
if (expectedAccept.length != receivedAccept.length) {
return error(
"Response header 'Sec-WebSocket-Accept' is the wrong length");}
for (int i = 0; i < expectedAccept.length; i++) {
if (expectedAccept[i] != receivedAccept[i]) {
return error("Bad response 'Sec-WebSocket-Accept' header");}}

这里的sha1.add(“noncenoncenonce_webSocketGUID”.codeUnits),nonce就是我们上面请求Headers中携带的Sec-WebSocket-key值,然后加上_webSocketGUID,_webSocketGUID就是前面一直提到的固定字符串。

const String _webSocketGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

看到这里我们是不是如释重负的感觉,完全和我们分析的一模一样,这里首先进行了长度比较,然后再判断每一个字符是不是相等。


1,长度比较
expectedAccept.length != receivedAccept.length2,循环遍历每个字符
for (int i = 0; i < expectedAccept.length; i++) {
if (expectedAccept[i] != receivedAccept[i]) {
return error("Bad response 'Sec-WebSocket-Accept' header");}}

通过上面的异常,比较之前的101状态码,还是可以接受一些,最起码告诉我们Headers有问题,这样我们找问题也就更直接了。

6,Request Headers中的Cookie,当然从上面的源码中也未发现这个东西。Cookie的概念其实在app中用的很少,至少在我开发过的项目里基本是没用到的,但是在浏览器中很常见。Cookie可以携带用户信息,如下面代码所示。

headers['Cookie'] = 'JSESSIONID=node0j43bo45tn9e81wkdqf9dau7bx1877.node0';

回到最开始项目中遇到的问题,就是Cookie导致的,由于我最开始不知道要携带这个Cookie导致服务器无法校验通过,也就无法成功返回101状态码,自然就无法成功切换到WebSocket协议了,JSESSIONID的值从哪获取呢,其实也是从Headers获取,一般我们调用登录接口,服务端返回的Response Header中就会携带这个值。


if (response.headers['set-cookie'] != null &&response.headers['set-cookie']!.isNotEmpty) {DataCenter.COOKIE = response.headers['set-cookie']!.first;}

当我们拿到这个值的时候,保存下来,等链接wss的业务的时候取出来赋值给Cookie,就可以了。

总结

通过过上面的分析,我们知道了WebSocket整个链接过程,以及是如何从Http切换到WebSocke协议的,以及分析了出现异常的原因,这里大致总结一下出现异常大概有以下问题导致:
1,服务端WebSocket本身就有问题(可以在postman上调试排查问题)
2,url有问题,不是正常的url
3,Headers问题,最有可能的就是Cookie,出现问题后我们应该立马和后台同事沟通,问清楚他们到底需要什么值,沟通清楚再实现。

至此我们分析了WebSocket的整个链接和切换流程,如果你觉得写的不错,欢迎点个关注,感谢🙏。

相关文章:

深入理解WebSocket协议

“ 一直以来对WebSocket仅停留在使用阶段&#xff0c;也没有深入理解其背后的原理。当看到 x x x was not upgraded to websocket&#xff0c;我是彻底蒙了&#xff0c;等我镇定下来&#xff0c;打开百度输入这行报错信息&#xff0c;随即看到的就是大家说的跨域&#xff0c;或…...

Vector的扩容机制

到需要扩容的时候&#xff0c;Vector会根据需要的大小&#xff0c;创建一个新数组&#xff0c;然后把旧数组的元素复制进新数组。 我们可以看到&#xff0c;扩容后&#xff0c;其实是一个新数组&#xff0c;内部元素的地址已经改变了。所以扩容之后&#xff0c;原先的迭代器会…...

22讲MySQL有哪些“饮鸩止渴”提高性能的方法

短连接风暴 是指数据库有很多链接之后只执行了几个语句就断开的客户端&#xff0c;然后我们知道数据库客户端和数据库每次连接不仅需要tcp的三次握手&#xff0c;而且还有mysql的鉴权操作都要占用很多服务器的资源。话虽如此但是如果连接的不多的话其实这点资源无所谓的。 但是…...

10.0自定义SystemUI下拉状态栏和通知栏视图(六)之监听系统通知

1.前言 在进行rom产品定制化开发中,在10.0中针对systemui下拉状态栏和通知栏的定制UI的工作开发中,原生系统的下拉状态栏和通知栏的视图UI在产品开发中会不太满足功能, 所以根据产品需要来自定义SystemUI的下拉状态栏和通知栏功能,首选实现的就是下拉通知栏左滑删除通知的部…...

怎样在外网登录访问CRM管理系统?

一、什么是CRM管理系统&#xff1f; Customer Relationship Management&#xff0c;简称CRM&#xff0c;指客户关系管理&#xff0c;是企业利用信息互联网技术&#xff0c;协调企业、顾客和服务上的交互&#xff0c;提升管理服务。为了企业信息安全以及使用方便&#xff0c;企…...

Activity工作流(三):Service服务

3. Service服务 所有的Service都通过流程引擎获得。 3.1 RepositoryService 仓库服务是存储相关的服务&#xff0c;一般用来部署流程文件&#xff0c;获取流程文件&#xff08;bpmn和图片&#xff09;&#xff0c;查询流程定义信息等操作&#xff0c;是引擎中的一个重要的服务。…...

算法--最长回文子串--java--python

这个算法题里面总是有 暴力解法 把所有字串都拿出来判断一下 这里有小小的优化&#xff1a; 就是当判断的字串小于等于我们自己求得的最长回文子串的长度&#xff0c;那么我们就不需要在进行对这个的判断这里的begin&#xff0c;还可以用来取得最小回文子串是什么 java // 暴…...

ElasticSearch-第二天

目录 文档批量操作 批量获取文档数据 批量操作文档数据 DSL语言高级查询 DSL概述 无查询条件 叶子条件查询 模糊匹配 match的复杂用法 精确匹配 组合条件查询(多条件查询) 连接查询(多文档合并查询) 查询DSL和过滤DSL 区别 query DSL filter DSL Query方式查…...

【AI大比拼】文心一言 VS ChatGPT-4

摘要&#xff1a;本文将对比分析两款知名的 AI 对话引擎&#xff1a;文心一言和 OpenAI 的 ChatGPT&#xff0c;通过实际案例让大家对这两款对话引擎有更深入的了解&#xff0c;以便大家选择合适的 AI 对话引擎。 亲爱的 CSDN 朋友们&#xff0c;大家好&#xff01;近年来&…...

美团笔试-3.18

1、捕获敌人 小美在玩一项游戏。该游戏的目标是尽可能抓获敌人。 敌人的位置将被一个二维坐标 (x, y) 所描述。 小美有一个全屏技能&#xff0c;该技能能一次性将若干敌人一次性捕获。 捕获的敌人之间的横坐标的最大差值不能大于A&#xff0c;纵坐标的最大差值不能大于B。 现在…...

【12】SCI易中期刊推荐——计算机信息系统(中科院4区)

🚀🚀🚀NEW!!!SCI易中期刊推荐栏目来啦 ~ 📚🍀 SCI即《科学引文索引》(Science Citation Index, SCI),是1961年由美国科学信息研究所(Institute for Scientific Information, ISI)创办的文献检索工具,创始人是美国著名情报专家尤金加菲尔德(Eugene Garfield…...

好不容易约来了一位程序员来面试,结果人家不做笔试题

感觉以后还是不要出面试题这环节好了。好不容易约来了一位程序员来面试。刚递给他一份笔试题&#xff0c;他一看到要做笔试题&#xff0c;说不做笔试题&#xff0c;有问题面谈就好了&#xff0c;搞得我有点尴尬&#xff0c;这位应聘者有3年多工作经验。关于程序员岗位&#xff…...

这几个过时Java技术不要再学了

Java 已经发展了近20年&#xff0c;极其丰富的周边框架打造了一个繁荣稳固的生态圈。 Java现在不仅仅是一门语言&#xff0c;而且还是一整个生态体系&#xff0c;实在是太庞大了&#xff0c;从诞生到现在&#xff0c;有无数的技术在不断的推出&#xff0c;也有很多技术在不断的…...

EEPROM芯片(24c02)使用详解(I2C通信时序分析、操作源码分析、原理图分析)

1、前言 (1)本文主要是通过24c02芯片来讲解I2C接口的EEPROM操作方法&#xff0c;包含底层时序和读写的代码&#xff1b; (2)大部分代码是EEPROM芯片通用的&#xff0c;但是其中关于某些时间的要求&#xff0c;是和具体芯片相关的&#xff0c;和主控芯片和外设芯片都有关系&…...

Django4.0新特性-主要变化

Django 4.0于2021年12月正式发布&#xff0c;标志着Django 4.X时代的来临。参考Django 4.0 release notes | Django documentation | Django Python 兼容性 Django 4.0 将支持 Python 3.8、3.9 与 3.10。强烈推荐并且仅官方支持每个系列的最新版本。 Django 3.2.x 系列是最后…...

MySQL高级面试题整理

1. 执行流程 mysql客户端先与服务器建立连接Sql语句通过解析器形成解析树再通过预处理器形成新解析树&#xff0c;检查解析树是否合法通过查询优化器将其转换成执行计划&#xff0c;优化器找到最适合的执行计划执行器执行sql 2. MYISAM和InNoDB的区别 MYISAM&#xff1a;不支…...

【Java】面向对象三大基本特征

【Java】面向对象三大基本特征 1.封装 On Java 8:研发程序员开发一个工具类&#xff0c;该工具类仅向应用程序员公开必要的内容&#xff0c;并隐藏内部实现的细节。这样可以有效地避免该工具类被错误的使用和更改&#xff0c;从而减少程序出错的可能。彼此职责划分清晰&#x…...

蓝桥杯C++组怒刷50道真题(填空题)

&#x1f33c;深夜伤感网抑云 - 南辰Music/御小兮 - 单曲 - 网易云音乐 &#x1f33c;多年后再见你 - 乔洋/周林枫 - 单曲 - 网易云音乐 18~22年真题&#xff0c;50题才停更&#xff0c;课业繁忙&#xff0c;有空就更&#xff0c;2023/3/18/23:01写下 目录 &#x1f44a;填…...

Shell自动化管理 for ORACLE DBA

1.自动收集每天早上9点到晚上8点之间的AWR报告。 auto_awr.sh #!/bin/bash# Set variables ORACLE_HOME/u01/app/oracle/product/12.1.0/dbhome_1 ORACLE_SIDorcl AWR_DIR/home/oracle/AWR# Set date format for file naming DATE$(date %Y%m%d%H%M%S)# Check current time - …...

Unity学习日记13(画布相关)

目录 创建画布 对画布的目标图片进行射线检测 拉锚点 UI文本框使用 按钮 按钮导航 按钮触发事件 输入框 实现单选框 下拉菜单 多选框选项加图片 创建画布 渲染模式 第一个&#xff0c;保持画布在最前方&#xff0c;画布内的内容显示优先级最高。 第二个&#xff0c;…...

初阶C语言:冒泡排序

冒泡排序是一种简单的排序算法&#xff0c;它重复地走访过要排序的数列&#xff0c;一次比较两个元素&#xff0c;如果他们的顺序错误就把他们交换过来。走访数列的工作是重复地进行直到没有再需要交换&#xff0c;也就是说该数列已经排序完成。1.冒泡排序关于冒泡排序我们在讲…...

带头双向循环链表

在前面我们学习了单链表&#xff0c;发现单链表还是有一些不够方便&#xff0c;比如我们要尾插&#xff0c;需要遍历一遍然后找到它的尾&#xff0c;这样时间复炸度就为O(N),现在我们引入双向带头链表就很方便了&#xff0c;我们先看看它的结构。通过观察&#xff0c;我们发现一…...

C#中的DataGridView中添加按钮并操作数据

背景&#xff1a;最近在项目中有需求需要在DataGridView中添加“删除”、“修改”按钮&#xff0c;用来对数据的操作以及显示。 在DataGridView中显示需要的按钮 首先在DataGridView中添加需要的列&#xff0c;此列是用来存放按钮的。 然后在代码中“画”按钮。 if (e.Column…...

WEB安全 PHP基础

WEB安全 PHP基础 PHP简述 PHP&#xff08;全称&#xff1a;PHP&#xff1a;Hypertext Preprocessor&#xff0c;即"PHP&#xff1a;超文本预处理器"&#xff09;是一种通用开源脚本语言。 在一个php文件中可以包括以下内容&#xff1a;  PHP 文件可包含文本、HTML、…...

基础篇:07-Nacos注册中心

1.Nacos安装部署 1.1 下载安装 nacos官网提供了安装部署教程&#xff0c;其下载链接指向github官网&#xff0c;选择合适版本即可。如访问受阻可直接使用以下最新稳定版压缩包&#xff1a;&#x1f4ce;nacos-server-2.1.0.zip&#xff0c;后续我们也可能会更改为其他版本做更…...

端口镜像讲解

目录 端口类型 镜像方向 观察端口位置 端口镜像实现方式 流镜像 Vlan镜像 MAC镜像 配置端口镜像 配置本地观察端口 配置远程流镜像&#xff08;基于流镜像&#xff09; 端口镜像是指将经过指定端口的报文复制一份到另一个指定端口&#xff0c;便于业务监控和故障定位…...

图形视图框架QGraphicsScene(场景,概念)

QGraphicsScene 该类充当 QGraphicsItems 的容器。它与 QGraphicsView 一起使用&#xff0c;用于在 2D 表面上可视化图形项目&#xff0c;例如线条、矩形、文本甚至自定义项目。 QGraphicsScene具有的功能&#xff1a; 提供用管理大量数据项的高速接口传播事件到每一个图形项…...

ChatGPT 拓展资料: 强化学习-SARSA算法

强化学习是一种机器学习技术,它关注的是在特定环境中,如何最大化一个智能体(agent)的累积奖励(reward)。强化学习算法会根据当前状态和环境的反馈来选择下一个动作,不断地进行试错,从而优化智能体的行为。 SARSA是一种基于强化学习的算法,它可以用于解决马尔可夫决策…...

SpringJDBC异常抽象

前言spring会将所有的常见数据库的操作异常抽象转换成他自己的异常&#xff0c;这些异常的基类是DataAccessException。DataAccessException是RuntimeException的子类&#xff08;运行时异常&#xff09;,是一个无须检测的异常,不要求代码去处理这类异常SQLErrorCodeSQLExcepti…...

我在字节的这两年

前言 作为脉脉和前端技术社区的活跃分子&#xff0c;我比较幸运的有了诸多面试机会并最终一路升级打怪如愿来到了这里。正式入职时间为2021年1月4日&#xff0c;也就是元旦后的第一个工作日。对于这一天&#xff0c;我印象深刻。踩着2020年的尾巴接到offer,属实是过了一个快乐…...