nodejs 聊天室

作者: 云龙789 | 来源:发表于2019-01-30 13:08 被阅读36次

    首先确保本地环境已经安装了 node 环境
    建立项目文件夹,我的是 node,以下操作都是在 node 文件夹下面操作的
    然后安装 express 框架

    npm install --save express@4.15.2
    

    安装 socket.io 模块

    npm install --save socket.io
    

    我的 demo 也是安装手册上来的,

    注意要点

    在 vue 中的 created(){} 里面的变量,你在 data 里初始值是什么,这个变量的值就永远是什么,这个个坑跟 vue 的生命周期有关。所有在做一对一聊天,房间聊天的处理中,监听聊天通道的时候需要注意聊天对象的ID号和房间号的选择,其实你可以把这个监听放在选择聊天对象或者房间的点击事件中

    • index.js
    var app = require('express')();
    var http = require('http').Server(app);
    var io = require('socket.io')(http);
    var config = require('config'); // 学习模块的引入
    
    app.get('/', function (req, res) {
        // res.send('<h1>Hello World</h1>');
        res.sendFile(__dirname + '/index.html');
    });
    
    
    // 监听连接处理
    io.on('connection', socket => {
        // socket.on('chat message', function (msg) {
        //     console.log('message:' + msg);
        // });
        //
        socket.on('disconnect', function () {
            console.log('user disconnected');
        })
    
        // 连接后,监听 chat message 通道
        socket.on('chat message', msg => {
            // 这个 msg 是熊 index.html 中发送过来的   io().emit('chat message', $('#m').val());
            // io.emit 是往某个通道发送信息,第一个参数是通道,第二个参数是信息 这个与 socket.on 监听的通道可以不一致
            io.emit('send message', msg);
        });
    
    })
    
    http.listen(config['socket_port'], function () {
        console.log('listening on *:3000');
    })
    
    • inde.html
    <!doctype html>
    <html>
    <head>
        <title>Socket.IO chat</title>
        <style>
            * {
                margin: 0;
                padding: 0;
                box-sizing: border-box;
            }
    
            body {
                font: 13px Helvetica, Arial;
            }
    
            form {
                background: #000;
                padding: 3px;
                position: fixed;
                bottom: 0;
                width: 100%;
            }
    
            form input {
                border: 0;
                padding: 10px;
                width: 90%;
                margin-right: .5%;
            }
    
            form button {
                width: 9%;
                background: rgb(130, 224, 255);
                border: none;
                padding: 10px;
            }
    
            #messages {
                list-style-type: none;
                margin: 0;
                padding: 0;
            }
    
            #messages li {
                padding: 5px 10px;
            }
    
            #messages li:nth-child(odd) {
                background: #eee;
            }
        </style>
        <!--<script src="/socket.io/socket.io.js"></script>-->
        <script src="/socket.io/socket.io.js"></script>
        <script src="https://code.jquery.com/jquery-1.11.1.js"></script>
        <script>
            $(function () {
                var socket = io();
                $('form').submit(function (e) {
                    e.preventDefault(); // prevents page reloading
                    // 向某个通道发送信息
                    socket.emit('chat message', $('#m').val());
                    $('#m').val('');
                    return false;
                });
                // 监听 chat message 通道
                socket.on('send message', msg => {
                    $('#messages').append($('<li>').text(msg));
                    // 上下滑动屏幕
                    window.scrollTo(0, document.body.scrollHeight);
                });
            });
        </script>
    </head>
    <body>
    <ul id="messages"></ul>
    <form action="">
        <input id="m" autocomplete="off"/>
        <button>Send</button>
    </form>
    
    </body>
    </html>
    
    • configs.js
    module.exports = {
       'socket_port': '3000',
    }
    
    

    程序讲解

    首先你需要对 nodejs 语法要有基本的了解,如果不了解,我建议看 菜鸟手册
    然后我们要知道,nodejs 是基于事件驱动的语法,也就是有事件,才会有相应的触发。
    事件可以理解为事件就是客户点击了某个url,触发就是我们对某个路由的控制器做的相应处理
    此处的触发,就比如 io.on('connection',function(socket){}), 此处的 on() 就相当于是监听 connection 是固定的参数,监听到有客户端连接的意思,
    on 的第二个参数就是连接后做的处理,相当于是控制器

    程序执行流程讲解

    index.js 中首 io模块 先监听了 connection连接 的处理,连接后监听了 'chat message'消息通道
    index.html 中,往 chat message 通道发送信息 socket.emit('chat message', $('#m').val());
    这个时候 index.js 中的 socket.on('chat message', msg => {}) 监听到有消息发送过来,这里的 msg 就是 index.html 中发送的消息。
    收到消息后,我们需要把消息发送到某个通道,此处的通道名字我用的是 sen message ,io.emit('send message', msg); ,我把这个消息发送到 send message 通道
    我们在 index.html 前端页面监听了 send message 通道 socket.on('send message',msg=>{}),然后对监听的数据做处理即可

    以上情况,我们是针对群聊处理的。也就是每个人都是监听的 send message 通道,发送信息都是发送到了 chat message 通道,如果需要私聊,或者指定房间,需要更改这两个参数即可


    一对一聊天

    • index.html
    <body>
    <h1>请选择聊天的对象</h1>
    <select name="username" id="choose">
        <option value="">null</option>
        <option value="test1">test1</option>
        <option value="test2">test2</option>
        <option value="test3">test3</option>
        <option value="test4">test4</option>
        <option value="test5">test5</option>
    </select>
    <button id="b_click">ok</button>
    
    <ul id="messages"></ul>
    <form action="">
        <input id="m" autocomplete="off"/>
        <button>Send</button>
    </form>
    <script>
        var to = '';
        $('#b_click').click(function () {
            to = $('#choose').val();
        });
        var username = prompt('请输入你的名字', '');
        // 聊天对象
        if (to == '') {
            to = username;
        }
    
        $(function () {
            var socket = io();
            $('form').submit(function (e) {
                e.preventDefault(); // prevents page reloading
                // 向某个通道发送信息
                var data = $('#m').val();
                var msg = {username: username, to: to, data: data};
                socket.emit('chat message', msg);
                $('#m').val('');
                return false;
            });
            // 监听 chat message 通道
            socket.on(to, msg => {
                // 如果 发送者和接收者都是自己,表示还没有开启对话,将对话对象更细昵称 msg.username
                if (username == to) {
                    to = msg.username;
                }
                $('#messages').append($('<li>').text(msg.data));
                // 上下滑动屏幕
                window.scrollTo(0, document.body.scrollHeight);
            });
    
        });
    </script>
    </body>
    
    • index.js
    同上面的 index.js 此处只展示改变代码的部分
    ...
    // 监听连接处理
    io.on('connection', socket => {
        // 连接后,监听 chat message 通道
        socket.on('chat message', msg => {
            io.emit(msg.to, msg);
        });
    
    })
    ...
    
    • 思路分析
      我们在 index.html 页面中监听的信息通道都是to 参数的值,这个通道在 demo 中,其实就是test
      long 主动跟 test 聊天,在 long 的页面中 username=long to =test
      在 test 页面中 username 和 to 的值都是 test 所以两人都是监听的 test 通道,都可以接收到信息

    • 不适用 express 框架也可以的

    var fs = require('fs');
    var app = require('http').createServer(function (req, res) {
        fs.readFile(__dirname + '/index.html', function (err, data) {
            if (err) {
                res.writeHead(500);
                return res.end('Error loading index.html');
            }
            res.writeHead(200);
            res.end(data);
        })
    })
    io = require('socket.io')(app);
    // 监听连接处理
    io.on('connection', socket => {
        socket.on('disconnect', function () {
            console.log('user disconnected');
        })
    
        // 连接后,监听 chat message 通道
        socket.on('chat message', msg => {
            io.emit(msg.to, msg);
            // 如果发送人和接收人都是同一个人,则不需要重复发送给自己
            if (msg.to != msg.username) {
                io.emit(msg.username, msg);
            }
        });
    
    })
    app.listen(3000, function () {
        console.log('listening on *:3000');
    })
    

    某人上线下线的提醒

    • index.html
        var username = prompt('请输入你的名字', '');
        var socket = io();
        socket.emit('coming', username);  // 在输入名字之后,将新用户推送到 coming 通道
    
    • index.js
    // 监听连接处理
    io.on('connection', socket => {
        // 连接后,监听 chat message 通道
        socket.on('coming', msg => {
            socket.name = msg; // 这个通道有消息过来的时候,将他赋值给 socket 的一个属性
    ,因为 disconnect 与这个不在这个作用域,所以要放在 socket 的属性上
            console.log(msg + '上线了');
        });
    
        socket.on('disconnect', function () {
         // 这里可以写自己的逻辑业务,比如通知某个房间的人
            console.log(socket.name + '离开了');
        })
    })
    

    vue 实现一对一聊天

    首先后端的服务还是使用上面的 index.js 去处理
    在前端

    • vue/index.html 中引入 socket.io.js 文件
     <div id="app"></div>
        <script src="http://localhost:3000/socket.io/socket.io.js"></script>
        <script type="text/javascript">
          const socket = io.connect('http://localhost:3000');
        </script>
    先引入后端的 socket.io.js 文件,就可以获取 io 这个对象。我们预定义 socket 变量方便程序中使用
    
    • vue/src/router/index.js 定义聊天路由
    import Vue from 'vue'
    import Router from 'vue-router'
    import HelloWorld from '@/components/HelloWorld'
    import Chat from '@/components/Chat'
    import ChatList from '@/components/ChatList'
    
    Vue.use(Router)
    
    export default new Router({
      routes: [
        {
          path: '/',
          name: 'HelloWorld',
          component: HelloWorld
        },{
          path: '/chat',
          name: 'Chat',
          component: Chat
        },{
          path: '/chatlist',
          name: 'ChatList',
          component: ChatList
        }
      ]
    })
    
    • vue/src/components/ChatList.vue 选择聊天对象列表,这步不重要
    <template>
        <!--  用户列表 -->
        <div>
            <h2>聊天对象用户列表</h2>
            <ul>
                <li v-for="(username,index) in usernameList">
                    <!--<router-link to="/chat">-->
                        <button @click="$router.push({path:'/chat',query: {username,index}})">{{ username }}</button>
                    <!--</router-link>-->
    
                </li>
            </ul>
        </div>
    </template>
    
    <script>
        export default {
            data() {
                return {
                    usernameList: [
                        'test1',
                        'test2',
                        'test3',
                        'test4',
                        'test5',
                    ],
                }
            }
        }
    </script>
    
    • vue/src/components/Chat.vue 聊天页面
    <template>
        <div>
            <ul id="messages">
                <li v-for="msg in msgList" :class="[msg.username === username ?'right':'left']">{{
                    msg.username+':'+msg.content }}
                </li>
            </ul>
            <form action="">
                <input id="m" autocomplete="off" v-model="content">
                <button @click="sendMessage">{{ sendInfo }}</button>
            </form>
        </div>
    </template>
    
    <script>
        export default {
            data() {
                return {
                    content: '',
                    sendInfo: "发送",
                    msgList: [],
                    username: '',
                    chatTo: '',
                    left: false,
                    right: false,
                }
            },
            methods: {
                sendMessage: function (e) {
                    e.preventDefault();
                    var msg = {username: this.username, chatTo: this.chatTo, content: this.content}
                    socket.emit('one to one message', msg);
                    this.content = '';
                }
            },
            created() {
                this.username = prompt('请输入你的名字');
                var query = this.$route.query;
                if (Object.keys(query).length === 0) {
                    this.chatTo = this.username;
                    // alert('请选择聊天对象');
                } else {
                    this.chatTo = query.username;
                }
                // 监听
                socket.on(this.chatTo, msg => {
                    this.msgList.push({
                        username: msg.username,
                        chatTo: msg.chatTo,
                        content: msg.content,
                    });
                    // 上下滑动屏幕
                    window.scrollTo(0, document.body.scrollHeight);
    
                });
            },
        }
    </script>
    <style>
        form {
            background: #000;
            padding: 3px;
            position: fixed;
            bottom: 0;
            width: 100%;
        }
    
        form input {
            border: 0;
            padding: 10px;
            width: 70%;
            margin-right: .5%;
        }
    
        form button {
            width: 20%;
            background: rgb(130, 224, 255);
            border: none;
            padding: 10px;
        }
    
        #messages {
            list-style-type: none;
        }
    
        #messages li {
            padding: 5px 10px;
            width: 50%;
        }
    
        #messages li:nth-child(odd) {
            background: #eee;
        }
    
        .left {
            float: left;
        }
    
        .right {
            float: right;
        }
    </style>
    
    image.png

    多对多聊天(房间聊天,群聊)

    • 后端 index.js
    ....
    // 监听连接处理
    io.on('connection', socket => {
        // 私聊通道监听
        socket.on('one to one message', msg => {
            // 发送给私聊对象
            io.emit(msg.chatTo, msg);
        });
    
        // f房间聊天室监听
        socket.on('chat room message', msg => {
            // 发送给某个房间
            io.emit('chat room receive' + msg.roomNum, msg)
        });
    })
    ....
    
    • ChatRoom.vue
    <template>
        <div>
            <ChooseRoom v-bind:roomList="roomList" v-if="!roomNum" v-on:chooseRoom="chooseRoom"></ChooseRoom>
            <h1>进入聊天室</h1>
            <ul id="messages">
                <li v-for="msg in msgList" :class="[msg.username === username ?'right':'left']">{{
                    msg.username+':'+msg.content }}
                </li>
            </ul>
            <form action="">
                <input id="m" autocomplete="off" v-model="content">
                <button @click="sendMessage">{{ sendInfo }}</button>
            </form>
        </div>
    </template>
    <script>
        import ChooseRoom from './ChooseRoom'
        export default {
            data() {
                return {
                    content: '',
                    sendInfo: "发送",
                    msgList: [],
                    username: '',
                    roomNum: '',
                    left: false,
                    right: false,
                    roomList: [
                        {id: '1', name: '1号房间'},
                        {id: '2', name: '2号房间'},
                        {id: '3', name: '3号房间'},
                        {id: '4', name: '4号房间'},
                        {id: '5', name: '5号房间'},
                    ],
                }
            },
            components: {
                ChooseRoom
            },
    
            methods: {
                // 发送信息
                sendMessage: function (e) {
                    e.preventDefault();
                    var msg = {username: this.username, roomNum: this.roomNum, content: this.content}
                    socket.emit('chat room message', msg);
                    this.content = '';
                },
                chooseRoom: function (id) {
                    this.roomNum = id;
                    socket.on('chat room receive'+this.roomNum, msg => {
                        this.msgList.push({
                            username: msg.username,
                            content: msg.content,
                        });
                        // 上下滑动屏幕
                        window.scrollTo(0, document.body.scrollHeight);
                    });
                }
            },
            created() {
                this.username = prompt('请输入你的名字');
            },
        }
    </script>
    
    • ChooseRoom.vue
    <template>
        <div>
            <h1>选择房间</h1>
            <ul>
                <li v-for="room in roomList">
                    <button @click="$emit('chooseRoom',room.id)">{{ room.name }}</button>
                </li>
            </ul>
        </div>
    </template>
    <script>
        export default {
            props: ['roomList'],
        }
    </script>
    

    在一个小图表上显示有多少条未读数据的做法是,直接监听相关的端口,j每次有消息通知的时候将一个变量加1即可

    data() {
        return {
            msgCount: 0
        }
    },
    
    created() {
        // socket.ChatList = [];
        // 一对一聊天
        socket.on(this.username, msg => {
            this.msgCount++;
            // socket.ChatList.push({
            //     username: msg.username,
            //     chatTo: msg.chatTo,
            //     content: msg.content,
            // });
        });
    }
    

    如果想点击小图标后显示聊天的内容,可以在上面的处理中将接受的数据赋值给 socket 的一个自定义变量,这样的话,跨页面也是可以获取到这个变量的

    一对一聊天,显示聊天对象列表和内容

    • 思路,聊天用户发送信息的时候,在后端使用 redis 的 rpush 保存数据,并将聊天人使用 sadd 存入 set ,其实也可以使用 hash 的 hincryby 每次将聊天记录加1 ,但是我看 nodejs 的 redis 没有这个操作指令。
      对了,用户一对一的通道名子,我根据两人的名字比较,大的值在前面。js 语法也是可以直接比较字符串的,这样就可以统一通道规则

    • 前端首页 index.vue

    <template>
        <div class="hello">
            <!--<button @click="toChat($router,username,chatTo)">-->
            <!--<h1>未查看信息条数{{ msgCount }}</h1>-->
            <!--</button>-->
            <h1>聊天列表</h1>
            <ul>
                <li v-for="(chatName,index) in chatList" :key="index">
    
                    <button @click="toChat($router,username,chatName)">
                        <h1>{{ chatName }}</h1>
                    </button>
                </li>
            </ul>
        </div>
    </template>
    
    <script>
        import originJsonp from '../jsonp';
    
        export default {
            name: 'HelloWorld',
            data() {
                return {
                    msgCount: 0,
                    username: '',
                    chatTo: '',
                    chatList: [], // 聊天人列表
                }
            },
            methods: {
                toChat: function ($router, username, chatTo) {
                    let chatSocket = username > chatTo ? username + '-' + chatTo : chatTo + '-' + username;
    // 也可以 let arr = [username,chatTo];
    //  let chatSocket =  arr.sort().join('_');
                    $router.push({path: '/chat', query: {username, chatTo, chatSocket}})
                }
            },
            created() {
                var vm = this;
                if (this.username === '') {
                    this.username = this.chatTo = prompt('请输入你的名字');
                }
                // 登陆后,获取此用户通道的未读信息
                originJsonp({
                    "method": 'get chat list',
                    "username": vm.username,
                }).then(function (response) {
                    console.log(response);
                    vm.chatList = response
                })
    
                // 一对一聊天
                socket.on(this.username, msg => {
                    this.msgCount++;
                });
            }
        }
    </script>
    
    • 前端聊天页面 chat.vue
    <template>
        <div>
            <h1>私聊</h1>
            <ul id="messages">
                <li v-for="msg in msgList" :class="[msg.username === username ?'right':'left']">
                    {{ msg.username+':'+msg.content }}
                </li>
            </ul>
            <form action="">
                <input id="m" autocomplete="off" v-model="content">
                <button @click="sendMessage">{{ sendInfo }}</button>
            </form>
        </div>
    </template>
    
    <script>
        import originJsonp from '../jsonp';
    
        export default {
            props: ['chatList'],
            data() {
                return {
                    content: '',
                    sendInfo: "发送",
                    msgList: [],
                    username: '',
                    chatTo: '',
                    left: false,
                    right: false,
                    chatSocket: '', // 两人的聊天通道
                }
            },
            methods: {
                sendMessage: function (e) {
                    e.preventDefault();
                    var msg = {
                        username: this.username,
                        chatTo: this.chatTo,
                        content: this.content,
                        chatSocket: this.chatSocket
                    }
                    socket.emit('one to one message', msg);
                    this.content = '';
                }
            },
            created() {
                var vm = this;
                var query = this.$route.query;
                if (Object.keys(query).length === 0) {
                    this.username = prompt('请输入你的名字');
                    // 如果直接进来的  用户和聊天对象都是自己
                    this.chatTo = this.username;
                } else {
                    // 否则是从 chatList 页面进来的
                    this.chatTo = query.chatTo;
                    if (this.username === '' && !query.username) {
                        //  从 ChatList 页面进来的参数有 ?chatTo=test1&index=0
                        this.username = prompt('请输入你的名字');
                    } else {
                        // 从首页进来的参数有 ?username=test1&chatTo=test1
                        this.username = query.username;
                        // todo ajax 请求redis 接口 赋值给 this.msgList
                        this.chatSocket = vm.username > vm.chatTo ? vm.username + '-' + vm.chatTo : vm.chatTo + '-' + vm.username;
                    }
                }
                if (!this.chatSocket) {
                    // 两人的聊天通道  从大到校排序
                    this.chatSocket = vm.username > vm.chatTo ? vm.username + '-' + vm.chatTo : vm.chatTo + '-' + vm.username;
                }
                originJsonp({
                    "method": "one to one message",
                    "username": vm.username,
                    "chatSocket": this.chatSocket,
                })
                    .then(function (response) {
                        response.forEach(function (value, index) {
                            // 原来存的是json 字符串  需要转换成对象
                            response[index] = JSON.parse(value);
                        })
                        vm.msgList = response
                    })
                    .catch(function (error) {
                        console.log(error);
                    });
                // 一对一聊天
                socket.on(this.chatSocket, msg => {
                    this.msgList.push({
                        username: msg.username,
                        chatTo: msg.chatTo,
                        content: msg.content,
                    });
                    // 上下滑动屏幕
                    window.scrollTo(0, document.body.scrollHeight);
                });
            },
        }
    </script>
    <style>
        form {
            background: #000;
            padding: 3px;
            position: fixed;
            bottom: 0;
            width: 100%;
        }
    
        form input {
            border: 0;
            padding: 10px;
            width: 70%;
            margin-right: .5%;
        }
    
        form button {
            width: 20%;
            background: rgb(130, 224, 255);
            border: none;
            padding: 10px;
        }
    
        #messages {
            list-style-type: none;
        }
    
        #messages li {
            padding: 5px 10px;
            width: 50%;
        }
    
        #messages li:nth-child(odd) {
            background: #eee;
        }
    
        .left {
            float: left;
        }
    
        .right {
            float: right;
        }
    </style>
    
    
    • 后端 index.js
    var app = require('express')();
    var http = require('http').Server(app);
    var io = require('socket.io')(http);
    
    var redis = require('redis');
    var client = redis.createClient(6379, '127.0.0.1');
    const ONE_TO_ONE = 'one to one message'; // 一对一聊天
    const ROOM_CHAT = 'chat room receive'; // 房间聊天
    const SET_CHAT_LIST = 'chat list'; // 聊天对象列表
    
    app.get('/', function (req, res) {
        res.sendFile(__dirname + '/index.html');
    });
    
    // 监听连接处理
    io.on('connection', socket => {
        // 私聊通道监听
        socket.on(ONE_TO_ONE, msg => {
            console.log(msg.chatSocket);
            // 发送给私聊对象
            io.emit(msg.chatSocket, msg);
            // 将聊天语句存入 redis
            client.rpush(ONE_TO_ONE + msg.chatSocket, JSON.stringify(msg), function (err, data) {
                console.log(data)
            })
            // 将聊天人发送给聊天对象的 set 集合中
            client.sadd(SET_CHAT_LIST + msg.chatTo, msg.username, function (err, data) {
                console.log(data)
            });
    
        });
    
        // f房间聊天室监听
        socket.on('chat room message', msg => {
            // 发送给某个房间
            io.emit(ROOM_CHAT + msg.roomNum, msg)
            // 将聊天语句存入 redis
            client.rpush(ROOM_CHAT + msg.roomNum, JSON.stringify(msg), function (err, data) {
                console.log(data)
            })
        });
    
    })
    
    http.listen(3000, function () {
        console.log('listening on *:3000');
    })
    
    • 后端PHP 接口 redis.php
    <?php
    
    header('Access-Control-Allow-Origin:*'); // 单个域名处理
    define('SET_CHAT_LIST', 'chat list'); // 聊天列表
    
    //连接本地的 Redis 服务
    $redis = new Redis();
    $redis->connect('127.0.0.1', 6379);
    //查看服务是否运行
    
    if (!isset($_GET['method'])) {
        echo $_GET['callback'] . "(请传入请求参数)";
    }
    
    $username = isset($_GET['username']) ? $_GET['username'] : ''; // 用户名
    $method = isset($_GET['method']) ? $_GET['method'] : ''; // 请求方法
    $chatSocket = isset($_GET['chatSocket']) ? $_GET['chatSocket'] : ''; // 请求方法
    $chatTo = isset($_GET['chatTo']) ? $_GET['chatTo'] : ''; // 请求方法
    
    $arr = [];
    switch ($method) {
        case 'one to one message': // 一对一聊天
            $arr = $redis->lRange($method . $chatSocket, 0, -1);
            break;
        case 'set chat list': //
            $chatTo = $_GET['chatTo'];
            $arr = $redis->hIncrBy($method . $username, $chatTo, HASH_INC_BY);
            break;
        case 'get chat list': // 获取聊天列表
            $arr = $redis->sMembers(SET_CHAT_LIST . $username);
            break;
    
    }
    
    $result = json_encode($arr);
    $callback = $_GET['callback'];
    echo $callback . "($result)";
    
    • 我这里设计的是只要发送信息,node 后端就直接使用 redis 的 sadd 存入集合,但是这样可能没必要,因为只需要存入一次就可以了,改善方式,可以是在进入本聊天页面的时候,直接请求一个 php 接口,将聊天用户存入 sadd 。但是这样就是还没有聊天就存入了聊天对象,好像也不合适


      image.png
      image.png
      image.png

    相关文章

      网友评论

        本文标题:nodejs 聊天室

        本文链接:https://www.haomeiwen.com/subject/ggoqsqtx.html