乐趣区

关于websocket:httpServer来代理WebSocket通信

1、简介
1.1、通信形式

单工:数据只反对在一个方向传输,即单向,在同一时间内只有一方可能承受 & 发送信息;
半双工:容许数据可能双向传输,然而,在某一时刻只容许数据在一个方向传输。相似切换方向的单工通信。http 就是半双工通信,先有申请,再有响应;
全双工:容许数据同时都能双向传输,相似两个单工通信的联合,要求 client & server 都有独立接管和发送的能力,在任意时刻都能接管 & 发送信息,socket 就是全双工通信;

1.2、websocket
websocket 实质是一种网络应用层协定,建设在单个 TCP 连贯上的全双工模式,用来补救了 http 协定在继续双向通信能力上的有余,容许服务端与客户端之间能够双向被动推送数据。
特点:

与 http 协定有着良好的兼容性,默认端口 80(协定标识为 ws)或者 443(加密传输,协定标识为 wss);
建设连贯的握手阶段采纳的是 http 协定,依据这个个性,能够在链路两头引入 http 代理服务器;
数据格式轻量,性能开销小,通信效率高(只有建设连贯后,就能够有限收发报文);
报文内容能够是文本,也能够是二进制数据;
没有同源的束缚,不存在跨域一说,客户端能够与任意服务器通信(前提是服务器能应答);
对外裸露的 URL 为:ws://${domain}:80/${path},或者 wss://${domain}:443/${path}

2、搭建 demo
2.1、server
采纳 ws 库疾速构建一个 websocket server,监听 connection 事件,收到音讯并且打印后,立马发送给客户端
const ws = require(‘ws’);

let wsServer = new ws.Server({

port: 3000,
host:'127.0.0.1',
path:'/websocket'

});

wsServer.on(‘connection’, function (server) {

console.log('client connected');

server.on('message', function (message) {console.dir(message)
    console.log(message.toString());
    server.send(`hello:${message}`)
});

});
复制代码
2.2、client
疾速搭建一个 websocket client,利用 http-server 在目录下启动,并且拜访该页面
<!DOCTYPE html>

<html>
<head>
    <title>websocket demo</title>
</head>
<body>
    <h1></h1>
    <br>
    <input type='text' id='sendText'>
    <button onclick='send()'>send</button>
</body>

</html>
<script>

const ws = new WebSocket('ws://127.0.0.1:3000/websocket');
ws.onopen = function () {console.log('服务器连贯')
}
ws.onmessage = (msg) => {console.log('来自服务器发来的数据:', msg)
    alert('服务器返回内容:' + msg.data)
}

ws.onclose = () => {console.log('服务器敞开')
}

function send() {if (ws) {let msg = document.getElementById('sendText').value;
        ws.send(msg)
    } else {alert('websocket server error')
    }
}

</script>
复制代码
2.3、建设连贯
先启动 websocket server,而后浏览器申请 websocket client 页面,抓包申请如下:

2.3.1、tcp 的三次握手
前三条为 tcp 的三次握手信息,既然谈到了,为了文章的完整性,还是简略形容一下;

client 发送连贯申请,设置 SYN=1,随机一个初始序列号 Seq(数据包 SYN = 1,seq = x),而后本人进入 SYN_SEND 状态(同步已发送),期待 server 确认;
server 收到 SYN 包后,也随机一个 Seq 为 y,并且让 ack = x + 1,示意收到了 client 的连贯申请,而后设置 SYN = 1,ACK = 1,返回给 client(数据包 SYN = 1, ACK = 1, seq = y, ack = x + 1),示意 SYN 握手通过,期待 ACK 应答,而后本人进入 SYN_RCVD 状态(同步已接管);
client 收到 [SYN, ACK] 包后,将 ACK 置 1,让 ack = y +1, 示意收到了 server 的确认申请,最初发送确认给 server(数据包 ACK = 1, ack = y + 1),而后本人进入 ESTABLISHED 状态(连贯已建设),server 收到 client 的确认后也进入 ESTABLISHED 状态;

三次握手必要性:

同步单方的初始序列号,防止反复连贯,必须三次,四次也行,然而开销太大影响效率;
序列号是牢靠传输的关键性,能够去除反复数据,依据数据包的序号来接管;

SYN(连贯申请)的攻打危害:

攻打方发送海量伪造源 IP 的第一次握手 SYN 包,将服务器的半连贯队列给打满(超过最大值),失常的客户发送 SYN 数据包申请连贯就会被服务器抛弃,导致失常的连贯申请无奈胜利,重大引起网络梗塞甚至零碎瘫痪

躲避形式:

限度 ip 连贯次数(限度同一 IP 一分钟内新建设的连接数仅为 10);增大半连贯状态的连接数容量(会增大内存资源占用,/etc/sysctl.d/sysctl.conf,字段 tcp_max_syn_backlog)

