webSocket详解:技术原理+前后端实现

2023-11-13

webSocket详解:技术原理+前后端实现

一、webSocket技术原理

1、内容简介

websocket就是通过服务器向客户端推送消息,客户端也可以主动向服务器发送消息,是真正的双向平等对话,是一种长连接,只需要通过一次请求进行初始化。

2、基本机制

1.websocket是一个持久化协议,通过一次http request建立连接后,后续不需要重新发送httprequest。2.websocket是双向通信的连接,一个tcp连接上既可以发送,也可以接收。
3.具有多路复用功能,几个不同的url用同一个websocket连接,类似/websocket/{userid},不同的user推送消息。
以下为协议机制:
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在http协议握手中,多了key,客户端请求时添加key,服务器根据加密解密方式返回accept,则成功建立。

3、实现原理

二、前端原理、实现

在这里插入图片描述

三、后端原理、实现

在这里插入图片描述
发送消息可以采用以下方法:
session.getBasicRemote().sendText(message, false);
session.getBasicRemote().sendBinary(data);
session.getBasicRemote().sendText(message, true);
在这里插入图片描述

四、实例:基于Netty实现websocket消息推送

要实现服务端推送消息至客户端,并且支持客户端对用户点对点消息发送的社交功能。服务端给客户端推送消息,可以选择原生的WebSocket,或者更加高级的Netty框架实现。

使用netty,因为一款好的框架一般都是在原生的基础上进行包装成更加实用方便,很多我们需要自己考虑的问题都基本可以不用去考虑,不过此文不会去讲netty有多么的高深莫测,因为这些概念性的东西随处可见,而是通过实战来达到推送消息的目的。

整合Netty和WebSocket。我们需要使用netty对接websocket连接,实现双向通信,这一步需要有服务端的netty程序,用来处理客户端的websocket连接操作,例如建立连接,断开连接,收发数据等。
后端为netty框架,前端为websocket

websocket消息推送实现思路

前端使用WebSocket与服务端创建连接的时候,将用户ID传给服务端,服务端将用户ID与channel关联起来存储,同时将channel放入到channel组中。

如果需要给所有用户发送消息,直接执行channel组的writeAndFlush()方法;

如果需要给指定用户发送消息,根据用户ID查询到对应的channel,然后执行writeAndFlush()方法;

前端获取到服务端推送的消息之后,将消息内容展示到文本域中

4.1 引入netty依赖
<dependency>
	<groupId>io.netty</groupId>
	<artifactId>netty-all</artifactId>
	<version>4.1.33.Final</version>
</dependency>
 
<dependency>
	<groupId>cn.hutool</groupId>
	<artifactId>hutool-all</artifactId>
	<version>5.2.3</version>
</dependency>
4.2 修改配置yml
server:
  port: 8899
 
#netty的配置信息(端口号,webSocket路径)
webSocket:
  netty:
    port: 58080
    path: /webSocket
    readerIdleTime: 30 #读空闲超时时间设置(Netty心跳检测配置)
    writerIdleTime: 30 #写空闲超时时间设置(Netty心跳检测配置)
    allIdleTime: 30 #读写空闲超时时间设置(Netty心跳检测配置)
4.3 创建config

创建单例channel组,管理所有通道,定义一个map,管理用户和通道的关系

import io.netty.channel.Channel;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.GlobalEventExecutor;
import java.util.concurrent.ConcurrentHashMap;
 
/**
 * NettyConfig类
 *
 * @author hs
 * @date 2021-09-18
 */
public class NettyConfig {
 
    /**
     * 定义一个channel组,管理所有的channel
     * GlobalEventExecutor.INSTANCE 是全局的事件执行器,是一个单例
     */
    private static ChannelGroup channelGroup = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
 
    /**
     * 存放用户与Chanel的对应信息,用于给指定用户发送消息
     */
    private static ConcurrentHashMap<String,Channel> userChannelMap = new ConcurrentHashMap<>();
 
