flask后端+网页前端:基于 socket.io 的双向通信和服务器部署

news2025/5/24 23:41:05

我想实现的效果是,我的服务器提供两个路由网址,网页A用于拍照、然后录音,把照片和录音传给服务器,服务器发射信号,通知另一个路由的网页B更新,把刚刚传来的照片和录音显示在网页上。

然后网页B用户根据这个照片和录音,回答一些问题,输入答案的文本,传给服务器。服务器拿到答案后,再发射信号,把这个结果显示在网页A上。

这就得用到双向通信(其实有点类似两个网页聊天的功能,而且支持发送语音、图片、文本三种消息)。这里用的是 socket.io 包。在本地写还是很好写的,但是,部署到服务器上之后,就出了很多 bug。很多坑,这里把我遇到的记下来,防止再次犯错。

这里只记录关键代码,也就是容易掉坑的代码。整个项目我之后会上传到 github 上。传好了补连接。

整体的逻辑

  1. 建立双向通道
  2. 网页A上传文件,得到服务器传回的提示信号(code: 200),然后 socket.emit("upload_completed"),通知服务器数据已经上传了
  3. 服务器监听 upload_completed 信号,收到该信号后,服务器作为中转站,广播信号 emit('data_updated', data, broadcast=True) 通知前端该更新数据了
  4. 网页 B 监听 data_updated 信号,修改自己的页面,展示图片和录音。等用户在该网页填好答案之后,点击发送按钮,网页 B 发生信号 socket.emit("annotated_answer")
  5. 服务器监听 annotated_answer 信号,收到该信号后,作为中转站,广播信号 emit('send_answer', answer, broadcast=True)
  6. 网页 A 监听信号 send_answer ,收到该信号后,把结果显示在网页上

特别拎出来的坑,特别注意

  1. 运行 flask 代码的时候调用 socketio 的 run 方法,不是用 app 的 run 方法,不然没法双向连接的;但是在服务器端部署的时候,用 uwsgi 跑上,它就是默认调用 app.run,很崩溃的啊这个;所以服务器端部署的时候,用 gunicorn (这个部署,真的,翻遍全网才找到,落泪了)
  2. socket 连接的地址,本地调试的时候填的是 localhost,但是传到服务器要改成服务器的地址,不该的话,连不上的!!
  3. 刚刚更新模型的时候又出毛病,爬上来更新。这次没有报任何错误,但是网页就是访问不到。检查了一个小时,发现是因为梯子忘记关了
  4. 部署后手机端打不开录音设备和摄像头,那是因为,媒体设备只能在 https 协议下,或者 http://localhost 下访问,所以要用这个功能,就必须去申请 ssl 证书。阿里云有免费的 20 张,好好把握。

flask 代码

我这里只贴最关键的代码,加上注释,直接把这个代码粘上去,是会报错的。

@app.route('/upload', methods=['POST'])
def app_upload_file():
    # 保存图片
    img_file = request.files['img']
    if img_file.filename == '':
        return jsonify({'error': 'No image'}), 400
    try:
        image_path = os.path.join(app.config['UPLOAD_FOLDER'], img_file.filename)
        img_file.save(image_path)
        shutil.copy(image_path, os.path.join(os.path.dirname(__file__), 'static/show.jpg'))  # 用于展示在网页上
        log(f"save image: {image_path}")
    except Exception as e:
        return jsonify({'error': str(e)}), 500

    try:
        # 传过来的就是文本
        question = request.form['question']
    except:
        question = "请描述图片内容"
    return jsonify({"image": img_file.filename, "question": question})


@app.route('/upload/speech', methods=['POST'])
def recognize_speech():
    speech_file = request.files['speech']
    try:
        save_path = os.path.join(app.config['UPLOAD_FOLDER'], speech_file.filename)
        speech_file_path = os.path.join(app.config['UPLOAD_FOLDER'], save_path)
        speech_file.save(speech_file_path)
        # question = speech2txt(speech_file_path)
        # print('百度识别结果:', question)
    except Exception as e:
        return jsonify({'error': str(e)}), 500
    return jsonify({"speech": speech_file.filename})