2.3.2、TCP window update
server 的接管窗口大小产生了变动,能够失常接收数据了,就会呈现这一条记录
2.3.3、正式连贯
抓包剖析看出,websocket 通信在单方 TCP 三次握手胜利后,还须要发送一次额定的 http 申请,能力正式建设连贯。http 申请报文如下:
GET /websocket HTTP/1.1
Host: 127.0.0.1:3000
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36
Upgrade: websocket
Origin: http://127.0.0.1:8080
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7
Sec-WebSocket-Key: Ap4ZCLgwbnDQ2ump+7ea3g==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: Ih1TB0gxAY3zGzvQCYrIeM5bEdw=
复制代码
申请 headers 的限定:

申请形式必须是 GET,且 http 版本必须为 1.1(keep-alive。因为 1.0 开启长连贯须要 Connection 字段设置,然而 websocket 握手时,Connection 曾经被占用了);
Host,Origin 字段必填:决定拜访哪个虚拟主机,申请起源站点(仅仅协定域名端口,没有任何 path)(默认会带上它俩);
Connection 字段必填,且字段为 Upgrade(触发 http 协定降级);
Upgrade 字段必填,表明协定降级为 web socket;
Sec-WebSocket-Key 字段必填,内容为客户端标识的 base64 编码格局;
Sec-WebSocket-Version 字段必填,表明 websocket 协定版本,RFC 6455 的协定版本为 13;
Sec-WebSocket-Extensions 字段可选,做客户端握手时的拓展项应用;

响应 header 剖析:

只有状态码为 101,才示意服务端批准了协定降级,对于其余状态码,client 会依据语义相应解决;

client 会检测响应 headers 中是否蕴含 Upgrade 字段,且检测值是否为 websokcet(不辨别大小写),若缺失或不匹配,会主动终止连贯;

client 会检测响应 headers 中是否蕴含 Sec-WebSocket-Protocol 字段,并校验它的合理性,若缺失或校验失败,会在主动终止连贯;

Sec-WebSocket-Protocol 校验算法(client & server 的约定):server 收到 Sec-WebSocket-Key 后,会将其与 websocket 魔数 258EAFA5-E914-47DA- 95CA-C5AB0DC85B11 进行字符串拼接,即 ${Sec-WebSocket-Key}258EAFA5-E914-47DA- 95CA-C5AB0DC85B11,而后对它做 SHA1 哈希运算后再做一次 base64 编码,就为 Sec-WebSocket-Protocol。

握手通过后,单方就是长连贯了,能够随时进行双向数据的传输。
3、http 代理
由上文可知,除去 tcp 三次握手外,websocket 实在的建设连贯是那次要害的 http 申请,那其实能够针对它来做一层 http 网关来代理后续的数据传输了。
3.1、创立 http Server
先形容 config.json 文件:
json 格局,websocketTestOne key 代表一个 webSocket,根下文协定降级申请的 path 相响应,即一个该配置对应的代理申请地址应该为:http://{domain}/websocketTestOne,增加多个配置,顺次类推
{
“websocketTestOne”: {

"host": "127.0.0.1",
"port": "3000"

}
}
复制代码
httpServer.js,如下所示,代码量不多,简略介绍一下流程:

加载配置文件,开启一个 http server,并监听 upgrade 事件;
如果有协定降级的申请过去后,会触发 upgrade,而不是 request,upgrade 事件中,针对 clientSocket 一系列监听的预处理;
如果 config.json 没有值,完结 clientSocket,如果 request.url 解析进去的 path 在 config 中找不到,完结 clientSocket;
找到对应的 config,建设 socket 连贯(连贯实在的 webSocket 服务),创立出 serverSocket,并进行一系列预处理设置;
clientSocket 监听 data 事件,将报文写入 serverSocket,serverSocket 监听 data 事件,将报文写入 clientSocket,交替进行;
组装握手连贯的 http 报文,serverSocket 开始正式向 webSocket 服务握手连贯,并触发后面的双向 data 监听事件;
握手胜利,传递的 clientSocket,示意也握手胜利,连贯建设,能够双向收发报文了……

/**

  • create by ikejcwang on 2022.07.25.
  • 注:这只是一个测试的 demo
    */

‘use strict’;
const http = require(‘http’);
const nodeUtil = require(‘util’);
const URL = require(‘url’);
const net = require(‘net’);
const settings = require(‘./settings’).settings;
const configs = require(‘./settings’).configs;
const connectTimeout = settings[‘connectTimeout’] ? settings[‘connectTimeout’] : 5000; // 建设连贯的超时设定
const connectKeepalive = settings[‘connectKeepalive’] ? settings[‘connectKeepalive’] : 60000; // 连贯后的 keepalive 超时设定
const socketTimeout = settings[‘socketTimeout’] ? settings[‘socketTimeout’] : 60000; // socket 的 timeout,

httpServer();

/**

  • 启动入口
    */

function httpServer() {

console.dir(settings)
startHttpServer();

}

function startHttpServer() {

let server = http.createServer();
server.on('upgrade', listenUpgradeEvent);
server.on('request', listenRequestEvent);
server.on('close', () => {console.log('http Server has Stopped At:' + settings['bindPort'])
});
server.on('error', err => {console.log('http Server error:' + err.toString());
    setTimeout(() => {process.exit(1);
    }, 3000);
});
server.listen(settings['bindPort'], settings['bindIP'], settings['backlog'] || 8191, () => {console.log('Started Http Server At:' + settings['bindIP'] + ':' + settings['bindPort']);
});

}

