select 函数重难点解析

select 函数重难点解析 select 函数是网络通信编程中非常常用的一个函数,因此应该熟练掌握它。虽然它是 BSD 标准之一的 Socket 函数之一,但在 Linux 和 Windows 平台,其行为表现还是有点区别的。我们先来看一下 Linux 平台上的 select 函数。 Linux 平台下的 select 函数 select 函数的作用是检测一组 socket 中某个或某几个是否有“事件”,这里的“**事件”**一般分为如下三类: 可读事件,一般意味着可以调用 recv 或 read 函数从该 socket 上读取数据;如果该 socket 是侦听 socket(即调用了 bind 函数绑定过 ip 地址和端口号,并调用了 listen 启动侦听的 socket),可读意味着此时可以有新的客户端连接到来,此时可调用 accept 函数接受新连接。 可写事件,一般意味着此时调用 send 或 write 函数可以将数据“发出去”。 异常事件,某个 socket 出现异常。 函数签名如下: int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); 参数说明: 参数 nfds, Linux 下 socket 也称 fd,这个参数的值设置成所有需要使用 select 函数监听的 fd 中最大 fd 值加 1。 参数 readfds,需要监听可读事件的 fd 集合。 参数 writefds,需要监听可写事件的 fd 集合。 参数 exceptfds,需要监听异常事件 fd 集合。 readfds、writefds 和 exceptfds 类型都是 fd_set,这是一个结构体信息,其定义位于 /usr/include/sys/select.h 中: /* The fd_set member is required to be an array of longs....

January 11, 2021

socket 的阻塞模式和非阻塞模式

socket 的阻塞模式和非阻塞模式 对 socket 在阻塞和非阻塞模式下的各个函数的行为差别深入的理解是掌握网络编程的基本要求之一,是重点也是难点。 阻塞和非阻塞模式下,我们常讨论的具有不同行为表现的 socket 函数一般有如下几个,见下表: connect accept send (Linux 平台上对 socket 进行操作时也包括 write 函数,下文中对 send 函数的讨论也适用于 write 函数) recv (Linux 平台上对 socket 进行操作时也包括 read 函数,下文中对 recv 函数的讨论也适用于 read 函数) 限于文章篇幅,本文只讨论 send 和recv函数,connect 和 accept 函数我们将在该系列的后面文章中讨论。在正式讨论之前,我们先解释一下阻塞模式和非阻塞模式的概念。所谓阻塞模式,就当某个函数“执行成功的条件”当前不能满足时,该函数会阻塞当前执行线程,程序执行流在超时时间到达或“执行成功的条件”满足后恢复继续执行。而非阻塞模式恰恰相反,即使某个函数的“执行成功的条件”不当前不能满足,该函数也不会阻塞当前执行线程,而是立即返回,继续运行执行程序流。如果读者不太明白这两个定义也没关系,后面我们会以具体的示例来讲解这两种模式的区别。 如何将 socket 设置成非阻塞模式 无论是 Windows 还是 Linux 平台,默认创建的 socket 都是阻塞模式的。 在 Linux 平台上,我们可以使用 fcntl() 函数或 ioctl() 函数给创建的 socket 增加 O_NONBLOCK 标志来将 socket 设置成非阻塞模式。示例代码如下: int oldSocketFlag = fcntl(sockfd, F_GETFL, 0); int newSocketFlag = oldSocketFlag | O_NONBLOCK; fcntl(sockfd, F_SETFL, newSocketFlag); ioctl() 函数 与 fcntl() 函数使用方式基本一致,这里就不再给出示例代码了。 当然,Linux 下的 socket() 创建函数也可以直接在创建时将 socket 设置为非阻塞模式,socket() 函数的签名如下: int socket(int domain, int type, int protocol); 给 type 参数增加一个 SOCK_NONBLOCK 标志即可,例如:...

January 11, 2021

TCP 协议如何解决粘包、半包问题

TCP 协议如何解决粘包、半包问题 一 TCP 协议是流式协议 很多读者从接触网络知识以来,应该听说过这句话:TCP 协议是流式协议。那么这句话到底是什么意思呢?所谓流式协议,即协议的内容是像流水一样的字节流,内容与内容之间没有明确的分界标志,需要我们人为地去给这些协议划分边界。 举个例子,A 与 B 进行 TCP 通信,A 先后给 B 发送了一个 100 字节和 200 字节的数据包,那么 B 是如何收到呢?B 可能先收到 100 字节,再收到 200 字节;也可能先收到 50 字节,再收到 250 字节;或者先收到 100 字节,再收到 100 字节,再收到 200 字节;或者先收到 20 字节,再收到 20 字节,再收到 60 字节,再收到 100 字节,再收到 50 字节,再收到 50 字节…… 不知道读者看出规律没有?规律就是 A 一共给 B 发送了 300 字节,B 可能以一次或者多次任意形式的总数为 300 字节收到。假设 A 给 B 发送的 100 字节和 200 字节分别都是一个数据包,对于发送端 A 来说,这个是可以区分的,但是对于 B 来说,如果不人为规定多长为一个数据包,B 每次是不知道应该把收到的数据中多少字节作为一个有效的数据包的。而规定每次把多少数据当成一个包就是协议格式规范的内容之一。 经常会有新手写出类似下面这样的代码: 发送端: //...省略创建socket,建立连接等部分不相关的逻辑... char buf[] = "the quick brown fox jumps over a lazy dog."; int n = send(socket, buf, strlen(buf), 0); //...省略出错处理逻辑... 接收端: //省略创建socket,建立连接等部分不相关的逻辑... char recvBuf[50] = { 0 }; int n = recv(socket, recvBuf, 50, 0); //省略出错处理逻辑....

January 11, 2021

TeamTalk源码解析

TeamTalk源码解析 01 TeamTalk介绍 02 服务器端的程序的编译与部署 03 服务器端的程序架构介绍 04 服务器端db_proxy_server源码分析 05 服务器端msg_server源码分析 06 服务器端login_server源码分析 07 服务器端msfs源码分析 08 服务器端file_server源码分析 09 服务器端route_server源码分析 10 开放一个TeamTalk测试服务器地址和几个测试账号 11 pc客户端源码分析

January 11, 2021

不定参数函数实现var_arg系列的宏

不定参数函数实现var_arg系列的宏 电驴的源码日志模块有一个叫 DebugLogError 函数,其签名如下: //代码位于easyMule-master/src/WorkLayer/Log.h 55行 void DebugLogError(LPCTSTR pszLine, ...); 电驴的源码可以在公众号【 高性能服务器开发 】后台回复“获取电驴源码”即可获取。 这个函数的申明在 Log.h 头文件中,是一个全局函数,其实现代码在 Log.cpp 文件中: //代码位于easyMule-master/src/WorkLayer/Log.cpp 111行 void DebugLogError(LPCTSTR pszFmt, ...) { va_list argp; va_start(argp, pszFmt); LogV(LOG_DEBUG | LOG_ERROR, pszFmt, argp); va_end(argp); } 这个函数是一个具有不定参数的函数(也就是参数个数不确定),比如调用这个函数我们可以传入一个参数,也可以传入二个或者三个参数等等: DebugLogError(L"我喜欢你!"); DebugLogError(L"我喜欢你!", L"你喜欢谁?"); DebugLogError(L"我喜欢你!", L"你喜欢谁?", L"萧雨萌!"); 与此类似, C 语言中最熟悉的函数 printf() 和 scanf() 就是能传入不定参数的函数的例子,可是你知道如何编写这样具有不定参数的函数么? 你可以通过这段代码学习到编写方法,奥秘就在DebugLogError()中使用的几个你从来没见过的宏,让我们欢迎它们: va_list va_start va_end 这几个宏是C函数库提供的,位于头文件stdarg.h中。下面我们利用这几个宏自定义一个ShowLove()函数: #include <stdio.h> #include <tchar.h> #include <stdarg.h> #include <locale.h> int ShowLove(wchar_t* szFirstSentence, ...) { //用来统计可变参数数量 int num = 0; //第一步: //申明一个va_list类型对象ap,用于对参数进行遍历 va_list ap; //第二步: //使用va_start对变量进行初始化 //这里需要注意的是: //在传统C语言中,va_start把ap中内部指针设置为传递给函数参数的【第一个实参】; //而在标准的C中,va_start接受一个额外参数,也就是最后一个【固定参数】的名称, //并把ap中的内部指针设置为传递给函数的第一个【可变参数】. //所以你在VC++ 6.0和VS2008等高版本的编译器中使用va_start需要注意区别 //这里使用标准C va_start(ap, szFirstSentence); //第三步: //使用va_arg宏返回实参列表中的下一个参数值,并把ap的内部指针推向下一个参数(如果有的话) //必须指定下一个参数的类型。 //在调用va_start之后第一次调用va_arg将返回第一个可变参数的值 wprintf(szFirstSentence); wchar_t* p = 0; while(p = va_arg(ap, wchar_t*)) { wprintf(L"%s", p); num ++; } //第四步: //待所有可变参数都读取完毕以后,调用va_end宏对ap和va_list做必要的清理工作 va_end(ap); return num; } int main(int argc, char* argv[]) { setlocale(LC_ALL, ""); int z = ShowLoveL"我喜欢你!\n"); int y = ShowLove(L"我喜欢你!", L"你喜欢谁?\n"); int l = ShowLove(L"我喜欢你!", L"你喜欢谁?", L"萧雨萌!\n"); printf("可变参数个数依次是:%d\t%d\t%d\n", z, y, l); return 0; } 上述代码的运行结果是:...

January 11, 2021

为什么你的简历没人看

为什么你的简历没人看 程序员如何写简历

January 11, 2021

从抓包的角度分析connect()函数的连接过程

从抓包的角度分析connect()函数的连接过程 这篇文章主要是从tcp连接建立的角度来分析客户端程序如何利用connect函数和服务端程序建立tcp连接的,了解connect函数在建立连接的过程中底层协议栈做了哪些事情。 tcp三次握手 在正式介绍connect函数时,我们先来看一下tcp三次握手的过程,下面这个实验是客户端通过telnet远程登录服务端的例子,telnet协议是基于tcp协议,我们可以通过wireshark抓包工具看到客户端和服务端之间三次握手的过程,12.1.1.1是客户端的ip地址,12.1.1.2是服务端的ip地址。 下面是我们通过wireshark抓取到的tcp三次握手的数据包: 我们看到客户端远程登录服务端时,首先发送了一个SYN报文,其中目标端口为23(远程登录telnet协议使用23端口),初始序号seq = 0,并设置自己的窗口rwnd = 4128(rwnd是一个对端通告的接收窗口,用于流量控制)。 然后服务端回复了一个SYN + ACK报文,初始序号seq = 0,ack = 1(在前一个包的seq基础上加1),同时也设置自己的窗口rwnd = 4128。 然后客户端收到服务端的SYN + ACK报文时,回复了一个ACK报文,表示确认建立tcp连接,序号为seq = 1, ack = 1*(在前一个包的seq基础上加1)*, 设置窗口rwnd = 4128,此时客户端和服务端之间已经建立tcp连接。 connect函数 前面我们在介绍tcp三次握手的时候说过,客户端在跟服务端建立tcp连接时,通常是由客户端主动向目标服务端发起tcp连接建立请求,服务端被动接受tcp连接请求;同时服务端也会发起tcp连接建立请求,表示服务端希望和客户端建立连接,然后客户端会接受连接并发送一个确认,这样双方就已经建立好连接,可以开始通信。 这里说明一下:可能有的小伙伴会感到疑惑,为啥服务端也要跟客户端建立连接呢?其实这跟tcp采用全双工通信的方式有关。对于全双工通信,简单来说就是两端可以同时收发数据,如下图所示: 我们再回到正题,那么在网络编程中,肯定也有对应的函数做到跟上面一样的事情,没错,就是connect(连接)。顾名思义,connect函数就是用于客户端程序和服务端程序建立tcp连接的。 一般来说,客户端使用connect函数跟服务端建立连接,肯定要指定一个ip地址和端口号(相当于客户端的身份标识),要不然服务端都不知道你是谁?凭什么跟你建立连接。同时还得指明服务端的ip地址和端口号,也就是说,你要跟谁建立连接。 connect函数原型: int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); 参数说明: sockfd:客户端的套接字文件描述符 addr:要连接的套接字地址,这是一个传入参数,指定了要连接的套接字地址信息(例如IP地址和端口号) addrlen:是一个传入参数,参数addr的大小,即sizeof(addr) 返回值说明:连接建立成功返回0,失败返回-1并设置errno connect函数在建立tcp连接的过程中用到了一个非常重要的队列,那就是未决连接队列,这个队列用来管理tcp的连接,包括已完成三次握手的tcp连接和未完成三次握手的tcp连接,下面我们就来详细介绍一下未决连接队列。 未决连接队列 未决连接队列是指服务器接收到客户端的连接请求,但是尚未被处理(也就是未被accept,后面会说)的连接,可以理解为未决连接队列是一个容器,这个容器存储着这些尚未被处理的链接。 当一个客户端进程使用 connect 函数发起请求后,服务器进程就会收到连接请求,然后检查未决连接队列是否有空位,如果未决队列满了,就会拒绝连接,那么客户端调用的connect 函数返回失败。 如果未决连接队列有空位,就将该连接加入未决连接队列。当 connect 函数成功返回后,表明tcp的“三次握手”连接已完成,此时accept函数获取到一个客户端连接并返回。 在上图中,在未决连接队列中又分为2个队列: 未完成队列(未决队列):即客户端已经发出SYN报文并到达服务器,但是在tcp三次握手连接完成之前,这些套接字处于SYN_RCVD状态,服务器会将这些套接字加入到未完成队列。 已完成队列:即刚刚完成tcp三次握手的tcp连接,这些套接字处于ESTABLISHED状态,服务器会将这些套接字加入到已完成队列。 我们来看一下连接建立的具体过程,如图所示: 服务端首先调用listen函数监听客户端的连接请求,然后调用accept函数阻塞等待取出未决连接队列中的客户端连接,如果未决连接队列一直为空,这意味着没有客户端和服务器建立连接,那么accept就会一直阻塞。 当客户端一调用connect函数发起连接时,如果完成tcp三次握手,那么accept函数会取出一个客户端连接(注意:是已经建立好的连接)然后立即返回。 上面就是客户端和服务端在网络中的状态变迁的具体过程,前面我们在学习tcp三次握手的过程中还知道,服务端和客户端在建立连接的时候会设置自己的一个接收缓冲区窗口rwnd的大小。 服务端在发送SYN + ACK数据报文时会设置并告知对方自己的接收缓冲区窗口大小,客户端在发送ACK数据报文时也会设置并告知对方自己的接收缓冲区窗口大小。 注意,accept函数调用成功,返回的是一个已经完成tcp三次握手的客户端连接。如果在三次握手的过程中(最后一步),服务端没有接收到客户端的ACK,则说明三次握手还没有建立完成,accept函数依然会阻塞。 关于tcp三次握手连接建立的几种状态:SYN_SENT,SYN_RCVD,ESTABLISHED。 SYN_SENT:当客户端调用connect函数向服务端发送SYN包时,客户端就会进入 SYN_SENT状态,并且还会等待服务器发送第二个SYN + ACK包,因此SYN_SENT状态就是表示客户端已经发送SYN包。 SYN_RCVD:当服务端接收到客户端发送的SYN包并确认时,服务端就会进入 SYN_RCVD状态,这是tcp三次握手建立的一个很短暂的中间状态,一般很难看到, SYN_RCVD状态表示服务端已经确认收到客户端发送的SYN包。 ESTABLISHED:该状态表示tcp三次握手连接建立完成。 对于这两个队列需要注意几点注意: 1. 未完成队列和已完成队列的总和不超过listen函数的backlog参数的大小。listen函数的签名如下: int listen(int sockfd, int backlog); 2. 一旦该连接的tcp三次握手完成,就会从未完成队列加入到已完成队列中 3. 如果未决连接队列已满,当又接收到一个客户端SYN时,服务端的tcp将会忽略该SYN,也就是不会理客户端的SYN,但是服务端并不会发送RST报文,原因是:客户端tcp可以重传SYN,并期望在超时前未决连接队列找到空位与服务端建立连接,这当然是我们所希望看到的。如果服务端直接发送一个RST的话,那么客户端的connect函数将会立即返回一个错误,而不会让tcp有机会重传SYN,显然我们也并不希望这样做。 但是不排除有些linux实现在未决连接队列满时,的确会发送RST。但是这种做法是不正确的,因此我们最好忽略这种情况,处理这种额外情况的代码也会降低客户端程序的健壮性。 connect函数出错情况 由于connect函数是在建立tcp连接成功或失败才返回,返回成功的情况本文上面已经介绍过了。这里我们介绍connect函数返回失败的几种情况: 第一种 当客户端发送了SYN报文后,没有收到确认则返回ETIMEDOUT错误,值得注意的是,失败一次并不会马上返回ETIMEDOUT错误。即当你调用了connect函数,客户端发送了一个SYN报文,没有收到确认就等6s后再发一个SYN报文,还没有收到就等24s再发一个(不同的linux系统设置的时间可能有所不同,这里以BSD系统为主)。这个时间是累加的,如果总共等了75s后还是没收到确认,那么客户端将返回ETIMEDOUT错误。 对于linux系统,改变这个系统上限值也比较容易,由于需要改变系统配置参数,你需要root权限。 相关的命令是sysctl net.ipv4.tcp_syn_retries(针对于ipv4)。 在设置该值时还是要比较保守的,因为每次syn包重试的间隔都会增大(比如BSD类的系统实现中间隔会以2到3倍增加),所有tcp_syn_retries的一个微小变化对connect超时时间的影响都非常大,不过扩大这个值也不会有什么坏处,因为你代码中设置的超时值都能够生效。但是如果代码中没有设置connect的超时值,那么connect就会阻塞很久,你发现对端机器down掉的间隔就更长。 作者建议设置这个值到6或者7,最多8。6对应的connect超时为45s,7对应90s,8对应190s。...

January 11, 2021

从零学习开源项目系列(一) 从一款多人联机实时对战游戏开始

从零学习开源项目系列(一) 从一款多人联机实时对战游戏开始 写在前面的话 经常有学生或者初学者问我如何去阅读和学习一个开源软件的代码,也有不少朋友在工作岗位时面对前同事留下的项目,由于文档不完善、代码注释少、工程数量大,而无从下手。本文将来通过一个多人联机实时对战游戏——最后一战,来解答以上问题。 其实,我以上问题在我是一个学生时,我也同样因此而困惑,但是后来,我发现,对于文档缺失、注释缺失的项目,需要自己摸索,虽然是挑战,同时也是机遇——一个不错的学习机会。因为至少有代码,正如侯捷大师所说的的,“源码面前,了无秘密”,所以我们应该**“read the fucking code”**。 所以,这个系列的文章,我们分析“最后一战”这个游戏源码时,我们不会按照传统的思路:先介绍总结的程序结构,再介绍各个模块的细节,因为,当我们面对一套陌生的源码时,尤其是在文档缺失的情况下,我们根本无法开始就掌握这个项目的总体结构,我们只能从零开始一个个模块的对代码进行阅读和调试,所以我们这个系列的文章也按这个思路来分析,以真实的案例来教会新手一步步读懂一个开源项目的代码。 我们先来看下这个游戏的内容吧,下面给出游戏画面的部分截图: 这是一款类似于王者荣耀、dota之类的5v5实时RPG竞技游戏。 客户端的逻辑比较简单,主要是一些游戏特效和动画(基于Unity 3D),所以这里我们主要分析游戏的服务器端源码。 先介绍一下推荐的源码的运行和开发环境(我的配置): Windows 7 Visual Studio 2010 服务器端有非常多的模块,这里先截一张主要模块的项目图示: 从下一篇文章开始,我们将介绍如何学习这样的源码。 欢迎阅读下一篇**《从零学习开源项目系列(二) 最后一战概况》**。 源码下载方法: 微信搜索公众号**『高性能服务器开发』(中文名:高性能服务器开发),关注公众号后,在公众号中回复『英雄联盟』**,即可得到下载链接。(喷子和代码贩子请远离!)

January 11, 2021

从零学习开源项目系列(三) CSBattleMgr服务源码研究

从零学习开源项目系列(三) CSBattleMgr服务源码研究 服务器项目工程如下图所示: 如上图所示,这篇文章我们将介绍CSBattleMgr的情况,但是我们不会去研究这个服务器的特别细节的东西(这些细节我们将在后面的文章中介绍)。阅读一个未知的项目源码如果我们开始就纠结于各种细节,那么我们最终会陷入“横看成岭侧成峰,远近高低各不同”的尴尬境界,浪费时间不说,可能收获也是事倍功半。所以,尽管我们不熟悉这套代码,我们还是尽量先从整体来把握,先大致了解各个服务的功能,细节部分回头再针对性地去研究。 这个系列的第二篇文章《从零学习开源项目系列(二) 最后一战概况》中我们介绍了,这套游戏的服务需要使用redis和mysql,我们先看下mysql是否准备好了(mysql服务启动起来,数据库建表数据存在,具体细节请参考第二篇文章)。打开Windows的cmd程序,输入以下指令连接mysql: mysql -uroot -p123321 连接成功以后,如下图所示: 然后我们输入以下指令,查看我们需要的数据库是否创建成功: show databases; 这些都是基本的sql语句,如果您不熟悉的话,可能需要专门学习一下。 数据库创建成功后如下图所示: 至于数据库中的表是否创建成功,我们这里先不关注,后面我们实际用到哪张数据表,我们再去研究。 mysql没问题了,接下来我们要启动一下redis,通过第二篇文章我们知道redis需要启动两次,也就是一共两个redis进程,我们游戏服务中分别称为redis-server和redis-login-server(它们的配置文件信息不一样),我们可以在Server\Bin\x64\Release目录下手动cmd命令行执行下列语句: start /min "redis-server" "redis-server.exe" redis.conf start /min "redis-Logicserver" "redis-server.exe" redis-logic.conf 但是这样比较麻烦,我将这两句拷贝出来,放入一个叫start-redis.bat文件中了,每次启动只要执行一下这个bat文件就可以: redis和redis-logic服务启动后如下图所示: 我们常见的redis服务都是linux下的源码,微软公司对redis源码进行了改造,出了一个Windows版本,稍微有点不尽人意(例如:Windows下没有完全与linux的fork()相匹配的API,所以只能用CreateProcess()去替代)。关于windows版本的redis源码官方下载地址为:https://github.com/MicrosoftArchive/redis/releases。 在启动好了mysql和redis后,我们现在正式来看一下CSBattleMgr这个服务。读者不禁可能要问,那么多服务,你怎么知道要先看这个服务呢?我们上一篇文章中也说过,我们再start.bat文件中发现除了redis以外,这是第三个需要启动的服务,所以我们先研究它(start.bat我们可以认为是源码作者为我们留下的部署步骤“文档”): 我们打开CSBattleMgr服务main.cpp文件,找到入口main函数,内容如下: 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(); } 通过调试,我们发下这个函数大致做了以下任务: //1. 设置程序异常处理函数 //2. 初始化一系列单例对象 //3. 初始化mysql //4. 进入一个被称作“主循环”的无限循环 步骤1设置程序异常处理函数没有好介绍的,我们看一下步骤2初始化一系列单例对象,总共初始化了三个类的对象CCSKernel、CCSUserMgr和CCSBattleMgr。单例模式本身没啥好介绍的,但是有人要提单例模式的线程安全性,所以出现很多通过加锁的单例模式代码,我个人觉得没必要;认为要加锁的朋友可能认为单例对象如果在第一次初始化时同时被多个线程调用就会有问题,我觉得加锁带来的开销还不如像上面的代码一样,在整个程序初始化初期获取一下单例对象,让单例对象生成出来,后面即使多个线程获取这个单例对象也都是读操作,无需加锁。以GetCSKernelInstance();为例: CCSKernel* GetCSKernelInstance(){ return &CCSKernel::GetInstance(); } CCSKernel& CCSKernel::GetInstance(){ if (NULL == pInstance){ pInstance = new CCSKernel; } return *pInstance; } GetCSKernelInstance()->Initialize()的初始化动作其实是加载各种配置信息和事先设置一系列的回调函数和定时器: INT32 CCSKernel::Initialize() { //JJIAZ加载配置的时候 不要随便调整顺序 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....

January 11, 2021

从零学习开源项目系列(二) 最后一战概况

从零学习开源项目系列(二) 最后一战概况 这份代码我也是无意中来自一个朋友,据他说也是来源于互联网,服务器端代码原来是Linux版本的,但被厉害的大神修改成可以在Windows上运行。(如果不小心侵犯了您的版权,请联系我删除)。好在,这份代码中使用的大多数方法和接口都是可以跨Windows和Linux两个平台的,所以Linux开发下的朋友请不要感到不适,我们学习这份代码更多的不是纠结细节而是学习思路和原理。 游戏主solution文件用Visual Studio打开后如下图所示: 这里总共有10个工程项目,模块比较多。**我们应该从何处入手呢?**我们先看下源码目录: 我们进入Server目录,发现如下一个文件: 我们打开看一下内容: cd Bin\x64\Release start.bat 这个代码进入Bin\x64\Release目录,执行另外一个start.bat,我们进入这个目录去看下这个文件内容: taskkill /f /t /im redis-server.exe taskkill /f /t /im CSBattleMgr.exe taskkill /f /t /im SSBattleMgr.exe taskkill /f /t /im GSConsole.exe taskkill /f /t /im BalanceServer.exe taskkill /f /t /im LoginServer.exe taskkill /f /t /im GSKernel.exe taskkill /f /t /im RobotConsole.exe taskkill /f /t /im LogServer.exe ping -n 1 127.0>nul start /min "redis-server" "redis-server.exe" redis.conf ping -n 1 127.0>nul start /min "redis-Logicserver" "redis-server.exe" redis-logic.conf ping -n 1 127.0>nul echo "start CSBattleMgr.exe" start /min "CSBattleMgr" "CSBattleMgr.exe" ping -n 1 127.0>nul echo "start SSBattleMgr.exe" start /min "SSBattleMgr" "SSBattleMgr.exe" ping -n 1 127....

January 11, 2021