Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

Learning from zero game server development (3) Research on CSBattleMgr service source code

2025-02-23 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Servers >

Share

Shulou(Shulou.com)06/02 Report--

As shown in the figure above, we will introduce CSBattleMgr in this article, but we will not look at the special details of this server (which we will cover in a later article). Reading the source code of an unknown project if we begin to dwell on all kinds of details, then we will eventually fall into the awkward situation of "looking across the winding mountains and looking at the steep peaks, and looking at the distance and height in a variety of ways", wasting time not to say, maybe getting twice the result with twice the effort. So, although we are not familiar with this set of code, we try to put me as a whole, first roughly understand the functions of each service, and then we will study the details later.

The second article in this series, "Learning from nothing Open Source Project Series (II) Last War Overview", we introduced that the service of this game needs to use redis and mysql. Let's first see if mysql is ready (the mysql service starts and the database table data exists. For details, please refer to the second article). Open the cmd program for Windows and enter the following instructions to connect to mysql:

Mysql-uroot-p123321

After the connection is successful, as shown in the following figure:

Then we enter the following instructions to see if the database we need has been created successfully:

Show databases

These are basic sql statements that you may need to learn if you are not familiar with them.

After the database is created successfully, it is shown in the following figure:

As for whether the table in the database is created successfully, we will not pay attention to it here, but we will study which data table we actually use later.

Mysql is no problem. Next, we need to start redis. Through the second article, we know that redis needs to be started twice, that is, a total of two redis processes, called redis-server and redis-login-server in our game service (their configuration file information is different). We can manually execute the following statement on the cmd command line in the Server\ Bin\ x64\ Release directory:

Start / min "redis-server"redis-server.exe" redis.conf

Start / min "redis-Logicserver"redis-server.exe" redis-logic.conf

But this is troublesome. I will copy out these two sentences and put them in a file called start-redis.bat. You only need to execute this bat file every time you start:

The redis and redis-logic services are started as shown in the following figure:

Our common redis services are source code under linux. Microsoft has modified the source code of redis and released a version of Windows, which is slightly unsatisfactory (for example, there is no API that completely matches linux's fork () under Windows, so it can only be replaced by CreateProcess ()). The official download address for the redis source code for the windows version is: https://github.com/MicrosoftArchive/redis/releases.

After launching mysql and redis, let's officially take a look at the CSBattleMgr service. Readers may have to ask, with so many services, how do you know to look at this service first? As we said in the last article, we found in the start.bat file that this is the third service that needs to be started besides redis, so let's study it first (start.bat we can think of as the deployment steps "documentation" left for us by the source author):

We open the CSBattleMgr service main.cpp file and find the entry main function, which is as follows:

Int main () {DbgLib::CDebugFx::SetExceptionHandler (true); DbgLib::CDebugFx::SetExceptionCallback (ExceptionCallback, NULL); GetCSKernelInstance (); GetCSUserMgrInstance (); GetBattleMgrInstance (); GetCSKernelInstance ()-> Initialize (); GetBattleMgrInstance ()-> Initialize (); GetCSUserMgrInstance ()-> Initialize (); GetCSKernelInstance ()-> Start (); mysql_library_init (0, NULL, NULL); GetCSKernelInstance ()-> MainLoop ();}

Through debugging, we send this function to roughly do the following tasks:

/ / 1. Set the program exception handling function / / 2. Initialize a series of singleton objects / / 3. Initialize mysql//4. Enter an infinite cycle called the "main cycle"

Step 1 setting the program exception handling function is not described, let's take a look at step 2 initialize a series of singleton objects, initializing a total of three classes of objects CCSKernel, CCSUserMgr, and CCSBattleMgr. There is nothing to introduce the singleton pattern itself, but someone wants to mention the thread safety of the singleton pattern, so there are a lot of locked singleton pattern code, which I personally don't think is necessary. I think that a friend who wants to add a lock may think that it will be a problem if the singleton object is called by multiple threads at the same time during the first initialization. I think the overhead of locking is not as good as the above code. Get the singleton object at the beginning of the initialization of the whole program and let the singleton object be generated. Later, even if multiple threads get the singleton object, it is a read operation without locking. Take GetCSKernelInstance (); as an example:

CCSKernel* GetCSKernelInstance () {return & CCSKernel::GetInstance ();} CCSKernel& CCSKernel::GetInstance () {if (NULL = = pInstance) {pInstance = new CCSKernel;} return * pInstance;}

The initialization action of GetCSKernelInstance ()-> Initialize () is actually loading various configuration information and setting a series of callback functions and timers in advance:

Do not adjust the order of INT32 CCSKernel::Initialize () {/ / JJIAZ when loading configuration CCSCfgMgr::getInstance (). Initalize (); INT32 n32Init = LoadCfg (); if (eNormal! = n32Init) {ELOG (LOG_ERROR, "loadCfg () .failed!"); return n32Init } if (m_sCSKernelCfg.un32MaxSSNum > 0) {m_psSSNetInfoList = new SSSNetInfo [m _ sCSKernelCfg.un32MaxSSNum]; memset (m_psSSNetInfoList, 0, sizeof (SSSNetInfo) * m_sCSKernelCfg.un32MaxSSNum); m_psGSNetInfoList = new SGSNetInfo [m _ sCSKernelCfg.un32MaxGSNum]; memset (m_psGSNetInfoList, 0, sizeof (SGSNetInfo) * m_sCSKernelCfg.un32MaxGSNum); m_psRCNetInfoList = new SRCNetInfo [10] } mGSMsgHandlerMap [eMsgToCSFromGS_AskRegiste:: eMsgToCSFromGS_AskRegiste] = std::bind (& CCSKernel::OnMsgFromGS_AskRegiste, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); mGSMsgHandlerMap [GSToCS:: eMsgToCSFromGS_AskPing] = std::bind (& CCSKernel::OnMsgFromGS_AskPing, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3) M _ GSMsgHandlerMap [GSToCS:: eMsgToCSFromGS_ReportGCMsg] = std::bind (& CCSKernel::OnMsgFromGS_ReportGCMsg, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3); mSSMsgHandlerMap [SSToCS:: eMsgToCSFromSS_AskPing] = std::bind (& CCSKernel::OnMsgFromSS_AskPing, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3) AddTimer (& CCSKernel::ProfileReport, this, std::placeholders::_1, std::placeholders::_2), 5000, true); return eNormal;}