/**

  • 监听 upgrade 事件
  • @param request
  • @param cliSocket
  • @param header
  • @returns {Promise<void>}
    */

async function listenUpgradeEvent(request, cliSocket, header) {

let serverSocket = null;
cliSocket.on('error', e => {if (serverSocket) {serverSocket.destroy();
    }
    logInfo('cliSocket has error', nodeUtil.inspect(e))
});
cliSocket.on('end', () => {logInfo('cliSocket has ended');
});
cliSocket.on('close', function () {logInfo('cliSocket has closed');
});
cliSocket.setTimeout(socketTimeout, () => {cliSocket.destroy(new Error('timeout'));
    if (serverSocket) {serverSocket.destroy();
    }
})
try {if (!configs || Object.keys(configs).length < 1) {cliSocket.end();
        return;
    }
    let sourceUrl = URL.parse(request.url, true);
    let pathArr = sourceUrl.pathname.split('/');
    if (pathArr.length === 1) {cliSocket.end();
        return;
    }
    let websocketName = pathArr[1];
    if (!websocketName || !configs[websocketName]) {cliSocket.end();
        return;
    }
    serverSocket = await connectSocket(configs[websocketName]);
    serverSocket.on('error', err => {cliSocket.end();
        logInfo('server socket error', nodeUtil.inspect(err));
    });
    cliSocket.on('data', chunk => {cliSocket.pause();  // 收到数据后,暂停以后 cliSocket
        if (serverSocket.write(chunk)) {cliSocket.resume(); // server socket 写胜利后,在激活以后 cliSocket
        }
    }).on('end', () => {console.log('end')
        serverSocket.end(); // 双写完解决});

    serverSocket.on('data', chunk => {serverSocket.pause();
        if (cliSocket.write(chunk)) {serverSocket.resume();
        } else {cliSocket.once('drain', () => serverSocket.resume());   // 如果调用 stream.write(chunk) 返回 false,则当能够持续写入数据到流时会触发 drain 事件
        }
    }).on('end', () => {cliSocket.end()
    });
    let connectHeaders = request.headers;
    connectHeaders['host'] = `${configs[websocketName].host}:${configs[websocketName].port}`;
    let headersTemp = '';
    for (let key in connectHeaders) {headersTemp += `${key}: ${connectHeaders[key]}\r\n`
    }
    serverSocket.write(`${request.method} ${request.url} HTTP/1.1\r\n${headersTemp}\r\n`); // 向实在的 webSocket 服务开始握手连贯
    if (header && header.length > 0) {serverSocket.write(header)
    }
} catch (e) {if (cliSocket.writable) {cliSocket.write(`HTTP/1.1 502 Server UnReachable\r\n\r\n`);
    }
    cliSocket.end();
    console.log(`request_error: ${nodeUtil.inspect(e)}`);
}

}

/**

  • 监听 request 事件
  • @param request
  • @param response
  • @returns {Promise<void>}
    */

async function listenRequestEvent(request, response) {

// 再次证实 websocket 握手时到不了这里,因为 headers 信息的 connection 字段为 Upgrade,触发的是 Upgrade 事件
console.log('listenRequestEvent')

}

/**

  • 连贯 socket
  • @param websocketConfig
  • @returns {Promise<unknown>}
    */

function connectSocket(websocketConfig) {

return new Promise((resolve, reject) => {let socket = net.connect(websocketConfig);
    let timer = setTimeout(() => {socket.removeListener('error', onError)
        socket.destroy();
        reject(Object.assign(new Error('connect timeout'), websocketConfig))
    }, connectTimeout);

    let onConnect = () => {socket.setKeepAlive(true, connectKeepalive);
        socket.removeListener('error', onError)
        clearInterval(timer);

        // TODO 创立 tcp 连贯时,默认都会启用 Nagle 算法,此处禁用它,(Nagle 试图以提早为代价来优化吞吐量,然而咱们并不需要),传参 true 或不传即禁用,socket.setNoDelay();
        socket.setTimeout(socketTimeout + 60000, () => {socket.destroy(new Error('socket server timeout'));
        })
        resolve(socket);
    }

    let onError = e => {clearInterval(timer);
        reject(e);
    }
    socket.once('connect', onConnect);
    socket.once('error', onError);
});

}

function logInfo(…args) {

console.dir(args)

}
复制代码
3.2、创立 webSocket Server
webSocketServer.js,比较简单,应用 ws 模块疾速构建;
连贯建设,输入信息,收到报文,输入报文,并增加前缀原路收回去;
const ws = require(‘ws’);

let wsServer = new ws.Server({

port: 3000,
host:'127.0.0.1',

});

wsServer.on(‘connection’, function (server) {

console.log('client connected');

server.on('message', function (message) {console.dir(message)
    console.log(message.toString());
    server.send(`hello:${message}`)
});

});

退出移动版