    private NettyConfig() {}
 
    /**
     * 获取channel组
     * @return
     */
    public static ChannelGroup getChannelGroup() {
        return channelGroup;
    }
 
    /**
     * 获取用户channel map
     * @return
     */
    public static ConcurrentHashMap<String,Channel> getUserChannelMap(){
        return userChannelMap;
    }
}
4.4 创建netty初始化

定义两个EventLoopGroup,bossGroup辅助客户端的tcp连接请求, workGroup负责与客户端之间的读写操作,需要说明的是,需要开启一个新的线程来执行netty server,要不然会阻塞主线程,到时候就无法调用项目的其他controller接口了

import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
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.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.codec.serialization.ObjectEncoder;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
 
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.net.InetSocketAddress;
import java.util.concurrent.TimeUnit;
 
/**
 * Netty初始化服务
 *
 * @author hs
 */
@Component
public class NettyServer{
 
    private static final Logger log = LoggerFactory.getLogger(NettyServer.class);
 
    /**
     * webSocket协议名
     */
    private static final String WEBSOCKET_PROTOCOL = "WebSocket";
 
    /**
     * 端口号
     */
    @Value("${webSocket.netty.port}")
    private int port;
 
    /**
     * webSocket路径
     */
    @Value("${webSocket.netty.path}")
    private String webSocketPath;
 
    /**
     * 在Netty心跳检测中配置 - 读空闲超时时间设置
     */
    @Value("${webSocket.netty.readerIdleTime}")
    private long readerIdleTime;
 
    /**
     * 在Netty心跳检测中配置 - 写空闲超时时间设置
     */
    @Value("${webSocket.netty.writerIdleTime}")
    private long writerIdleTime;
 
    /**
     * 在Netty心跳检测中配置 - 读写空闲超时时间设置
     */
    @Value("${webSocket.netty.allIdleTime}")
    private long allIdleTime;
 
    @Autowired
    private WebSocketHandler webSocketHandler;
 
    private EventLoopGroup bossGroup;
    private EventLoopGroup workGroup;
 
