[游戏服务器]第一章:多人聊天室-客户端

news/2024/7/3 5:09:49

游戏服务器


  • 多人房间

  • 高并发

  • 低延时

  • 数据可靠

  • ...

那么怎么去实现这些功能呢,下面我将会带着大家一起去探寻游戏服务器的奥秘

我不是巨人,我只是站在巨人的肩膀上
我将会分城多个章节去研究游戏服务器的开发;依旧是 自上而下,由表及内,由浅入深。

第一章:解决多人房间问题


准备工作

  • 新建一个git项目 game-server

思考方向
多人房间:进入房间的用户,可以感知到该房间内其他的用户,其他用户也可以感知该用户。网络聊天室就是最常见的多人聊天的实现,ex. Slack 等。ok!work!work!

项目初始化


多人聊天室根据业务拆成 服务端和客户端,前后端分离;

mkdir game-server //新建项目目录

服务端初始化


服务端我们选择了兼容性最好的socket.io

cd game-server
mkdir gm-server //服务端
cd gm-server && npm init -y //默认初始化
npm install --save socket.io

客户端初始化


由于最近正在学习vue.js,就顺手拿vue来练练手

vue init webpack gm-client //使用vue官方推荐的项目构建工具vue-cli来初始化客户端,依旧eslint,单元测试、端到端测试的都选n

客户端实现


我的客户端才用的是 vue+vuex+vue-router来进行开发,如果对vue+vuex+vue-router 三者结合有些许生疏的话,可以参考[vue+vuex+vue-router] 强撸一发暗黑风 markdown 日记应用;所以重复的我就不赘述了,我们把重心放在具体实现上。

界面设计


我有一个爱好,希望在纸上画画写写,做到心中有物,言之有物

图片描述
图片描述
上面两张图够简单吧,加入房间页面 和 聊天页面,同理,路由也就有了两个join 和 index

客户端初始化


安装依赖

cd gm-client
npm install -D vuex vue-router socket.io 
修改index.html

//index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>gm-client</title>
  </head>
  <body>
      <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
</html>
初始化src目录
cd src
mkdir views
mkdir vuex
touch router.js

入口文件

//main.js
import Vue from 'vue'
import App from './App'
import VueRouter from 'vue-router'
import {get_token} from './vuex/getters'
import store from './vuex/store'

import configRouter from './router'

Vue.use(VueRouter)
var router = new VueRouter();

configRouter(router)

router.beforeEach((transition)=>{
    const token = get_token(store.state)
    if(transition.to.auth){
        if(token){
            transition.next()
        }else {
            const redirect = encodeURIComponent(transition.to.path);
              transition.redirect({ name: 'join', query: { redirect } });
        }
    }else {
        transition.next()
    }
})

router.start(Vue.extend(App),'#app')

export default router;

初始化组件App.vue

//app.vue
<template>
  <div id="main">
    <button id="delay1" v-bind:class="[delay_flag?'green':'red']"></button>
    <button id="delay2" v-bind:class="[delay_flag?'green':'red']"></button>
    <button id="delay3" v-bind:class="[delay_flag?'green':'red']"></button>
    <button id="delay4" v-bind:class="[delay_flag?'green':'red']"></button>
    <span id="delay_flag">{{get_delay}}ms</span>
    <router-view></router-view>
  </div>
</template>
<script>
import {get_delay} from './vuex/getters'
import {connect} from './vuex/actions'
import store from './vuex/store';

export default {
  store,
  vuex:{
    getters:{
      get_delay
    },
    actions:{
      connect
    }
  },
  ready(){
    this.connect()
  },
  computed:{
    delay_flag(){
      return this.get_delay<60
    }
  }
}
</script>

<style>
html {
  height: 100%;
}

body {
  width: 100%;
  height: 100%;
  padding:0 0;
  margin:0 0;
}

#main {
  width:500px;
  margin: 0 auto;
  height: 100%;
}

.green {
  background-color:#86e468;
}

.red {
  background-color:red;
}

#delay1 {
  padding:0 0;
  width:5px;
  height:5px;
  border-radius: 50%;
  border:none;
}

#delay2 {
  padding:0 0;
  width:7px;
  height:7px;
  border-radius: 50%;
  border:none;
}

#delay3 {
  padding:0 0;
  width:9px;
  height:9px;
  border-radius: 50%;
  border:none;
}

#delay4 {
  padding:0 0;
  width:11px;
  height:11px;
  border-radius: 50%;
  border:none;
}

 #delay_flag {
  font-size:5px;
 }
