Mir2源码详解之服务端-登录网关(LoginGate)
传奇这款游戏,一直对我的影响很大。当年为了玩传奇,逃课,被老师叫过N次家长。言归正传,网上有很多源码,当然了,都是delphi的。并且很多源码还不全,
由于一直学习的c、c++。delphi还真不懂。无奈硬着头皮上。好了。废话不多说。开始。
登录网关,负责游戏最开始的登录处理(与账户服务器LoginSvr通讯)。验证登录器输入的账户密码是否正确。
界面上的控件很多。其实干活的就 就 三个:“TServerSocket”、“TClientSocket”、“DecodeTimer”这三个控件。
ServerSocket:负责与登录器进行通讯,它做的操作:
1、接收连接,代码如下:
{ 函数功能:接受客户端连接,发送消息到 登录服务器 } procedure TFrmMain.ServerSocketClientConnect(Sender: TObject; Socket: TCustomWinSocket); var UserSession:pTUserSession; sRemoteIPaddr,sLocalIPaddr:String; nSockIndex:Integer; IPaddr :pTSockaddr; begin Socket.nIndex:=-1; // 客户端IP地址 sRemoteIPaddr:=Socket.RemoteAddress; if g_boDynamicIPDisMode then begin sLocalIPaddr:=ClientSocket.Socket.RemoteAddress; end else begin sLocalIPaddr:=Socket.LocalAddress; end; // 过滤ip if IsBlockIP(sRemoteIPaddr) then begin MainOutMessage('过滤连接: ' + sRemoteIPaddr,1); Socket.Close; exit; end; // 当前IP是否可以连接 if IsConnLimited(sRemoteIPaddr) then begin case BlockMethod of // 断开 mDisconnect: begin Socket.Close; end; // 动态过滤 mBlock: begin New(IPaddr); IPaddr.nIPaddr:=inet_addr(PChar(sRemoteIPaddr)); TempBlockIPList.Add(IPaddr); CloseConnect(sRemoteIPaddr); end; // 永久过滤 mBlockList: begin New(IPaddr); IPaddr.nIPaddr:=inet_addr(PChar(sRemoteIPaddr)); BlockIPList.Add(IPaddr); CloseConnect(sRemoteIPaddr); end; end; MainOutMessage('端口攻击: ' + sRemoteIPaddr,1); exit; end; // 如果网关准备好了 if boGateReady then begin for nSockIndex:= 0 to GATEMAXSESSION - 1 do begin UserSession:=@g_SessionArray[nSockIndex]; if UserSession.Socket = nil then begin UserSession.Socket:=Socket; UserSession.sRemoteIPaddr:=sRemoteIPaddr; UserSession.nSendMsgLen:=0; UserSession.bo0C:=False; UserSession.dw10Tick:=GetTickCount(); UserSession.dwConnctCheckTick:=GetTickCount(); UserSession.boSendAvailable:=True; UserSession.boSendCheck:=False; UserSession.nCheckSendLength:=0; UserSession.n20:=0; UserSession.dwUserTimeOutTick:=GetTickCount(); UserSession.SocketHandle:=Socket.SocketHandle; UserSession.sIP:=sRemoteIPaddr; UserSession.MsgList.Clear; Socket.nIndex:=nSockIndex; Inc(nSessionCount); break; end; end; // 和本地登录服务器进行通讯 if Socket.nIndex >= 0 then begin ClientSocket.Socket.SendText('%O' + IntToStr(Socket.SocketHandle) + '/' + sRemoteIPaddr + '/' + sLocalIPaddr + '$'); MainOutMessage('Connect: ' + sRemoteIPaddr,5); end else begin Socket.Close; MainOutMessage('Kick Off: ' + sRemoteIPaddr,1); end; end else begin //0x004529EF Socket.Close; MainOutMessage('Kick Off: ' + sRemoteIPaddr,1); end; end;
说白了,别看 那些IP过滤规则和连接限制什么的。就是 来了一用户,直接保存到一个 UserSession中。然后通知LoginSvr,有人连接了。。现在市面上的什么:GOM引擎、Hero、HGE(原3K、IGE)、Legend、GEEM2、77M2都是换汤不换药。变的就是 加密方式。这里不得不说,JsocketJ就是TServerSocket、TClientSocket的控件,懒得看源码,看了下其属性,大致就可以了解到。
是采用的线程池的select模型。早期的游戏,都采用这种方式,不过,对于私*服,连接量不大,完全足够应付。其实有更好的解决办法,那就是IOCP来管理。效率更高。windows下最适合的模型了。
2、断开连接,代码如下:
procedure TFrmMain.ServerSocketClientDisconnect(Sender: TObject; Socket: TCustomWinSocket); var I:Integer; UserSession:pTUserSession; nSockIndex:Integer; sRemoteIPaddr:String; IPaddr :pTSockaddr; nIPaddr :Integer; begin sRemoteIPaddr:=Socket.RemoteAddress; nIPaddr:=inet_addr(PChar(sRemoteIPaddr)); nSockIndex:=Socket.nIndex; for I := 0 to CurrIPaddrList.Count - 1 do begin IPaddr:=CurrIPaddrList.Items[I]; if IPaddr.nIPaddr = nIPaddr then begin Dec(IPaddr.nCount); if IPaddr.nCount <= 0 then begin Dispose(IPaddr); CurrIPaddrList.Delete(I); end; Break; end; end; if (nSockIndex >= 0) and (nSockIndex < GATEMAXSESSION) then begin UserSession:=@g_SessionArray[nSockIndex]; UserSession.Socket:=nil; UserSession.sRemoteIPaddr:=''; UserSession.SocketHandle:=-1; UserSession.MsgList.Clear; Dec(nSessionCount); if boGateReady then begin ClientSocket.Socket.SendText('%X' + IntToStr(Socket.SocketHandle) + '$'); MainOutMessage('DisConnect: ' + sRemoteIPaddr,5); end; end; end;
删除,释放,并通知 LoginSvr,有人断开连接了。。。
3、接收数据,代码如下:
procedure TFrmMain.ServerSocketClientRead(Sender: TObject; Socket: TCustomWinSocket); var UserSession:pTUserSession; nSockIndex:Integer; sReviceMsg,s10,s1C:String; nPos:Integer; nMsgLen:Integer; begin nSockIndex:=Socket.nIndex; if (nSockIndex >= 0) and (nSockIndex < GATEMAXSESSION) then begin UserSession:=@g_SessionArray[nSockIndex]; sReviceMsg:=Socket.ReceiveText; if (sReviceMsg <> '') and (boServerReady) then begin nPos:=Pos('*',sReviceMsg); if nPos > 0 then begin UserSession.boSendAvailable:=True; UserSession.boSendCheck:=False; UserSession.nCheckSendLength:=0; s10:=Copy(sReviceMsg,1,nPos -1); s1C:=Copy(sReviceMsg,nPos + 1,Length(sReviceMsg) - nPos); sReviceMsg:=s10 + s1C; end; nMsgLen:=length(sReviceMsg); if (sReviceMsg <> '') and (boGateReady) and (not boKeepAliveTimcOut)then begin UserSession.dwConnctCheckTick:=GetTickCount(); if (GetTickCount - UserSession.dwUserTimeOutTick) < 1000 then begin Inc(UserSession.n20,nMsgLen); end else UserSession.n20:= nMsgLen; ClientSocket.Socket.SendText('%A' + IntToStr(Socket.SocketHandle) + '/' + sReviceMsg + '$'); end; end; end; end;
拿到数据,转发到 登录账户服务器。。。。。它主要干的事完了。。
ClientSocket:负责与LoginSvr进行通讯,它做的主要工作就是,
procedure TFrmMain.ClientSocketRead(Sender: TObject; Socket: TCustomWinSocket); var sRecvMsg:String; begin sRecvMsg:=Socket.ReceiveText; ClientSockeMsgList.Add(sRecvMsg); end;
你没有看错,就是接收到数据。然后保存到链表,然后等待定时器来解析操作。。
DecodeTimer 定时器的工作。源码中设置的 时间精度是 1毫秒:
procedure TFrmMain.DecodeTimerTimer(Sender : TObject); var sProcessMsg :String; sSocketMsg :String; sSocketHandle :String; nSocketIndex :Integer; nMsgCount :Integer; nSendRetCode :Integer; nSocketHandle :Integer; dwDecodeTick :LongWord; dwDecodeTime :LongWord; sRemoteIPaddr :String; UserSession :pTUserSession; IPaddr :pTSockaddr; begin ShowMainLogMsg(); if boDecodeLock or (not boGateReady)then exit; try dwDecodeTick:=GetTickCount(); boDecodeLock:=True; sProcessMsg:=''; while (True) do begin if ClientSockeMsgList.Count <= 0 then break; sProcessMsg:=sProcMsg + ClientSockeMsgList.Strings[0]; sProcMsg:=''; ClientSockeMsgList.Delete(0); while (True) do begin if TagCount(sProcessMsg,'$') < 1 then break; sProcessMsg:=ArrestStringEx(sProcessMsg,'%','$',sSocketMsg); if sSocketMsg = ''then break; if sSocketMsg[1] = '+' then begin if sSocketMsg[2] = '-' then begin CloseSocket(Str_ToInt(Copy(sSocketMsg,3,Length(sSocketMsg) - 2),0)); Continue; end else begin //0x004521B7 dwKeepAliveTick:=GetTickCount(); boKeepAliveTimcOut:=False; Continue; end; end; //0x004521CD sSocketMsg:=GetValidStr3(sSocketMsg,sSocketHandle,['/']); nSocketHandle:=Str_ToInt(sSocketHandle,-1); if nSocketHandle < 0 then Continue; for nSocketIndex:= 0 to GATEMAXSESSION - 1 do begin if g_SessionArray[nSocketIndex].SocketHandle = nSocketHandle then begin g_SessionArray[nSocketIndex].MsgList.Add(sSocketMsg); break; end; end; end; //0x00452246 end; //0x452252 //if sProcessMsg <> '' then ClientSockeMsgList.Add(sProcessMsg); if sProcessMsg <> '' then sProcMsg:=sProcessMsg; nSendMsgCount:=0; n456A2C:=0; StringList318.Clear; for nSocketIndex:= 0 to GATEMAXSESSION - 1 do begin if g_SessionArray[nSocketIndex].SocketHandle <= -1 then Continue; //踢除超时无数据传输连接 if (GetTickCount - g_SessionArray[nSocketIndex].dwConnctCheckTick) > dwKeepConnectTimeOut then begin sRemoteIPaddr:=g_SessionArray[nSocketIndex].sRemoteIPaddr; case BlockMethod of // mDisconnect: begin g_SessionArray[nSocketIndex].Socket.Close; end; mBlock: begin New(IPaddr); IPaddr.nIPaddr:=inet_addr(PChar(sRemoteIPaddr)); TempBlockIPList.Add(IPaddr); CloseConnect(sRemoteIPaddr); end; mBlockList: begin New(IPaddr); IPaddr.nIPaddr:=inet_addr(PChar(sRemoteIPaddr)); BlockIPList.Add(IPaddr); CloseConnect(sRemoteIPaddr); end; end; MainOutMessage('端口空连接攻击: ' + sRemoteIPaddr,1); Continue; end; while (True) do begin; if g_SessionArray[nSocketIndex].MsgList.Count <= 0 then break; UserSession:=@g_SessionArray[nSocketIndex]; nSendRetCode:=SendUserMsg(UserSession,UserSession.MsgList.Strings[0]); if (nSendRetCode >= 0) then begin if nSendRetCode = 1 then begin UserSession.dwConnctCheckTick:=GetTickCount(); UserSession.MsgList.Delete(0); Continue; end; if UserSession.MsgList.Count > 100 then begin nMsgCount:=0; while nMsgCount <> 51 do begin UserSession.MsgList.Delete(0); Inc(nMsgCount); end; end; Inc(n456A2C,UserSession.MsgList.Count); MainOutMessage(UserSession.sIP + ' : ' + IntToStr(UserSession.MsgList.Count),5); Inc(nSendMsgCount); end else begin //0x004523A4 UserSession.SocketHandle:= -1; UserSession.Socket:= nil; UserSession.MsgList.Clear; end; end; end; if (GetTickCount - dwSendKeepAliveTick) > 2 * 1000 then begin dwSendKeepAliveTick:=GetTickCount(); if boGateReady then ClientSocket.Socket.SendText('%--$'); end; if (GetTickCount - dwKeepAliveTick) > 10 * 1000 then begin boKeepAliveTimcOut:=True; ClientSocket.Close; end; finally boDecodeLock:=False; end; dwDecodeTime:=GetTickCount - dwDecodeTick; if dwDecodeMsgTime < dwDecodeTime then dwDecodeMsgTime:=dwDecodeTime; if dwDecodeMsgTime > 50 then Dec(dwDecodeMsgTime,50); end;
又是一坨代码,简而言之,就是把刚才保存接收到的数据。分发到 每个用户的自己的消息链表中,然后遍历,发送出去,
代码如下:
// 发送用户消息 function TFrmMain.SendUserMsg(UserSession:pTUserSession;sSendMsg:String):Integer; begin Result:= -1; // 如果 if UserSession.Socket <> nil then begin // 取反 if not UserSession.bo0C then begin // 如果不能发送,则置可用 if not UserSession.boSendAvailable and (GetTickCount > UserSession.dwSendLockTimeOut) then begin UserSession.boSendAvailable := True; UserSession.nCheckSendLength := 0; boSendHoldTimeOut := True; dwSendHoldTick := GetTickCount(); end; //004525DD if UserSession.boSendAvailable then begin if UserSession.nCheckSendLength >= 250 then begin if not UserSession.boSendCheck then begin UserSession.boSendCheck:=True; sSendMsg:='*' + sSendMsg; end; if UserSession.nCheckSendLength >= 512 then begin UserSession.boSendAvailable:=False; UserSession.dwSendLockTimeOut:=GetTickCount + 3 * 1000; end; end; //00452620 UserSession.Socket.SendText(sSendMsg); Inc(UserSession.nSendMsgLen,length(sSendMsg)); Inc(UserSession.nCheckSendLength,length(sSendMsg)); Result:= 1; end else begin //0x0045264A Result:= 0; end; end else begin //0x00452651 Result:= 0; end; end; end;
登录网关,是不是很简单,这不是 重点,重点是市面上的很多引擎的登录网关都基于这套机制,只需要逆向分析下其加密算法,一个自定义网关则出来了。至于过滤规则,什么IP通道,都是浮云。。。。