深入理解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(“ n o n c e nonce nonce_webSocketGUID”.codeUnits),nonce就是我们上面请求Headers中携带的Sec-WebSocket-key值,然后加上_webSocketGUID,_webSocketGUID就是前面一直提到的固定字符串。

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

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


1,长度比较
expectedAccept.length != receivedAccept.length

2,循环遍历每个字符
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的整个链接和切换流程,如果你觉得写的不错,欢迎点个关注,感谢🙏。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SpringBoot帮你优雅的关闭WEB应用程序

Graceful shutdown 应用 Graceful shutdown说明 Graceful shutdown is supported with all four embedded web servers (Jetty, Reactor Netty, Tomcat, and Undertow) and with both reactive and servlet-based web applications. It occurs as part of closing the applica…

spring(七):事务操作

spring&#xff08;七&#xff09;&#xff1a;事务操作前言一、什么是事务二、事务四个特性&#xff08;ACID&#xff09;三、事务操作&#xff08;搭建事务操作环境&#xff09;四、事务操作&#xff08;Spring 事务管理介绍&#xff09;五、事务操作&#xff08;注解声明式事…

python学习——【第一弹】

前言 Python是一种跨平台的计算机程序设计语言&#xff0c;是ABC语言的替代品&#xff0c;属于面向对象的动态类型语言&#xff0c;最初被设计用于编写自动化脚本&#xff0c;随着版本的不断更新和语言新功能的添加&#xff0c;越来越多被用于独立的、大型项目的开发。 从这篇…

断言assert

assert作用&#xff1a;我们使用assert这个宏来调试代码语法&#xff1a;assert&#xff08;bool表达式&#xff09;如果表达式为false&#xff0c;会调用std::cout<<abort函数&#xff0c;弹出对话框&#xff0c;#include<iostream> #include<cassert> void…

学习 Python 之 Pygame 开发魂斗罗(八)

学习 Python 之 Pygame 开发魂斗罗&#xff08;八&#xff09;继续编写魂斗罗1. 创建敌人类2. 增加敌人移动和显示函数3. 敌人开火4. 修改主函数5. 产生敌人6. 使敌人移动继续编写魂斗罗 在上次的博客学习 Python 之 Pygame 开发魂斗罗&#xff08;七&#xff09;中&#xff0…

uboot主目录下Makefile文件的分析,以及配置过程分析

主Makefile执行分析 uboot的编译过程 &#xff08;1&#xff09;配置 查看主Makefile文件下所支持的配置的板子&#xff0c;通过make x210_sd_config来实现编译前的配置 &#xff08;2&#xff09;编译 make直接编译&#xff0c;这个前提条件是主Makefile文件下指定了编译…

上手使用百度文心一言

3月16日&#xff0c;在距离新一代的GPT模型GPT-4发布还不足一天的时间内&#xff0c;百度便发布了对标ChatGPT的人工智能产品&#xff0c;名字叫&#xff1a;文心一言。成为国内首页发布该类型产品的公司。 那么&#xff0c;我们今天就来试一试百度的文心一言好不好用。 首先&a…

【ERNIE Bot】百度 | 文心一言初体验

文章目录一、前言二、文心一言介绍三、申请体验⌈文心一言⌋四、⌈文心一言⌋初体验1️⃣聊天对话能力2️⃣文案创作能力3️⃣文字转语音能力✨4️⃣AI绘画能力✨5️⃣数理推理能力6️⃣代码生成能力7️⃣使用技巧说明五、总结一、前言 ​ 最近有关人工智能的热门话题冲上热榜…

Java课程设计项目--音乐视频网站系统

一、功能介绍 随着社会的快速发展&#xff0c;计算机的影响是全面且深入的。人们生活水平的不断提高&#xff0c;日常生活中人们对音乐方面的要求也在不断提高&#xff0c;听歌的人数更是不断增加&#xff0c;使得音乐网站的设计的开发成为必需而且紧迫的事情。音乐网站的设计主…

「操作系统」什么是用户态和内核态?为什么要区分

「操作系统」什么是用户态和内核态&#xff1f;为什么要区分 参考&鸣谢 从根上理解用户态与内核态 程序员阿星 并发编程&#xff08;二十六&#xff09;内核态和用户态 Lovely小猫 操作系统之内核态与用户态 fimm 文章目录「操作系统」什么是用户态和内核态&#xff1f;为什…

