使用Socket.io实现一对一网络通信

Socket.IO什么?

Socket.io是一个面向实时web 应用的 JavaScript 库。它使得服务器和客户端之间实时双向的通信成为可能。他有两个部分:在浏览器中运行的客户端库,和一个面向Node.js的服务端库。两者有着几乎一样的API。

使用Socket.io实现多人通信很方便,但是实现一对一通信,就需要添加部分逻辑。我下面整理了一对一通信的实现方法

实现过程

因为需要服务端,所以选择创建express作为服务端,客户端,我们用Vue3简单模拟一下。

首先我们先创建一个文件,cd到文件中,在终端输入

npm init -y

然后再执行以下命令

npm install express

npm install socket.io




npm install -g nodemon

下载express和socket.io的第三方依赖。

随后我们在当前文件夹下,创建app.js文件,然后修改packge.json文件。

"scripts": {
    "dev": "nodemon ./app.js"
  },

修改成这样,使用npm run dev命令启动时,会用nodemon启动app.js文件,方便我们后续修改app.js文件。

const express=require('express');

const app=express();

const {Server} =require('socket.io');

const io = new Server(3000,{

    cors:{

        origin:['http://localhost:8080']

    }

})




app.listen(8000,()=>{
    console.log("服务器启动咯~")
})

随后,我们在app.js文件中,先将express和socket.io初始化,我们将socket.io建立在3000端口上,然后配置跨域(以为后续的vue cli为我们创建的项目默认端口是8080,所以我们需要设置8080端口可以跨域),最后我们让app监听8000端口,让这个程序持续启动。

接下来,我们创建客户端项目,这里我用的是vue cli创建,使用vue create client

image.png

我们选择第三个,按回车。

image.png

进去之后,我们选择有Router配置,将其他的取消,然后按回车下一步,因为我们会用到路由模拟用户,所以这里会用到路由中的useRouter。

创建好项目后,我们只需要一个页面,删除多余的路由,保留App.vue中的,然后创建一个Chatting.vue组件,绑定在chatting路由下,随后我们打开三个页面。

image.png

image.png

image.png

这三个页面都用到了query传参,所以我们模拟了三个用户。

打开终端,安装socket.io第三方依赖

npm install socket.io-client

随后我们需要初始化socket.io,然后创建一个响应式对象,通过useRouter拿到query,模拟用户。

import {io} from "socket.io-client";
import {useRouter} from "vue-router"
import { reactive } from "vue";






const router =useRouter();
const state =reactive({
    username:router.currentRoute.value.query.username,
})

const socket = io('http://localhost:3000',{
    query:{
        username:state.username
    }
})

在这里,我们拿到用户之后,作为query参数,传输到服务端。

我们在服务端通过io.on监听connection时间,可以拿到客户端传输过来的query参数。

io.on('connection',(socket)=>{

    const username=socket.handshake.query.username;

})

拿到参数之后,我们服务端肯定需要做一个用户列表,看那些用户在线,所以我们需要声明一个userList数组

这个数组中存储用户对象,用户对象中有用户名和用户id,这个用户id就是socket.id,每个用户都是唯一的(当用户重新登录时,这个id会和上一次的不一样,所以我们需要判断用户是否在userList中)

const userList=[]

io.on('connection',(socket)=>{

    const username=socket.handshake.query.username;

    if(!username) return;

    const userInfo=userList.find(user=>user.username===username)

    if(userInfo){

        userInfo.id=socket.id

    }

    else{

        userList.push({

            id:socket.id,

            username

        })

    }

})

如果用户之前登录过,就说明已经在userList中了,所以我们需要更新用户id,如果用户没登录过,我们直接将用户对象存储到userList中。

当我们存储玩userList后,我们需要将userList返回给客户端,告诉用户,当前那些用户在线,所以这里我们可以使用socket.emit(online)事件去传输给客户端。

const userList=[]

io.on('connection',(socket)=>{

    const username=socket.handshake.query.username;

    if(!username) return;

    const userInfo=userList.find(user=>user.username===username)

    if(userInfo){

        userInfo.id=socket.id

    }

    else{

        userList.push({

            id:socket.id,

            username

        })

    }

    io.emit('online',{
        userList
    })
})

在客户端,我们可以使用socket.om(’online’)拿到服务端传输的userList

socket.on('online',(data)=>{
    console.log(data)

})

image.png

在客户端拿到userList后,我们需要将userList存在我们的响应式对象中,所以我们需要在reactive中添加一个useList的对象,然后将data赋值给它。

const state =reactive({

    username:router.currentRoute.value.query.username,
    userList:[],
})
socket.on('online',(data)=>{
    console.log(data)
    state.userList=data.userList
})


当我们在客户端拿到了用户列表之后,我们就需要在视图中显示出来了,这里我就不写样式了。

<template>



    <div>



        <ul>



            <template v-for="userInfo of state.userList" :key="userInfo.id">



                <li v-if="userInfo.username===state.username">



                    {{userInfo.username}}



                </li>



                <li v-else>



                    <a href="javascript:;" ">
                        {{userInfo.username}}



                    </a>



                </li>



            </template>



        </ul>



    </div>
