本篇文章给大家谈谈wtcr2021哪直播,以及wicreset怎么使用的知识点,希望对各位有所帮助,不要忘了收藏本站喔。
文:懂车帝原创 许博
[懂车帝原创 产品] 日前,懂车帝从WTCR房车世界杯官网获悉,2021赛季的首站比赛将于6月3日正式在德国纽伯格林赛道正式开赛。此后,该赛事还将在7个国家和地区进行比赛,整个赛程将持续到11月份,并将在中国澳门收官。据了解,在本赛季中,领克Cyan Racing车队依然采用了与2020赛季相同的车手阵容。
最初,WTCR 2021赛季计划在5月份举办首站比赛,但由于一些客观原因,不得不将比赛推迟至6月进行。而赛事的举办地点也与最早的规划有所改变,本赛季的揭幕战将在德国的纽伯格林赛道举办,而原定在斯洛伐克举办的比赛已决定取消。此外,欧洲地区的巡回赛还有葡萄牙站、西班牙站、意大利站以及匈牙利站。
宁波国际赛车场
本赛季目前还在亚洲地区设有三站比赛,分别是将在10月份举办的韩国站以及11月份将举办的中国站和中国澳门站。有消息称,11月6日至7日的中国站比赛,将在宁波国际赛车场举办,不过最终的比赛日期,要依临近赛期的实际情况而定。
领克Cyan Racing车队在本赛季依然将使用领克03 TCR车型参赛;而车手阵容方面,则与去年相同,依然派出由Yann Ehrlacher、Yvan Muller、ThedBjörk和Santiago Urrutia四人参赛。在2020赛季中,Yann Ehrlacher凭借着领克03 TCR车型的强大性能,夺得了年度冠军,而他的叔叔Yvan Muller获得了亚军,同时车队夺得了年度冠军车队的殊荣。
目前,领克Cyan Racing车队已经在西班牙的MotoLand完成了第一轮对赛车的测试。而根据上赛季各车手的表现,预计车队依然将Ehrlacher和Muller作为第一梯队车手,以取得更好的比赛成绩;Björk和Urrutia则作为第二梯队车手,冲击领奖台为主要目标。
2020年注定是不平凡的一年,尤其是对于想要留学和已经在留学路上的小伙伴们。特别是在经历了今年冬季学期申请的同学们是否觉得心力憔悴呢?
但是!请大家不要忘记,即使因为种种原因错过或者放弃了2020年冬季学期的申请,我们还可以申请明年4月开学的夏季学期。
给大家奉上的是德国TU9九所理工类大学2021年研究生夏季申请的截止时间,请大家手动保存~
1. 柏林工大2021年夏季硕士申请截止时间详见:https://www.tu.berlin/studieren/bewerben-und-einschreiben/fristen-termine/
2. 亚琛工大2021年夏季硕士申请截止时间详见:https://www.rwth-aachen.de/global/show_document.asp?id=aaaaaaaaabwicqz&download=1
3. 汉诺威大学不是所有的专业都是统一的申请时间!上图显示只是大部分专业的申请时间,具体各专业申请时间请看以下网址:
https://www.uni-hannover.de/fileadmin/Studienberatung/Neu_ab_Relaunch/02_Studium_Vor_dem_Studium/02_Bewerbung_und_Zulassung/02_Studienplatzbewerbung/03_MA-Bewerbung_D_EU/Master_Bewerbungsfriste-alleSem.pdf
4. 慕尼黑工大申请的各个专业都有详细的申请页面(截止时间),请对照专业列表。部分专业如:信息学/计算机,数据工程与分析,机器人学,电气工程与信息技术,管理与技术等专业夏季学期正常截止时间至每年11月30日,2021年因疫情影响单次延长至12月18日。另,慕尼黑工大全部需要VPD流程,需尽早提交材料准备。
5. 德累斯顿工大申请各专业截止时间在各专业详细页内,详细信息请对照专业列表。
6.卡尔斯鲁厄理工学院各专业2021夏季申请时间详情请见:https://www.sle.kit.edu/downloads/Sonstige/Tabelle_Studiengaenge.pdf
7. 达姆施达特工业大学硕士2021年夏季学期申请开始时间是从2020年12月1日开放,具体结束日期现在还未给出,请关注官网申请截止日期:
https://www.tu-darmstadt.de/studieren/studieninteressierte/bewerbung_zulassung_tu/bewerbungsfristen/index.de.jsp
8. 斯图加特大学不是所有硕士专业都有夏季学期,logistikmanagement专业的申请时间是01.12.20-15.03.21,各专业详见:https://www.uni-stuttgart.de/studium/bewerbung/master/zulassung/
9. 布伦瑞克工业大学硕士2021夏季学期个专业申请时间详见:
https://www.tu-braunschweig.de/studienangebot?
tx_kesearch_pi1%5Bsword%5D=&
tx_kesearch_pi1%5Bfilter%5D%5B5%5D=abschluss_master&
tx_kesearch_pi1%5Bfilter%5D%5B6%5D=&
tx_kesearch_pi1%5Bfilter%5D%5B7%5D=beginn_sommer&
tx_kesearch_pi1%5Bfilter%5D%5B35%5D=&
tx_kesearch_pi1%5Bfilter%5D%5B23%5D=&id=2973&tx_kesearch_pi1%5Bpage%5D=1&
tx_kesearch_pi1%5BresetFilters%5D=0&
tx_kesearch_pi1%5BsortByField%5D=&
tx_kesearch_pi1%5BsortByDir%5D=
最后我们还是要提醒大家,在疫情的影响下,德国各官方机构也在不停的更新要求。在信息快速更迭的时代我们只能尽量更新信息以保持信息的准确性,建议大家及时关注相关官网,才能得到最准确的信息。
▌前言:
HTML5 拥有众多引人注目的新特性,如 Canvas、本地存储、多媒体编程接口、WebSocket 等等。
其中,WebSocket 的出现使得浏览器提供对 Socket 的支持成为可能,从而在浏览器和服务器之间提供了一个基于 TCP 连接的双向通道。
使用 WebSocket,web开发人员可以很方便地构建实时 web 应用。
说明:
本文含三大Java高阶知识:
WebSocket协议原理,
Netty+Websocket 安全验证技巧
Nginx动态负载均衡
▌背景:
以前,很多网站使用轮询实现推送技术。轮询是在特定的的时间间隔(比如1秒),由浏览器对服务器发出HTTP request,然后由服务器返回最新的数据给浏览器。轮询的缺点很明显,浏览器需要不断的向服务器发出请求,然而HTTP请求的header是非常长的,而实际传输的数据可能很小,这就造成了带宽和服务器资源的浪费。
Comet使用了AJAX改进了轮询,可以实现双向通信。但是Comet依然需要发出请求,而且在Comet中,普遍采用了长链接,这也会大量消耗服务器带宽和资源。
于是,WebSocket协议应运而生。
注:本文以 PDF 持续更新,最新尼恩 架构笔记、面试题 的PDF文件,请到《技术自由圈》公众号获取
▌WebSocket协议:
浏览器通过 JavaScript 向服务器发出建立 WebSocket 连接的请求,连接建立以后,客户端和服务器通过 TCP 连接直接交换数据。WebSocket 连接本质上是一个 TCP 连接。
WebSocket在数据传输的稳定性和数据传输量的大小方面,具有很大的性能优势。Websocket.org 比较了轮询和WebSocket的性能优势:
从上图可以看出,WebSocket具有很大的性能优势,流量和负载增大的情况下,优势更加明显。
WebSocket 协议解决了浏览器和服务器之间的全双工通信问题。在WebSocket出现之前,浏览器如果需要从服务器及时获得更新,则需要不停的对服务器主动发起请求,也就是 Web 中常用的 poll 技术。这样的操作非常低效,这是因为每发起一次新的 HTTP 请求,就需要单独开启一个新的 TCP 链接,同时 HTTP 协议本身也是一种开销非常大的协议。为了解决这些问题,所以出现了 WebSocket 协议。WebSocket 使得浏览器和服务器之间能通过一个持久的 TCP 链接就能完成数据的双向通信。关于 WebSocket 的 RFC 提案,可以参看 RFC6455。
WebSocket 和 HTTP 协议一般情况下都工作在浏览器中,但 WebSocket 是一种完全不同于 HTTP 的协议。尽管,浏览器需要通过 HTTP 协议的 GET 请求,将 HTTP 协议升级为 WebSocket 协议。升级的过程被称为 握手(handshake)。当浏览器和服务器成功握手后,则可以开始根据 WebSocket 定义的通信帧格式开始通信了。像其他各种协议一样,WebSocket 协议的通信帧也分为控制数据帧和普通数据帧,前者用于控制 WebSocket 链接状态,后者用于承载数据。下面我们将一一分析 WebSocket 协议的握手过程以及通信帧格式。
▌Websocket协议握手报文:
握手的过程也就是将 HTTP 协议升级为 WebSocket 协议的过程。前面我们说过,握手开始首先由浏览器端发送一个 GET 请求开发,该请求的 HTTP 头部信息如下:
▌客户端请求
在客户端,new WebSocket实例化一个新的WebSocket客户端对象,
请求类似 ws://yourdomain:port/ws 的服务端WebSocket URL,
客户端WebSocket对象会自动解析并识别为WebSocket请求,并连接服务端端口,执行双方握手过程,客户端发送数据格式类似:
GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://localhost:8080
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13
可以看到,客户端发起的WebSocket连接报文类似传统HTTP报文,
可以看到,浏览器发送的 HTTP 请求中,增加了一些新的字段,其作用如下所示:
Upgrade: 规定必需的字段,其值必需为 websocket, 如果不是则握手失败;
Connection: 规定必需的字段,值必需为 Upgrade, 如果不是则握手失败;
Sec-WebSocket-Key: 必需字段,一个随机的字符串;
Sec-WebSocket-Version: 必需字段,代表了 WebSocket 协议版本,值必需是 13, 否则握手失败;
Upgrade:websocket参数值表明这是WebSocket类型请求,
Sec-WebSocket-Key是WebSocket客户端发送的一个 base64编码的密文,要求服务端必须返回一个对应加密的Sec-WebSocket-Accept应答,否则客户端会抛出Error during WebSocket handshake错误,并关闭连接。
▌服务器回应
当服务器端,成功验证了以上信息后,则会返回一个形如以下信息的响应:
服务端收到报文后返回的数据格式类似:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Origin: null
Sec-WebSocket-Location: ws://example.com/
返回的响应中,如果握手成功会返回状态码为 101 的 HTTP 响应。同时其他字段说明如下:
Upgrade: 规定必需的字段,其值必需为 websocket, 如果不是则握手失败;
Connection: 规定必需的字段,值必需为 Upgrade, 如果不是则握手失败;
Sec-WebSocket-Accept: 规定必需的字段,该字段的值是通过固定字符串258EAFA5-E914-47DA-95CA-C5AB0DC85B11加上请求中Sec-WebSocket-Key字段的值,然后再对其结果通过 SHA1 哈希算法求出的结果。
HTTP/1.1 101 Switching Protocols表示服务端接受WebSocket协议的客户端连接,
Sec-WebSocket-Accept的值是服务端采用与客户端一致的密钥计算出来后返回客户端的,
客户端过来的 Sec-WebSocket-Key是随机的,服务器端会用这些数据来构造出一个SHA-1的信息摘要。把Sec-WebSocket-Key加上一个魔幻字符串,使用 SHA-1 加密,之后进行 BASE-64编码,将结果作为 Sec-WebSocket-Accept 头的值,返回给客户端。
经过这样的请求-响应处理后,两端的WebSocket连接握手成功, 后续就可以进行TCP通讯了。
在开发方面,WebSocket API 也十分简单:只需要实例化 WebSocket,创建连接,然后服务端和客户端就可以相互发送和响应消息。在WebSocket 实现及案例分析部分可以看到详细的 WebSocket API 及代码实现。
▌WebSocket 协议数据帧:
当浏览器和服务器端成功握手后,就可以传送数据了,传送数据是按照 WebSocket 协议的数据格式生成的。
数据帧的定义类似于 TCP/IP 协议的格式定义,具体看下图:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
以上这张图,一行代表 32 bit (位) ,也就是 4 bytes。总体上包含两份,帧头部和数据内容。每个从 WebSocket 链接中接收到的数据帧,都要按照以上格式进行解析,这样才能知道该数据帧是用于控制的还是用于传送数据的。
▌WebSocket与HTTP的关系:
相比HTTP长连接,WebSocket有以下特点:
1)是真正的全双工方式,建立连接后客户端与服务器端是完全平等的,可以互相主动请求。而HTTP长连接基于HTTP,是传统的客户端对服务器发起请求的模式。
2)HTTP长连接中,每次数据交换除了真正的数据部分外,服务器和客户端还要大量交换HTTP header,信息交换效率很低。Websocket协议通过第一个request建立了TCP连接之后,之后交换的数据都不需要发送 HTTP header就能交换数据,这显然和原有的HTTP协议有区别所以它需要对服务器和客户端都进行升级才能实现(主流浏览器都已支持HTML5)。此外还有 multiplexing、不同的URL可以复用同一个WebSocket连接等功能。这些都是HTTP长连接不能做到的。
▌WebSocket与Http相同点
都是一样基于TCP的,都是可靠性传输协议。
都是应用层协议。
▌WebSocket与Http不同点
WebSocket是双向通信协议,模拟Socket协议,可以双向发送或接受信息。HTTP是单向的。
WebSocket是需要浏览器和服务器握手进行建立连接的。而http是浏览器发起向服务器的连接,服务器预先并不知道这个连接。
传统HTTP客户端与服务器请求响应模式如下图所示:
WebSocket模式客户端与服务器请求响应模式如下图:
上图对比可以看出,相对于传统HTTP每次请求-应答都需要客户端与服务端建立连接的模式,WebSocket是类似Socket的TCP长连接通讯模式。一旦WebSocket连接建立后,后续数据都以帧序列的形式传输。在客户端断开WebSocket连接或Server端中断连接前,不需要客户端和服务端重新发起连接请求。
▌WebSocket与Http联系
传统的http通讯模式是:客户端发起请求,服务端接收请求并作出响应。
WebSocket在建立握手时,数据是通过HTTP传输的。
第一步,建立连接,客户端使用http报文的格式发起协议升级的请求,服务端响应协议升级。
但是建立之后,在真正传输时候是不需要HTTP协议的。而websocket协议复用了http的握手通道,具体指的是,客户端通过HTTP请求与WebSocket服务端协商升级协议。
第二步,交换数据,客户端与服务端可以使用websocket协议进行双向通讯。
在WebSocket中,只需要服务器和浏览器通过HTTP协议进行一个握手的动作,然后单独建立一条TCP的通信通道进行数据的传送。
WebSocket连接的过程是:
1)客户端发起http请求,经过3次握手后,建立起TCP连接;
http请求里存放WebSocket支持的版本号等信息,如:Upgrade、Connection、WebSocket-Version等;
2)服务器收到客户端的握手请求后,同样采用HTTP协议回馈数据;
3)客户端收到连接成功的消息后,开始借助于TCP传输信道进行全双工通信。
▌浏览器兼容性:
最新的主流浏览器对WebSocket支持良好:
Chrome 4+
Firefox 4+
Internet Explorer 10+
Opera 10+
Safari 5+
▌客户端案例:
▌JavaScript客户端
WebSocket协议本质上是一个基于TCP的协议,为了建立一个WebSocket连接,浏览器需要向服务器发起一个HTTP请求,这个请求和普通的HTTP请求不同,它包含了一些附加头信息,服务器解析这些附加头信息后产生应答信息返回给客户端,客户端和服务端的WebSocket连接就建立起来了,双方可以通过连接通道自由的传递信息,并且这个连接会持续存在直到客户端或服务端某一方主动关闭连接。
function webSocket(){
if("WebSocket" in window){
console.log("您的浏览器支持WebSocket");
var ws = new WebSocket("ws://localhost:8080"); //创建WebSocket连接
//...
}else{
console.log("您的浏览器不支持WebSocket");
}
}
客户端支持WebSocket的浏览器中,在创建socket后,可以通过onopen、onmessage、onclose和onerror四个事件对socket进行响应。
浏览器通过Javascript向服务器发出建立WebSocket连接的请求,连接建立后,客户端和服务器就可以通过TCP连接直接交换数据。当你获取WebSocket连接后,可以通多send()方法向服务器发送数据,可以通过onmessage事件接收服务器返回的数据。
var ws = new WebSocket("ws://localhost:8080");
//申请一个WebSocket对象,参数是服务端地址,同http协议使用http://开头一样,WebSocket协议的url使用ws://开头,另外安全的WebSocket协议使用wss://开头
ws.onopen = function(){
//当WebSocket创建成功时,触发onopen事件
console.log("open");
ws.send("hello"); //将消息发送到服务端
}
ws.onmessage = function(e){
//当客户端收到服务端发来的消息时,触发onmessage事件,参数e.data包含server传递过来的数据
console.log(e.data);
}
ws.onclose = function(e){
//当客户端收到服务端发送的关闭连接请求时,触发onclose事件
console.log("close");
}
ws.onerror = function(e){
//如果出现连接、处理、接收、发送数据失败的时候触发onerror事件
console.log(error);
}
WebSocket的所有操作都是采用事件的方式触发的,这样不会阻塞UI,是的UI有更快的响应时间,有更好的用户体验。
WebSocke的方法
WebSocke的属性
▌Socket.IO客户端
Socket.IO是一个封装了WebSocket的JavaScript模块。
因为完全使用JavaScript编写,所以在每个浏览器和移动设备中都可以方便地通过Socket.IO使用WebSocket。
服务器端
var io = require('socket.io').listen(80);
io.sockets.on('connection', function (socket) {
socket.emit('news', { hello: 'world' });
socket.on('my other event', function (data) {
console.log(data);
});
});
客户端
var socket = io.connect('http://localhost');
socket.on('news', function (data) {
console.log(data);
socket.emit('my other event', { my: 'data' });
});
▌netty客户端模块
package com.crazymaker.springcloud.websocket.client;
import com.crazymaker.springcloud.common.constants.SessionConstants;
import com.crazymaker.springcloud.common.util.JsonUtil;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.DefaultHttpHeaders;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import java.net.URI;
import java.util.HashMap;
import java.util.Map;
/**
* 基于websocket的netty客户端
*/
public class WebSocketMockClient {
private static String account = "1860000000";
// static String uriString = "ws://127.0.0.1:9999/push";
static String uriString = "ws://cdh2:9999/push";
static String token = "eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiIxIiwic2lkIjoiNGFiMzVkNDMtZWNhZC00ZDhkLTkwN2MtZjA4NTIxYjU2ODVkIiwiZXhwIjoxNjQ5MzI2NDA4LCJpYXQiOjE2NDkyOTQwMDh9.cN6QTW__p3-RznkU4TqUo1sFIz2Ww_piWFTOvFJ7QoGqcq93ynNsE7RTMgGGYpX3Dpe6W_3vaWmJsHdzt8hme3kxwfKPnZfUF3hUwYCCU4WvXpQjwCFH1W_FSMZjZT2tvyPAmP75_4NDbTJ6sAw1hPVoEKIiGVkO0Aml_CixgqTY0UIyY0nCcz8T1yGkR5wPMhIyxQKPSjWU0UfyPovzIfwSKePfxnqgF42-_BA_YnrVL2qS9pNtTrtm-Bd2LNp5XLbOg-1mWCrHBl7DrYsBj9Q5hMSgy2cJxteyOz2gmfj4HiGeE_KCQO5ZcIChBkOJ9JV5HrzQ8xjGGoPtIReRiA";
public static void main(String[] args) throws Exception {
//netty基本操作,线程组
EventLoopGroup group = new NioEventLoopGroup();
//netty基本操作,启动类
Bootstrap boot = new Bootstrap();
boot.option(ChannelOption.SO_KEEPALIVE, true)
.option(ChannelOption.TCP_NODELAY, true)
.group(group)
.handler(new LoggingHandler(LogLevel.INFO))
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer
▌handler
package com.crazymaker.springcloud.websocket.client;
import io.netty.channel.*;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.websocketx.*;
import io.netty.util.CharsetUtil;
public class WebSocketClientHandler extends SimpleChannelInboundHandler
▌Netty中Websocket握手安全验证:
在使用Netty开发Websocket服务时,通常需要解析来自客户端请求的URL、Headers等等相关内容,并做相关检查或处理。
这里将讨论两种实现方法。
▌方法一:基于HandshakeComplete事件进行安全验证
特点:使用简单、校验在握手成功之后、失败信息可以通过Websocket发送回客户端。
下面的代码展示了如何监听自定义事件。
通过抛出异常可以终止链接,同时可以利用ctx向客户端以Websocket协议返回错误信息。
private final class ServerHandler extends SimpleChannelInboundHandler
验证案例
* WebSocket 帧:WebSocket 以帧的方式传输数据,每一帧代表消息的一部分。一个完整的消息可能会包含许多帧
*/
@Slf4j
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandlerpackage com.crazymaker.springcloud.websocket.netty;
import com.crazymaker.springcloud.common.dto.UserDTO;
import com.crazymaker.springcloud.websocket.netty.event.SecurityCheckCompleteEvent;
import com.crazymaker.springcloud.websocket.processer.RpcProcesser;
import com.crazymaker.springcloud.websocket.session.ServerSession;
import com.crazymaker.springcloud.websocket.session.SessionMap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import lombok.extern.slf4j.Slf4j;
/**
* Created by 尼恩 @ 疯狂创客圈
*
▌
WebSocketServerProtocolHandshakeHandler源码分析
一般地,我们将netty内置的
WebSocketServerProtocolHandler作为Websocket协议的主要处理器。
通过研究其代码我们了解到在本处理器被添加到Pipline后handlerAdded方法将会被调用。
此方法经过简单的检查后将WebSocketHandshakeHandler添加到了本处理器之前,用于处理握手相关业务。
我们都知道Websocket协议在握手时是通过HTTP(S)协议进行的,那么这个WebSocketHandshakeHandler应该就是处理HTTP相关的数据的吧?
package io.netty.handler.codec.http.websocketx;
public class WebSocketServerProtocolHandler extends WebSocketProtocolHandler {
@Override
public void handlerAdded(ChannelHandlerContext ctx) {
ChannelPipeline cp = ctx.pipeline();
if (cp.get(WebSocketServerProtocolHandshakeHandler.class) == null) {
// Add the WebSocketHandshakeHandler before this one.
cp.addBefore(ctx.name(), WebSocketServerProtocolHandshakeHandler.class.getName(),
new WebSocketServerProtocolHandshakeHandler(serverConfig));
}
//...
}
}
我们来看看
WebSocketServerProtocolHandshakeHandler都做了什么操作。
channelRead方法会尝试接收一个FullHttpRequest对象,表示来自客户端的HTTP请求,随后服务器将会进行握手相关操作,此处省略了握手大部分代码,感兴趣的同学可以自行阅读。
可以注意到,在确认握手成功后,channelRead将会调用两次fireUserEventTriggered,此方法将会触发自定义事件。
其他(在此处理器之后)的处理器会触发userEventTriggered方法。
其中一个方法传入了
WebSocketServerProtocolHandler对象,此对象保存了HTTP请求相关信息。
那么解决方案逐渐浮出水面,通过监听自定义事件即可实现检查握手的HTTP请求。
package io.netty.handler.codec.http.websocketx;
/**
* Handles the HTTP handshake (the HTTP Upgrade request) for {@link WebSocketServerProtocolHandler}.
*/
class WebSocketServerProtocolHandshakeHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(final ChannelHandlerContext ctx, Object msg) throws Exception {
final FullHttpRequest req = (FullHttpRequest) msg;
if (isNotWebSocketPath(req)) {
ctx.fireChannelRead(msg);
return;
}
try {
//...
if (!future.isSuccess()) {
} else {
localHandshakePromise.trySuccess();
// Kept for compatibility
ctx.fireUserEventTriggered(
WebSocketServerProtocolHandler.ServerHandshakeStateEvent.HANDSHAKE_COMPLETE);
ctx.fireUserEventTriggered(
new WebSocketServerProtocolHandler.HandshakeComplete(
req.uri(), req.headers(), handshaker.selectedSubprotocol()));
}
} finally {
req.release();
}
}
}
说明: 以上源码比较复杂,具体的解读,请参见19章视频
▌方法一的流水线装配
附上Channel初始化代码作为参考。
private final class ServerInitializer extends ChannelInitializer
▌方法一的问题:
方法一中,ws握手已经完成,所以虽然这种方案简单的过分,但是效率并不高,耗费服务端资源(都握手了又给人家踢了)。
▌方法二:在ws握手之前,进行安全检查处理器
特点:使用相对复杂、校验在握手成功之前、失败信息可以通过HTTP返回客户端。
▌解决方案
编写一个入站处理器,接收FullHttpMessage消息,在Websocket处理器之前检测拦截请求信息。
下面的例子主要做了四件事情:
从HTTP请求中提取关心的数据
安全检查
将结果和其他数据绑定在Channel
触发安全检查完毕自定义事件
package com.crazymaker.springcloud.websocket.netty.handler;
import com.crazymaker.springcloud.base.auth.AuthUtils;
import com.crazymaker.springcloud.base.auth.Payload;
import com.crazymaker.springcloud.common.constants.SessionConstants;
import com.crazymaker.springcloud.websocket.netty.event.SecurityCheckCompleteEvent;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.handler.codec.http.FullHttpMessage;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.util.AttributeKey;
import lombok.extern.slf4j.Slf4j;
import java.util.Objects;
import static com.crazymaker.springcloud.netty.util.HttpUtil.closeUnauthChannelAfterWrite;
@Slf4j
public class AuthCheckHandler extends ChannelInboundHandlerAdapter {
public static final AttributeKey SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY =
AttributeKey.valueOf("SECURITY_CHECK_COMPLETE_ATTRIBUTE_KEY");
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
if (msg instanceof FullHttpMessage) {
//extracts token information from headers
HttpHeaders headers = ((FullHttpMessage) msg).headers();
String token = Objects.requireNonNull(headers.get(SessionConstants.AUTHORIZATION_HEAD));
//extracts account information from headers
String account = Objects.requireNonNull(headers.get(SessionConstants.APP_ACCOUNT));
if (null == token || null == account) {
// 参数校验、设置响应
String content = "请登陆之后,再发起websocket连接!!!";
closeUnauthChannelAfterWrite(ctx, content);
return;
}
Payload
在业务逻辑处理器中,可以通过组合自定义的安全检查事件和Websocket握手完成事件。
▌方法二流水线装配
附上Channel初始化代码作为参考。
package com.crazymaker.springcloud.websocket.netty;
import com.crazymaker.springcloud.standard.context.SpringContextUtil;
import com.crazymaker.springcloud.websocket.netty.handler.AuthCheckHandler;
import com.crazymaker.springcloud.websocket.netty.handler.ServerExceptionHandler;
import com.crazymaker.springcloud.websocket.netty.handler.TextWebSocketFrameHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketServerCompressionHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;
/**
* Netty 服务
* Created by 尼恩 @ 疯狂创客圈
*/
@Component
@Slf4j
public class WebSocketServer implements ApplicationContextAware {
@Value("${tunnel.websocket.port}")
private int websocketPort;
@Value("${websocket.register.gateway}")
private String websocketRegisterGateway;
private final EventLoopGroup group = new NioEventLoopGroup();
private Channel channel;
/**
* 停止即时通讯服务器
*/
public void stopServer() {
if (channel != null) {
channel.close();
}
group.shutdownGracefully();
}
/**
* 启动即时通讯服务器
*/
public void startServer(int port) {
ChannelFuture channelFuture = null;
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(group)
.channel(NioServerSocketChannel.class)
.childHandler(new ChatServerInitializer());
InetSocketAddress address = new InetSocketAddress(port);
channelFuture = bootstrap.bind(address);
// channelFuture.syncUninterruptibly();
channel = channelFuture.channel();
// 返回与当前Java应用程序关联的运行时对象
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
stopServer();
}
});
log.info("\n----------------------------------------------------------\n\t" +
"Nett WebSocket 服务 is running! Access Port:{}\n\t", websocketPort);
channelFuture.channel().closeFuture().syncUninterruptibly();
}
/**
* 内部类
*/
class ChatServerInitializer extends ChannelInitializer
▌方法一与方法二的对比
上述两种方式分别在握手完成后和握手之前拦截检查;实现复杂度和性能略有不同,可以通过具体业务需求选择合适的方法。
Netty增强了责任链模式,使用userEvent传递自定义事件使得各个处理器之间减少耦合,更专注于业务。
但是、相比于流动于各个处理器之间的"主线"数据来说,userEvent传递的"支线"数据往往不受关注。
通过阅读Netty内置的各种处理器源码,探索其产生的事件,同时在开发过程中加以善用,可以减少冗余代码。
另外在开发自定义的业务逻辑时,应该积极利用userEvent传递事件数据,降低各模块之间代码耦合。
▌Netty的WebSocket开发常见问题:
▌1、proxy_http_version 1.1,为什么使用http1.1协议?
proxy_http_version 设置代理使用的HTTP协议版本。
proxy_http_version 默认使用的版本是1.0,而1.0版本默认是短链接,如果换成长链接,需要和 keepalive连接时一起使用。
http1.0没有加keepalive选型,后端服务会返回101错误,然后断开连接。
所以,默认情况下,1.0版本,显然不合适ws协议
proxy_http_version 1.1版本默认为长链接,推荐在使用
传统HTTP客户端与服务器请求响应模式如下图所示:
WebSocket模式客户端与服务器请求响应模式如下图:
上图对比可以看出,相对于传统HTTP每次请求-应答都需要客户端与服务端建立连接的模式,WebSocket是类似Socket的TCP长连接通讯模式。一旦WebSocket连接建立后,后续数据都以帧序列的形式传输。在客户端断开WebSocket连接或Server端中断连接前,不需要客户端和服务端重新发起连接请求。
▌2、为什么HTTP Upgrade的时候,需要Connection: upgrade
HTTP的Upgrade协议头
HTTP的Upgrade协议头机制用于将连接从HTTP连接升级到WebSocket连接,
但是,Upgrade机制使用了Upgrade协议头和Connection协议头;
结论是:
为了让Nginx可以将来自客户端的Upgrade请求发送到后端服务器,Upgrade和Connection的头信息必须被显式的设置。
也就是说:
WebSocket等协议的Upgrade请求,需要同时带上Connection和Upgrade头部。
如果是仅仅Upgrade的话,Connection头部不就是多余的设计了么?
具体原因,这里慢慢道来.
▌一个典型的WebSocket升级请求如下:
GET /chat HTTP/1.1
Host: example.com:8000
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
▌Connection的起源
最开始,在HTTP/1.0出现没多久,人们就意识到HTTP持久连接的重要性(毕竟三次握手还是很慢的)
所以各个服务器实现都采用了Keep-Alive头部来表示这个请求支持连接持久化。
▌HTTP/1.1中的Connection
在HTTP/1.1中,正式标准化了Connection头部:
Connection头部一般表示后面的头部属于逐跳头部(hop-by-hop header)类型,比如Connection: Custom-Header,
就表示在这个连接中,Custom-Header是一个逐跳头部,不应当被代理原样传递给upstream。
有两个例外:close表示会话不持久化,keep-alive表示会话支持持久化(虽然有一个Keep-Alive头部,但是大小写不一样)。
什么是: 逐跳头部(hop-by-hop header)?
用来描述当前浏览器与直连服务器(比如Nginx反向代理)的连接信息。
比如Keep-Alive头部,仅仅表示浏览器尝试和Nginx之间连接持久化,而不管Nginx和后端服务器之间的连接。
proxy要处理这些头部,并按照自己的需要来修改这些头部。
默认的逐跳头部(hop-by-hop header)如下:
Connection
Keep-Alive
Proxy-Authenticate
Proxy-Authorization
TE
Trailers
Transfer-Encoding
Upgrade
出了上面的hop-by-hop header,还有一大类型的头部,叫做端到端头部(end-to-end header)
端到端头部(end-to-end header) 用来描述这个浏览器和最终处理请求的服务器之间的信息,比如Accept头部,表示客户端想从后端服务器得到的数据类型,而和中间的Nginx无关。
proxy不能修改这些端到端头部(end-to-end header),但是,可以处理 逐跳头部(hop-by-hop header)
再回到HTTP 1.1的Connection头部,这儿有一个兼容性问题:
我们以Upgrade头部为例,某个proxy实现了HTTP 1.0协议,将Upgrade原样转发给后端,后端和proxy升级协议,
但是,这个情况下,proxy不认识升级后的协议啊。
所以,RFC有增加了一条规定:
如果只有Upgrade: xxx,而没有Connection: Upgrade,那么就当作普通请求来处理。
▌WebSocket的Upgrade请求:
Connection: Upgrade
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Key: lGrvj+i7B76RB3YYbScQ9g==
Sec-WebSocket-Version: 13
Upgrade: websocket
▌结论
Connection头部和Upgrade头部有不同的语义和使用场景:
Connection: Upgrade 表示Upgrade是一个hop-by-hop的字段。这个头部是给proxy看的
Upgrade: websocket 表示浏览器想要升级到WebSocket协议。这个头部是给最终处理请求的程序看的。
如果只有Upgrade: websocket,说明proxy不支持WebSocket升级,按照标准应该视为普通HTTP请求。
▌3、map的作用
报错内容:nginx: [emerg] unknown "connection_upgrade" variable
一天更新完主分支后启动nginx,结果报错:nginx: [emerg] unknown "connection_upgrade" variable
解决办法:在nginx.conf配置文件http区块顶部加上一段配置
map $http_upgrade $connection_upgrade{
default upgrade;
'' close;
}
server {
listen 80;
------
▌nginx反向代理websocket
首先,客户端发起协议升级的请求,而nginx在拦截时需要识别出这是一个协议升级(upgrade)的请求,所以必须显式设置升级(Upgrade head)和连接头(Connection head),如下:
location /ws/ {
proxy_pass http://127.0.0.1:4200/ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
完成后,nginx将其作为WebSocket连接处理。
根据配置,需要根据变量 $http_upgrade 的值创建新的变量 $connection_upgrade。
▌map指令的作用:
根据客户端请求中$http_upgrade 的值,来构造改变$ connection_upgrade的值
即根据变量$http_upgrade的值创建新的变量$connection_upgrade,
创建$connection_upgrade的规则就是{}里面的东西:
其中的规则没有做匹配,因此使用默认的.
如果 变量$http_upgrade的值为upgrade, 即 $connection_upgrade 的值会一直是 upgrade。
如果 $http_upgrade为空字符串的话, 那 $connection_upgrade 值会是 close。
有点复杂,具体的介绍,请参考视频第19章
▌4、Nginx代理webSocket经常中断的解决方法(也就是如何保持长连接)
现象描述:
用nginx反代代理某个业务,发现平均1分钟左右,就会出现webSocket连接中断,然后查看了一下,是nginx出现的问题。
产生原因:
nginx等待第一次通讯和第二次通讯的时间差,超过了它设定的最大等待时间,简单来说就是超时!
▌解决方法1
其实只要配置nginx.conf的对应localhost里面的这几个参数就好
proxy_connect_timeout;
proxy_read_timeout;
proxy_send_timeout;
proxy_connect_timeout
语法 proxy_connect_timeout time
默认值 60s
上下文 http server location
说明 该指令设置与upstream server的连接超时时间,有必要记住,这个超时不能超过75秒。
这个不是等待后端返回页面的时间,那是由proxy_read_timeout声明的。
如果你的upstream服务器起来了,并且在系统层面完成了三次或者握手,只是没有传输数据(例如,Java应用STW卡顿,没有足够的线程处理请求,所以把你的请求放到请求池里稍后处理),那么这个声明是没有用的,由于与upstream服务器的连接已经建立了。
proxy_read_timeout
语法 proxy_read_timeout time
默认值 60s
上下文 http server location
说明 该指令设置与代理服务器的读超时时间。它决定了nginx会等待多长时间来获得响应的数据,业务数据。
这个时间不仅仅是单次response到达的时间,还包括两次业务数据之间的间隔时间。
proxy_send_timeout
语法 proxy_send_timeout time
默认值 60s
上下文 http server location
说明 这个指定设置了发送请求给upstream服务器的超时时间。
超时设置不是为了整个发送期间,而是在两次write操作期间。如果超时后,没有数据发送出去,或者说,upstream没有收到新的数据,nginx会关闭连接
▌配置示例:
http {
server {
location / {
root html;
index index.html index.htm;
proxy_pass http://webscoket;
proxy_http_version 1.1;
proxy_connect_timeout 4s; #配置点1
proxy_read_timeout 60s; #配置点2,如果没效,可以考虑这个时间配置长一点
proxy_send_timeout 12s; #配置点3
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
}
}
关于上面配置2的解释
这个是服务器对你等待最大的时间,也就是说当你webSocket使用nginx转发的时候,
用上面的配置2来说,如果60秒内没有通讯,依然是会断开的,所以,你可以按照你的需求来设定。
比如说,我设置了10分钟,那么如果我10分钟内有通讯,或者10分钟内有做心跳的话,是可以保持连接不中断的,详细看需求
▌解决方法2
发心跳包,原理就是在有效地再读时间内进行通讯,重新刷新再读时间
参考网上的前端代码:
href = "ws://"+baseIP+"/user/connect/"
ws = new WebSocket(href)
var heartCheck = {
timeout: 5000, //5秒发一次心跳
timeoutObj: null,
serverTimeoutObj: null,
reset: function(){
clearTimeout(this.timeoutObj);
clearTimeout(this.serverTimeoutObj);
return this;
},
start: function(){
var self = this;
this.timeoutObj = setTimeout(function(){
ws.send("keepalive");
console.log("发送:keepalive")
self.serverTimeoutObj = setTimeout(function(){
ws.close();
}, self.timeout)
}, this.timeout)
}
}
ws.onopen = function(){
console.log("websocket已连接")
heartCheck.reset().start()
ws.send(user_id)
}
ws.onmessage = function(evt){
heartCheck.reset().start();
if (evt.data != "keepalive"){
msg = JSON.parse(evt.data)
that.messageNotice(msg)
}else{
console.log("接收:"+evt.data)
}
}
ws.onclose = function(e){
console.log("websocket已断开")
console.log(e)
}
▌Nginx的负载均衡:
本节就聊聊采用Nginx负载均衡之后碰到的问题:
Session问题
文件上传下载
通常解决服务器负载问题,都会通过多服务器分载来解决。常见的解决方案有:
网站入口通过分站链接负载(天空软件站,华军软件园等)
DNS轮询
F5物理设备
Nginx等轻量级架构
那我们看看Nginx是如何实现负载均衡的,Nginx的upstream目前支持以下几种方式的分配
1、轮询(默认)
每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。
2、weight
指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。
2、ip_hash
每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。
3、fair(第三方)
按后端服务器的响应时间来分配请求,响应时间短的优先分配。
4、url_hash(第三方)
按访问url的hash结果来分配请求,使每个url定向到同一个后端服务器,后端服务器为缓存时比较有效。
Upstream配置如何实现负载
http {
upstream www.test1.com {
ip_hash;
server 172.16.125.76:8066 weight=10;
server 172.16.125.76:8077 down;
server 172.16.0.18:8066 max_fails=3 fail_timeout=30s;
server 172.16.0.18:8077 backup;
}
upstream www.test2.com {
server 172.16.0.21:8066;
server 192.168.76.98:8066;
}
server {
listen 80;
server_name www.test1.com;
location /{
proxy_pass http://www.test1.com;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
server {
listen 80;
server_name www.test2.com;
location /{
proxy_pass http://www.test2.com;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
当有请求到
www.test1.com/www.test2.com 时请求会被分发到对应的upstream设置的服务器列表上。
test2的每一次请求分发的服务器都是随机的,就是第一种情况列举的。
而test1刚是根据来访问ip的hashid来分发到指定的服务器,也就是说该IP的请求都是转到这个指定的服务器上。
根据服务器的本身的性能差别及职能,可以设置不同的参数控制。
down 表示负载过重或者不参与负载
weight 权重过大代表承担的负载就越大
backup 其它服务器时或down时才会请求backup服务器
max_fails 失败超过指定次数会暂停或请求转往其它服务器
fail_timeout 失败超过指定次数后暂停时间
以上就Nginx的负载均衡的简单配置。那继续我们的本节讨论内容:
▌Session问题
当我们确定一系列负载的服务器后,那我们的WEB站点会分布到这些服务器上。
这个时候如果采用Test2 每一次请求随机访问任何一台服务器上,这样导致你访问A服务器后,下一次请求又突然转到B服务器上。这个时候与A服务器建立的Session,传到B站点服务器肯定是无法正常响应的。我们看一下常用的解决方案:
Session或凭据缓存到独立的服务器
Session或凭据保存数据库中
nginx ip_hash 保持同一IP的请求都是指定到固定的一台服务器
第一种缓存的方式比较理想,缓存的效率也比较高。但是每一台请求服务器都去访问Session会话服务器,那不是加载重了这台Session服务器的负担吗?
第二种保存到数据库中,除了要控制Session的有效期,同时加重了数据库的负担,所以最终的转变为SQL Server 负载均衡,涉及读,写,过期,同步。
第三种通过nginx ip_hash负载保持对同一服务器的会话,这种看起来最方便,最轻量。
正常情况下架构简单的话, ip_hash可以解决Session问题,但是我们来看看下面这种情况
这个时候ip_hash 收到的请求都是来自固定IP代理的请求,如果代理IP的负载过高就会导致ip_hash对应的服务器负载压力过大,这样ip_hash就失去了负载均衡的作用了。
如果缓存可以实现同步共享的话,我们可以通过多session服务器来解决单一负载过重的问题。
那Memcached是否可以做Session缓存服务器呢?MemcachedProvider提供了Session的功能,即将Session保存到数据库中。
那为什么不直接保存到数据库中,而要通过Memcached保存到数据库中呢?
很简单,如果直接保存到数据库中,每一次请求Session有效性都要回数据库验证一下。
其次,即使我们为数据库建立一层缓存,那这个缓存也无法实现分布式共享,还是针对同一台缓存服务器负载过重。
网上也看到有用Memcached实现Session缓存的成功案例,当然数据库方式实现的还是比较常用的,比如开源Disuz.net论坛。
缓存实现的小范围分布式也是比较常用的,比如单点登录也是一种特殊情况。
▌Nginx进行WebSocket代理:
然后修改 Hosts, 添加, 比如 ws.repo, 指向 127.0.0.1
然后是 Nginx 配置:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name ws.repo;
location / {
proxy_pass http://127.0.0.1:3000/;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
Reload Nginx 然后从浏览器控制台尝试链接, OK
new WebSocket('ws://ws.repo/')
或者通过 Upstream 的写法:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
upstream ws_server {
server 127.0.0.1:3000;
}
server {
listen 80;
server_name ws.repo;
location / {
proxy_pass http://ws_server/;
proxy_redirect off;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
WebSocket 先是通过 HTTP 建立连接,
然后通过 101 状态码, 表示切换协议,, 在配置里是 Upgrade
▌动态负载均衡:
▌具体思路
利用lua中 "lua_shared_dict" 指令开辟一个共享内存空间;
通过API动态根据key值&参数修改 upstream (这里使用 host 作为key);
利用 proxy_pass 可使用变量特性及lua指令 "set_by_lua" 动态修改当前 upstream 变量即可;
▌结合 lua 实现一个 http协议负载均衡
含三个文件
updateserver.lua
takeoneserver.lua
nginx.conf
主要是利用ngx.upstream、ngx.balancer 这两个模块,动态设置upstream,
▌lua/updateserver.lua
local balancer = require "ngx.balancer"
local upstream = require "ngx.upstream"
-- 加载cjson
local cjson = require("cjson");
local cache = ngx.shared.cache
--读取get参数
--local uri_args = ngx.req.get_uri_args()
--读取post参数
ngx.req.read_body();
--local uri_args = ngx.req.get_post_args()
local data = ngx.req.get_body_data(); --获取消息体
--ngx.say(data)
local restOut = { respCode = 0, resp_msg = "操作成功", datas = {} };
local errorOut = { respCode = -1, resp_msg = "操作失败", datas = {} };
ngx.log(ngx.DEBUG,"data=" .. data);
local args=cjson.decode(data);
ngx.log(ngx.DEBUG,"args=" .. tostring(args));
local serverCount =args["serverCount"];
ngx.log(ngx.DEBUG,"serverCount=" .. tostring(serverCount));
if not serverCount or serverCount == ngx.null then
errorOut.resp_msg="serverCount 为空!";
ngx.say(cjson.encode(errorOut));
return ;
end
local iServerCount = tonumber(serverCount)-1;
ngx.log(ngx.DEBUG,"iServerCount=" .. iServerCount);
for i = 0,iServerCount do
cache:set(i, args["server"..tostring(i)])
ngx.log(ngx.DEBUG,"i=" .. args["server"..tostring(i)])
end
cache:set("serverCount",tonumber(serverCount));
restOut.datas = "更新的server数:"..serverCount;
ngx.say(cjson.encode(restOut));
▌takeoneserver.lua
local balancer = require "ngx.balancer"
local upstream = require "ngx.upstream"
local upstream_name = 'backend'
local cache = ngx.shared.cache
local serverCount = cache:get("serverCount")
ngx.log(ngx.DEBUG, "serverCount=" .. tostring(serverCount));
local key = "req_index"
local req_index = cache:get(key);
ngx.log(ngx.DEBUG, "req_index=" .. tostring(req_index));
--ngx.log(ngx.DEBUG, "0==nil=" .. tostring(not 0));
if not req_index or req_index == ngx.null or req_index >= serverCount then
req_index = 0
cache:set(key, req_index)
end
cache:incr(key, 1)
ngx.log(ngx.DEBUG, "req_index=" .. tostring(req_index));
local server = cache:get(req_index);
local index = string.find(server, ':')
local host = string.sub(server, 1, index - 1)
local port = string.sub(server, index + 1)
ngx.log(ngx.DEBUG, "host=" .. host);
balancer.set_current_peer(host, tonumber(port))
▌nginx.conf
#user nobody;
worker_processes 1;
#worker_processes 8;
#开发环境
error_log logs/error.log debug;
#生产环境
#error_log logs/error.log;
#一个Nginx进程打开的最多文件描述数目 建议与ulimit -n一致
#如果面对高并发时 注意修改该值 ulimit -n 还有部分系统参数 而并非这个单独确定
worker_rlimit_nofile 2000000;
pid logs/nginx.pid;
events {
use epoll;
worker_connections 409600;
multi_accept on;
accept_mutex off;
}
http {
default_type 'text/html';
charset utf-8;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log off;
#access_log logs/access_main.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
#keepalive_timeout 65;
keepalive_timeout 1200s; #客户端链接超时时间。为0的时候禁用长连接。即长连接的timeout
keepalive_requests 20000000; #在一个长连接上可以服务的最大请求数目。当达到最大请求数目且所有已有请求结束后,连接被关闭。默认值为100。即每个连接的最大请求数
gzip off;
#gzip on;
#lua扩展加载
# for linux
# lua_package_path "./?.lua;/vagrant/LuaDemoProject/src/?.lua;/usr/local/ZeroBraneStudio-1.80/?/?.lua;/usr/local/ZeroBraneStudio-1.80/?.lua;;";
# lua_package_cpath "/usr/local/ZeroBraneStudio-1.80/bin/clibs/?.so;;";
lua_package_path "./?.lua;/vagrant/LuaDemoProject/src/?.lua;/vagrant/LuaDemoProject/vendor/template/?.lua;/vagrant/LuaDemoProject/src/?/?.lua;/usr/local/openresty/lualib/?/?.lua;/usr/local/openresty/lualib/?.lua;;";
lua_package_cpath "/usr/local/openresty/lualib/?/?.so;/usr/local/openresty/lualib/?.so;;";
# for windows
# lua_package_path "./?.lua;C:/dev/refer/LuaDemoProject/src/vendor/jwt/?.lua;C:/dev/refer/LuaDemoProject/src/?.lua;E:/tool/ZeroBraneStudio-1.80/lualibs/?/?.lua;E:/tool/ZeroBraneStudio-1.80/lualibs/?.lua;E:/tool/openresty-1.13.6.2-win32/lualib/?.lua;;";
# lua_package_cpath "E:/tool/ZeroBraneStudio-1.80/bin/clibs/?.dll;E:/tool/openresty-1.13.6.2-win32/lualib/?.dll;;";
#调试模式(即关闭lua脚本缓存)
# lua_code_cache off;
# for windows
# lua_package_path "C:/dev/refer/LuaDemoProject/src/vendor/jwt/?.lua;C:/dev/refer/LuaDemoProject/src/?.lua;E:/tool/ZeroBraneStudio-1.80/lualibs/?/?.lua;E:/tool/ZeroBraneStudio-1.80/lualibs/?.lua;E:/tool/openresty-1.13.6.2-win32/lualib/?.lua;;";
# lua_package_cpath "E:/tool/ZeroBraneStudio-1.80/bin/clibs/?.dll;E:/tool/openresty-1.13.6.2-win32/lualib/?.dll;;";
# 初始化项目
# init_by_lua_file luaScript/initial/loading_config.lua;
# nginx跟后端服务器连接超时时间(代理连接超时)
proxy_connect_timeout 600;
proxy_read_timeout 600;
#指定缓存信息
#lua_shared_dict ngx_cache 128m;
#保证只有一个线程去访问redis或是mysql-lock for cache
# lua_shared_dict cache_lock 100k;
lua_shared_dict cache 1m;
#调试模式(即关闭lua脚本缓存)
lua_code_cache off;
# lua_code_cache on;
upstream backend {
server "127.0.0.1:8080";
balancer_by_lua_file luaScript/module/dynamicBalance/takeOneServer.lua;
}
map $http_upgrade $connection_upgrade{
default upgrade;
'' close;
}
server {
listen 9999 default;
server_name localhost;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-real-ip $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_redirect off;
# proxy_set_header X-Forwarded-For $http_x_forwarded_for;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}
server {
listen 8000 ;
server_name localhost;
lua_need_request_body on;
#更新API接口
location = / {
content_by_lua_file luaScript/module/dynamicBalance/updateServers.lua;
}
}
}
具体的调试和使用,请参见视频的第19.2章
▌使用 OpenResty Docker 镜像:
▌需要提前了解的内容:
Docker
Nginx 配置
OpenResty 基本了解
选择 OpenResty 的原因:
配置基本等同于 Nginx
必要的时候可以使用 Lua 脚本
提供基于 CentOS 的镜像,调测方便
▌相关链接
OpenResty 官网
选择的标签:openresty/openresty:centos
https://github.com/openresty/docker-openresty/blob/master/README.md#nginx-config-files
▌镜像内部信息
OpenResty 默认安装位置:
/usr/local/openresty/
安装目录中 Nginx 相关文件:
/usr/local/openresty/nginx/
默认服务指向 Web 文件夹:
/usr/local/openresty/nginx/html/
映射关系:
/bin/openresty -> /usr/local/openresty/nginx/sbin/nginx
/bin/opm -> /usr/local/openresty/bin/opm
默认配置文件位置(后续的配置会覆盖这里的内容):
/etc/nginx/conf.d/default.conf
/etc/nginx/conf.d/
在绝大多数情况,覆盖上面的配置文件就可以了。
但是,这些配置文件的内容,只能是包含在 http 段内的配置,并不能作为完整的配置文件使用。
比如:
可以包含:upstream、server
不能包含:tcp
完整配置文件位置:
/usr/local/openresty/nginx/conf/nginx.conf
配置文件相关信息:
https://github.com/openresty/docker-openresty/blob/master/README.md#nginx-config-files
▌性能大坑:与 Docker 使用的网络瓶颈:
说说 Docker 与 OpenResty 之间的"坑"吧,大家肯定对这个更感兴趣。
我们刚开始使用的时候,是这样启动的:
docker run -d -p 80:80 openresty
首次压测过程中发现 Docker 进程 CPU 占用率 100%,单机接口 4-5 万的 QPS 就上不去了。
经过多方探讨交流,终于明白原来是网络瓶颈所致(OpenResty 太彪悍,Docker 默认的虚拟网卡受不了了 _)。
最终我们绕过这个默认的桥接网卡,使用 --net 参数即可完成。
docker run -d --net=host openresty
多么简单,就这么一个参数,居然困扰了我们好几天。
一度怀疑我们是否要忍受引入 Docker 带来的低效率网络。虽然这个点是自己挖出来的,但是在交流过程中还学到了很多好东西。
Docker Network settings,引自:
www.lupaworld.com/article-250439-1.html
默认情况下,所有的容器都开启了网络接口,同时可以接受任何外部的数据请求。
--dns=[] : Set custom dns servers for the container
--net="bridge" : Set the Network mode for the container
'bridge': creates a new network stack for the container on the docker bridge
'none': no networking for this container
'container:
你可以通过 docker run --net none 来关闭网络接口,此时将关闭所有网络数据的输入输出,你只能通过 STDIN、STDOUT 或者 files 来完成 I/O 操作。
默认情况下,容器使用主机的 DNS 设置,你也可以通过 --dns 来覆盖容器内的 DNS 设置。
同时 Docker 为容器默认生成一个 MAC 地址,你可以通过 --mac-address 12:34:56:78:9a:bc 来设置你自己的 MAC 地址。
Docker 支持的网络模式有:
none。关闭容器内的网络连接
bridge。通过 veth 接口来连接容器,默认配置。
host。允许容器使用 host 的网络堆栈信息。注意:这种方式将允许容器访问 host 中类似 D-BUS 之类的系统服务,所以认为是不安全的。
container。使用另外一个容器的网络堆栈信息。
▌None 模式
将网络模式设置为 none 时,这个容器将不允许访问任何外部 router。这个容器内部只会有一个 loopback 接口,而且不存在任何可以访问外部网络的 router。
▌Bridge 模式
Docker 默认会将容器设置为 bridge 模式。此时在主机上面将会存在一个 docker0 的网络接口,同时会针对容器创建一对 veth 接口。其中一个 veth 接口是在主机充当网卡桥接作用,另外一个 veth 接口存在于容器的命名空间中,并且指向容器的 loopback。Docker 会自动给这个容器分配一个 IP ,并且将容器内的数据通过桥接转发到外部。
▌Host 模式
当网络模式设置为 host 时,这个容器将完全共享 host 的网络堆栈。host 所有的网络接口将完全对容器开放。容器的主机名也会存在于主机的 hostname 中。这时,容器所有对外暴露的端口和对其它容器的连接,将完全失效。
▌Bridge 模式:
Bridge 模式是 Docker 的默认网络模式,不指定 --net 参数,就是Bridge模式;
bridge 模式俗称桥接模式,不难理解的是 bridge 的作用,bridge 可以连接不同的东西。
早期的二层网络中,bridge 可以连接不同的 LAN 网,如下图所示。
当主机 1 发出一个数据包时,LAN 1 的其他主机和网桥 br0 都会收到该数据包。
网桥再将数据包从入口端复制到其他端口上(本例中就是另外一个端口)。因此,LAN 2 上的主机也会接收到主机 A 发出的数据包,从而实现不同 LAN 网上所有主机的通信。
随着网络技术的发展,传统 bridge 衍生出适用不同应用场景的模式,其中最典型要属 Linux bridge 模式,它是 Linux Kernel 网络模块的一个重要组成部分,用以保障不同虚拟机之间的通信,或是虚拟机与宿主机之间的通信,如下图所示 :
Docker bridge 是用来连接不同容器网络,或是连接容器与宿主机的。
Docker bridge 模式不仅使用了 veth pair 技术,还使用了网络命名空间技术,采用了 NAT 方式。
Docker bridge 和 Linux bridge 二者,初看如出一辙,再看又相去甚远,还真是傻傻分不清楚。
先从 Linux bridge 模式的基本工作原理入手分析。
▌Linux bridge 模式的虚拟机
Linux bridge 模式下,Linux Kernel 会创建出一个虚拟网桥 ,用以实现主机网络接口与虚拟网络接口间的通信。
从功能上来看,Linux bridge 像一台虚拟交换机,所有桥接设置的虚拟机分别连接到这个交换机的一个接口上,接口之间可以相互访问且互不干扰,这种连接方式对物理主机而言也是如此。
▌Linux bridge 模式
Linux bridge 模式下,Linux Kernel 会创建出一个虚拟网桥 ,用以实现主机网络接口与虚拟网络接口*间的通信。从功能上来看,Linux bridge 像一台虚拟交换机,所有桥接设置的虚拟机分别连接到这个交换机的一个接口上,接口之间可以相互访问且互不干扰,这种连接方式对物理主机而言也是如此。
在桥接的作用下,虚拟网桥会把主机网络接口接收到的网络流量转发给虚拟网络接口,于是后者能够接收到路由器发出的 DHCP(动态主机设定协议,用于获取局域网 IP)信息及路由更新。
这样的工作流程,同样适用于不同虚拟网络接口间的通信。
具体的实现方式如下:
虚拟机与宿主机通信: 用户可以手动为虚拟机配置IP 地址、子网掩码,该 IP 需要和宿主机 IP 处于同一网段,这样虚拟机才能和宿主机进行通信。
虚拟机与外界通信: 如果虚拟机需要联网,还需为它手动配置网关,该网关也要和宿主机网关保持一致。
除此之外,还有一种较为简单的方法,那就是虚拟机通过 DHCP 自动获取 IP,实现与宿主机或宿主机以外的世界通信,小白亲测有效。
▌Docker bridge 模式
大致清楚 Linux bridge 模式后,再来看 Docker bridge 模式。
Docker Daemon 会创建出一个名为 docker0 的虚拟网桥 ,用来连接宿主机与容器,或者连接不同的容器,
Docker 利用 veth pair 技术,在宿主机上创建了两个虚拟网络接口 veth0 和 veth1(veth pair 技术的特性可以保证无论哪一个 veth 接收到网络报文,都会无条件地传输给另一方)。
容器与宿主机通信 : 在桥接模式下,Docker Daemon 将 veth0 附加到 docker0 网桥上,保证宿主机的报文有能力发往 veth0。再将 veth1 添加到 Docker 容器所属的网络命名空间,保证宿主机的网络报文若发往 veth0 可以立即被 veth1 收到。
容器与外界通信 : 容器如果需要联网,则需要采用 NAT 方式。准确的说,是 NATP (网络地址端口转换) 方式。NATP 包含两种转换方式:SNAT 和 DNAT 。
DNAT——目的 NAT (Destination NAT,DNAT): 修改数据包的目的地址。
当宿主机以外的世界需要访问容器时,数据包的流向如下图所示:
由于容器的 IP 与端口对外都是不可见的,所以数据包的目的地址为宿主机的 ip 和端口,为 192.168.1.10:24 。
数据包经过路由器发给宿主机 eth0,再经 eth0 转发给 docker0 网桥。由于存在 DNAT 规则,会将数据包的目的地址转换为容器的 ip 和端口,为 172.17.0.n:24 。
宿主机上的 docker0 网桥识别到容器 ip 和端口,于是将数据包发送附加到 docker0 网桥上的 veth0 接口,veth0 接口再将数据包发送给容器内部的 veth1 接口,容器接收数据包并作出响应。
SNAT——源 NAT (Source NAT,SNAT): 修改数据包的源地址。
当容器需要访问宿主机以外的世界时,数据包的流向为下图所示:
此时数据包的源地址为容器的ip和端口,为 172.17.0.n:24,容器内部的 veth1 接口将数据包发往 veth0 接口,到达 docker0 网桥。
宿主机上的 docker0 网桥发现数据包的目的地址为外界的 IP 和端口,便会将数据包转发给 eth0 ,并从 eth0 发出去。
由于存在 SNAT 规则,会将数据包的源地址转换为宿主机的 ip 和端口,为 192.168.1.10:24 。
由于路由器可以识别到宿主机的 ip 地址,所以再将数据包转发给外界,外界接受数据包并作出响应。
这时候,在外界看来,这个数据包就是从 192.168.1.10:24 上发出来的,Docker 容器对外是不可见的。
▌Docker 网桥上容器之间的网络流量:
默认情况下,默认网桥上同一主机上的容器之间允许所有网络通信。
如果不需要,限制所有的容器间通信,将需要通信的特定容器链接在一起,或者创建自定义网络,并只加入需要与该自定义网络通信的容器。
▌参看网络参数
[root@cdh2 ~]# docker network ls -q | xargs docker network inspect --format '{{.Name}}:{{.Options}}'
base-env-network:map[]
base-env_default:map[]
bridge:map[com.docker.network.bridge.host_binding_ipv4:0.0.0.0 com.docker.network.bridge.name:docker0 com.docker.network.driver.mtu:1500 com.docker.network.bridge.default_bridge:true com.docker.network.bridge.enable_icc:true com.docker.network.bridge.enable_ip_masquerade:true]
host:map[]
monitor-network:map[]
mysql-canal-network:map[]
none:map[]
▌MTU
最大传输单元(Maximum Transmission Unit,MTU)用来通知对方所能接受数据服务单元的最大尺寸,说明发送方能够接受的有效载荷大小。 [1]
是包或帧的最大长度,一般以字节记。如果MTU过大,在碰到路由器时会被拒绝转发,因为它不能处理过大的包。如果太小,因为协议一定要在包(或帧)上加上包头,那实际传送的数据量就会过小,这样也划不来。大部分操作系统会提供给用户一个默认值,该值一般对用户是比较合适的。 [2]
▌为啥缺省的MTU为1500
这个问题不是非常严谨,应该说标准以太网接口缺省的MTU为1500,而现在的以太网接口普遍可以通过配置使得MTU远远大于1500。
以太网帧长度上下限标准以太网帧长度下限为:64 字节标准以太网帧长度上限为:1518 字节
最早的以太网工作方式:载波多路复用/冲突检测CSMA/CD,因为网络是共享的,即任何一个节点发送数据之前,先要侦听线路上是否有数据在传输,如果有,需要等待,如果线路可用,才可以发送。
假设A发出第一个bit位,到达B,而B也正在传输第一个bit位,于是产生冲突,冲突信号得让A在完成最后一个bit位之前到达A,这个一来一回的时间间隙slot time是57.6μs.
在10Mbps的网络中,在57.6μs的时间内,能够传输576个bit,所以要求以太网帧最小长度为576个bits,从而让最极端的碰撞都能够被检测到。
这个576bit换算一下就是72个字节,去掉8个字节的前导符和帧开始符,以太网帧的最小长度为64字节。
如果说以太网帧的最小长度64byte是由CSMA/CD限制所致,那最大长度1500byte又是处于什么考虑的呢?
IP头total length为两个byte,理论上IP packet可以有65535 byte,加上Ethernet Frame头和尾,可以有65535 +14 + 4 = 65553 byte。
如果在10Mbps以太网上,将会占用共享链路长达50ms,这将严重影响其它主机的通信,特别是对延迟敏感的应用是无法接受的。
由于线路质量差而引起的丢包,发生在大包的概率也比小包概率大得多,所以大包在丢包率较高的线路上不是一个好的选择。
但是如果选择一个比较小的长度,传输效率又不高,拿TCP应用来说,如果选择以太网长度为218byte,
TCP payload = 218 - Ethernet Header -IP Header - TCP Header=[218-18 - 20](tel:218-18 - 20) -20= 160 byte
那有效传输效率=160/218= 73%
而如果以太网长度为1518,那有效传输效率=1460/1518=96%通过比较,选择较大的帧长度,有效传输效率更高,
而更大的帧长度同时也会造成上述的问题,于是最终选择一个折衷的长度:1518 byte !
对应的IP packet 就是 1500 byte,这就是最大传输单元MTU的由来。
▌技术自由的实现路径 PDF获取:
▌实现你的架构自由:
《吃透8图1模板,人人可以做架构》PDF
《10Wqps评论中台,如何架构?B站是这么做的!!!》PDF
《阿里二面:千万级、亿级数据,如何性能优化? 教科书级 答案来了》PDF
《峰值21WQps、亿级DAU,小游戏《羊了个羊》是怎么架构的?》PDF
《100亿级订单怎么调度,来一个大厂的极品方案》PDF
《2个大厂 100亿级 超大流量 红包 架构方案》PDF
… 更多架构文章,正在添加中
▌实现你的 响应式 自由:
《响应式圣经:10W字,实现Spring响应式编程自由》PDF
这是老版本 《Flux、Mono、Reactor 实战(史上最全)》PDF
▌实现你的 spring cloud 自由:
《Spring cloud Alibaba 学习圣经》 PDF
《分库分表 Sharding-JDBC 底层原理、核心实战(史上最全)》PDF
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之间混乱关系(史上最全)》PDF
▌实现你的 linux 自由:
《Linux命令大全:2W多字,一次实现Linux自由》PDF
▌实现你的 网络 自由:
《TCP协议详解 (史上最全)》PDF
《网络三张表:ARP表, MAC表, 路由表,实现你的网络自由!!》PDF
▌实现你的 分布式锁 自由:
《Redis分布式锁(图解 - 秒懂 - 史上最全)》PDF
《Zookeeper 分布式锁 - 图解 - 秒懂》PDF
▌实现你的 王者组件 自由:
《队列之王: Disruptor 原理、架构、源码 一文穿透》PDF
《缓存之王:Caffeine 源码、架构、原理(史上最全,10W字 超级长文)》PDF
《缓存之王:Caffeine 的使用(史上最全)》PDF
《Java Agent 探针、字节码增强 ByteBuddy(史上最全)》PDF
▌实现你的 面试题 自由:
4000页《尼恩Java面试宝典》PDF 40个专题
....
注:以上尼恩 架构笔记、面试题 的PDF文件,请到《技术自由圈》公众号领取
还需要啥自由,可以告诉尼恩。 尼恩帮你实现.......