    /**
     * 启动
     * @throws InterruptedException
     */
    private void start() throws InterruptedException {
        bossGroup = new NioEventLoopGroup();
        workGroup = new NioEventLoopGroup();
        ServerBootstrap bootstrap = new ServerBootstrap();
        // bossGroup辅助客户端的tcp连接请求, workGroup负责与客户端之前的读写操作
        bootstrap.group(bossGroup,workGroup);
        // 设置NIO类型的channel
        bootstrap.channel(NioServerSocketChannel.class);
        // 设置监听端口
        bootstrap.localAddress(new InetSocketAddress(port));
        // 连接到达时会创建一个通道
        bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
 
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                // 心跳检测(一般情况第一个设置,如果超时了,则会调用userEventTriggered方法,且会告诉你超时的类型)
                ch.pipeline().addLast(new IdleStateHandler(readerIdleTime, writerIdleTime, allIdleTime, TimeUnit.MINUTES));
                // 流水线管理通道中的处理程序(Handler),用来处理业务
                // webSocket协议本身是基于http协议的,所以这边也要使用http编解码器
                ch.pipeline().addLast(new HttpServerCodec());
                ch.pipeline().addLast(new ObjectEncoder());
                // 以块的方式来写的处理器
                ch.pipeline().addLast(new ChunkedWriteHandler());
                /*
                    说明:
                    1、http数据在传输过程中是分段的,HttpObjectAggregator可以将多个段聚合
                    2、这就是为什么,当浏览器发送大量数据时,就会发送多次http请求
                 */
                ch.pipeline().addLast(new HttpObjectAggregator(8192));
                /*
                    说明:
                    1、对应webSocket,它的数据是以帧(frame)的形式传递
                    2、浏览器请求时 ws://localhost:58080/xxx 表示请求的uri
                    3、核心功能是将http协议升级为ws协议,保持长连接
                */
                ch.pipeline().addLast(new WebSocketServerProtocolHandler(webSocketPath, WEBSOCKET_PROTOCOL, true, 65536 * 10));
                // 自定义的handler,处理业务逻辑
                ch.pipeline().addLast(webSocketHandler);
            }
        });
        // 配置完成,开始绑定server,通过调用sync同步方法阻塞直到绑定成功
        ChannelFuture channelFuture = bootstrap.bind().sync();
        log.info("Server started and listen on:{}",channelFuture.channel().localAddress());
        // 对关闭通道进行监听
        channelFuture.channel().closeFuture().sync();
    }
 
    /**
     * 释放资源
     * @throws InterruptedException
     */
    @PreDestroy
    public void destroy() throws InterruptedException {
        if(bossGroup != null){
            bossGroup.shutdownGracefully().sync();
        }
        if(workGroup != null){
            workGroup.shutdownGracefully().sync();
        }
    }
 
    /**
     * 初始化(新线程开启)
     */
     //PostConstruct为spring初始加载的一个注解
    @PostConstruct()
    public void init() {
        //需要开启一个新的线程来执行netty server 服务器
        new Thread(() -> {
            try {
                start();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

注意:启动方法需要开启一个新线程执行netty server服务,服务中配置了IdleStateHandler心跳检测,此类要在创建一个通道的第一个设置,如果超时了,则会调用userEventTriggered方法,且会告诉你超时的类型)

4.5 实现webhandle业务

创建Netty配置的操作执行类WebSocketHandler,userEventTriggered为心跳检测超时所调用的方法,超时后ctx.channel().close()执行完毕会主动调用handlerRemoved删除通道及用户信息

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import io.netty.channel.*;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.util.AttributeKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
 
 
/**
 * 操作执行类
 *
 * TextWebSocketFrame类型,表示一个文本帧
 * @author hs
 */
@Component
@ChannelHandler.Sharable
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
 
    private static final Logger log = LoggerFactory.getLogger(WebSocketHandler.class);
 
    /**
     * 一旦连接,第一个被执行
     * @param ctx
     * @throws Exception
     */
    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        log.info("handlerAdded 被调用"+ctx.channel().id().asLongText());
        // 添加到channelGroup 通道组
        NettyConfig.getChannelGroup().add(ctx.channel());
    }
 
    /**
     * 读取数据
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        // 获取用户ID,关联channel
        JSONObject jsonObject = JSONUtil.parseObj(msg.text());
        String uid = jsonObject.getStr("uid");
        // 当用户ID已存入通道内,则不进行写入,只有第一次建立连接时才会存入,其他情况发送uid则为心跳需求
        if(!NettyConfig.getUserChannelMap().containsKey(uid)){
            log.info("服务器收到消息:{}",msg.text());
            NettyConfig.getUserChannelMap().put(uid,ctx.channel());
            // 将用户ID作为自定义属性加入到channel中,方便随时channel中获取用户ID
            AttributeKey<String> key = AttributeKey.valueOf("userId");
            ctx.channel().attr(key).setIfAbsent(uid);
            // 回复消息
            ctx.channel().writeAndFlush(new TextWebSocketFrame("服务器连接成功!"));
        }else{
            // 前端定时请求,保持心跳连接,避免服务端误删通道
            ctx.channel().writeAndFlush(new TextWebSocketFrame("keep alive success!"));
        }
    }
 
    /**
     * 移除通道及关联用户
     * @param ctx
     * @throws Exception
     */
    @Override
    public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
        log.info("handlerRemoved 被调用"+ctx.channel().id().asLongText());
        // 删除通道
        NettyConfig.getChannelGroup().remove(ctx.channel());
        removeUserId(ctx);
    }
 
    /**
     * 异常处理
     * @param ctx
     * @param cause
     * @throws Exception
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.info("异常:{}",cause.getMessage());
        // 删除通道
        NettyConfig.getChannelGroup().remove(ctx.channel());
        removeUserId(ctx);
        ctx.close();
    }
 
    /**
     * 心跳检测相关方法 - 会主动调用handlerRemoved
     * @param ctx
     * @param evt
     * @throws Exception
     */
    @Override
    public void userEventTriggered(final ChannelHandlerContext ctx, Object evt) throws Exception {
        if(evt instanceof IdleStateEvent){
            IdleStateEvent event = (IdleStateEvent)evt;
            if(event.state() == IdleState.ALL_IDLE){
                //清除超时会话
                ChannelFuture writeAndFlush = ctx.writeAndFlush("you will close");
                writeAndFlush.addListener(new ChannelFutureListener() {
 
                    @Override
                    public void operationComplete(ChannelFuture future) throws Exception {
                        ctx.channel().close();
                    }
                });
            }
        }else{
            super.userEventTriggered(ctx, evt);
        }
    }
 
    /**
     * 删除用户与channel的对应关系
     * @param ctx
     */
    private void removeUserId(ChannelHandlerContext ctx){
        AttributeKey<String> key = AttributeKey.valueOf("userId");
        String userId = ctx.channel().attr(key).get();
        NettyConfig.getUserChannelMap().remove(userId);
        log.info("删除用户与channel的对应关系,uid:{}",userId);
    }
}
4.6 具体消息推送接口
/**
 * 推送消息接口
 *
 * @author hs
 */