</template>

因为用户在客户端只能跟其他用户通信,所以在这里我们需要做一个区分,我们不可以选择自己,其他用户我们用a标签显示。

image.png

当我们在客户端选择一个用户通信之后,我们需要记住跟哪一个用户通信,所以我们需要在a标签上添加一个点击事件,然后在响应式对象中添加一个targetUser的对象。

<a href="javascript:;" @click="()=>selectUser(userInfo)">
      {{userInfo.username}}
</a>




const state =reactive({
    username:router.currentRoute.value.query.username,
    userList:[],
    targetUser:null,
})

const selectUser=(userInfo)=>{
    state.targetUser=userInfo
}

随后我们记住了用户跟谁通信之后,我们需要显示出正在跟谁通信和一个input标签。

<template>



    <div>



        <ul>



            <template v-for="userInfo of state.userList" :key="userInfo.id">



                <li v-if="userInfo.username===state.username">



                    {{userInfo.username}}



                </li>



                <li v-else>



                    <a href="javascript:;" @click="()=>selectUser(userInfo)">


                        {{userInfo.username}}



                    </a>



                </li>



            </template>



        </ul>



        <div v-if="state.targetUser">


            <h3>{{state.targetUser.username}}</h3>


            <input type="text" placeholder="input some ..." v-model="state.msgText">


            <button @click="sendMessage">发送</button>


        </div>


    </div>
</template>

image.png

点击button按钮发送信息,我们需要给button按钮添加一个sendMessage事件,也需要让响应式对象添加一个msgText string类型与input标签双向绑定,我们发送信息的时候,我们需要将发送的信息存储到一个响应式对象中,方便后续显示信息,所以我们需要在响应式中添加一个messageBox对象,去存储用户的信息对象。

const state =reactive({

  username:router.currentRoute.value.query.username,
  userList:[],
  targetUser:null,
  msgText:"",
  messageBox:{}
})


const sendMessage=()=>{
  if(!state.msgText.length){
    return 
  }
  !state.messageBox[state.username] && (state.messageBox[state.username]=[])
  state.messageBox[state.username].push({
    fromUsername:state.username,
    toUsername:state.targetUser.username,
    msg:state.msgText,
    dateTime:new Date().getTime()
  })


  socket.emit('send',{
    fromUsername:state.username,
    targetId:state.targetUser.id,
    msg:state.msgText
  })
}

我们需要判断messageBox中有没有当前用户对象,如果没有则等于一个空数组,有的话我们就往这个数组中添加消息信息(发送用户,接收用户,消息信息,时间),如果是我们自己发送的,我们就把消息对象存在本地,然后我们使用socket.emit触发send事件,就可以将这个消息对象传输给服务端,需要注意的是,我们在这里传输给服务端的target目标需要传输targetId,也就是用户id,因为我们的服务端,需要使用用户id拿到具体的用户实例。

io.on('connection',(socket)=>{

    const username=socket.handshake.query.username;

    if(!username) return;






    const userInfo=userList.find(user=>user.username===username)



    if(userInfo){
        userInfo.id=socket.id
    }
    else{
        userList.push({
            id:socket.id,
            username
        })
    }
    io.emit('online',{
        userList
    })
    
    socket.on('send',({fromUsername,targetId,msg})=>{
        const targetSocket=io.sockets.sockets.get(targetId)
        const toUser=userList.find(user=>user.id===targetId)
        targetSocket.emit('receive',{
            fromUsername,
            toUsername:toUser.username,
            msg,
            dateTime:new Date().getTime()
        })
    })
})

在服务端,我们通过socket.on监听send事件,拿到发送方,目标Id,,信息后,因为io.sockets.sockets是一个map对象,存储着所有的用户实例,所以我们可以用get(targetId)方法拿到目标用户实例,然后我们再通过targetId找到目标名称,使用目标实例(targetSocket)触发receive事件,将对应的消息对象传送给目标用户(只有目标实例的用户可以接收到信息,其他用户接收不到)

image.png

image.png

当我们给alibaba用户发送信息时,只有alibaba可以看到,其他用户则接受不到信息,

在客户端用户接收信息我们需要用到socket.on(‘receive’)事件去监听。

socket.on('receive',(data)=>{
    console.log(data)

    !state.messageBox[state.username] && (state.messageBox[state.username]=[])
    state.messageBox[state.username].push(data)
    console.log(state.messageBox[state.username])
})

我们拿到服务端传输过来的信息(data)后,我们一样需要判断messageBox是否有当前用户的信息对象,如果没有则等于空数组,随后将data添加到数组中,因为接受方能收到data,所以我们这个data只存储在了接收方的messageBox中,发送方也是如此。。

