酒店行业的网站建设,新闻10条摘抄大全,企业小程序要多少钱,洪涛怎么样海城市建设网站“ 一直以来对WebSocket仅停留在使用阶段#xff0c;也没有深入理解其背后的原理。当看到 x x x was not upgraded to websocket#xff0c;我是彻底蒙了#xff0c;等我镇定下来#xff0c;打开百度输入这行报错信息#xff0c;随即看到的就是大家说的跨域#xff0c;或…
“ 一直以来对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链接情况如下
WebSocket严格意义上和http没什么关系从上面的流程可以看出首先是发出了get请求然后返回101进行了一次协议切换如下图 切换的过程是这样的 从上面截图中可以清楚的看到请求的Request Headers携带有以下值
Connection: Upgrade
Upgrade: websocket
Cookie: useremh1aHVpdGFvX3N0cnVnZ2xlQDE2My5jb20%3D; passwordemh1aHVpdGFvMTIz; JSESSIONIDnode01l5dg6m63bgvvbu8il1jvkylo0.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是保证安全的一个keySec-WebSocket-Version就是版本的意思根据查看flutter WebSocket部分源码使用Postman以及结合浏览器发现这个应该是写死的都是13。
服务端返回的Response Headers Connection: upgrade
Date: Tue, 28 Feb 2023 16:07:47 GMT
Sec-WebSocket-Accept: 1VYpY/UEy4X03a8zMw05SjCtw
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/UEy4X0a8zMw05SjCtwSec-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, {
IterableString? protocols,
MapString, 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 {
MapString, String headers {};
///升级到WebSocket协议headers[Connection] Upgrade;headers[Upgrade] websocket;headers[Sec-WebSocket-Extensions]
permessage-deflate; client_max_window_bits;///验证headers[Cookie] JSESSIONIDnode0j43bo45tn9e81wkdqf9dau7bx1877.node0;print(headers);_channel
await IOWebSocketChannel.connect(ApiConfig.wss, headers: headers);_channel?.stream.listen((message) {print(message);print(json.decode(message));
MapString, 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 FutureWebSocket connect(String url, IterableString? protocols, MapString, 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) {
FutureWebSocket error(String message) {
// Flush data.response.detachSocket().then((socket) {socket.destroy();});
return FutureWebSocket.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);Listint expectedAccept sha1.close();Listint 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().thenWebSocket((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。
3Get请求并携带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.switchingProtocolsHttpStatus.switchingProtocols这个值是什么呢跟进去看发现就是常量101。 static const int switchingProtocols 101;看到这里是不是和我们前面分析的一模一样。还有重要的一点这里出现了Connection to ‘$uri’ was not upgraded to websocket。也就是我们最开始提到的异常其实这里太笼统。只要返回的状态码不为101或者接收的connectionHeader为null都直接抛出了这个头疼的not upgraded to websocket,所以不了解的时候就像我开头说的根本无从下手针对此问题在后面我也会分析会出现这个异常的原因。其实看到这里我们心里或许也知道是什么导致的了。
5Sec-WebSocket-Accept的值是怎么来呢答案就是Sec-WebSocket-key加上固定的字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11通过Sha1加密转Base64的来的。
_SHA1 sha1 _SHA1();sha1.add($nonce$_webSocketGUID.codeUnits);Listint expectedAccept sha1.close();Listint 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有问题这样我们找问题也就更直接了。
6Request Headers中的Cookie当然从上面的源码中也未发现这个东西。Cookie的概念其实在app中用的很少至少在我开发过的项目里基本是没用到的但是在浏览器中很常见。Cookie可以携带用户信息如下面代码所示。
headers[Cookie] JSESSIONIDnode0j43bo45tn9e81wkdqf9dau7bx1877.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上调试排查问题 2url有问题不是正常的url 3Headers问题最有可能的就是Cookie出现问题后我们应该立马和后台同事沟通问清楚他们到底需要什么值沟通清楚再实现。
至此我们分析了WebSocket的整个链接和切换流程如果你觉得写的不错欢迎点个关注感谢。