public interface PushService {
 
    /**
     * 推送给指定用户
     * @param userId 用户ID
     * @param msg 消息信息
     */
    void pushMsgToOne(String userId,String msg);
 
    /**
     * 推送给所有用户
     * @param msg 消息信息
     */
    void pushMsgToAll(String msg);
 
    /**
     * 获取当前连接数
     * @return 连接数
     */
    int getConnectCount();
}
 
4.7 实现类
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentHashMap;
 
/**
 * 推送消息接口实现类
 *
 * @author hs
 */
@Service
public class PushServiceImpl implements PushService {
 
    /**
     * 推送给指定用户
     * @param userId 用户ID
     * @param msg 消息信息
     */
    @Override
    public void pushMsgToOne(String userId, String msg){
        ConcurrentHashMap<String, Channel> userChannelMap = NettyConfig.getUserChannelMap();
        Channel channel = userChannelMap.get(userId);
        channel.writeAndFlush(new TextWebSocketFrame(msg));
    }
 
    /**
     * 推送给所有用户
     * @param msg 消息信息
     */
    @Override
    public void pushMsgToAll(String msg){
        NettyConfig.getChannelGroup().writeAndFlush(new TextWebSocketFrame(msg));
    }
 
    /**
     * 获取当前连接数
     * @return 连接数
     */
    @Override
    public int getConnectCount() {
        return NettyConfig.getChannelGroup().size();
    }
}
 
4.8 提供消息推送服务的Controller
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
 
/**
 * 请求Controller(用于postman测试)
 *
 * @author hs
 */
@RestController
@RequestMapping("/push")
public class PushController {
 
    @Autowired
    private PushService pushService;
 
    /**
     * 推送给所有用户
     * @param msg 消息信息
     */
    @PostMapping("/pushAll")
    public void pushToAll(@RequestParam("msg") String msg){
        pushService.pushMsgToAll(msg);
    }
 
    /**
     * 推送给指定用户
     * @param userId 用户ID
     * @param msg 消息信息
     */
    @PostMapping("/pushOne")
    public void pushMsgToOne(@RequestParam("userId") String userId,@RequestParam("msg") String msg){
        pushService.pushMsgToOne(userId,msg);
    }
 
