3

游戏服务器


  • 多人房间

  • 高并发

  • 低延时

  • 数据可靠

  • ...

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

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

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


准备工作

思考方向
多人房间:进入房间的用户,可以感知到该房间内其他的用户,其他用户也可以感知该用户。网络聊天室就是最常见的多人聊天的实现,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

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


njaulj
876 声望17 粉丝