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 部署

配置文件:

运行命令:

相关推荐
wanhengidc几秒前
云手机存在的意义是什么
运维·服务器·arm开发·安全·智能手机
扯蛋4381 小时前
LangChain的学习之路( 一 )
前端·langchain·mcp
Mr.Jessy1 小时前
Web APIs学习第一天:获取 DOM 对象
开发语言·前端·javascript·学习·html
午安~婉1 小时前
javaScript八股问题
开发语言·javascript·原型模式
西西学代码2 小时前
Flutter---个人信息(5)---持久化存储
java·javascript·flutter
芝麻开门-新起点2 小时前
flutter 生命周期管理:从 Widget 到 State 的完整解析
开发语言·javascript·ecmascript
ConardLi3 小时前
Easy Dataset 已经突破 11.5K Star,这次又带来多项功能更新!
前端·javascript·后端
报错小能手3 小时前
计算机网络自顶向下方法25——运输层 TCP流量控制 连接管理 “四次挥手”的优化
服务器·网络·计算机网络
冴羽3 小时前
10 个被严重低估的 JS 特性,直接少写 500 行代码
前端·javascript·性能优化
rising start3 小时前
四、CSS选择器(续)和三大特性
前端·css