</style>

路由功能


//router.js
export default (router)=>router.map({
    '/':{
        name:'join',
        component:require('./views/join')
    },
    '/index':{
        name:'index',
        component:require('./views/index'),
        auth:true
    }
})

vuex设计


根据vuex的核心思想

初始化vuex目录

cd vuex
touch store.js //管理state和mutations
touch actions.js //管理dispatch
touch getters.js //通过纯粹的函数获取到state的值
store.js 实现

//store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

const state = {
    token:'',
    account:'',
    room:'',
    socket:undefined,
    delay:0,
    messages:{}
}

const mutations = {
    CHANGE_SOCKET (state,socket){
        state.socket = {...socket}
    },
    CHANGE_ACCOUNT (state,account){
        state.account = account
    },
    CHANGE_ROOM (state,room){
        state.room = room
    },
    CHANGE_DELAY (state,delay){
        state.delay = delay
    },
    CHANGE_TOKEN (state,token){
        state.token = token
    },
    CHANGE_MESSAGES (state,data){
        if(state.messages[data.room] && state.messages[data.room].length){
            state.messages[data.room].splice(0,0,data)
        }else{
            console.log(data)
            var new_room = {}
            Object.defineProperty(new_room,data.room.toString(),{
                value: [],
                writable: true,
                enumerable: true,
                configurable: true
            })
            console.log(new_room)
            state.messages = Object.assign({},state.messages,new_room)
            console.log(state.messages)
            state.messages[data.room].push(data)
        }
    }
}

export default new Vuex.Store({
    state,
    mutations
})
actions.js

//actions.js
import io from 'io';
let socket;
import store from './store'
import router from '../main'

export const connect = ({dispatch}) =>{
    socket = io('http://localhost:3000')
    dispatch('CHANGE_SOCKET',socket)
    start_socket()
}

export const input_account = ({dispatch},e) => dispatch('CHANGE_ACCOUNT',e.target.value);

export const input_room = ({dispatch},e) => dispatch('CHANGE_ROOM',e.target.value);

export const join = ({dispatch},account,room)=>{
    socket.emit('join',{account,room})
}

export const post_message = ({dispatch},room,content) =>{
    socket.emit('post',{room:room,content:content})
}

function start_socket(){
    socket.on('conn',function(data){
        console.log(data)
    })

    socket.on('heart',function(_data){
        var data = {..._data,timestamp:new Date().getTime()}
        store.dispatch('CHANGE_DELAY',data.timestamp-data._timestamp)
    })

    socket.on('join',function(_data){
        store.dispatch('CHANGE_TOKEN',_data._id)
        router.go({name:'index'})
    })

    socket.on('message',function(_data){
        console.log(_data)
        store.dispatch('CHANGE_MESSAGES',_data)
    })
}
getters.js

//getters.js
export const get_account = (state) => state.account;
export const get_room = (state) => state.room;
export const get_delay = (state) => state.delay;
export const get_token = (state) => state.token;
export const get_messages = (state) => state.messages;

views页面实现


两个路由对应两个界面

join.vue 加入房间页面
//join.vue
<template>
    <div id="join-form" v-on:keyup.enter="join_btn">
        <h1>多人聊天室</h1>
        <input @input="input_account" value="{{account}}" placeholder="用户名"><br/>
        <input @input="input_room" value="{{room}}" placeholder="房间名"><br/>    
        <button  @click.prevent.stop="join_btn">进入房间</button>
    </div>
</template>
<script>
import {input_account,join,input_room} from '../vuex/actions'
import {get_account,get_room} from '../vuex/getters'

export default {
    vuex:{
        actions:{
            input_account,
            join,
            input_room
        },
        getters:{
            account:get_account,
            room:get_room
        }
    },
    methods:{
        join_btn(){
            this.join(this.account,this.room)
        }
    }

}
</script>
<style>
    #join-form {
        width:500px;
        margin:0 auto;
        text-align: center;
    }
</style>
index页面,聊天页面

//index.vue
<template>
    <div id="chat-room">
        <h1>[{{room}}]: welcome {{account}}</h1>
        <div id="main">
            <ul>
                <li v-for="message in room_messages">
                    {{message.from_account}}:{{message.content}}
                </li>
            </ul>
        </div>
        <div id="post_block">
            <input v-on:keyup.enter="post_btn" v-model="content" >
            <button v-on:keyup.enter="post_btn" @click="post_btn">发送</button>
        </div>
    </div>