现在,我们就可以实现一对一用户通信,通信信息都存储在messageBox中,我们需要将通信信息显示在页面中,但是我们需要将messageBox做一次剔除,只有两个人彼此的通信记录可以观看,因为同一个用户给很多其他用户发送的信息都会存储在messageBox中,所以我们现在需要判断正在通信的人是否是targetUser,然后在message中找到信息,展示出来,这里我们用到了computed方法

const messageList=computed(()=>{
    if(state.messageBox[state.username] && state.targetUser){
        return state.messageBox[state.username].filter(item=>{
            return item.fromUsername===state.targetUser.username || item.toUsername===state.targetUser.username
        })
    }
    else{
        return []
    }
})

最后,我们将messageList渲染到页面即可。

<template>



    <div>



        <ul>



            <template v-for="userInfo of state.userList" :key="userInfo.id">



                <li v-if="userInfo.username===state.username">



                    {{userInfo.username}}



                </li>



                <li v-else>



                    <a href="javascript:;" @click="()=>selectUser(userInfo)">


                        {{userInfo.username}}



                    </a>



                </li>



            </template>



        </ul>



        <div v-if="state.targetUser">


            <h3>{{state.targetUser.username}}</h3>


            <input type="text" placeholder="input some ..." v-model="state.msgText">


            <button @click="sendMessage">发送</button>


        </div>


        <div>

            <ul>

                <li v-for="(msg,index) of messageList" :key="index">

                    <h3>{{msg.fromUsername}}</h3>

                    <h4>{{msg.msg}}</h4>

                </li>

            </ul> 

        </div>

    </div>

</template>

看看最终效果吧。

image.png

image.png

image.png

image.png

源码

服务端

const express=require('express');

const app=express();

const {Server} =require('socket.io');

const io = new Server(3000,{

    cors:{

        origin:['http://localhost:8080']

    }

})





const userList=[]


io.on('connection',(socket)=>{
    const username=socket.handshake.query.username;
    if(!username) return;


    const userInfo=userList.find(user=>user.username===username)



    if(userInfo){
        userInfo.id=socket.id
    }
    else{
        userList.push({
            id:socket.id,
            username
        })
    }
    io.emit('online',{
        userList
    })
    
    socket.on('send',({fromUsername,targetId,msg})=>{
        const targetSocket=io.sockets.sockets.get(targetId)
        const toUser=userList.find(user=>user.id===targetId)
        targetSocket.emit('receive',{
            fromUsername,
            toUsername:toUser.username,
            msg,
            dateTime:new Date().getTime()
        })
    })
})





app.listen(8000,()=>{
    console.log("服务器启动咯~")
})

客户端

<template>



    <div>



        <ul>



            <template v-for="userInfo of state.userList" :key="userInfo.id">



                <li v-if="userInfo.username===state.username">



                    {{userInfo.username}}



                </li>



                <li v-else>



                    <a href="javascript:;" @click="()=>selectUser(userInfo)">


                        {{userInfo.username}}



                    </a>



                </li>



            </template>



        </ul>



        <div v-if="state.targetUser">


            <h3>{{state.targetUser.username}}</h3>


            <input type="text" placeholder="input some ..." v-model="state.msgText">


            <button @click="sendMessage">发送</button>


        </div>


        <div>

            <ul>

                <li v-for="(msg,index) of messageList" :key="index">

                    <h3>{{msg.fromUsername}}</h3>

                    <h4>{{msg.msg}}</h4>

                </li>

            </ul> 

        </div>

    </div>

</template>



<script setup>
import {io} from "socket.io-client";
import {useRouter} from "vue-router"
import { computed, reactive } from "vue";


const router =useRouter();
const state =reactive({
    username:router.currentRoute.value.query.username,
    userList:[],
    targetUser:null,
    msgText:"",
    messageBox:{}
})

const socket = io('http://localhost:3000',{
    query:{
        username:state.username
    }
})


const selectUser=(userInfo)=>{
    state.targetUser=userInfo
}


const messageList=computed(()=>{
    if(state.messageBox[state.username] && state.targetUser){
        return state.messageBox[state.username].filter(item=>{
            return item.fromUsername===state.targetUser.username || item.toUsername===state.targetUser.username
        })
    }
    else{
        return []
    }
})


const sendMessage=()=>{
    if(!state.msgText.length){
        return 
    }
    !state.messageBox[state.username] && (state.messageBox[state.username]=[])
    state.messageBox[state.username].push({
        fromUsername:state.username,
        toUsername:state.targetUser.username,
        msg:state.msgText,
        dateTime:new Date().getTime()
    })


    socket.emit('send',{
        fromUsername:state.username,
        targetId:state.targetUser.id,
        msg:state.msgText
    })
}


socket.on('online',(data)=>{
    console.log(data)
    state.userList=data.userList
})


socket.on('receive',(data)=>{
    console.log(data)
    !state.messageBox[state.username] && (state.messageBox[state.username]=[])
    state.messageBox[state.username].push(data)
    console.log(state.messageBox[state.username])
})


</script>
<style socpe>
    
</style>

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYb1jIeG' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片