As shown in the figure above, these configuration information are game terms, including various skills, heroes, models, and so on.

GetBattleMgrInstance ()-> Initialize () actually starts a timer for the CSKernel object: INT32 CCSBattleMgr::Initialize () {GetCSKernelInstance ()-> AddTimer (std::bind (& CCSMatchMgr::Update, m_pMatchMgr, std::placeholders::_1, std::placeholders::_2), c_matcherDelay, true); return eNormal;}

GetCSUserMgrInstance ()-> Initialize () is some information about initializing mysql and redis. Since redis is used for service caching, we generally see that words like cacheServer refer to redis in projects:

Void CCSUserMgr::Initialize () {SDBCfg cfgGameDb = CCSCfgMgr::getInstance (). GetDBCfg (eDB_GameDb); SDBCfg cfgCdkeyDb=CCSCfgMgr::getInstance (). GetDBCfg (eDB_CdkeyDb); m_UserCacheDBActiveWrapper = new DBActiveWrapper (std::bind (& CCSUserMgr::UserCacheDBAsynHandler, this, std::placeholders::_1), cfgGameDb, std::bind (& CCSUserMgr::DBAsyn_QueryWhenThreadBegin, this)); masking UserCacheDBActiveWrapper-> Start () M_CdkeyWrapper = new DBActiveWrapper (& CCSUserMgr::UserAskDBAsynHandler, this, std::placeholders::_1), cfgCdkeyDb, std::bind (& CCSUserMgr::CDKThreadBeginCallback, this); masked CdkeyWrapper-> Start (); for (int I = 0; I)

< gThread ; i++) { DBActiveWrapper* pThreadDBWrapper(new DBActiveWrapper(std::bind(&CCSUserMgr::UserAskDBAsynHandler, this, std::placeholders::_1), cfgGameDb)); pThreadDBWrapper->

Start (); m_pUserAskDBActiveWrapperVec.push_back (pThreadDBWrapper);}}

Pay attention to one point, ha. I don't know if you have noticed that we use a lot of functions like std::bind () in the code. Note that since the version of Visual Studio we use is 2010 Visual Studio and this version does not support Category 11, so the std::bind here is not in Category 11, but in the draft tr1 before it is released, so all the namespaces should be tr1::std::bind, and other similar functions are the same. So you can see in the code a statement that introduces a namespace like this:

