游戏服务器
多人房间
高并发
低延时
数据可靠
...
那么怎么去实现这些功能呢,下面我将会带着大家一起去探寻游戏服务器的奥秘
我不是巨人,我只是站在巨人的肩膀上
我将会分城多个章节去研究游戏服务器的开发;依旧是 自上而下,由表及内,由浅入深。
第一章:解决多人房间问题
准备工作
新建一个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://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。