기본 요구 사항
고객 서비스 GM은 게임 서버의 모든 플레이어를 친구로 추가하고 채팅을 할 수 있습니다. 구체적인 기능은 다음과 같습니다.
* GM 온라인 및 오프라인
* 게이머를 친구로 추가
* 게임 플레이어를 친구로 삭제
*GM이 채팅 메시지를 보냅니다
* 플레이어 푸시 채팅 메시지
추가 제한 사항: 하나의 GM 계정은 여러 게임 플레이어를 친구로 추가할 수 있지만, 하나의 게임 플레이어는 하나의 GM 계정으로만 추가할 수 있습니다.
요구 사항 분석
서버 간 채팅이 없기 때문입니다. 서버 간 친구 기능은 향후 지원되지 않으므로 GM이 게임에서 캐릭터를 생성한 후 각 게임 서버의 플레이어를 채팅에 추가하는 솔루션은 구현이 불가능합니다. 게다가 GM은 실제로 게임 캐릭터가 아니므로 게임 내에서 생성할 필요도 없습니다.
전체적인 어려움은 각 게임 서버가 GM이 전송한 다양한 데이터에 액세스할 수 있도록 하는 방법과 플레이어 데이터를 GM에 푸시하는 방법입니다.
특정 구현
GM 데이터를 다양한 서버로 전송하기 위해 간단한 솔루션을 채택했습니다. GM 데이터를 웹 서버에 저장하면 각 게임 서버가 다음에서 전송됩니다. 웹 서버가 데이터를 가져옵니다.
이 솔루션은 매우 간단합니다. 웹 서버와 게임 서버 사이에 긴 연결이 필요하지 않으며 http의 get 및 post 메소드를 직접 사용하여 데이터를 얻을 수 있습니다. 전체 구조는 다음과 같습니다.
<code><span>GM1</span><span>-</span><span>-</span><span>-</span><span>-</span><span>-</span><span>-</span><span>-</span><span>运维聊天服</span><span>-</span><span>-</span><span>-</span><span>-</span><span>-</span><span>-</span><span>-</span><span>-</span><span>游戏web服务器</span><span>-</span><span>-</span><span>-</span><span>-</span><span>-</span><span>-</span><span>游戏服务器1</span><span>-</span><span>-</span><span>-</span><span>-</span><span>-</span><span>-</span><span>-</span><span>-</span><span>游戏客户端1</span><span>|</span><span>|</span><span>|</span><span>|</span><span>|</span><span>|</span><span>GM2</span><span>游戏服务器2</span><span>游戏客户端2</span></code>
여기서 고객 서비스 GM1과 GM2는 모두 웹 인터페이스를 사용하여 게임 클라이언트와 채팅합니다.
운영 및 유지보수 채팅 서버가 존재하는 이유는 다음과 같습니다.
* GM을 생성하려면 운영 및 유지 관리 측면의 승인이 필요합니다. .
* 게임 웹 서버는 화이트리스트 등록 및 검토가 가능하며, 운영 및 유지 관리 채팅 서버의 IP만 게임 웹 서버에 접속할 수 있습니다.
위 그림에서는 게임 클라이언트와 게임 서버만 tcp long 연결을 사용하고 나머지는 http short 연결을 사용하여 구현되었습니다.
웹 서버는 nodejs 대신 nginx를 사용합니다. nginx 솔루션은 매우 성숙하고 배포가 쉽습니다.
나는 Lua보다 Python에 훨씬 더 익숙하기 때문에 Lua에서 직접 작성하는 대신 uwsgi를 프록시로 사용했습니다.
데이터베이스는 Redis를 사용하고 Redis는 예약된 저장을 설정했습니다. 데이터 형식 설정은 앞서 말씀드린 글을 참고해주세요. 기본적으로 gm:%d:name
형식으로 되어있습니다. key는 gmx의 이름을 나타내고, val은 이름을 나타냅니다.
구현 코드
nginx, uwsgi 구성이 생략되었습니다. 개인정보 보호를 위해 관련 IP는 생략했으며, 코드에 주석이 충분하므로 자세한 내용은 다루지 않겠습니다. 블로거의 원본 기사이며 블로거의 승인을 받지 않았습니다. 복제는 허용되지 않습니다.
<code><span>#encoding: utf-8</span><span>""" 新功能: * GM注册 * GM上线 * GM下线 * 加游戏玩家为好友 * 删除游戏好友 * GM推聊天信息 * 玩家推聊天信息 --- 消息数据格式为utf-8处理后的base64编码:游戏服和GM发过来的都是base64格式,要注意分隔符没做base64处理 GS只能用get方式推送消息,所以参数用类似于urllib quote(urlencode)进行了封装。运维客户端也用get 一个GM账号能添加多个游戏玩家为好友,而一个游戏玩家只能被一个GM账号添加 """</span><span>from</span> config <span>import</span> * <span>from</span> json <span>import</span> dumps, loads <span>import</span> base64 <span>import</span> urllib <span>import</span> urllib2 <span>import</span> copy <span>import</span> redis MSG_SEPARATOR = <span>","</span><span>#分割信息</span> MAX_RECV_AMOUNT = <span>10</span><span>#每次消息10条吧</span> MSG_MAX_LEN = <span>500</span><span>#消息不弄太长了</span>CONTENT_TYPE = [(<span>"Content-Type"</span>,<span>"text/html"</span>)] HTTP_STATUS = { <span>200</span>: <span>"200 OK"</span>, <span>404</span>: <span>"404 Not Found"</span>, } GAME_SERVER_INFO_URL = <span>"http://xxxxxyyyyy"</span>ROLE_INFO_URL = <span>"http://xxyyy?uid=%s&hostnum=%s"</span>red = redis.StrictRedis(host=REDIS.HOST, port=REDIS.PORT, db=REDIS.DB) <span>#游戏服务器IP白名单</span><span>if</span><span>not</span> globals().has_key(<span>"gServerIP"</span>): gServerIP = {} res_data = urllib2.urlopen(GAME_SERVER_INFO_URL) res = res_data.read() res_list = res.split(<span>"\n"</span>) <span>for</span> val <span>in</span> res_list: <span>if</span><span>not</span> val: <span>continue</span> _, port, ip, _ = val.split(<span>" "</span>) gServerIP[ip] = port gGMIP = { <span>"xxxxyyyy"</span> : <span>1</span>, } <span><span>def</span><span>is_gm_account_exist</span><span>(account_id)</span>:</span><span>if</span> red.get(<span>"gm_account:%s:name"</span> % account_id): <span>return</span><span>1</span><span>return</span><span>0</span><span><span>def</span><span>is_inter_server</span><span>(hostnum)</span>:</span><span>if</span> ( int(hostnum) >= <span>1000</span> ): <span>return</span><span>0</span><span>return</span><span>1</span><span><span>def</span><span>check_is_int</span><span>(account_id)</span>:</span><span>try</span>: int(account_id) <span>except</span>: <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>0</span>, <span>"errno"</span>: -<span>1</span>}) <span>return</span> () <span>#gm client ensures the id is unique</span><span><span>def</span><span>gm_create_account</span><span>(env, params)</span>:</span> account_id, account_name = params[<span>"gm_account"</span>], urllib.unquote(params[<span>"gm_name"</span>]) check_res = check_is_int(account_id) <span>if</span> check_res: <span>return</span> check_res <span>if</span> is_gm_account_exist(account_id): <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>0</span>, <span>"errno"</span>: <span>1</span>}) <span>#1 the role exists</span> red.set(<span>"gm_account:%s:name"</span> % account_id, account_name) red.sadd(<span>"gm_online_list"</span>, account_id) <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>1</span>}) <span>#check param</span><span><span>def</span><span>gm_add_friend</span><span>(env, params)</span>:</span> var = gm_account, hostnum, usernum = params[<span>"gm_account"</span>], params[<span>"host"</span>], params[<span>"uid"</span>] <span>for</span> num <span>in</span> var: check_res = check_is_int(num) <span>if</span> check_res: <span>return</span> check_res <span>if</span><span>not</span> is_gm_account_exist(gm_account): <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>0</span>, <span>"errno"</span>: <span>2</span>}) <span>#2 the role doesn't exist</span><span>if</span> ( red.get(<span>"gs_usernum:%s:friend"</span> % usernum) ): <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>0</span>, <span>"errno"</span>: <span>3</span>}) <span>#3 the usernum has gotten a friend</span><span>#内服计费没存数据,就不处理了</span><span>if</span><span>not</span> is_inter_server(hostnum): http_res_data = urllib2.urlopen(ROLE_INFO_URL % (usernum, hostnum)) res = loads(http_res_data.read()) <span>if</span> (type(res) != type({})) <span>or</span> (res.get(<span>"code"</span>, <span>0</span>) != <span>1</span>): <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>0</span>, <span>"errno"</span>: <span>4</span>}) <span>#4 the uid doesn't exist</span> red.sadd(<span>"gm_account:%s:friend"</span> % gm_account, usernum) <span>#两边都处理下</span> red.sadd(<span>"gs_hostnum:%s"</span> % hostnum, usernum) <span>#记录该服务器的所有玩家</span> red.set(<span>"gs_usernum:%s:hostnum"</span> % usernum, hostnum) <span>#该玩家的信息</span> red.set(<span>"gs_usernum:%s:friend"</span> % usernum, gm_account) <span>#一个玩家只能被一个gm添加为好友</span> red.sadd(<span>"apply_frd_list"</span>, usernum) <span>#usernum will be added </span> red.hdel(<span>"remove_frd_list"</span>, usernum) <span>#信息残留</span><span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>1</span>}) <span><span>def</span><span>gm_remove_friend</span><span>(env, params)</span>:</span> account_id, uid = params[<span>"gm_account"</span>], params[<span>"uid"</span>] <span>if</span><span>not</span> is_gm_account_exist(account_id): <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>0</span>, <span>"errno"</span>: <span>2</span>}) <span>#2 the role doesn't exist</span><span>if</span> red.get(<span>"gs_usernum:%s:friend"</span> % uid) != account_id: <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>0</span>, <span>"errno"</span>: <span>5</span>}) <span># the usernum has friend but isn't the gm</span><span>if</span><span>not</span> red.srem(<span>"gm_account:%s:friend"</span> % account_id, uid): <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>0</span>, <span>"errno"</span>: <span>4</span>}) <span># the usernum is not a friend of the gm</span> hostnum = red.get(<span>"gs_usernum:%s:hostnum"</span> % uid) red.delete(<span>"gs_usernum:%s:hostnum"</span> % uid) <span>#合服考虑,如果合服了GM手动删除这个玩家吧</span> red.srem(<span>"gs_hostnum:%s"</span> % hostnum, uid) red.delete(<span>"gs_usernum:%s:friend"</span> % uid) red.hset(<span>"remove_frd_list"</span>, uid, hostnum) <span>#uid的信息已经丢失,先额外保存下hostnum信息</span> red.srem(<span>"apply_frd_list"</span>, uid) <span>#信息残留</span><span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>1</span>}) <span>#GM账号很少</span><span><span>def</span><span>gm_online</span><span>(env, params)</span>:</span> account_id = params[<span>"gm_account"</span>] <span>#可能客户端bug没发下线,直接sadd吧</span><span>if</span><span>not</span> is_gm_account_exist(account_id): <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>0</span>, <span>"errno"</span>: <span>2</span>}) <span>#2 the role doesn't exist</span> red.sadd(<span>"gm_online_list"</span>, account_id) <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>1</span>}) <span><span>def</span><span>gm_offline</span><span>(env, params)</span>:</span> account_id = params[<span>"gm_account"</span>] <span>if</span><span>not</span> red.srem(<span>"gm_online_list"</span>, account_id): <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>0</span>}) <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>1</span>}) <span>#存在usernum上,gs_msg和gm_msg</span><span><span>def</span><span>gm_sendmsg</span><span>(env, params)</span>:</span> account_id, uid, msg = params[<span>"gm_account"</span>], params[<span>"uid"</span>], urllib.unquote(params[<span>"msg"</span>]) <span>#只能向好友发</span><span>if</span><span>not</span> red.sismember(<span>"gm_account:%s:friend"</span> % account_id, uid): <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>0</span>, <span>"errno"</span>: <span>4</span>}) <span># the usernum is not a friend of the gm</span><span>if</span> red.get(<span>"gs_usernum:%s:friend"</span> % uid) != account_id: <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>0</span>, <span>"errno"</span>: <span>5</span>}) <span># the usernum has friend but isn't the gm or doesn't have</span> red.lpush(<span>"gs_usernum:%s:msg_from_gm"</span> % uid, msg) red.sadd(<span>"gm_newmsg_list"</span>, uid) <span>#gs get msg from this set</span><span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>1</span>}) <span>#gm轮训所有的,他那边还有个服务器...</span><span>#{gm_account:{"uid": msg, "uid2": msg2}}</span><span><span>def</span><span>gm_receivemsg</span><span>(env, params)</span>:</span> user_set = copy.copy(red.smembers(<span>"gs_newmsg_list"</span>)) msg_data = {} <span>for</span> uid <span>in</span> user_set: gm_account = red.get(<span>"gs_usernum:%s:friend"</span> % uid) <span>if</span><span>not</span> gm_account: <span>#理论上是不会</span><span>continue</span> msg_list = pop_msg(uid, <span>"msg_from_gs"</span>) send_msg = MSG_SEPARATOR.join(msg_list) <span>if</span><span>not</span> send_msg: <span>continue</span><span>if</span><span>not</span> gm_account <span>in</span> msg_data: msg_data[gm_account] = [] msg_data[gm_account].append({<span>"uid"</span> : uid, <span>"msg"</span> : send_msg}) <span>#red.srem("gs_newmsg_list", uid)</span><span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>1</span>, <span>"data"</span>: base64.b64encode(dumps(msg_data))}) <span><span>def</span><span>pop_msg</span><span>(uid, msg_type)</span>:</span> msg_list = [] msg_key = <span>"gs_usernum:%s:%s"</span> % (uid, msg_type) msg_len = min(MAX_RECV_AMOUNT, red.llen(msg_key)) <span>for</span> i <span>in</span> xrange(msg_len): piece_msg = red.rpop(msg_key) msg_list.append(piece_msg) <span>return</span> msg_list <span>#---------------------GS----------------------</span><span>#apply and remove</span><span><span>def</span><span>get_frd_relation</span><span>(env, params)</span>:</span> host = params[<span>"host"</span>] apply_user_set = copy.copy(red.smembers(<span>"apply_frd_list"</span>)) apply_data = {} <span>#{"res":1 "data":base64({uid: gm_account})}</span><span>for</span> uid <span>in</span> apply_user_set: hostnum = red.get(<span>"gs_usernum:%s:hostnum"</span> % uid) <span>if</span> hostnum != host: <span>continue</span> account_id = red.get(<span>"gs_usernum:%s:friend"</span> % uid) <span>if</span><span>not</span> account_id: <span>#error </span><span>continue</span> apply_data[uid] = [account_id, red.get(<span>"gm_account:%s:name"</span> % account_id)] red.srem(<span>"apply_frd_list"</span>, uid) del_user_list = red.hkeys(<span>"remove_frd_list"</span>) remove_list = [] <span>for</span> uid <span>in</span> del_user_list: hostnum = red.hget(<span>"remove_frd_list"</span>, uid) <span>if</span> hostnum != host: <span>continue</span> remove_list.append(uid) red.hdel(<span>"remove_frd_list"</span>, uid) <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>1</span>, <span>"apply_data"</span>: base64.b64encode(dumps(apply_data)), <span>"remove_data"</span>: base64.b64encode(dumps(remove_list))}) <span><span>def</span><span>gs_sendmsg</span><span>(env, params)</span>:</span> uid, msg = params[<span>"uid"</span>], urllib.unquote(params[<span>"msg"</span>]) <span>if</span><span>not</span> red.get(<span>"gs_usernum:%s:friend"</span> % uid): <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>0</span>, <span>"errno"</span>: <span>5</span>}) <span># the usernum has friend but isn't the gm or doesn't have</span> red.lpush(<span>"gs_usernum:%s:msg_from_gs"</span> % uid, msg) red.sadd(<span>"gs_newmsg_list"</span>, uid) <span>#gm get msg from this set</span><span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>1</span>}) <span><span>def</span><span>gs_receivemsg</span><span>(env, params)</span>:</span> host = params[<span>"host"</span>] user_set = copy.copy(red.smembers(<span>"gm_newmsg_list"</span>)) total_msg_list = [] <span>for</span> uid <span>in</span> user_set: hostnum = red.get(<span>"gs_usernum:%s:hostnum"</span> % uid) <span>if</span> hostnum != host: <span>continue</span> msg_list = pop_msg(uid, <span>"msg_from_gm"</span>) user_msg = MSG_SEPARATOR.join(msg_list) <span>if</span><span>not</span> user_msg: <span>continue</span> msg_data = { <span>"uid"</span> : uid, <span>"msg"</span> : user_msg, } total_msg_list.append(msg_data) <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>1</span>, <span>"data"</span>: base64.b64encode(dumps(total_msg_list))}) <span><span>def</span><span>get_online_list</span><span>(env, params)</span>:</span> host = params[<span>"host"</span>] send_list = [] online_list = red.smembers(<span>"gm_online_list"</span>) <span>for</span> account_id <span>in</span> online_list: frd_set = red.smembers(<span>"gm_account:%s:friend"</span> % account_id) <span>for</span> uid <span>in</span> frd_set: <span>if</span> red.get(<span>"gs_usernum:%s:hostnum"</span> % uid) == host: send_list.append(account_id) <span>#只有这个服务器有gm的好友,才通知</span><span>break</span><span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, dumps({<span>"res"</span>: <span>1</span>, <span>"data"</span>: base64.b64encode(dumps(send_list))}) <span>#get: action=create&gm_account&gm_name 创建账号</span><span>#get: action=add&gm_account&host&uid 添加好友</span><span>#get: action=del&gm_account&uid 删除好友</span><span>#get: action=online&gm_account上线</span><span>#get: action=offline&gm_account 下线</span><span>#get: action=send&gm_account&uid&msg 发送消息</span><span>#get: action=receive 轮训消息</span> GM_FUNC = { <span>"create"</span> : gm_create_account, <span>"add"</span> : gm_add_friend, <span>"del"</span> : gm_remove_friend, <span>"online"</span> : gm_online, <span>"offline"</span> : gm_offline, <span>"send"</span> : gm_sendmsg, <span>"receive"</span> : gm_receivemsg, } <span><span>def</span><span>handle_gm_ticket</span><span>(env, params)</span>:</span><span>if</span><span>not</span> gGMIP.get(env[<span>"REMOTE_ADDR"</span>], <span>0</span>): <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, <span>"%s has no access to the website"</span> % env[<span>"REMOTE_ADDR"</span>] func = GM_FUNC.get(params[<span>"action"</span>], <span>None</span>) <span>if</span><span>not</span> func: <span>return</span> HTTP_STATUS[<span>404</span>], CONTENT_TYPE, <span>"err action %s"</span> % params[<span>"action"</span>] <span>return</span> func(env, params) <span>#get action=relation&host</span><span>#get action=send&uid&msg</span><span>#get action=receive&host</span><span>#get action=online&host </span> GS_FUNC = { <span>"relation"</span> : get_frd_relation, <span>"send"</span> : gs_sendmsg, <span>"receive"</span> : gs_receivemsg, <span>"online"</span> : get_online_list, } <span><span>def</span><span>handle_gs_ticket</span><span>(env, params)</span>:</span><span>if</span><span>not</span> gServerIP.get(env[<span>"REMOTE_ADDR"</span>], <span>0</span>): <span>return</span> HTTP_STATUS[<span>200</span>], CONTENT_TYPE, <span>"%s has no access to the website"</span> % env[<span>"REMOTE_ADDR"</span>] func = GS_FUNC.get(params[<span>"action"</span>], <span>None</span>) <span>if</span><span>not</span> func: <span>return</span> HTTP_STATUS[<span>404</span>], CONTENT_TYPE, <span>"err action %s"</span> % params[<span>"action"</span>] <span>return</span> func(env, params) </code>위 내용은 게임의 측면을 포함하여 게임 GM 채팅 기능을 구현하기 위한 nginx+uwsgi+redis의 사용을 소개합니다. PHP 튜토리얼에 관심이 있는 친구들에게 도움이 되기를 바랍니다.