GetCSKernelInstance ()-> Start (); is the Session manager that initializes all network connections. The so-called Session is translated as "session". Its lower layer corresponds to the connection of network communication, and each connection corresponds to a Session. The object that manages these Session is Session Manager, which in our code is CSNetSessionMgr, which inherits from the interface class INetSessionMgr:

Class CSNetSessionMgr: public INetSessionMgr {public: CSNetSessionMgr (); virtual ~ CSNetSessionMgr (); public: virtual ISDSession* UCAPI CreateSession (ISDConnection* pConnection) {return NULL; / * rewrite * /} virtual ICliSession* UCAPI CreateConnectorSession (SESSION_TYPE type); virtual bool CreateConnector (SESSION_TYPE type, const char* ip, int port, int recvsize, int sendsize, int logicId); private: CSParser masked CSParser;}; the code to initialize CSNetSessionMgr is as follows: INT32 CCSKernel::Start () {CSNetSessionMgr* pNetSession = new CSNetSessionMgr GetBattleMgrInstance ()-> RegisterMsgHandle (m_SSMsgHandlerMap, m_GSMsgHandlerMap, m_GCMsgHandlerMap, m_RCMsgHandlerMap); GetCSUserMgrInstance ()-> RegisterMsgHandle (m_SSMsgHandlerMap, m_GSMsgHandlerMap, m_GCMsgHandlerMap, m_RCMsgHandlerMap); ELOG (LOG_INFO, "success!"); return 0;}

After connecting to the database successfully, the console of our CSBattleMgr program displays a line indicating that the mysql connection is successful:

Readers will find that these log messages have three colors, the error information is red, the important normal information is green, and the general output information is gray. How is this achieved? We will introduce the specific implementation principles in the next article "Learning from zero open source project series (3) LogServer service source code research", which I think is a more eye-catching way than using log-level tags.

After introducing the initialization process, let's introduce the MainLoop () function, the main part of the service, and take a look at the overall code:

Void CCSKernel::MainLoop () {TIME_TICK tHeartBeatCDTick = 10; / / listening port 10002 INetSessionMgr::GetInstance ()-> CreateListener (massisCSKernelCfg.n32GSNetListenerPort.1024000.102400memo 0recogGateSessionFactory); / / listening port 10001 INetSessionMgr::GetInstance ()-> CreateListener (mleavsCSKernCfg.n32SSNetListenerPortStart 1024000Cfg. N32SSNetListenerPortReporter 1024000Cfg10240000); / listening port 10010 INetSessionMgr::GetInstance ()-> CreateListener (mSecretsCSnelCfg.n32RCNetListenerPortEnergGateGateSessionFactory) / / connect LogServer 1234 port INetSessionMgr::GetInstance ()-> CreateConnector (ST_CLIENT_C2Log, m_sCSKernelCfg.LogAddress.c_str (), m_sCSKernelCfg.LogPort, 102400 if 102400); / / connect redis 6379 if (m_sCSKernelCfg.redisAddress! = "0") {INetSessionMgr::GetInstance ()-> CreateConnector (ST_CLIENT_C2R, m_sCSKernelCfg.redisAddress.c_str (), missus CSKernelCfg.redisPortBook 102400, 102400) } / / Connect to redis 6380, which is also redis-logic if (m_sCSKernelCfg.redisLogicAddress! = "0") {INetSessionMgr::GetInstance ()-> CreateConnector (ST_CLIENT_C2LogicRedis, m_sCSKernelCfg.redisLogicAddress.c_str (), massisCSKernelCfg.redisLogicPort102400);} while (true) {if (kbhit ()) {static char CmdArray [1024] = {0}; static int CmdPos = 0 Char CmdOne= getche (); CmdArray [CmdPos++] = CmdOne; bool bRet = 0; if (CmdPos > = 1024 | | CmdOne==13) {CmdArray [--CmdPos] = 0; bRet = DoUserCmd (CmdArray); CmdPos=0; if (bRet) break;}} INetSessionMgr::GetInstance ()-> Update (); GetCSUserMgrInstance ()-> OnHeartBeatImmediately (); + + masked RunCounts; m_BattleTimer.Run () Sleep (1);}}

Although this function is called MainLoop (), the actual MainLoop () is only the second half. The first part creates a total of three listening ports and three connectors, that is, the so-called Listener and Connector, all of which are managed by the above-mentioned CSNetSessionMgr. The so-called Listener is that the service uses the socket API bind () and listen () functions to bind to a binary of an address + port number. For other programs to connect (other programs may be other service programs or clients, which one we will dig further in a later article), the statistics of the listening port are as follows:

Listening port 10002

Listening port 10001

Listening port 10010

There are also three connectors (Connector). The service and port numbers to which they connect are:

Connect port 6379 of redis

Connect port 6380 of redis-logic

Connect to port 1234 of a service

Which service is this port 1234? Through the code, we can see that it is LogServer, so whether it is LogServer or not, we will verify it later.

INetSessionMgr::GetInstance ()-> CreateConnector (ST_CLIENT_C2Log, m_sCSKernelCfg.LogAddress.c_str (), m_sCSKernelCfg.LogPort, 102400 and 102400)

Then we officially enter a while loop:

While (true) {if (kbhit ()) {static char CmdArray [1024] = {0}; static int CmdPos=0; char CmdOne= getche (); CmdArray [CmdPos++] = CmdOne; bool bRet = 0; if (CmdPos > = 1024 | | CmdOne==13) {CmdArray [- CmdPos] = 0; bRet = DoUserCmd (CmdArray); CmdPos=0; if (bRet) break;}} INetSessionMgr::GetInstance ()-> Update () GetCSUserMgrInstance ()-> OnHeartBeatImmediately (); + + masked Runcounts; m_BattleTimer.Run (); Sleep (1);}

For what the loop does, let's first look at INetSessionMgr::GetInstance ()-> Update (); code:

Void INetSessionMgr::Update () {mNetModule- > Run (); vector tempQueue; EnterCriticalSection (& mNetworkCs); tempQueue.swap (m_SafeQueue); LeaveCriticalSection (& mNetworkCs); for (auto it=tempQueue.begin (); itinerant tempQueue.end (); + + it) {char* pBuffer = (* it); int nType = * ((int*) pBuffer) + 0); int nSessionID = * ((int*) pBuffer) + 1) Send ((SESSION_TYPE) nType,nSessionID,pBuffer+2*sizeof (int)); delete [] pBuffer;} auto & map = m_AllSessions.GetPointerMap (); for (auto it=map.begin (); itinerant mapping. End (); + + it) {(* it)-> Update ();}}

From this code, we can see that this function first uses the swap () method of the std::vector object to swap the data from a public queue into a temporary queue, which is a common technique to reduce the granularity of locks: because common queues need to be used by both producers and consumers, we want to reduce the granularity and time of locking. Switch the existing data in the current queue to a temporary queue local to the consumer at one time, so that consumers can use the temporary queue, thus avoiding the need to lock the data from the public queue every time. Efficiency has been improved. Then, we find that the data in this queue are Session objects, traversing the data sent by the opposite end of each Session object's connection, while executing the Update () method of the Session object. What specific data have been sent, we will study it in the following article.

Let's look at the second function in the loop, GetCSUserMgrInstance ()-> OnHeartBeatImmediately ();, whose code is as follows:

INT32 CCSUserMgr::OnHeartBeatImmediately () {OnTimeUpdate (); SynUserAskDBCallBack (); return eNormal;}

These names are self-explanatory, first synchronizing the time, and then synchronizing some operations in the database:

INT32 CCSUserMgr::SynUserAskDBCallBack () {while (! m_DBCallbackQueue.empty ()) {Buffer* pBuffer = NULL; m_DBCallbackQueue.try_pop (pBuffer); switch (pBuffer- > m_LogLevel) {case DBToCS::eQueryUser_DBCallBack: SynHandleQueryUserCallback (pBuffer); break; case DBToCS::eQueryAllAccount_CallBack: SynHandleAllAccountCallback (pBuffer); break Case DBToCS::eMail_CallBack: SynHandleMailCallback (pBuffer); break; case DBToCS::eQueryNotice_CallBack: DBCallBack_QueryNotice (pBuffer); break; default: ELOG (LOG_WARNNING, "not hv handler:%d", pBuffer- > m_LogLevel); break } if (pBuffer) {m_DBCallbackQueuePool.ReleaseObejct (pBuffer);}} return 0;}

Take another look at the third function, m_BattleTimer.Run (), in the while loop; the code is as follows:

Void CBattleTimer::Run () {TimeKey nowTime = GetInternalTime (); while (! m_ThreadTimerQueue.empty ()) {ThreadTimer& sThreadTimer = m_ThreadTimerQueue.top (); if (! m_InvalidTimerSet.empty ()) {auto iter = m_InvalidTimerSet.find (sThreadTimer.sequence); if (iter! = m_InvalidTimerSet.end ()) {m_InvalidTimerSet.erase (iter) M_ThreadTimerQueue.pop (); continue;}} if (nowTime > = sThreadTimer.nextexpiredTime) {m_PendingTimer.push_back (sThreadTimer); m_ThreadTimerQueue.pop ();} else {break }} if (! m_PendingTimer.empty ()) {for (auto iter = m_PendingTimer.begin (); iter! = m_PendingTimer.end (); + + iter) {ThreadTimer& sThreadTimer = * iter; nowTime = GetInternalTime (); int64_t tickSpan = nowTime-sThreadTimer.lastHandleTime; sThreadTimer.pHeartbeatCallback (nowTime, tickSpan) If (sThreadTimer.ifPersist) {TimeKey newTime = nowTime + sThreadTimer.interval; sThreadTimer.lastHandleTime = nowTime; sThreadTimer.nextexpiredTime = newTime; m_ThreadTimerQueue.push (sThreadTimer);}} m_PendingTimer.clear () } if (! m_ToAddTimer.empty ()) {for (auto iter = m_ToAddTimer.begin (); iter! = m_ToAddTimer.end (); + + iter) {m_ThreadTimerQueue.push (* iter);} m_ToAddTimer.clear ();}}

This is also a time-related operation. We will also introduce the details in a later article.

After the CSBattleMgr service runs, the cmd window appears as follows:

In the figure above, we see that both the Mysql and redis services are connected, but the program will always prompt that the connection 127.0.0.1 is not connected to port 1234. From this we conclude that the service using port 1234 is not started. This is not the focus of our introduction, the key point is to explain that this service will automatically reconnect the port 1234 regularly, and the automatic reconnection mechanism is a function that we must be proficient in server development. So I suggest you take a good look at this piece of code. Let's go through it briefly with you here.

First, we follow the prompt to find line 42 of INetSessionMgr::LogText and add a breakpoint there:

Soon, due to the reconnection mechanism, this breakpoint is triggered, and we take a look at the call stack at this time:

Let's switch to the stack code shown in the figure arrow:

The description is mNetModule- > Run (); the log output generated by the call. Let's take a look at the call to this:

Bool CUCODENetWin::Run (INT32 nCount) {CConnDataMgr::Instance ()-> RunConection (); do {/ / # ifdef UCODENET_HAS_GATHER_SEND / / # pragma message ("[preconfig] sdnet collect buffer, has an internal timer") / / if (m_pTimerModule) / / {/ / mroompTimerModule-> Run () / /} / / # endif#ifdef UCODENET_HAS_GATHER_SEND static INT32 sendCnt = 0; + + sendCnt; if (sendCnt = = 10) {sendCnt = 0; UINT32 now = GetTickCount (); if (now

< m_dwLastTick) { /// 溢出了,发生了数据回绕 \/// m_dwLastTick = now; } if ((now - m_dwLastTick) >

50) {m_dwLastTick = now; FlushBufferedData ();}} # endif / SNetEvent stEvent; SNetEvent * pstEvent = CEventMgr::Instance ()-> PopFrontNetEvt (); if (pstEvent = = NULL) {return false;} SNetEvent & stEvent = * pstEvent Switch (stEvent.nType) {case NETEVT_RECV: _ ProcRecvEvt (& stEvent.stUn.stRecv); break; case NETEVT_SEND: _ ProcSendEvt (& stEvent.stUn.stSend); break; case NETEVT_ESTABLISH: _ ProcEstablishEvt (& stEvent.stUn.stEstablish); break Case NETEVT_ASSOCIATE: _ ProcAssociateEvt (& stEvent.stUn.stAssociate); break; case NETEVT_TERMINATE: _ ProcTerminateEvt (& stEvent.stUn.stTerminate); break; case NETEVT_CONN_ERR: _ ProcConnErrEvt (& stEvent.stUn.stConnErr); break; case NETEVT_ERROR: _ ProcErrorEvt (& stEvent.stUn.stError) Break; case NETEVT_BIND_ERR: _ ProcBindErrEvt (& stEvent.stUn.stBindErr); break; default: SDASSERT (false); break;} CEventMgr::Instance ()-> ReleaseNetEvt (pstEvent);} while (--nCount! = 0); return true;}