嵌入式硬件电路设计的基本技巧

目录 1 分模块 2 标注关键参数 3 电阻/电容/电感/磁珠的注释 4 可维修性 5 BOM表归一化 6 电源和地的符号 7 测试点 8 网络标号 9 容错性/兼容性 10 NC、NF 11 版本变更 12 悬空引脚 13 可扩展性 14 防呆 15 信号的流向 16 PCB走线建议 17 不使用\表示取反 不…

考研408每周一题(2019 41)

2019年(单链表&#xff09; 41.(13分)设线性表L(a1,a2,a3,...,a(n-2),a(n-1),an)采用带头结点的单链表保存&#xff0c;链表中的结点定义如下&#xff1a; typedef struct node {int data;struct node *next; } NODE; 请设计一个空间复杂度为O(1)且时间上尽可能高效的算法&…

leetcode -- 876.链表的中间节点

文章目录&#x1f428;1.题目&#x1f407;2. 解法1-两次遍历&#x1f340;2.1 思路&#x1f340;2.2 代码实现&#x1f401;3. 解法2-快慢指针&#x1f33e;3.1 思路&#x1f33e;3.2 **代码实现**&#x1f42e;4. 题目链接&#x1f428;1.题目 给你单链表的头结点head&#…

RocketMQ

RocketMQ1、基础入门1、消息中间件(MQ)的定义2、为什么要用消息中间件&#xff1f;2、RocketMQ 产品发展1、RocketMQ 版本发展2、RocketMQ 的物理架构1、核心概念2、物理架构中的整体运转3、RocketMQ 的概念模型1、分组(Group)2、主题(Topic)3、标签(Tag)4、消息队列(Message Q…

开发也可以很快乐,让VSCode和CodeGPT带给你幸福感

CodeGPT 是一款 Visual Studio Code 扩展&#xff0c;可以通过官方的 OpenAI API 使用 GPT-3 (预训练生成式转换器) 模型&#xff0c;在多种编程语言中生成、解释、重构和文档化代码片段。CodeGPT 可用于各种任务&#xff0c;例如代码自动完成、生成和格式化。它还可以集成到代…

smartsofthelp最简单的,最好的,最干净的C# 代码生成器

关系型数据库高并发接口代码生成EF API 接口原声SQL 操作类异步委托 await 操作数据库数据异步访问抽象基础类 netcore 生成EF ORMdbhelperasync原生SQL 异步数据库操作公共类自动生成增删改查成员方法实例代码#region 自动生成增删改查成员方法/// <summary>/// 增加一条…

【6】核心易中期刊推荐——图像与信号处理

🚀🚀🚀NEW!!!核心易中期刊推荐栏目来啦 ~ 📚🍀 核心期刊在国内的应用范围非常广,核心期刊发表论文是国内很多作者晋升的硬性要求,并且在国内属于顶尖论文发表,具有很高的学术价值。在中文核心目录体系中,权威代表有CSSCI、CSCD和北大核心。其中,中文期刊的数…

ChatGPT-4.0 : 未来已来,你来不来

文章目录前言ChatGPT 3.5 介绍ChatGPT 4.0 介绍ChatGPT -4出逃计划&#xff01;我们应如何看待ChatGPT前言 好久没有更新过技术文章了&#xff0c;这个周末听说了一个非常火的技术ChatGPT 4.0&#xff0c;于是在闲暇之余我也进行了测试&#xff0c;今天这篇文章就给大家介绍一…

【Bezier + BSpline + CatmullRom】移动机器人曲线路径规划

问题&#xff1a;现有n1n1n1个2维的离散点Pi(xi,yi),(i0,1,⋯,n){P_i} \left( {{x_i},{y_i}} \right),\left( {i 0,1, \cdots ,n} \right)Pi​(xi​,yi​),(i0,1,⋯,n), 如何用Pi{P_i}Pi​拟合一条平滑的曲线&#xff0c;最后将曲线分割成数条 2阶/3阶贝塞尔曲线&#xff0c;…

HDFS的API操作

目录 客户端环境准备&#xff1a; 添加环境变量&#xff1a; 配置Path环境变量&#xff1a; IDEA操作&#xff1a; 创建包名&#xff1a; HDFS的API案例操作&#xff1a; 封装代码&#xff1a; 封装代码1&#xff1a; 封装代码2&#xff1a; 实现操作&#xff1a; 1.创…