精华内容
下载资源
问答
  • 介绍imi 是基于 PHP Swoole 的高性能协程应用开发框架,它支持 HttpApi、WebSocketTCP、UDP 服务的开发。在 Swoole 的加持下,相比 php-fpm 请求响应能力,I/O密集型场景处理能力,有着本质上的提升。imi 框架拥有...
    8a6f0b5b74f04cf686e4c16d2db335ce

    介绍

    imi 是基于 PHP Swoole 的高性能协程应用开发框架,它支持 HttpApi、WebSocket、TCP、UDP 服务的开发。

    在 Swoole 的加持下,相比 php-fpm 请求响应能力,I/O密集型场景处理能力,有着本质上的提升。

    imi 框架拥有丰富的功能组件,可以广泛应用于互联网、移动通信、企业软件、云计算、网络游戏、物联网(IOT)、车联网、智能家居等领域。可以使企业 IT 研发团队的效率大大提升,更加专注于开发创新产品。

    核心组件

    • HttpApi、WebSocket、TCP、UDP 服务器
    • MySQL 连接池 (主从+负载均衡)
    • Redis 连接池 (主从+负载均衡)
    • 超好用的 ORM (Db、Redis、Tree)
    • 毫秒级热更新
    • AOP
    • Bean 容器
    • 缓存 (Cache)
    • 配置读写 (Config)
    • 枚举 (Enum)
    • 事件 (Event)
    • 门面 (Facade)
    • 验证器 (Validate)
    • 锁 (Lock)
    • 日志 (Log)
    • 异步任务 (Task)

    扩展组件

    • RPC
    • Hprose
    • 权限控制
    • Smarty 模版引擎
    • 限流
    • 跨进程变量共享
    • Swoole Tracker

    开始使用

    创建 Http Server 项目:composer create-project imiphp/project-http

    创建 WebSocket Server 项目:composer create-project imiphp/project-websocket

    创建 TCP Server 项目:composer create-project imiphp/project-tcp

    创建 UDP Server 项目:composer create-project imiphp/project-udp

    运行环境

    • Linux 系统 (Swoole 不支持在 Windows 上运行)
    • PHP >= 7.1
    • Composer
    • Swoole >= 4.3.0
    • Redis、PDO 扩展

    版权信息

    imi 遵循 木兰宽松许可证(Mulan PSL v1) 开源协议发布,并提供免费使用。

    环境要求

    Redis、MySQL

    首次运行测试

    • 创建 db_imi_test 数据库,将 tests/db/db.sql 导入到数据库
    • 配置系统环境变量,如果默认值跟你的一样就无需配置了
    2ff946640dd244ac858bbcff0588814f

    配置命令:export NAME=VALUE

    • 首次运行测试脚本:composer install-test
    • 首次之后再运行测试的命令:composer test

    更多使用方法可以查看官方文档

    开源地址:

    https://gitee.com/yurunsoft/IMI

    更多更优质的资讯,请关注我,你的支持会鼓励我不断分享更多更好的优质文章。

    展开全文
  • 在该协议下,与服务端只需要一次握手,之后建立一条快速通道,开始互相传输数据,实际是基于TCP双向全双工,比http半双工提高了很大的性能,常用于网络在线聊天室等。继续netty实例的学习,本期内容主要做WebSocket...

    描述

    WebSocket是html5开始浏览器和服务端进行全双工通信的网络技术。在该协议下,与服务端只需要一次握手,之后建立一条快速通道,开始互相传输数据,实际是基于TCP双向全双工,比http半双工提高了很大的性能,常用于网络在线聊天室等。继续netty实例的学习,本期内容主要做WebSocket实例的实现和相关协议的验证。

    WebSocket与http比较

    httpWebSocket
    半双工,可以双向传输,不能同时传输全双工
    消息冗长繁琐,消息头,消息体,换行...对代理、防火墙、路由器透明
    http轮询实现推送请求量大,而comet采用长连接无头部、Cookie等
    -ping/pong帧保持链路激活
    -特点:服务端可以主动传递给客户端,不需要轮询

    代码示例和运行结果

    服务端

    服务端比较简单,是启动一个端口来处理请求。

    主程序

    import io.netty.bootstrap.ServerBootstrap;
    import io.netty.channel.Channel;
    import io.netty.channel.ChannelInitializer;
    import io.netty.channel.EventLoopGroup;
    import io.netty.channel.nio.NioEventLoopGroup;
    import io.netty.channel.socket.SocketChannel;
    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.stream.ChunkedWriteHandler;
    import org.junit.Test;

    public class NettyWebSocketServer {

    public void run(final int port) {
    EventLoopGroup bossGroup = new NioEventLoopGroup();
    EventLoopGroup workerGroup = new NioEventLoopGroup();

    try {
    ServerBootstrap bootstrap = new ServerBootstrap();
    bootstrap.group(bossGroup, workerGroup)
    .channel(NioServerSocketChannel.class)
    .childHandler(new ChannelInitializer<SocketChannel>() {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
    ch.pipeline()
    .addLast(new HttpServerCodec())
    .addLast(new HttpObjectAggregator(65535))
    .addLast(new ChunkedWriteHandler())
    .addLast(new WebSocketServerHandler());
    }
    });
    Channel ch = bootstrap.bind(port).sync().channel();
    System.out.println("websocket @" + port);
    ch.closeFuture().sync();
    } catch (Exception e) {
    e.printStackTrace();
    }
    }

    @Test
    public void runServer() {
    run(23123);
    }
    }

    请求处理handler

    核心处理WebSocket请求,重点地方加了注释

    import io.netty.buffer.ByteBufUtil;
    import io.netty.channel.ChannelFuture;
    import io.netty.channel.ChannelFutureListener;
    import io.netty.channel.ChannelHandlerContext;
    import io.netty.channel.SimpleChannelInboundHandler;
    import io.netty.handler.codec.http.*;
    import io.netty.handler.codec.http.websocketx.*;

    import java.time.LocalDateTime;
    import java.util.concurrent.TimeUnit;

    public class WebSocketServerHandler extends SimpleChannelInboundHandler<Object> {
    private WebSocketServerHandshaker handshaker;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
    //首次请求之后先进行握手,通过http请求来实现
    if (msg instanceof FullHttpRequest) {
    handleHttpRequest(ctx, (FullHttpRequest) msg);
    } else if (msg instanceof WebSocketFrame) {
    handleWebSocketRequest(ctx, (WebSocketFrame) msg);
    }
    }

    private void handleHttpRequest(ChannelHandlerContext ctx, FullHttpRequest request) {
    //解码http失败返回
    if (!request.decoderResult().isSuccess()) {
    sendResponse(ctx, request, new DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.BAD_REQUEST, ctx.alloc().buffer()));
    return;
    }

    if (!HttpMethod.GET.equals(request.method())) {
    sendResponse(ctx, request, new DefaultFullHttpResponse(request.protocolVersion(), HttpResponseStatus.FORBIDDEN, ctx.alloc().buffer()));
    return;
    }

    //参数分别是ws地址,子协议,是否扩展,最大frame长度
    WebSocketServerHandshakerFactory factory = new WebSocketServerHandshakerFactory(getWebSocketLocation(request), null, true, 5 * 1024 * 1024);
    handshaker = factory.newHandshaker(request);
    if (handshaker == null) {
    WebSocketServerHandshakerFactory.sendUnsupportedVersionResponse(ctx.channel());
    } else {
    handshaker.handshake(ctx.channel(), request);
    }
    }

    //SSL支持采用wss://
    private String getWebSocketLocation(FullHttpRequest request) {
    String location = request.headers().get(HttpHeaderNames.HOST) + "/websocket";
    return "ws://" + location;
    }

    private void handleWebSocketRequest(ChannelHandlerContext ctx, WebSocketFrame frame) {
    //关闭
    if (frame instanceof CloseWebSocketFrame) {
    handshaker.close(ctx.channel(), (CloseWebSocketFrame) frame.retain());
    return;
    }

    //握手 PING/PONG
    if (frame instanceof PingWebSocketFrame) {
    ctx.write(new PongWebSocketFrame(frame.content().retain()));
    return;
    }

    //文本接收和发送
    if (frame instanceof TextWebSocketFrame) {
    String recv = ((TextWebSocketFrame) frame).text();
    String text = "now@" + LocalDateTime.now() + "\n recv:" + recv;

    //这里加了个循环,每隔一秒服务端主动发送内容给客户端
    for (int i = 0; i < 5; i++) {
    String res = i + "-" + text;
    try {
    TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    ctx.writeAndFlush(new TextWebSocketFrame(res));
    }
    System.out.println(recv);
    return;
    }

    if (frame instanceof BinaryWebSocketFrame) {
    ctx.write(frame.retain());
    }
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
    ctx.flush();
    }

    private void sendResponse(ChannelHandlerContext ctx, FullHttpRequest req, FullHttpResponse resp) {
    HttpResponseStatus status = resp.status();
    if (status != HttpResponseStatus.OK) {
    ByteBufUtil.writeUtf8(resp.content(), status.toString());
    HttpUtil.setContentLength(req, resp.content().readableBytes());
    }
    boolean keepAlive = HttpUtil.isKeepAlive(req) && status == HttpResponseStatus.OK;
    HttpUtil.setKeepAlive(req, keepAlive);
    ChannelFuture future = ctx.write(resp);
    if (!keepAlive) {
    future.addListener(ChannelFutureListener.CLOSE);
    }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
    cause.printStackTrace();
    ctx.close();
    }
    }

    客户端

    客户端使用了html来实现,可以直接在浏览器打开,代码比较简单,不做多的介绍。本地使用注意ws地址和服务端的一致。

    <html>
    <head>
    <meta charset="UTF-8">
    head>
    <body>
    <form onsubmit="return false;">
    <input type="text" name="message" value="test"/>
    <br/>
    <input type="button" value="send", onclick="send(this.form.message.value)"/>
    <hr/>
    resp:
    <br/>
    <textarea id="responseText" style="width: 200px;height:300px;">textarea>
    form>

    form>
    body>
    <script type="text/javascript">var socket;if (!window.WebSocket) {
    window.WebSocket = window.MozWebSocket;}if (window.WebSocket) {
    socket = new WebSocket("ws://127.0.0.1:23123/websocket");
    socket.onmessage = function (ev) {var ta = document.getElementById('responseText');
    ta.value = '';
    ta.value = ev.data;}
    socket.onopen = function (ev) {var ta = document.getElementById('responseText');
    ta.value = 'start open websocket ...';}
    socket.onclose = function (ev) {var ta = document.getElementById('responseText');
    ta.value = '';
    ta.value = 'close websocket ...';}}else {alert('not support websocket !')}function send(msg) {if (!window.WebSocket) {return;}if (socket.readyState == WebSocket.OPEN) {
    socket.send(msg);}else {alert('connect fail !')}}
    script>
    html>

    试验结果

    1. 请求抓包说明

      7e904f008a400bc1ecd3bf91b07e5ede.png

      ws请求

      如图中,首先是TCP三次握手,然后HTTP请求,建立WebSocket连接。之后客户端请求一次,然后服务端每隔1秒发送内容到客户端。

      90b892c93cf26c89c7598681383ad80c.png

      keepAlive


      上图是空闲时候的keepAlive调用

    2. 客户端调用

      66781c342a07e35f436c7511be59c313.png

      服务端关闭的时候效果

      677e816609b4c1f796f54f0c2b286c6a.png

      服务端主动发送的效果

      上图是服务端主动发送的效果,随着内容不同数据会变化。

    参考资料

    [1]示例原始地址:github
    [2]《Netty权威指南(第二版)》P213

    结语

    以上就是本期的内容,后面有时间会继续netty相关实例的文章,确实在网络方面有很多新的收获。

    展开全文
  • 我之前是做IM相关桌面端软件的开发,基于TCP长链接自己封装的一套私有协议,目前公司也有项目用到了ws协议,好像无论什么行业,都会遇到这个ws协议。首先它的使用是很简单的,在H5和Node.js中都是基于事件驱动在H5中...

    (给前端大全加星标,提升前端技能)

    作者:前端巅峰 公号 /  Peter

    写在开头:

    为什么要使用websocket协议(以下简称ws协议),什么场景会使用?

    我之前是做IM相关桌面端软件的开发,基于TCP长链接自己封装的一套私有协议,目前公司也有项目用到了ws协议,好像无论什么行业,都会遇到这个ws协议。

    首先它的使用是很简单的,在H5和Node.js中都是基于事件驱动

    在H5中

    40c8ebc4-6c13-eb11-8da9-e4434bdf6706.png

    在H5中的使用案例:

    <html><head><meta charset="utf-8"><script type="text/javascript">function WebSocketTest() {if ("WebSocket" in window) {                alert("您的浏览器支持 WebSocket!");// 打开一个 web socketvar ws = new WebSocket("ws://localhost:9998");                ws.onopen = function () {// Web Socket 已连接上,使用 send() 方法发送数据                    ws.send("发送数据");                    alert("数据发送中...");                };                ws.onmessage = function (evt) {var received_msg = evt.data;                    alert("数据已接收...");                };                ws.onclose = function () {// 关闭 websocket                    alert("连接已关闭...");                };            }else {// 浏览器不支持 WebSocket                alert("您的浏览器不支持 WebSocket!");            }        }script>head><body><div id="sse"><a href="javascript:WebSocketTest()">运行 WebSocketa>div>body>html>

    Node.js中的服务端搭建:

    const {Server} = require('ws');//引入模块const wss = new Server({ port: 9998 });//创建一个WebSocketServer的实例,监听端口9998wss.on('connection', function connection(socket)  {    socket.on('message', function incoming(message) {console.log('received: %s', message);    socket.send('Hi Client');  });//当收到消息时,在控制台打印出来,并回复一条信息});

    这样你就愉快的通信了,不需要关注协议的实现,但是真正的项目场景中,可能会有UDP、TCP、FTP、SFTP等场景,你还是需要了解不同的协议实现细节,这里我推荐一下某金的张师傅小册《TCP协议》,看过都说好。(这里没收钱,就是觉得好)


    正式开始:

    为什么要使用ws协议?

    传统的Ajax轮询(即一直不听发请求去后端拿数据)或长轮询的操作太过于粗暴,性能更不用说。

    ws协议在目前浏览器中支持已经非常好了,另外这里说一句,它也是一个应用层协议,成功升级ws协议,是101状态码,像webpack热更新这些都有用ws协议

    42c8ebc4-6c13-eb11-8da9-e4434bdf6706.png

    这就是连接了本地的ws服务器


    现在开始,我们实现服务端的ws协议,就是自己实现一个websocket类,并且继承Node.js的自定义事件模块,还要一个起一个进程占用端口,那么就要用到http模块

    const { EventEmitter } = require('events');const { createServer } = require('http');class MyWebsocket extends EventEmitter {}module.exports = MyWebsocket;

    这是一个基础的类,我们继承了自定义事件模块,还引入了http的createServer方法,此时先实现端口占用

    const { EventEmitter } = require('events');const { createServer } = require('http');class MyWebsocket extends EventEmitter {constructor(options) {super(options);this.options = options;this.server = createServer();    options.port ? this.server.listen(options.port) : this.server.listen(8080); //默认端口8080  }}module.exports = MyWebsocket;

    接下来,要先分析下请求ws协议的请求头、响应头

    43c8ebc4-6c13-eb11-8da9-e4434bdf6706.png

    正常一个ws协议成功建立分下面这几个步骤

    客户端请求升级协议 

    GET / HTTP/1.1Upgrade: websocketConnection:UpgradeHost: example.comOrigin: http://example.comSec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==Sec-WebSocket-Version:13

    服务端响应,

    HTTP/1.1101SwitchingProtocolsUpgrade: websocketConnection:UpgradeSec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=Sec-WebSocket-Location: ws://example.com/

    以下是官方对这些字段的解释:

    •  Connection 必须设置 Upgrade,表示客户端希望连接升级。

    •  Upgrade 字段必须设置 Websocket,表示希望升级到 Websocket 协议。

    •  Sec-WebSocket-Key 是随机的字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 BASE-64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 Websocket 协议。

    •  Sec-WebSocket-Version 表示支持的 Websocket 版本。RFC6455 要求使用的版本是 13,之前草案的版本均应当弃用。

    •  Origin 字段是可选的,通常用来表示在浏览器中发起此 Websocket 连接所在的页面,类似于 Referer。但是,与 Referer 不同的是,Origin 只包含了协议和主机名称。

    •  其他一些定义在 HTTP 协议中的字段,如 Cookie 等,也可以在 Websocket 中使用。

    这里得先看这张图

    47c8ebc4-6c13-eb11-8da9-e4434bdf6706.png

    在第一次Http握手阶段,触发服务端的upgrade事件,我们把浏览器端的ws地址改成我们的自己实现的端口地址

    websocket的协议特点:

    • 建立在 TCP 协议之上,服务器端的实现比较容易。

    • 与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。

    • 数据格式比较轻量,性能开销小,通信高效。

    • 可以发送文本,也可以发送二进制数据。

    • 没有同源限制,客户端可以与任意服务器通信

    • 协议标识符是ws(如果加密,则为wss),服务器网址就是 URL。

    如果你想系统学习TCP通信协议,我给你一个优惠码,可以系统学习一下,将来如果你是去做一些有技术深度要求的工作,是很需要这个知识的(回扣我都会用来公众号抽奖送礼物,并非盈利性质,是真的觉得这个学习资料好才推荐)

    4bc8ebc4-6c13-eb11-8da9-e4434bdf6706.png

    回到正题,将客户端ws协议连接地址选择我们的服务器地址,然后改造服务端代码,监听upgrade事件看看

    const { EventEmitter } = require('events');const { createServer } = require('http');class MyWebsocket extends EventEmitter {constructor(options) {super(options);this.options = options;this.server = createServer();    options.port ? this.server.listen(options.port) : this.server.listen(8080); //默认端口8080// 处理协议升级请求this.server.on('upgrade', (req, socket, header) => {this.socket = socket;console.log(req.headers)      socket.write('hello');    });  }}module.exports = MyWebsocket;

    我们可以看到,监听到了协议请求升级事件,而且可以拿到请求头部。上面提到过:

    4fc8ebc4-6c13-eb11-8da9-e4434bdf6706.png

    •  Sec-WebSocket-Key 是随机的字符串,服务器端会用这些数据来构造出一个 SHA-1 的信息摘要。把 “Sec-WebSocket-Key” 加上一个特殊字符串 “258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算 SHA-1 摘要,之后进行 BASE-64 编码,将结果做为 “Sec-WebSocket-Accept” 头的值,返回给客户端。如此操作,可以尽量避免普通 HTTP 请求被误认为 Websocket 协议。

    说人话

    就是要给一个特定的响应头,告诉浏览器,这ws协议请求升级,我同意了。

    代码实现:

    const { EventEmitter } = require('events');const { createServer } = require('http');const crypto = require('crypto');const MAGIC_STRING = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; // 固定的字符串function hashWebSocketKey(key) {const sha1 = crypto.createHash('sha1'); // 拿到sha1算法  sha1.update(key + MAGIC_STRING, 'ascii');return sha1.digest('base64');}class MyWebsocket extends EventEmitter {constructor(options) {super(options);this.options = options;this.server = createServer();    options.port ? this.server.listen(options.port) : this.server.listen(8080); //默认端口8080this.server.on('upgrade', (req, socket, header) => {this.socket = socket;console.log(req.headers['sec-websocket-key'], 'key');const resKey = hashWebSocketKey(req.headers['sec-websocket-key']); // 对浏览器生成的key进行加密// 构造响应头const resHeaders = ['HTTP/1.1 101 Switching Protocols','Upgrade: websocket','Connection: Upgrade','Sec-WebSocket-Accept: ' + resKey,      ]        .concat('', '')        .join('\r\n');console.log(resHeaders, 'resHeaders');      socket.write(resHeaders); // 返回响应头部    });  }}module.exports = MyWebsocket;

    看看network面板,状态码已经变成了101,到这一步,我们已经把协议升级成功,并且写入了响应头

    50c8ebc4-6c13-eb11-8da9-e4434bdf6706.png

    剩下的就是数据交互了,既然ws是长链接+双工通讯,而且是应用层,建立在TCP之上封装的,这张图应该能很好的解释(来自阮一峰老师的博客)

    52c8ebc4-6c13-eb11-8da9-e4434bdf6706.png


    网络链路已经通了,协议已经打通,剩下一个长链接+数据推送了,但是我们目前还是一个普通的http服务器

    这是一个websocket的基本帧协议(其实websocket可以看成基于TCP封装的私有协议,只不过大家采用了某个标准达成了共识,有兴趣的可以看看微服务架构的相关内容,设计私有协议,端到端加密等)

    54c8ebc4-6c13-eb11-8da9-e4434bdf6706.png

    其中FIN代表是否为消息的最后一个数据帧(类似TCP的FIN,TCP也会分片传输)

    55c8ebc4-6c13-eb11-8da9-e4434bdf6706.png

    • RSV1,RSV2,Rsv3(每个占1位),必须是0,除非一个扩展协商为非零值定义的

    • Opcode表示帧的类型(4位),例如这个传输的帧是文本类型还是二进制类型,二进制类型传输的数据可以是图片或者语音之类的。(这4位转换成16进制值表示的意思如下):

    • 0x0 表示附加数据帧

    • 0x1 表示文本数据帧

    • 0x2 表示二进制数据帧

    • 0x3-7 暂时无定义,为以后的非控制帧保留

    • 0x8 表示连接关闭

    • 0x9 表示ping

    • 0xA 表示pong

    • 0xB-F 暂时无定义,为以后的控制帧保留

    Mask(占1位):表示是否经过掩码处理, 1 是经过掩码的,0是没有经过掩码的。如果Mask位为1,表示这是客户端发送过来的数据,因为客户端发送的数据要进行掩码加密;如果Mask为0,表示这是服务端发送的数据。

    payload length (7位+16位,或者 7位+64位),定义负载数据的长度。

       1. 如果数据长度小于等于125的话,那么该7位用来表示实际数据长度

       2. 如果数据长度为126到65535(2的16次方)之间,该7位值固定为126,也就是 1111110,往后扩展2个字节(16为,第三个区块表示),用于存储数据的实际长度。

       3. 如果数据长度大于65535, 该7位的值固定为127,也就是 1111111 ,往后扩展8个字节(64位),用于存储数据实际长度。

    Masking-key(0或者4个字节),该区块用于存储掩码密钥,只有在第二个子节中的mask为1,也就是消息进行了掩码处理时才有,否则没有,所以服务器端向客户端发送消息就没有这一块。

    Payload data 扩展数据,是0字节,除非已经协商了一个扩展。


    现在我们需要保持长链接

    58c8ebc4-6c13-eb11-8da9-e4434bdf6706.png

    ⚠️:如果你是使用Node.js开启基于TCP的私有双工长链接协议,也要开启这个选项

    const { EventEmitter } = require('events');const { createServer } = require('http');const crypto = require('crypto');const MAGIC_STRING = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; // 固定的字符串function hashWebSocketKey(key) {  const sha1 = crypto.createHash('sha1'); // 拿到sha1算法  sha1.update(key + MAGIC_STRING, 'ascii');  return sha1.digest('base64');}class MyWebsocket extends EventEmitter {  constructor(options) {    super(options);    this.options = options;    this.server = createServer();    options.port ? this.server.listen(options.port) : this.server.listen(8080); //默认端口8080    this.server.on('upgrade', (req, socket, header) => {      this.socket = socket;      socket.setKeepAlive(true);      console.log(req.headers['sec-websocket-key'], 'key');      const resKey = hashWebSocketKey(req.headers['sec-websocket-key']); // 对浏览器生成的key进行加密      // 构造响应头      const resHeaders = [        'HTTP/1.1 101 Switching Protocols',        'Upgrade: websocket',        'Connection: Upgrade',        'Sec-WebSocket-Accept: ' + resKey,      ]        .concat('', '')        .join('\r\n');      console.log(resHeaders, 'resHeaders');      socket.write(resHeaders); // 返回响应头部    });  }}module.exports = MyWebsocket;

    OK,现在最重要的一个通信长链接和头部已经实现,只剩下两点:

    • 进行与掩码异或运行拿到真实数据

    • 处理真实数据(根据opcode)

    提示:如果这两点你看不懂没关系,只是一个运算过程,当你自己基于TCP设计私有协议时候,也要考虑这些,msgType、payloadLength、服务端发包粘包、客户端收包粘包、断线重传、timeout、心跳、发送队列等


    给socket对象挂载事件,我们已经继承了EventEmitter模块

     socket.on('data', (data) => {        // 监听客户端发送过来的数据,该数据是一个Buffer类型的数据        this.buffer = data; // 将客户端发送过来的帧数据保存到buffer变量中        this.processBuffer(); // 处理Buffer数据      });      socket.on('close', (error) => {        // 监听客户端连接断开事件        if (!this.closed) {          this.emit('close', 1006, 'timeout');          this.closed = true;        }

    每次接受到了data,触发事件,解析Buffer,进行运算

     processBuffer() {    let buf = this.buffer;    let idx = 2; // 首先分析前两个字节    // 处理第一个字节    const byte1 = buf.readUInt8(0); // 读取buffer数据的前8 bit并转换为十进制整数    // 获取第一个字节的最高位,看是0还是1    const str1 = byte1.toString(2); // 将第一个字节转换为二进制的字符串形式    const FIN = str1[0];    // 获取第一个字节的后四位,让第一个字节与00001111进行与运算,即可拿到后四位    let opcode = byte1 & 0x0f; //截取第一个字节的后4位,即opcode码, 等价于 (byte1 & 15)    // 处理第二个字节    const byte2 = buf.readUInt8(1); // 从第一个字节开始读取8位,即读取数据帧第二个字节数据    const str2 = byte2.toString(2); // 将第二个字节转换为二进制的字符串形式    const MASK = str2[0]; // 获取第二个字节的第一位,判断是否有掩码,客户端必须要有    let length = parseInt(str2.substring(1), 2); // 获取第二个字节除第一位掩码之后的字符串并转换为整数    if (length === 126) {      // 说明125      length = buf.readUInt16BE(2); // 就用第三个字节及第四个字节表示数据的长度      idx += 2; // 偏移两个字节    } else if (length === 127) {      // 说明数据长度已经大于65535,16个位也已经不足以描述数据长度了,就用第三到第十个字节这八个字节来描述数据长度      const highBits = buf.readUInt32BE(2); // 从第二个字节开始读取32位,即4个字节,表示后8个字节(64位)用于表示数据长度,其中高4字节是0      if (highBits != 0) {        // 前四个字节必须为0,否则数据异常,需要关闭连接        this.close(1009, ''); //1009 关闭代码,说明数据太大;协议里是支持 63 位长度,不过这里我们自己实现的话,只支持 32 位长度,防止数据过大;      }      length = buf.readUInt32BE(6); // 获取八个字节中的后四个字节用于表示数据长度,即从第6到第10个字节,为真实存放的数据长度      idx += 8;    }    let realData = null; // 保存真实数据对应字符串形式    if (MASK) {      // 如果存在MASK掩码,表示是客户端发送过来的数据,是加密过的数据,需要进行数据解码      const maskDataBuffer = buf.slice(idx, idx + 4); //获取掩码数据, 其中前四个字节为掩码数据      idx += 4; //指针前移到真实数据段      const realDataBuffer = buf.slice(idx, idx + length); // 获取真实数据对应的Buffer      realData = handleMask(maskDataBuffer, realDataBuffer); //解码真实数据      console.log(`realData is ${realData}`);    }    let realDataBuffer = Buffer.from(realData); // 将真实数据转换为Buffer    this.buffer = buf.slice(idx + length); // 清除已处理的buffer数据    if (FIN) {      // 如果第一个字节的第一位为1,表示是消息的最后一个分片,即全部消息结束了(发送的数据比较少,一次发送完成)      this.handleRealData(opcode, realDataBuffer); // 处理操作码    }  }

    如果FIN不为0,那么意味着分片结束,可以解析Buffer。

    处理mask掩码(客户端发过来的是1,服务端发的是0)得到真正到数据

    function handleMask(maskBytes, data) {  const payload = Buffer.alloc(data.length);  for (let i = 0; i < data.length; i++) {    // 遍历真实数据    payload[i] = maskBytes[i % 4] ^ data[i]; // 掩码有4个字节依次与真实数据进行异或运算即可  }  return payload;}

    根据opcode(接受到的数据是字符串还是Buffer)进行处理:

      const OPCODES = {  CONTINUE: 0,  TEXT: 1,  BINARY: 2,  CLOSE: 8,  PING: 9,  PONG: 10,};  // 处理客户端发送过来的真实数据  handleRealData(opcode, realDataBuffer) {    switch (opcode) {      case OPCODES.TEXT:        this.emit('data', realDataBuffer.toString('utf8')); // 服务端WebSocket监听data事件即可拿到数据        break;      case OPCODES.BINARY: //二进制文件直接交付        this.emit('data', realDataBuffer);        break;      default:        this.close(1002, 'unhandle opcode:' + opcode);    }  }

    如果是Buffer就转换为utf8的字符串(如果是protobuffer协议,那么还要根据pb文件进行解析)


    接受数据已经搞定,传输数据无非两种,字符串和二进制,那么发送也是。


    下面把发送搞定

      send(data) {    let opcode;    let buffer;    if (Buffer.isBuffer(data)) {      // 如果是二进制数据      opcode = OPCODES.BINARY; // 操作码设置为二进制类型      buffer = data;    } else if (typeof data === 'string') {      // 如果是字符串      opcode = OPCODES.TEXT; // 操作码设置为文本类型      buffer = Buffer.from(data, 'utf8'); // 将字符串转换为Buffer数据    } else {      throw new Error('cannot send object.Must be string of Buffer');    }    this.doSend(opcode, buffer);  }  // 开始发送数据  doSend(opcode, buffer) {    this.socket.write(encodeMessage(opcode, buffer)); //编码后直接通过socket发送  }

    首先把要发送的数据都转换成二进制,然后进行数据帧格式拼装

    function encodeMessage(opcode, payload) {  let buf;  // 0x80 二进制为 10000000 | opcode 进行或运算就相当于是将首位置为1  let b1 = 0x80 | opcode; // 如果没有数据了将FIN置为1  let b2; // 存放数据长度  let length = payload.length;  console.log(`encodeMessage: length is ${length}`);  if (length < 126) {    buf = Buffer.alloc(payload.length + 2 + 0); // 服务器返回的数据不需要加密,直接加2个字节即可    b2 = length; // MASK为0,直接赋值为length值即可    buf.writeUInt8(b1, 0); //从第0个字节开始写入8位,即将b1写入到第一个字节中    buf.writeUInt8(b2, 1); //读8―15bit,将字节长度写入到第二个字节中    payload.copy(buf, 2); //复制数据,从2(第三)字节开始,将数据插入到第二个字节后面  }  return buf;}

    服务端发送的数据,Mask的值为0

    此时在外面监听事件,像平时一样使用ws协议一样即可。

    const MyWebSocket = require('./ws');const ws = new MyWebSocket({ port: 8080 });ws.on('data', (data) => {  console.log('receive data:' + data);  ws.send('this message from server');});ws.on('close', (code, reason) => {  console.log('close:', code, reason);});

    本文仓库地址源码:

    https://github.com/JinJieTan/my-websocket
    推荐阅读  点击标题可跳转

    JS 服务器推送技术 WebSocket 入门指北

    为什么你的网页需要 CSP?

    前端部署的发展历程

    觉得本文对你有帮助?请分享给更多人

    关注「前端大全」加星标,提升前端技能

    59c8ebc4-6c13-eb11-8da9-e4434bdf6706.png

    好文章,我在看❤️

    展开全文
  • WebSocket与http协议一样都是基于TCP的,所以他们都是可靠的协议,Web开发者调用的WebSocket的send函数在browser的实现中最终都是通过TCP的系统接口进行传输的。WebSocket和Http协议一样都属于应用层的协议,那么...

    WebSocket与http协议一样都是基于TCP的,所以他们都是可靠的协议,Web开发者调用的WebSocket的send函数在browser的实现中最终都是通过TCP的系统接口进行传输的。WebSocket和Http协议一样都属于应用层的协议,那么他们之间有没有什么关系呢?答案是肯定的,WebSocket在建立握手连接时,数据是通过http协议传输的,正如我们上一节所看到的“GET/chat HTTP/1.1”,这里面用到的只是http协议一些简单的字段。但是在建立连接之后,真正的数据传输阶段是不需要http协议参与的。

    具体关系可以参考下图:

    转载于:https://www.cnblogs.com/douglasvegas/p/4747200.html

    展开全文
  • WebSocket与http协议一样都是基于TCP的,所以他们都是可靠的协议,Web开发者调用的WebSocket的send函数在browser的实现中最终都是通过TCP的系统接口进行传输的。WebSocket和Http协议一样都属于应用层的协议,那么...
  • WebSocket与http协议一样都是基于TCP的,所以他们都是可靠的协议,Web开发者调用的WebSocket的send函数在browser 的实现中最终都是通过TCP的系统接口进行传输的。WebSocket和Http协议一样都属于应用层的协议,那么...
  • 看了之后思维还是很混乱,甚是说websocket基于tcp的。现在想想误导了我好久。既然来问区别的了对吧,基础谁不知道呢。直接上关键:websocket与tcp的联系:websocket建立握手是通过http的,后面真正传输不再进行...
  • 1. http协议:是用在应用层的协议,基于tcp协议,http协议建立链接也必须要有三次握手才能发送信息。 只能从客户端申请相应的请求到服务端,再从服务端获取返回数据。2.WebSocket:应用层协议,高效的解决Http协议...
  • 马夫 模板待办事项列表 创建一个新的项目。 验证 ,和。 查看。 首次。 在上述自述标记中设置插件ID。... 单击顶部的“监视”按钮,以通知有关包含新功能和修复的版本。... 这个花哨的IntelliJ平台插件将成为... 基于
  • 有四层,而且我们都知道高层的协议是基于低层协议的,所以当有人问我tcp和ip或者ip和 1.TCP和UDP TCP是面向连接的一种传输控制协议。TCP连接之后,客户端和服务器可以互相发送和接收消息,在客户端或者服务器没有...
  • 详情见: ...sub=B79B7EF52C9D420F83D28E201B48FCD7 ...关于TCP/IP协议所处的位置,上层模型基于下层模型,也就是我们在使用HTTP协议时时基于TCP/IP实现的。 三次握手(建立连接),四次挥手(断开连接) ssl...
  • 网页通常不能直接与tcp连接的设备直接通讯,虽然都是基于tcp,但是协议解析有点差别,通常是在服务器弄一个websocket代理,把tcp协议的数据转成websocket协议规范的数据,然后转发到真正的websocket服务器。...
  • websocket基于b/s的全双工通信

    千次阅读 2018-01-28 13:59:38
    websocket是一种h5的b/s长连接全双工通信,与ajax不同,ajax是基于http协议的...但是websocket与服务端通信时虽然底层也是基于tcp协议的,但是他不是浏览器发起请求然后服务端响应的这种模式,websocket是全双工通信的
  • WebSocket是在HTML5基础上单个TCP连接上进行全双工通讯的协议,只要浏览器和服务器进行一次握手,就可以建立一条快速通道,两者就可以实现数据互传了。说白了,就是打破了传统的http协议的无状态传输(只能浏览器...
  • 1)ws客户端其实是能直接连接tcp服务器的 但是可见连接后,直接error-->close了,这是因为没有握手协议的支持,所以失败, 2)下面添加了握手协议后,tcp服务器就可以升级为ws服务器 3)client.html <!...
  • 与此同时随着互联网的发展在HTML5中提出了websocket协议,能更好的节省服务器资源和带宽并且服务器和浏览器能够双向实时通讯。为了能让用户体验传统客户端和web带来的即时通信结合的超爽体验,本次...
  • MagicSocketDebugger:套接字调试工具,TCP服务器,TCP客户端,websocket服务器,websocket客户端,心跳包(基于qt5.9)----套接字调试工具,TCP服务端,TCP客户端,WebSocket服务端,WebSocket客户端,心跳包(基于...
  • Websocket TCP/IP Http 协议的关系

    千次阅读 2016-09-13 11:27:26
    作者:吴桐 ...来源:知乎 著作权归作者所有,转载...先说结论:“websocket出现是因为浏览器不给开后门”,“不是WebSocket基于HTTP,相反,可以看成可以看成可以看成HTTP基于WebSocket”。 要理解为什么会出现HTTP,Web
  • JSTP / JavaScript传输协议 JSTP是一个RPC协议和框架,提供双向异步数据传输,并支持多个并行非阻塞交互,该交互非常透明,以至于应用程序甚至无法区分本地异步功能和远程过程。 而且,作为一项不错的奖励,捆绑了...
  • WebSocket、HTTP 与 TCP

    2020-01-16 11:31:25
    从上面的图中可以看出,HTTP、WebSocket 等应用层协议,都是基于 TCP 协议来传输数据的。我们可以把这些高级协议理解成对 TCP 的封装。 既然大家都使用 TCP 协议,那么大家的连接和断开,都要遵循TCP 协议中的三次...
  • websocket学习

    2018-08-01 15:21:27
    WebSocket概念 ... 在WebSocket API中,浏览器和服务器只需要做一个握手动作,两者之间...WebSocket基于TCP双向全双工进行消息传递,相比HTTP的半双工协议,性能得到很大提升。 具有以下特点: 单一的TCP连接,采...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 1,674
精华内容 669
关键字:

websocket基于tcp