When we see SNetEvent * pstEvent = CEventMgr::Instance ()-> PopFrontNetEvt ();, we can roughly see here that this is another producer-consumer model, except that here is the consumer-- take the data out of the queue, and the corresponding switch-case branch is:

Case NETEVT_CONN_ERR:

_ ProcConnErrEvt (& stEvent.stUn.stConnErr)

That is, the connection failed. So where is the connection? We only need to look at where the producer of the queue can find it. Because the connection is not successful, put a connection error data into the queue. Let's look at the implementation of CEventMgr::Instance ()-> PopFrontNetEvt () and find the specific queue name:

/ * * @ brief gets an unhandled network event (currently the first inserted network event) * @ return returns an unhandled network event. Return NULL * @ remark if processing fails, since this class can only be called in the main thread, thread safety is not guaranteed inside this function * / inline SNetEvent* PopFrontNetEvt () {return (SNetEvent*) m_oEvtQueue.PopFront ();}

Through this code, we find that the name of the queue is m_oEvtQueue. We search for the name of the queue to find the producer, and then add a breakpoint where the producer adds data to the queue:

After the breakpoint is triggered, let's take a look at the call stack at this time:

Let's switch to the code pointed to by the arrow in the figure above:

So far, we basically understand that the asynchronous connect () used by the connection here, that is, connect socket in thread A, then bind the socket with WSAEventSelect and set the socket to non-blocking mode. When the connection has a result (success or failure), use Windows API WSAEnumNetworkEvents to detect the connection event (FD_CONNECT) of this socket, and then add the judgment result to the queue m_oEvtQueue, and another thread B takes the judgment result out of the queue and prints out the log. If you are not clear about this process, learn how to use asynchronous connect and the use of WSAEventSelect and WSAEnumNetworkEvents. So where is this asynchronous connect? Let's search for the socket API connect function (actually I could have searched for the connect function in the first place, but the reason I didn't do this was to let you know how I studied the code of an unfamiliar project) and got the following figure:

Let's add a breakpoint at the above red mark:

With the port information 1234 in the figure above, we verify that this is indeed the process mentioned above. Then let's look at the call stack:

Found that here is another consumer, and there is a queue!

By the same token, we find the producer through the queue name m_oReqQueue:

Let's take a look at the call stack of the producer at this time:

Switch to the code shown in the figure:

Bool ICliSession::Reconnect ()

{

If (IsHadRecon () & & mReconnectTag)

{

UINT32 curTime = GetTickCount ()

If (curTime > mReconTime) {mReconTime = curTime+10000; if (client reconnect server (% s)-> ReConnect ()) {/ / printf ("client reconnect server (% s)...\ n", mRemoteEndPointer.c_str ()); ResetRecon (); return true;} return false

}

Here we can finally take a good look at how the logic of reconnection is designed. The specific code is analyzed by the readers themselves, ha, but it will not be introduced here because of the limited space.

Seeing here, many readers may have a difficulty when comparing the code I have provided: why can the same code be analyzed in this way in my hands, but may stumble in your hands? It can only be said that experience and self-learning are complementary processes, such as the producer-consumer model and task queue mentioned above. I used to be like you, and I was not familiar with these things. but when I know these things, I learn what I think is the "basic" knowledge, and practice it over and over again, so I slowly accumulate experience. Therefore, Confucius is right: learning without thinking is labor lost, and thinking without learning is perilous. The ancients did not deceive me when to learn and when to think.

So far we have a rough idea of what CSBattleMgr has done. Later, we will go through all the services and then introduce them as a whole. We will continue to study this LogServer listening on port 1234 in the next article. Please look forward to it.

Due to the limited experience of the author, there may be mistakes and omissions in the article. Criticism and correction are welcome.

In addition, some friends want me to provide the source code without my modification. Here is also provided. Xxx method: Wechat searches the official account "easyserverdev" (Chinese name: high-performance server development). After following the official account, reply "the last battle of the original source code" in the official account to get the download link. (sprinklers and code sellers please stay away! )

Welcome to the official account "easyserverdev". If you need my help with any technical or professional problems, you can contact me through this official account, which not only shares the experience and stories of high-performance server development, but also provides free technical answers and career answers for the majority of technical friends. If you have any questions, you can leave a message on the Wechat official account directly, and I will reply to you as soon as possible.

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Servers

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report