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

我想实现的效果是,我的服务器提供两个路由网址,网页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 代码

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

python 复制代码
@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

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 的代码

html 复制代码
<!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 部署

配置文件:

运行命令:

相关推荐
小宋102112 分钟前
玩转RabbitMQ声明队列交换机、消息转换器
服务器·分布式·rabbitmq
m0_6090004215 分钟前
向日葵好用吗?4款稳定的远程控制软件推荐。
运维·服务器·网络·人工智能·远程工作
kejijianwen2 小时前
JdbcTemplate常用方法一览AG网页参数绑定与数据寻址实操
服务器·数据库·oracle
编程零零七2 小时前
Python数据分析工具(三):pymssql的用法
开发语言·前端·数据库·python·oracle·数据分析·pymssql
北岛寒沫3 小时前
JavaScript(JS)学习笔记 1(简单介绍 注释和输入输出语句 变量 数据类型 运算符 流程控制 数组)
javascript·笔记·学习
everyStudy3 小时前
JavaScript如何判断输入的是空格
开发语言·javascript·ecmascript
(⊙o⊙)~哦4 小时前
JavaScript substring() 方法
前端
无心使然云中漫步5 小时前
GIS OGC之WMTS地图服务,通过Capabilities XML描述文档,获取matrixIds,origin,计算resolutions
前端·javascript
Bug缔造者5 小时前
Element-ui el-table 全局表格排序
前端·javascript·vue.js
xnian_5 小时前
解决ruoyi-vue-pro-master框架引入报错,启动报错问题
前端·javascript·vue.js