</template>
<script>
import {get_account,get_room,get_messages} from '../vuex/getters' 
import {post_message} from '../vuex/actions'

export default {
    data(){
        return {
            content:''
        }
    },
    vuex:{
        getters:{
            account:get_account,
            room:get_room,
            messages:get_messages
        },
        actions:{
            post_message
        }
    }, 
    methods:{
        post_btn(){
            this.post_message(this.room,this.content)
            this.content= ''
        }
    },
    computed:{
        room_messages:{
            get(){
                return this.messages[this.room]
            }
        }
    }
}
</script>
<style scoped>
    #chat-room {
        width: 500px;
        margin:0 auto;
        text-align: left;
    }

    #main{
        width:100%;
        height:400px;
        overflow: scroll;
        font-size:10px;
        text-align: left;
        background-color: #f2f2f2;
    }

    #post_block{
        float:right;
        width:200px;
        height:100px;
    }
</style>

运行


cd gm-server && node index.js
//再开一个terminal
cd gm-client 
npm run dev

至此,聊天室服务端和客户端能够跑起来了,大家可以下载源代码去试一试,也可以自己撸出新高度,本文旨在自我学习与分享。如有错误或者不理解的可以留言。


http://www.niftyadmin.cn/n/4585626.html

相关文章

解决joomla报错 JFolder::create: Path not in open_basedir paths

解决方法&#xff1a;Goto this file: /libraries/joomla/filesystem/folder.php编辑&#xff1a;$obd ini_get(open_basedir);-->//$obd ini_get(open_basedir);敬请关注&#xff1a;www.right365.com 最In的推理门户网站&#xff01;转载于:https://blog.51cto.com/1178…

小白前端摸索记(一)—— 从手工作坊到自动化生产

​ *本文需知&#xff0c;渣小白&#xff0c;可能会有很多错误&#xff0c;希望指出&#xff0c;亲身踩坑经历总结 ​ *本文内容&#xff0c;关于前端自动化的基本理解以及从零开始构建一个自动化项目 ​ 学习前端也半年有余吧&#xff0c;在网上的前端学习初级教程中&#xff…

apache sysctl 重启_日常问题解决:记一次因信号量不足引起的APACHE启动错误解决以及kernel.sem值优化...

一、 问题描述重启apache失败&#xff0c;刷新页面无法反应&#xff0c;查看apache报错日志&#xff0c;如下&#xff1a;[Fri Jun 19 08:09:20.596555 2020] [core:emerg] [pid 29740:tid 139882613802816] (28)No space left on device: AH00023: Couldnt create the proxy m…

swiper基础使用(八)

2019独角兽企业重金招聘Python工程师标准>>> 本文为H5EDU机构官方的HTML5培训教程 swiper教程 这节课我们介绍swiper页面的free模式。 第一步&#xff0c;我们还是要创建一个swiper的基础页面。 <div class"swiper-container"><div class"…

跟着太白老师学python day10 函数嵌套, global , nonlocal

函数嵌套&#xff1a; 第一种嵌套方法 def func():count 123def inner():print(count)inner() func() 第二种嵌套方法 count 123 def func_1():print(count)def func_2():func_1() func_2() 1. global count 0 def func1():global count #把count变量变成全局变量&#xff…

awk if 包含_awk 命令计算磁盘使用率,强大的编程功能,何不了解一下?

背景利用 df 命令计算 Linux 系统上磁盘的使用率&#xff0c;脚本编写过程有些周折&#xff0c;没想到不同操作系统上 df 命令的输出内容还有差异&#xff0c;所以脚本需要定制编写。脚本思路利用 awk &#xff0c;累加 used 列得到已使用的空间大小&#xff0c;然后将 used 和…

UIButton图片拉伸方法(很多需要按钮的地方我们只需要一张小图来进行缩放)...

系统提供3种方法来满足不同的需求&#xff08;直接上代码&#xff09;&#xff1a; #import <UIKit/UIKit.h>interface UIImage (Common)/*** 根据图片名返回一张能自由拉伸的图片(图片缩放)*/(UIImage *)resizedImage:(NSString *)name;/***iOS5提供的方法*/ (UIImage*…

如何在mysql配置文件输入_MySQL配置文件my.cnf设置

转自 http://www.blogjava.net/baoyaer/articles/209466.html设置建议:对于单台运行的WEB服务器,建议加上:skip-lockingskip-name-resolveskip-networking在PHP链接数据库时使用"LOCALHOST".这样MySQL 客户端库将覆盖之并尝试连接到本地套接字.(我们可以从PHP.INI中代…