    /**
     * 获取当前连接数
     */
    @GetMapping("/getConnectCount")
    public int getConnectCout(){
        return pushService.getConnectCount();
    }
}
4.9. Web前端通过websocket与服务端连接
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="/static/jquery-2.2.4.min.js" charset="utf-8"></script>
</head>
<body>
<script>
    var socket;
    var userId = "123456";
    // 判断当前浏览器是否支持webSocket
    if(window.WebSocket){
        socket = new WebSocket("ws://127.0.0.1:58080/webSocket")
        // 相当于channel的read事件,ev 收到服务器回送的消息
        socket.onmessage = function (ev) {
            var rt = document.getElementById("responseText");
            rt.value = rt.value + "\n" + ev.data;
        }
        // 相当于连接开启
        socket.onopen = function (ev) {
            var rt = document.getElementById("responseText");
            rt.value = "连接开启了..."
            socket.send(
                JSON.stringify({
                    // 连接成功将,用户ID传给服务端
                    uid: userId
                })
            );
        }
 
        //接受到服务端关闭连接时的回调方法
        socket.onclose = function (ev) {
            var rt = document.getElementById("responseText");
            rt.value = rt.value + "\n" + "连接关闭了...";
        }
 
   // 监听窗口事件,当窗口关闭时,主动断开websocket连接,防止连接没断开就关闭窗口,server端报错
         window.onbeforeunload = function(){
            socket.close();
         }
 
    }
    else
    {
        alert("当前浏览器不支持webSocket")
    }
 
    // 如果前端需要保持连接,则需要定时往服务器针对自己发送请求,返回的参数和发送参数一致则证明时间段内有交互,服务端则不进行连接断开操作
    var int = self.setInterval("clock()",10000);
    function clock() {
        socket.send(
            JSON.stringify({
                // 连接成功将,用户ID传给服务端
                uid: userId
            })
        );
    }
 
</script>
<form onsubmit="return false">
    <textarea id="responseText" style="height: 150px; width: 300px;"></textarea>
    <br>
    <input type="button" value="清空内容" onclick="document.getElementById('responseText').value=''">
</form>
</body>
</html>
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

webSocket详解:技术原理+前后端实现 的相关文章

