使用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
我们选择第三个,按回车。
进去之后,我们选择有Router配置,将其他的取消,然后按回车下一步,因为我们会用到路由模拟用户,所以这里会用到路由中的useRouter。
创建好项目后,我们只需要一个页面,删除多余的路由,保留App.vue中的,然后创建一个Chatting.vue组件,绑定在chatting路由下,随后我们打开三个页面。
这三个页面都用到了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)
})
在客户端拿到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标签显示。
当我们在客户端选择一个用户通信之后,我们需要记住跟哪一个用户通信,所以我们需要在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>
点击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事件,将对应的消息对象传送给目标用户(只有目标实例的用户可以接收到信息,其他用户接收不到)
当我们给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>
看看最终效果吧。
源码
服务端
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>
转载自:https://juejin.cn/post/7256983702811377720