@socketio.on('upload_completed')
def handle_upload_completed(data):
    # pip install flask-socketio eventlet
    print(data)
    try:
        emit('data_updated', data, broadcast=True)
    except Exception as e:
        print(e)
        emit('error', {'error': str(e)})


@socketio.on('upload_speech_completed')
def handle_upload_speech_completed(data):
    # pip install flask-socketio eventlet
    try:
        emit('data_speech_updated', data, broadcast=True)
    except Exception as e:
        print(e)
        emit('error', {'error': str(e)})


@socketio.on('annotated_answer')
def handle_annotated_answer(answer):
    log(f'get answer from annotator: {answer}')
    try:
        emit('send_answer', answer, broadcast=True)
    except Exception as e:
        print(e)

if __name__ == '__main__':
    # app.run(host='0.0.0.0', port=8099)
    # 这个地方!!看清楚!看清楚!要调用 socketio 的 run 方法,不是用 app 的 run 方法,不然没法双向连接的
    socketio.run(app, host='0.0.0.0', allow_unsafe_werkzeug=True, port=8099)

网页 A 的代码

注意这里只贴了一部分代码,关于文件怎么上传的,也就是引入的 camera.js 和 recorder.js 这俩文件的内容,在我这这篇文章里贴了: flask 后端 + 微信小程序和网页两种前端:调用硬件(相机和录音)和上传至服务器

html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='css/full_button.css') }}" type="text/css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script>
</head>
<body>
    <div style="display: flex">
        <div>
            <video id="videoElement" autoplay="autoplay" muted="muted" style="width: 40px"></video>
            <img id="photo" alt="你的照片" src="" style="display: none">
        </div>
        <div id="answer" class="answer-text">答案等待中...</div>
    </div>

    <div class="button-grid">
        <button id="snapButton">拍摄照片</button>
        <button id="recorderButton">录音</button>
        <button id="captionButton">描述图片</button>
        <button id="vqaButton">回答问题</button>
    </div>