随机推荐

  • elk笔记13--Queries-geo queries

    elk笔记13 Queries geo queries 1 geo查询简介 2 geo 查询案例 3 说明 1 geo查询简介 geo queries 地理位置查询 支持2类geo数据 分别为geo point 和geo shape 2中类
  • 16-数据结构-图的存储结构

    简介 主要为图的顺序存储和链式存储 其中顺序存储即邻接矩阵的画法以及代码 邻接矩阵又分为有权图和无权图 区别就是有数据的地方填权值 无数据的地方可以填0或者 而有权图和无权图 又细分为有向图和无向图 无向图为对称矩阵 因为没有方向可言 出度
  • nRF52832 — 使用nRF52832的I2S播放音频

    XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX XX 作 者 文化人 XX 联系方式 XX 版权声明 原创文章 欢迎评论和转载 转载时能告诉我一声就最好了 XX 要说的话
  • VSCode命令行常用快捷操作

    一 VSCode基本设置 1 打开命令面板 F1 或 Command Shift P 2 将VSCode安装到系统环境变量 打开命令面板 搜索PATH 选择在PATH中安装code命令 然后重启终端就可以生效 以后在命令行就可以通过code
  • uniapp软键盘把界面顶起问题解决

    pages json文件加上解决 app plus softinputMode adjustResize pages path pages index index style navigationBarTitleText 中奥一卡通 app
  • GoJS-FlowChart样例代码分析

    var go GraphObject make 别名 方便使用 myDiagram go Diagram myDiagramDiv 通过id指定画布绑定的div LinkDrawn showLinkLabel 监听LinkDrawn事件 其
  • Canvas-drawImage无法绘图,出现空白的解决办法

    目录 文章目录 问题剖析 解决办法 例子代码 测试样例 问题剖析 在操作drawImage 函数时 经常会出现调取正常 但canvas绘制出现空白的情况 这种情况 原因可以归为 浏览器在加载图片时 图片尚未加载完毕 便开始绘图 主要原因为
  • 本地项目上传到gitlab上

    1 gitlab上新建项目 2 本地操作 进入到项目目录 打开git操作命令窗口 鼠标右键选择Git Bash Here 1 将git初始化至该目录下 会生成 git配置文件 执行命令 git init 2 将远程的仓库分支来取到本地 执行
  • Qt信号详解

    QAbstractButton clicked 按下松开后执行 pressed 按下后执行 released 松开后执行 clicked bool checked 需要选中checkable属性 每次点击checked的值都会切换 可以使用
  • Linux 下配置JDK 并运行springboot项目进行访问 Tomcat的安装

    前言 记忆 1 步骤 一 JDK安装 我们开发java程序必须要的环境 1 下载JDK rpm 去oralce 官网下载即可 百度jdk即可 2 安装java环境 检测当前系统是否存在java环境 java version 如果有的话就需要
  • 机器学习入门教学——人工智能、机器学习、深度学习

    1 人工智能 人工智能相当于人类的代理人 我们现在所接触到的人工智能基本上都是弱AI 主要作用是正确解释从外部获得的数据 并对这些数据加以学习和利用 以便灵活的实现特定目标和任务 例如 阿尔法狗 智能汽车 简单来说 人工智能使机器像人类一样
  • 后端转前端,该如何快速学习

    后端转前端还是比较容易的 学习难度降低了 多花点时间实操 基本可以接手项目 首先初学者要知道web前端工程师的目前核心技术 主要有HTML5 CSS JavaScript JQuery Ajax等技术 而且想要成为一名优秀的web前端工程师
  • 4月13面试网络工程师实习岗位的感想

    第一个自我介绍 说一下自我介绍吧 面试官一来就叫我自我介绍 这是必须的 然后我就说了一下我叫谁 我现在读大几 学什么专业 然后说一下平时爱听音乐 打球啊 爬山啊 爱看电影 然后说到爱看电影 面试官就笑了 然后就行了 就开始问问题了 聊聊天了
  • zookeeper、dubbo、kafka简单了解

    1 zookeeper如何实现高可用 1 zookeeper 多台构成集群实现高可用 有三种角色群首 leader 追随者 follower 观察者 observer Leader作为整个ZooKeeper集群的主节点 负责响应所有对Zoo
  • 自动化设计-框架介绍

    3 框架介绍 由于软件测试的工作量很大 40 到60 的总开发时间 而又有很大部分适于自动化 因此 测试的改进会对整个开发工作的质量 成本和周期带来非常显著的效果 通过第二部分对Ruby Watir框架的介绍 下面我们正式进入自动化测试框架
  • XSS跨站脚本攻击

    以下是自己的一些见解与总结 若有不足或者错误的地方请各位指出 目录 1 简介 2 XSS攻击的危害包括 3 XSS攻击分类 4 XSS攻击实例分析 5 XSS漏洞修复 1 简介 跨站脚本 cross site script 为了避免与样式c
  • Python~ModuleNotFoundError: No module named ‘pydotplus‘

    错误代码 import pydotplus 错误原因 没有安装pydotplus库 解决方案 安装对应的库 pip install pydotplus 重新导入模块 import pydotplus
  • 获取内网IP地址

    工作中遇到一个需求 要求根据不同场地的IP地址 显示相应的大屏页面 首先要配置一下Chrome浏览器 1 在Chrome浏览器中输入 chrome flags 2 搜索 enable webrtc hide local ips with m
  • 量化交易,关于止损止盈的一点思考

    如何设置止损止盈指标才能达到 使损失基本稳定 让盈利充分攀升 止损位设置 如果当天可以卖出 止损位就是成本 1 X 如x设为5 就是股价跌5 就止损 一般就是5 略多一点损失 略多一点是因为可能需要降价保证抛出和交易成本 如果考虑交易成本
  • webSocket详解:技术原理+前后端实现

    webSocket详解 技术原理 前后端实现 一 webSocket技术原理 1 内容简介 websocket就是通过服务器向客户端推送消息 客户端也可以主动向服务器发送消息 是真正的双向平等对话 是一种长连接 只需要通过一次请求进行初始化