最近忙这工作上的项目,整个公司就自己一个搞游戏的,负责整个项目,为了就是入职时候一个承诺会开游戏部。客户端代码因为是个外包半成品,入职一个月终于把第一个版本弄出来了,bug都修改差不多,该换的UI,增加的特效都搞了,连工具都写,服务端web资源下载都要搞。第一个版本出来后还是不错的。后面的需求才是坑啊,要添加功能但是服务端呢?难道我连服务端也要搞吗?确实忍不了,真想把整个客户端,服务器重写。晚上回家就用erlang尝试重写服务端,搭一个erlang服务端框架,剩下就是添加功能了。
选择erlang原因很简单,曾经写过卡牌服务器就是用erlang,也有其它源码做参考,记录下。
数据库:mnesia
框架:OTP
TCP层参考的是rabbitmq,所以采用了异步方式
init({AcceptorCount, Port}) ->
process_flag(trap_exit, true),
case gen_tcp:listen(Port, ?TCP_OPTIONS) of %%启动tcp服务器,监听端口
{ok, LSock} ->
lists:foreach(fun (_) -> %%并且通过 tcp_acceptor_sup启动N个tcp_accetpor
{ok, _APid} = supervisor:start_child(
coc_tcp_acceptor_sup, [LSock])
end,
lists:duplicate(AcceptorCount, dummy)), %% 返回一个由AcceptorCount个dummy组成的列表。
%{ok, {LIPAddress, LPort}} = inet:sockname(LSock),
{ok, LSock};
{error, Reason} ->
{stop, {cannot_listen, Reason}}
end.
accept(State = #state{sock=LSock}) ->
case prim_inet:async_accept(LSock, -1) of %% 异步等待连接
{ok, Ref} -> {noreply, State#state{ref=Ref}};
Error -> {stop, {cannot_accept, Error}, State}
end.
%% 开启客户端服务
start_client(Sock) ->
{ok, Child} = supervisor:start_child(coc_tcp_client_sup, []),
ok = gen_tcp:controlling_process(Sock, Child),
Child ! {go, Sock}.
流程大概就是:先开启mnesia数据库,ets临时保存在线信息,开启若干个tcp_accetpor等等链接,采用异步方式,因为客户端使用的是字节流方式自己写的一套协议工具,所以服务端也用Bit.
数据来到服务端先做个解析Decode,再做个MD5验证
case pp_help:checkmd5(ProtocolId,MD51,MD5Data) of
true ->
%%MD5验证通过,派发协议
routing({ProtocolId,Socket,BodyData,Client});
Other->
%%MD5验证失败
%%先检查是否已经登录,已经登录就退出再lost,暂时先直接退出
login_lost(Socket, Client, 0, Other)
end;
通过routing做协议的分发派送,先处理登陆和注册,Client保存了一登陆之后的信息,包括玩家进程pid,登陆超时次数,使用超时处理可以做玩家长时间没有发生消息自动退出登陆服务端做下线处理
%%断开连接
login_lost(Socket, _Client, _Cmd, Reason) ->
case _Client#client.login == 0 of
true ->
gen_tcp:close(Socket),
exit({unexpected_message, Reason});
Other ->
%% 下线
mod_player:stop(_Client#client.player),
gen_tcp:close(Socket),
exit({unexpected_message, Reason})
end.
登陆注册就是检查各种合法性,如果是失败重新登陆,已经登陆就先退出再登陆。
%%登陆检查入口
login(R, Socket) ->
% 名字就是key
KeyUserName = R#player.username,
% 检查是否已经在线
case mod_help:get_online_info(KeyUserName) of
[]->
% 不在线
{ok, Pid} = mod_player:start(), %开启玩家进程
% 上次登录时间
Time = util:unixtime()+5,
%%更新在线时间
case db:set(player,KeyUserName,[{last_login_time,Time},{online_flag,1}]) of
{atomic,ok} ->
%登陆成功,加载或更新数据
States = login_success(R,Pid,Socket,Time),
{true, States};
Other ->
erlang:display(Other),
{false, ?ERROR_6}
end;
_R ->
%% 退出重新登录
logout(_R#ets_online.pid),
login(R,Socket)
end.
登陆成功后就读取数据库里面的玩家信息,保存到一个mod_player进程里面,以后就靠这个做处理了。ets保存一些在线信息。
%% 登陆成功,加载或更新数据
login_success(Player, Pid, Socket, LastLoginTime) ->
%{Username,Password,Last_login_time,Online_flag,Sex,Golds,Dimoa} = Player,
%% 玩家信息
PlayerStatus = #player_status{
username = Player#player.username,
online_flag= Player#player.online_flag, % 在线标志
pid = Pid,
sex= Player#player.sex, % 性别
golds= Player#player.golds, % 金币
dimoa= Player#player.dimoa % 钻石
},
%% 设置玩家信息
gen_server:cast(Pid, {'SET_PLAYER', PlayerStatus}),
%% 在线信息
ets:insert(?ETS_ONLINE, #ets_online{
nickname = Player#player.username,
pid = Pid
}),
PlayerStatus.
到这里就应该搭建好服务端框架了,里面还有很多封装的东西,客户端和服务端的协议都是自动生成太爽。调试也成功了,服务端已经放回数据给客户端,解析成功。接下来是做个多人在线打牌系统。想法是这样:单独一个监听sup,专门做房间的监听,加入到里面的人新开个GamePlayer进程,使用ets保存房间信息,还有玩家Game进程,玩家断开连接的时候重新拉房间信息更新客户端。(说的确实不清楚,但是想法已经有就是)。慢慢来。