{#    <input type="text" id="textQuestion" placeholder="请输入问题...">#}
    <script>
    	// 这里最最最关键的就是这个网址,如果你在本地跑,要填 localhost,不能填 127.0.0.1;如果是部署在服务器,要填成服务器的地址,不然肯定是连不上的。
        const socket = io.connect('http://localhost:8099'); // 连接到Flask服务器
        socket.on('send_answer', function (data) {
            // 接收到服务器返回的答案,震动提示,把答案显示在页面上
            console.log('接收到答案:', data);
            document.getElementById('answer').textContent = data;
            navigator.vibrate([200]);  // 震动提示收到答案
        })
        var imageBlob = null;  // 拍摄的图片
        var speechBlob = null;  // 提出的问题

        // 生成随机文件名
        function randomFilename() {
            let now = new Date().getTime();
            let str = `xxxxxxxx-xxxx-${now}-yxxx`;
            return str.replace(/[xy]/g, function(c) {
                const r = Math.random() * 16 | 0;
                const v = c === 'x' ? r : (r & 0x3 | 0x8);
                return v.toString(16)
            })
        }
    </script>
    <script type="text/javascript" src="../static/js/user_camera.js"></script>
    <script type="text/javascript" src="../static/js/user_recorder.js"></script>
    <script>
        // 绑定 caption 按钮
        document.getElementById('captionButton').onclick = function () {
            if (imageBlob == null) {
                alert('请先拍摄照片,再点击“描述图片”按钮')
            } else {
                const captionFormData = new FormData();
                let imgFilename = randomFilename()+'.jpg';
                captionFormData.append('img', imageBlob, imgFilename);
                captionFormData.append('question', '请描述图片内容');
                fetch('http://localhost:8099/upload', {
                method: 'POST',
                body: captionFormData
                })
                .then(response => {
                    console.log('response:', response);
                    if (response.status === 200) {
                        console.log('发射信号 upload_completed');
                        // 注意!!这里发射的信号,带的数据,得是URL.createObjectURL(imageBlob)不能是别的不能是别的不能是别的,重要的事情说3遍!!不然无法正确地显示在网页 B 上
                        socket.emit('upload_completed', {'image': URL.createObjectURL(imageBlob),
                            'question': '请描述图片内容'});
                    }
                    })
                .then(data => console.log('data:', data))
                .catch(error => console.error(error));
            }
        };
        // 绑定 vqa 按钮
        document.getElementById('vqaButton').onclick = function () {
            if (imageBlob == null) {
                alert('请先拍摄照片,再点击“描述图片”按钮')
            } else {
                if (speechBlob == null) {
                    alert('您还没有提问,请先点击录音按钮录音提问')
                } else {
                    let filename = randomFilename();
                    // 先发语音再发图片,因为发了图片之后会提示听录音
                    const speechFormData = new FormData();
                    speechFormData.append('speech', speechBlob, filename+'.wav');
                    fetch('http://localhost:8099/upload/speech', {
                        method: 'POST',
                        body: speechFormData
                    })
                    .then(response => {
                        console.log('response:', response);
                        if (response.status === 200) {
                            console.log('成功上传音频', response);
                            socket.emit('upload_speech_completed',
                                {'speech': window.URL.createObjectURL(speechBlob)})
                        }
                    })
                    .then(data => console.log('data:', data))
                    .catch(error => console.error(error));

                    const imgFormData = new FormData();
                    imgFormData.append('img', imageBlob, filename+'.jpg');
                    fetch('http://localhost:8099/upload', {
                        method: 'POST',
                        body: imgFormData
                        })
                        .then(response => {
                            console.log('response:', response);
                            if (response.status === 200) {
                                console.log('发射信号 upload_completed');
                                socket.emit('upload_completed', {
                                    'image': URL.createObjectURL(imageBlob),
                                    'question': '请听录音'});
                            }
                            })
                        .then(data => console.log('data:', data))
                        .catch(error => console.error(error));
                }
            }
        };
    </script>
</body>
</html>

网页 B 的代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>human-annotation</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script>

</head>
<body>
    <img id="image" src="" alt="Your Image">
    <audio id="audioPlayer" controls class="audio-player"></audio>

    <div style="display: flex">提问:<div id="question"></div></div>

    <input type="text" id="textInput" placeholder="请输入答案...">

    <button id="submitButton">发送</button>

    <script>
    		// 这里也是,大坑,大坑啊!!这个地址要填对,本地用 localhost,云端用云端服务器地址啊!
            var socket = io.connect('http://localhost:8099'); // 连接到Flask服务器

            socket.on('data_updated', function(data) {
                // 当接收到来自服务器的数据时,更新页面内容
                var img = document.getElementById('image');
                img.src = data.image;
                console.log('img.src');
                // document.getElementById('image').innerHTML = '<img src="' + data.image + '" alt="Uploaded Image">';
                document.getElementById('question').textContent = data.question;
            });
            socket.on('data_speech_updated', function (data) {
                var audioPlayer = document.getElementById("audioPlayer");
                audioPlayer.src = data.speech;
            });

            // 监听按钮点击事件
            document.getElementById('submitButton').addEventListener('click', function() {
                // 获取输入框中的文本
                var message = document.getElementById('textInput').value;

                // 验证消息是否为空
                if (message.trim() !== '') {
                    // 通过Socket.IO发送消息给服务器
                    socket.emit('annotated_answer', message);

                    // 清空输入框
                    document.getElementById('textInput').value = '';
                } else {
                    alert('Please enter a message.');
                }
            });
        </script>
</body>
</html>

部署

用 gunicorn 部署

配置文件:
gunucorn配置文件
运行命令:运行命令

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1593290.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

分享 WebStorm 2024 激活的方案,支持JetBrains全家桶

大家好&#xff0c;欢迎来到金榜探云手&#xff01; WebStorm公司简介 JetBrains 是一家专注于开发工具的软件公司&#xff0c;总部位于捷克。他们以提供强大的集成开发环境&#xff08;IDE&#xff09;而闻名&#xff0c;如 IntelliJ IDEA、PyCharm、和 WebStorm等。这些工具…

K3 BOS插件实现作息时间的选择

要求做一个工时登记表&#xff0c;其中要扣减中午休息时间。公司夏令休息1.5个小时、冬令休息1个小时是不变的&#xff0c;基本思路是根据日期来判断&#xff0c;但在做表单时发现值更新事件中这个日期判断不好写&#xff0c;于是通过写BOS插件来实现这个功能。 首先新建的BOS…

【学习】软件信创测试中,如何做好兼容性适配

在软件信创测试的领域中&#xff0c;兼容性适配是至关重要的一环。如何确保软件在不同的操作系统、硬件设备和软件环境中稳定运行&#xff0c;是每个测试人员需要面对的挑战。本文将从几个方面探讨如何做好兼容性适配&#xff0c;以提高软件的稳定性和用户体验。 首先&#xf…

网页端HTML使用MQTTJs订阅RabbitMQ数据

最近在做一个公司的日志组件时有一个问题难住了我。今天问题终于解决了。由于在解决问题中&#xff0c;在网上也查了很多资料都没有一个完整的实例可以参考。所以本着无私分享的目的记录一下完整的解决过程和实例。 需求&#xff1a;做一个统一日志系统可以查看日志列表和一个可…

轻量化模块整理,即插即用

轻量化模块整理&#xff0c;即插即用&#xff08;持续更新&#xff09; 整理一些轻量化的结构&#xff0c;作为知识储备&#xff0c;可以用到后续的项目和研究中 Mobilenetv3 深度可分离卷积 MobileNetV3 是一个轻量级的深度学习模型&#xff0c;专为移动和边缘设备上的高效…

IntelliJ IDEA 2024 for Mac/Win:引领Java开发新纪元的高效集成环境

在日新月异的软件开发领域&#xff0c;一款高效、智能的集成开发环境&#xff08;IDE&#xff09;无疑是程序员们不可或缺的神兵利器。今天&#xff0c;我要为大家介绍的&#xff0c;正是这样一款集大成之作——IntelliJ IDEA 2024。无论是Mac用户还是Windows用户&#xff0c;只…

Golang | Leetcode Golang题解之第27题移除元素

题目&#xff1a; 题解&#xff1a; func removeElement(nums []int, val int) int {left, right : 0, len(nums)for left < right {if nums[left] val {nums[left] nums[right-1]right--} else {left}}return left }

8个 可以让 Python 加速的 tips

Python 是一种脚本语言&#xff0c;相比 C/C 这样的编译语言&#xff0c;在效率和性能方面存在一些不足。但是&#xff0c;有很多时候&#xff0c;Python 的效率并没有想象中的那么夸张。本文对一些 Python 代码加速运行的技巧进行整理。 0. 代码优化原则 本文会介绍不少的 P…

攻防世界12-baby_web

12-baby_web 题目说想想初始页面是哪个&#xff0c;一般都是index.php&#xff0c;然后如题分析即可。 我们在链接后面拼接上/index.php&#xff0c;返回后发现界面又回到了1.php&#xff0c;有可能是重定向。 我们点击检查-网络&#xff0c;发现没有index的请求&#xff0c;…

番外篇 | YOLOv8改进之在C2f中引入即插即用RepViTBlock模块 | CVPR2024清华RepViT

前言:Hello大家好,我是小哥谈。YOLOv8是一种基于深度学习的实时物体检测算法,其通过将物体检测任务转化为目标框回归问题,并使用卷积神经网络实现高效的特征提取和目标分类。然而,YOLOv8在处理一些复杂场景和小目标时可能存在一定的性能限制。为了克服YOLOv8的局限性,清华…

ES6: set和map数据结构以及使用场景

ES6:set和map数据结构 一、Set 数据结构&#xff1a;二、使用场景&#xff1a;使用Set 进行去重三、Map 数据结构四、使用场景&#xff1a;使用Map进行树型数据懒加载刷新五、Set和Map的区别六、Map、Set的实际使用场景 Set 和 Map 是 ES6 中引入的两种新的数据结构&#xff0c…

代码随想录算法训练营三刷day53 | 动态规划之子序列 1143.最长公共子序列 1035.不相交的线 53. 最大子序和

day53 1143.最长公共子序列1.确定dp数组&#xff08;dp table&#xff09;以及下标的含义2.确定递推公式3.dp数组如何初始化4.确定遍历顺序5.举例推导dp数组 1035.不相交的线53. 最大子序和1.确定dp数组&#xff08;dp table&#xff09;以及下标的含义2.确定递推公式3.dp数组如…

2024 十五届蓝桥杯省赛Python B组

以下仅是我的答案&#xff0c;仅供参考&#xff0c;欢迎讨论。 A&#xff1a;穿越时空之门 二进制、四进制转换。答案&#xff1a;63。 B&#xff1a;数字串个数 排除0&#xff0c;总的方案数9^10000,减去不存在3和不存在7的2*8^10000&#xff0c;再加上同时不存在3和7的7^…

Coding and Paper Letter(八十九)

CPL之第八十九期。 1 Coding: 1.openai通用代理转换是一个用于将其他厂商服务转为openai 标准接口相应的工具. 通过该工具, 可以将其他厂商的服务转为openai 标准接口. 讯飞星火,通义千问,gemini,openai,copilot,double&#xff0c;kimi&#xff0c;智谱清言 使用spring2webf…

Quantum Temple借助Sui通过NFT推动再生旅游

从金融到艺术&#xff0c;从游戏到无线网络&#xff0c;各行各业都涌现出大量初创公司&#xff0c;利用区块链技术颠覆现状。说到旅游业&#xff0c;让人联想到拥挤的机场、快节奏的旅游和豪华游轮&#xff0c;可能看起来对区块链创新持守旧态度。一家初创公司认为现在是时候改…

FreeRTOS创建第一个程序

使用freeRTOS创建任务时使用如下函数 函数的参数 创建一个FreeRTOS任务点亮led灯实现led灯500毫秒翻转一次 具体的代码实现 #include "stm32f10x.h" // Device header #include "Delay.h" #include "freeRTOS.h" #include &quo…

使用 Python 开发一个 Python 解释器

计算机只能理解机器码。归根结底&#xff0c;编程语言只是一串文字&#xff0c;目的是为了让人类更容易编写他们想让计算机做的事情。真正的魔法是由编译器和解释器完成&#xff0c;它们弥合了两者之间的差距。解释器逐行读取代码并将其转换为机器码。 在本文中&#xff0c;我…

C#简单工厂模式的实现

using System.Diagnostics.Metrics; using System.Runtime.InteropServices; using static 手写工厂模式.Program;namespace 手写工厂模式 {internal class Program{public interface eats {void eat();}//定义了一个接口public class rice : eats{public void eat() {Console.…

代码随想录算法训练营DAY25|C++回溯算法Part.2|216. 组合总和III、17.电话号码的字母组合

文章目录 216. 组合总和III题意理解树形结构伪代码实现剪枝操作CPP代码实现 17.电话号码的字母组合解题思路树形结构伪代码实现隐藏回溯CPP代码 216. 组合总和III 力扣题目链接 文章讲解&#xff1a;216. 组合总和III 视频讲解&#xff1a;和组合问题有啥区别&#xff1f;回溯算…