技术面试与HR谈薪资技巧

技术面试与HR谈薪资技巧 作为“生在红旗下,长在春风里”的“四有新人”(现在90后00后还有知道这个词的吗?^_^),张小方同志从毕业至今,与各路HR、HRD斗智斗勇,再加上自己的不懈努力,历尽千辛万苦终于将毕业时的1500每月的薪资提高了二十几倍。本文就和大家唠唠这些年风里来雨里去无数次铩羽而归、兢兢业业、如履薄冰、诚惶诚恐、夜不能寐、枕戈待旦、惴惴不安、临盆一脚,最终守得云开见月明的谈薪经历。当然,本文说的主要是技术面试中谈薪的经历,主要针对的是一些社会人士求职,当然一些通用的原则同样适用于应届生求职。 面试官的级别 一般技术面试的模式是 n + 1 或者是 1 + n + 1,什么意思呢?其中 n 指的是你见到的不同级别的面试官的个数,1 指的你见到的hr。 两种模式 模式一:一般技术面试有两种情形,你进入公司以后,会让你填写一些个人资料,如果有笔试题,也会做一些笔试题,接着HR会先找你简单地聊几句了解一下你的情况,然后通知技术面试官过来面试,如果一轮或者多轮技术面试后,面试官觉得你还不错,HR会接着详细地了解一下你的情况,如之前做什么工作的、是否已经离职、是否成家有小孩,当然最最关键的是询问你期望的薪资。这就是所谓的“1 + n + 1”模式,即开始由HR面试,中间是技术面试,最后还是HR面试。 模式二: 在登记个人信息和笔试(如果有的话)完以后,直接就是技术面试,技术面试结束后如果面试官觉得你还不错,HR会接着进行人事面试。这就是所谓的“n + 1”模式,即开始是技术面试,最后由HR面试。 当然,可能在你去公司现场面试之前,会有一些电话或者远程视频的面试的形式,这类基本上也属于技术面试的范畴。这里就不再赘述了。 面试官的级别和面试轮次 一般公司的的级别是按如下方式划分的:普通员工上面是部门经理(或技术主管),部分经理(或技术主管)上面是总监(即所谓的大领导)(规模小的公司或者扁平化的公司上面就是CTO、副总或者CEO了,因此到这一层就没有了),总监(大领导)上面一般是公司的CTO、副总或者老板了。 如果是应聘初、中级的岗位,第一轮面试官一般是你的职位所在的部门的经理。一般经理觉得没问题,接着就是HR面试了,也就是说就这么两类人来面试你。 如果你面试的是高级、资深岗位,第一轮面试官是所在部门的经理,只要面试表现不是太差,经理会让总监来继续第二轮面试你。如果总监觉得也没有问题,接着就是HR面试了。 如果你面试的是经理或开发主管的职位,第一轮面试一般是相关部门与你职级差不多的部门经理来面试你,注意这一轮大多数一般只是简单地聊一聊(走过场),然后由上一级总监(大领导)来进行第二轮面试(这轮面试很关键),如果这一轮面试也OK的话,会由大领导的上一级,如公司CTO或副总甚至CEO进行第三轮面试甚至第四轮面试。如果以上都没问题,接着就是HR或HRD的人事面试了。 注意,我这里面试的轮次是按职级划分的,而不是按次数划分的。实际上在大多数公司,会由搭配交叉部门的其他同事来面试。举个例子,例如你求职的是高级开发,除了部门经理 A1 和大领导 B 会面试你,部门经理 A1 或者大领导 B 可能会邀请其他部门的某个同事C或者领导A2来面试你,这里的职级按从低到高依次是C < A1 = A2 < B。也就是说,面试官 C 和 A1、A2 这种级别的面试官可能会出现多位。 人事面试 说完了技术面试官的职级和面试次数,再来说说人事面试,一般人事面试是最后一轮面试。你需要注意的是——HR 一般没有决策权,也就是说 HR 没有权利决定你最终的去留,她们只是转达用人部门的意见。当然,也不排除有少数强势的 HR 或 HRD,大多数的HR都没有一票否决权。所以,如果HR和你详谈时,也说明你前面的面试结果不是太差。这个时候,你要做的就是尽量在公司可接受的范围内达到自己利益最大化就可以了。 在我看来,大多数HR虽然看起来美丽大方,但是都是“妖精”,尤其是一些阅人无数的资深HR,那简直是“职场白骨精”(调侃一下,没有恶意,请各位HR勿怪)。 首先,由于她们有自己的绩效考核,即最短时间、最低的成本招到最合适的人才。据我所知,举个例子,比如一个计划最高可以用25K招到的人,现在某个HR用20K就招到了,那节省下来的5K就会算作HR的绩效,所以这也是HR为什么会找你谈工资的目的了,其核心目的其实就是为了压工资。减少人力资源成本是负责招聘的HR的一项重要职责。 其次,由于大多数HR没有否决权,只是忠实地转达用人部门的意见。所以你问她的一些问题,大多数情况下是得不到任何实质性的答案的,一般都是些场面上的官话。所以,你也不用问诸如“面试成绩如何”、“面试官对你的影响如何”、“什么时候给面试结果”之类的傻傻的问题。 当然,HR与你谈论很多问题,其实是通过交流中了解你这个人的性格、反应能力、情商、经历和资历等信息,以最大化地为公司招到一个合适的人,排除一些人事隐患。比如,HR一般会问男同胞是否有女朋友、是否结婚、老家是哪里的等等,这些不是说HR要查你的户口(这个从身份证信息上就能看出来),而是看你这个人未来几年是否稳定,一般成家就意味着责任感,而不是要刺探你的婚姻状况。 当然读者最关心的可能就是如何谈薪资。这里单独来开一节来详细讨论下这个问题。 谈薪的基本要点与脱坑技巧 谈薪是一个与HR斗智斗勇的过程,在谈薪的过程中有很多坑。一般HR会问你期望的薪资,然后就你的报价(请原谅我用这个词,谈好薪确实就等于把自己卖了 - -!)进行讨价还价,当然不和你讨价还价的HR也有,一般有两种:第一种,你的报价实在太高,已经远远超过公司的预算,HR觉得没有谈下去的必要;第二种,天使。第二种,我反正是从来没遇到过。除了总监及以上职位,一般你求职的JD上都会有一个薪资范围,你报价时可以参考一下这个。其次就是,除非你能力特别优秀,面试效果特别好,否则 IT 行业一般的涨薪最大幅度是你前一份工作的百分之三十,也就是说如果你前一份工作月薪是20K,那么你这份工作你最多可以报价27K。 一般与HR谈薪的过程中,即要展示自己对求职的职位有很大的兴趣,但又不要暴露自己想尽快找到工作的想法,尤其是在你手头上没有offer 、且已经离职的情况下,这样会让自己很被动,你迫切需要一份工作,而现在又无多余的选择,这样HR就会使劲压制你的薪资。 HR与你谈论薪资经常有如下套路: HR: 您期望的薪资是多少? 你: 25K。 OK,你已经被HR成功套路。这个时候你的最高价就是25K了,然后HR会顺着这个价往下砍,所以你最终的薪资一般都会低于25K。等你接到offer,你的心里肯定充满了各种“悔恨”:其实当时报价26、27甚至28、29也是可以的。 正确的回答可以这样,并且还能够反套路一下HR: HR: 您期望的薪资是多少? 你: 就我的面试表现,贵公司最高可以给多少薪水? 哈哈,如果经验不够老道的HR可能就真会说出一个报价(如25K)来,然后,你就可以很开心地顺着这个价慢慢地往上谈了。所以这种情况下,你最终的薪资肯定是大于25K的。当然,经验老道的HR会给你一句很官方的套话: HR: 您期望的薪资是多少? 你: 就我的面试表现,贵公司最高可以给多少薪水? HR: 这个暂且没法确定,要结合您几轮面试结果和用人部门的意见来综合评定。 如果HR这么回答你,我的建议是这样的: 虽然薪资很重要,但是我个人觉得这不是最重要的。我有以下建议: 如果你觉得你技术面试效果很好,可以报一个高一点的薪资,这样如果HR想要你,会找你商量的。 如果你觉得技术面试效果一般,但是你比较想进这家公司,可以报一个折中的薪资。 如果你觉得面试效果很好,但是你不想进这家公司,你可以适当“漫天要价”一下。 如果你觉得面试效果不好,但是你想进这家公司,你可以开一个稍微低一点的工资。 需要注意的是,面试求职是一个双向选择的过程。面试应该做到不卑不亢,千万不要因为面试结果不好,就低声下气地乞求工作,每个人的工作经历和经验都是不一样的,技术面试不好,知道自己的短板针对性地补缺补差就行,而不是在人事关系上动歪脑筋。当然也不要盲目自信,把自己的无知当理所当然,谦虚一点。笔者曾经面试Intel时,因为单词“cache”的读音和面试官争论了很久,面试官读“cash”,我坚持认为读“cake”,这里就闹了一个笑话。 除了和HR谈薪有陷阱,和技术面试官谈薪也一般存在一些陷阱。大多数公司都要求总监级别以下的面试官不得询问面试者期望薪资,但是也不排除一些面试官的个人好奇心,”无意中“向面试者询问该问题。除了级别高的领导,一般面试官是无权询问薪资的,此时面试者就要留心了。如果被问到,可以委婉地回答一下,如可以说:现在还不确定,会按招聘信息的薪资范围和结合自己的面试结果来提出一个期望薪资。如果实在绕不过去这个问题,可以把薪资说的低一点(口头上只是说说而已),实际薪资还是由高层领导决定并最终和HR谈的。原因是因为,在信息不对称的情况下,如果你报的薪资过高,超过当前面试官的薪资,很可能引起当前面试官的不愉快,造成对自己非技术上的差评,造成失去入职该公司的机会。 谈薪资还有一个坑,你一定要搞清楚公司的薪资构成,就是尽量把月薪或者基础薪资谈高一点。说说我之前的两段经历: 经历一:我之前有份工作,HR和我谈的时候,说是月薪14K。但是我实际进去以后,发现14K分为基础工资和绩效工资,这其中6K是基本工资,剩下的8K是绩效工资,而每个月会有一个绩效系数,系数范围是从0.8~1.5,也就是说当你某个月绩效系数是1时,你拿到完整的8K;如果你系数是0.8时,你只能拿到6400,也就是辛辛苦苦干了一个月实际拿到手的只有6000 + 6400 = 12400,平白无故地少了1600。而公司对外宣称你的月薪是14K,还有就是基本上绩效系数在1以上的都没有。...

January 11, 2021

拒绝了一家公司的offer后,他们的副总和hr总监同时打电话来询问拒绝原因并极力要求加入,我该不该去?

拒绝了一家公司的offer后,他们的副总和hr总监同时打电话来询问拒绝原因并极力要求加入,我该不该去? 网友提问: 拒绝了一家公司的offer后,他们的副总和hr总监同时打电话来询问拒绝原因并极力要求加入,我该不该去? 面试的时候双方都感觉还可以,一面后hr开始压价,比预期低了3K,我拒绝。 然后邀请我去复试,跟经理交流后,觉得我可以,然而hr又压价,比预期低了1K,我拒绝,因为我对他们提出的期望薪资是我的最低底线。 然而过去不到1小时,hr说可以给到我期望薪资,并发了offer,但是试用期6个月打8折这个条件我不是很满意,而且offer居然没说明具体薪资组成部分,感觉他们很没诚意,然后思前想后决定发邮件拒绝了。 然而过去三天后,接到他们副总的电话,询问我拒绝原因,说自己公司管理制度如何如何人性化,还说条件可以再谈,非常诚恳也非常谦逊,感觉不好意思拒绝。然后我说自己再考虑考虑。随后hr再次电话来谈条件,说试用期不打折,并把薪资组成部分说的很详细,邀请我加入。 我犹豫了,现在有点纠结,手里还有另一家offer,待遇差不多,但是这家录用我很爽快,感觉我合适就直接发offer了,并说清楚了具体待遇。但是考虑这家公司目前规模和发展趋势不是符合自己预期的,所以也在犹豫中。 反正个人感觉第一家公司想要我但是却各种理由为难我,被我拒绝后又放低标准极力邀请我,这是不是一个坑啊,这么着急招人估计这个岗位肯定是个大坑啊。 现在很纠结,跳槽需要慎重考虑啊,真怕自己跳入一个更大的坑爬不出来。 小方老师建议: 先说结论:现在拿到offer的话,如果比较纠结就都不去,哪怕一家公司都去不了。再找就是了。 说两段我的经验吧,第一段,几年前,在A公司和B公司之间两家公司的offer纠结,A公司离我住的地方太远不想去,B公司感觉氛围不太喜欢,最后在各种纠结中去了B公司,没干三个月受不了离职了。 某年年底找工作时,有个猎头,给我推荐了一个公司,我开始不太想去,因为这家公司我并不了解,所以我和猎头说,除非月薪可以给到30k,否则我不想去,后来猎头反复和对方公司沟通,最终满足了我的要求,但是,猎头说由于对方公司有一定的涨薪限制,所以不能给到月薪30k,但是由于我要求的是30k*13薪,对方公司换了种方式,给26k*15薪,这样一年下来也是39万。我开始是很不愿意这种所谓的变通方法,于是猎头就和他的领导那段时间反复做我的思想工作。我当时也非常纠结要不要去,因为相对于我当前的薪资确实翻了一番,但是我隐约觉得当初面试我的面试官(也就是我进去后我的直系领导)是个"不好相处"的人。后来猎头又反复和我说,面试我的面试官是和乐于传道受业解惑的和蔼可亲的技术大神最终在各种纠结中我还是去入职了。但是没过多久,我就干的很不开心。那个领导的代码风格稀烂,例如写C++代码,没有任何注释文档,甚至所有的实现文件都写在*.h文件中;其次,与人交流非常没耐心,下面的同事问他问题说不到三句可能就不耐烦,所以隔壁组的同事也都不太喜欢他,但是由于这个项目一直是他一个人做也没商业化,所以公司也没多管他。等到他带团队,与其他同事打交道时就暴露各种问题了。更让人受不了的是,他经常在项目要发版本时修改代码,也不通知其他同事,直到我们发现不对劲,排查很久发现问题,他才说。最让我们受不了的是,有次周六周日加了两天班,周六晚上到夜里十二点,周日到凌晨三点,他还冲团队成员发火。所以那天夜里,我四点钟半到家,直接给CTO写了封投诉他的信。 后来的情况不用说了,我当初面试的时顾虑都一一应验了,和我一起来的几个同事,年后都陆续离职了。 我想说的是,不管是薪资还是公司面试的时候面试官和人事的种种举动,如果你有顾虑或者觉得不适合你,千万不要为了钱本身就去了。 小方老师联系方式:微信 easy_coder。

January 11, 2021

整型变量赋值是原子操作吗?

整型变量赋值是原子操作吗? 整型变量赋值操作不是原子操作 那么为什么整型变量的操作不是原子性的呢?常见的整型变量操作有如下几种情况: 给整型变量赋值一个确定的值,如 int a = 1; 这条指令操作一般是原子的,因为对应着一条计算机指令,cpu将立即数1搬运到变量a的内存地址中即可,汇编指令如下: mov dword ptr [a], 2 然后这确是最不常见的情形,由于现代编译器一般有优化策略,如果变量a的值在编译期间就可以计算出来(例如这里的例子中a的值就是1),那么a这个变量本身在正式版本的软件中(release版)就很有可能被编译器优化掉,使用a的地方,直接使用常量1来代替。所以实际的执行指令中,这样的指令存在的可能性比较低。 变量自身增加或者减去一个值,如 a ++; 从C/C++语法的级别来看,这是一条语句,是原子的;但是从实际执行的二进制指令来看,也不是原子的,其一般对应三条指令,首先将变量a对应的内存值搬运到某个寄存器(如eax)中,然后将该寄存器中的值自增1,再将该寄存器中的值搬运回a的内存中: mov eax, dword ptr [a] inc eax mov dword ptr [a], eax 现在假设a的值是0,有两个线程,每个线程对变量a的值递增1,我们预想的结果应该是2,可实际运行的结果可能是1!是不是很奇怪?分析如下: int a = 0; //线程1 void thread_func1() { a ++; } //线程2 void thread_func2() { a ++; } 我们预想的结果是线程1和线程2的三条指令各自执行,最终a的值为2,但是由于操作系统线程调度的不确定性,线程1执行完指令①和②后,eax寄存器中的值为1,此时操作系统切换到线程2执行,执行指令③④⑤,此时eax的值变为1;接着操作系统切回线程1继续执行,执行指令⑦,得到a的最终结果1。 把一个变量的值赋值给另外一个变量,或者把一个表达式的值赋值给另外一个变量,如 int a = b; 从C/C++语法的级别来看,这是也是一条语句,是原子的;但是从实际执行的二进制指令来看,由于现代计算机CPU架构体系的限制,数据不可以直接从内存搬运到另外一块内存,必须借助寄存器中断,这条语句一般对应两条计算机指令,即将变量b的值搬运到某个寄存器(如eax)中,再从该寄存器搬运到变量a的内存地址: mov eax, dword ptr [b] mov dword ptr [a], eax 既然是两条指令,那么多个线程在执行这两条指令时,某个线程可能会在第一条指令执行完毕后被剥夺CPU时间片,切换到另外一个线程而产生不确定的情况。这和上一种情况类似,就不再详细分析了。 说点题外话,网上很多人强调某些特殊的整型数值类型(如bool类型)的操作是原子的,这是由于,某些CPU生产商开始有意识地从硬件平台保证这一类操作的原子性,但这并不是每一种类型的CPU架构都支持,在这一事实成为标准之前,我们在多线程操作整型时还是老老实实使用下文介绍的原子操作或线程同步技术来对这些数据类型进行保护。

January 11, 2021

服务器开发中网络数据分析与故障排查经验漫谈

服务器开发中网络数据分析与故障排查经验漫谈 ​ 一、 操作系统提供的网络接口 为了能更好的排查网络通信问题,我们需要熟悉操作系统提供的以下网络接口函数,列表如下: 接口函数名称 接口函数描述 接口函数签名 socket 创建套接字 int socket(int domain, int type, int protocol); connect 连接一个服务器地址 int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); send 发送数据 ssize_t send(int sockfd, const void *buf, size_t len, int flags); recv 收取数据 ssize_t recv(int sockfd, void *buf, size_t len, int flags); accept 接收连接 int accept4(int sockfd, struct sockaddr *addr, socklen_t *addrlen, int flags); shutdown 关闭收发链路 int shutdown(int sockfd, int how); close 关闭套接字 int close(int fd); setsockopt 设置套接字选项 int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); 注意:这里以bekeley提供的标准为例,不包括特定操作系统上特有的接口函数(如Windows平台的WSASend,linux的accept4),也不包括实际与网络数据来往不相关的函数(如select、linux的epoll),这里只讨论与tcp相关的接口函数,像与udp相关的函数sendto/recvfrom等函数与此类似。 下面讨论一下以上函数的一些使用注意事项: 1 以上函数如果调用出错后,返回值均为-1;但是返回值是-1,不一定代表出错,这还得根据对应的套接字模式(阻塞与非阻塞模式)。 2 默认使用的socket函数创建的套接字是阻塞模式的,可以调用相关接口函数将其设置为非阻塞模式(Windows平台可以使用ioctlsocket函数,linux平台可以使用fcntl函数,具体设置方法可以参考这里。)。阻塞模式和非阻塞模式的套接字,对服务器的连接服务器和网络数据的收发行为影响很大。详情如下: 阻塞模式下,connect函数如果不能立刻连上服务器,会导致执行流阻塞在那里一会儿,直到connect连接成功或失败或网络超时;而非阻塞模式下,无论是否连接成功connect将立即返回,此时如果未连接成功,返回值将是-1,错误码是EINPROGRESS,表示连接操作仍然在进行中。Linux平台后续可以通过使用select/poll等函数检测该socket是否可写来判断连接是否成功。 阻塞套接字模式下,send函数如果由于对端tcp窗口太小,不足以将全部数据发送出去,将阻塞执行流,直到出错或超时或者全部发送出去为止;同理recv函数如果当前协议栈系统缓冲区中无数据可读,也会阻塞执行流,直到出错或者超时或者读取到数据。send和recv函数的超时时间可以参考下文关于常用socket选项的介绍。 非阻塞套接字模式下,如果由于对端tcp窗口太小,不足以将数据发出去,它将立刻返回,不会阻塞执行流,此时返回值为-1,错误码是EAGAIN或EWOULDBLOCK,表示当前数据发不出去,希望你下次再试。但是返回值如果是-1,也可能是真正的出错了,也可能得到错误码EINTR,表示被linux信号中断了,这点需要注意一下。recv函数与send函数情形一样。 3 send函数虽然名称叫“send”,但是其并不是将数据发送到网络上去,只是将数据从应用层缓冲区中拷贝到协议栈内核缓冲区中,具体什么时候发送到网络上去,与协议栈本身行为有关系(socket选项nagle算法与这个有关系,下文介绍常见套接字选项时会介绍),这点需要特别注意,所以即使send函数返回一个大于0的值n,也不能表明已经有n个字节发送到网络上去了。同样的道理,recv函数也不是从网络上收取数据,只是从协议栈内核缓冲区拷贝数据至应用层缓冲区,并不是真正地从网络上收数据,所以,调用recv时,操作系统的协议栈已经将数据从网络上收到自己的内核缓冲区中了,recv仅仅是一次数据拷贝操作而已。...

January 11, 2021

服务器开发案例实战

服务器开发案例实战 从零实现一个http服务器 从零实现一款12306刷票软件 从零实现一个邮件收发客户端 从零开发一个WebSocket服务器 从零学习开源项目系列(一) 从一款多人联机实时对战游戏开始 从零学习开源项目系列(二) 最后一战概况 从零学习开源项目系列(三) CSBattleMgr服务源码研究 从零学习开源项目系列(四)LogServer源码探究

January 11, 2021

服务器开发通信协议设计介绍

服务器开发通信协议设计介绍 一、选择TCP还是UDP协议 由于我们的即时通讯软件的用户存在用户状态问题,即用户登录成功以后可以在他的好友列表中看到哪些好友在线,所以客户端和服务器需要保持长连接状态。另外即时通讯软件一般要求信息准确、有序、完整地到达对端,而这也是TCP协议的特点之一。综合这两个所以这里我们选择TCP协议,而不是UDP协议。 二、协议的结构 由于TCP协议是流式协议,所谓流式协议即通讯的内容是无边界的字节流:如A给B连续发送了三个数据包,每个包的大小都是100个字节,那么B可能会一次性收到300个字节;也可能先收到100个字节,再收到200个字节;也可能先收到100个字节,再收到50个字节,再收到150个字节;或者先收到50个字节,再收到50个字节,再收到50个字节,最后收到150个字节。也就是说,B可能以任何组合形式收到这300个字节。即像水流一样无明确的边界。为了能让对端知道如何给包分界,目前一般有三种做法: 以固定大小字节数目来分界,上文所说的就是属于这种类型,如每个包100个字节,对端每收齐100个字节,就当成一个包来解析; 以特定符号来分界,如每个包都以特定的字符来结尾(如\n),当在字节流中读取到该字符时,则表明上一个包到此为止。 固定包头+包体结构,这种结构中一般包头部分是一个固定字节长度的结构,并且包头中会有一个特定的字段指定包体的大小。这是目前各种网络应用用的最多的一种包格式。 上面三种分包方式各有优缺点,方法1和方法2简单易操作,但是缺点也很明显,就是很不灵活,如方法一当包数据不足指定长度,只能使用占位符如0来凑,比较浪费;方法2中包中不能有包界定符,否则就会引起歧义,也就是要求包内容中不能有某些特殊符号。而方法3虽然解决了方法1和方法2的缺点,但是操作起来就比较麻烦。我们的即时通讯协议就采用第三种分包方式。所以我们的协议包的包头看起来像这样: struct package_header { int32_t bodysize; }; 一个应用中,有许多的应用数据,拿我们这里的即时通讯来说,有注册、登录、获取好友列表、好友消息等各种各样的协议数据包,而每个包因为业务内容不一样可能数据内容也不一样,所以各个包可能看起来像下面这样: struct package_header { int32_t bodysize; }; //登录数据包 struct register_package { package_header header; //命令号 int32_t cmd; //注册用户名 char username[16]; //注册密码 char password[16]; //注册昵称 char nickname[16]; //注册手机号 char mobileno[16]; }; //登录数据包 struct login_package { package_header header; //命令号 int32_t cmd; //登录用户名 char username[16]; //密码 char password[16]; //客户端类型 int32_t clienttype; //上线类型,如在线、隐身、忙碌、离开等 int32_t onlinetype; }; //获取好友列表 struct getfriend_package { package_header header; //命令号 int32_t cmd; }; //聊天内容 struct chat_package { package_header header; //命令号 int32_t cmd; //发送人userid int32_t senderid; //接收人userid int32_t targetid; //消息内容 char chatcontent[8192]; }; 看到没有?由于每一个业务的内容不一样,定义的结构体也不一样。如果业务比较多的话,我们需要定义各种各样的这种结构体,这简直是一场噩梦。那么有没有什么方法可以避免这个问题呢?有,我受jdk中的流对象的WriteInt32、WriteByte、WriteInt64、WriteString,这样的接口的启发,也发明了一套这样的协议,而且这套协议基本上是通用协议,可用于任何场景。我们的包还是分为包头和包体两部分,包头和上文所说的一样,包体是一个不固定大小的二进制流,其长度由包头中的指定包体长度的字段决定。 struct package_protocol { int32_t bodysize; //注意:C/C++语法不能这么定义结构体, //这里只是为了说明含义的伪代码 //bodycontent即为一个不固定大小的二进制流 char binarystream[bodysize]; }; 接下来的核心部分就是如何操作这个二进制流,我们将流分为二进制读和二进制写两种流,下面给出接口定义:...

January 11, 2021

服务器端发数据时,如果对端一直不收,怎么办?

服务器端发数据时,如果对端一直不收,怎么办? 这类问题一般出现在跨部门尤其是与外部开发人员合作的时候。假设现在有这样一种情况,我们的服务器提供对外的服务,指定好了协议,然后对外提供服务,客户端由外部人员去开发,由于存在太多的不确定性,如果我们在给对端(客户端)发送数据时,对端因为一些问题(可能是逻辑 bug 或者其他的一些问题)一直不从 socket 系统缓冲区中收取数据,而服务器端可能定期产生一些数据需要发送给客户端,再发了一段时间后,由于 TCP 窗口太小,导致数据发送不出去,这样待发送的数据会在服务器端对应的连接的发送缓冲区中积压,如果我们不做任何处理,很快系统就会因为缓冲区过大内存耗尽,导致服务被系统杀死。 对于这种情况,我们一般建议从以下几个方面来增加一些防御措施: 设置每路发送连接的发送缓冲区大小上限(如 2 M,或者小于这个值),当某路连接上的数据发送不出去的时候,即将数据存入发送缓冲区时,先判断一下缓冲区最大剩余空间,如果剩余空间已经小于我们要放入的数据大小,也就是说缓冲区中数据大小会超过了我们规定的上限,则认为该连接出现了问题,关闭该路连接并回收相应的资源(如清空缓冲区、回收套接字资源等)。示例代码如下: //outputBuffer_为发送缓冲区对象 size_t remainingLen = outputBuffer_.remainingBytes(); //如果加入到缓冲区中的数据长度超出了发送缓冲区最大剩余量 if (remainingLen < dataToAppend.length()) { forceClose() return } outputBuffer_.append(static_cast<const char*>(dataToAppend.c_str()), dataToAppend.length()); 还有另外一种场景,当有一部分数据已经积压在发送缓冲区了,此后服务器端未产生新的待发送的数据,此时如果不做任何处理,发送缓冲区的数据会一直积压,但是发送缓冲区的数据容量也不会超过上限。如果不做任何处理的话,该数据会一直在缓冲区中积压,白白浪费系统资源。对于这种情况一般我们会设置一个定时器,每隔一段时间(如 3 秒)去检查一下各路连接的发送缓冲区中是否还有数据未发送出去,也就是说如果一个连接超过一定时间内还存在未发送出去的数据,我们也认为该连接出现了问题,我们可以关闭该路连接并回收相应的资源(如清空缓冲区、回收套接字资源等)。示例代码如下: //每3秒检测一次 const int SESSION_CHECK_INTERVAL = 3000; SetTimer(SESSION_CHECK_TIMER_ID, SESSION_CHECK_INTERVAL); void CSessionManager::OnTimer() { for (auto iter = m_mapSession.begin(); iter != m_mapSession.end(); ++iter) { if (!CheckSession(iter->value)) { //关闭session,回收相关的资源 iter->value->ForceClose(); iter = m_mapSession.erase(iter); } } } void CSessionManager::CheckSession(CSession* pSession) { if (!pSession->GetConnection().OutputBuffer.IsEmpty()) return false; return true; } 上述代码,每隔 3 秒检测所有的 Session 的对应的 Connection 对象,如果发现发送缓冲区非空,说明该连接中发送缓冲区中数据已经驻留 3 秒了,将该连接关闭并清理资源。

January 11, 2021

深入理解C/C++中的指针

深入理解C/C++中的指针 C和C++中最强大的功能莫过于指针了(pointer),但是对于大多数人尤其是新手来说,指针是一个最容易出错、也最难掌握的概念了。本文将从指针的方方面面来讲述指针的概念和用法,希望对大家有所帮助。 内存模型 为了更好地理解指针,让我们来看一下计算机的内存模型。 内存分为物理内存和虚拟内存,物理内存对应计算机中的内存条,虚拟内存是操作系统内存管理系统假象出来的。由于这些不是我们本文的重点,下面不做区分。有不清楚这些概念的同学,可以给我留言或者在线询问。 在不考虑cpu缓存的情况下,计算机运行程序本质上就是对内存中的数据的操作,通俗地来说,就是将内存条某些部分的数据搬进搬出或者搬来搬去,其中“搬进搬出”是指将内存中的二进制数据搬入cpu寄存器及运算器中进行相应的加减运算或者将寄存器中的数据搬回内存单元中,而“搬来搬去”是指将内存中的数据由这个位置搬到另外一个位置(当然,一般不是直接搬,而是借助寄存器作为中间存储区)。如下图所示: 计算机为了方便管理内存,将内存的每个单元用一个数字编号,如下图所以: 图中所示,是一个大小为128个字节的内存空间,其中每一个空格代表一个字节,所以内存编号是0~127。 对于一个32位的操作系统来说,内存空间中每一个字节的编号是一个32位二进制数,所以内存编号从0000 0000 0000 0000 0000 0000 0000 0000至1111 1111 1111 1111 1111 1111 1111 1111,转换成16进制也就是0x00000000至0xFFFFFFFF,由于是从0开始的,所以化成10机制就是从0至2的32次方减1;对于64位操作系统,内存编号也就是从64个0至64个1。 大家需要注意的是,从上面两个图我们可以发现,我们一般将编号小的内存单元画在上面,编号大的画在下面,也就是说从上至下,内存编号越来越大。 指针与指针变量 指针的本意就是内存地址,我们可以通俗地理解成内存编号,既然计算机通过编号来操作内存单元,这就造就了指针的高效率。 那么什么是指针变量呢?指针变量可通俗地理解成存储指针的变量,也就是存储内存地址(内存编号)的变量。首先指针变量和整型变量、字符型变量以及其他数据类型的变量一样都是变量类型;但是,反过来,我们不应该按这样的方式来分类,即:整型指针变量、字符型指针变量、浮点型指针变量等等。为什么不推荐这样的分类方法呢?首先,指针变量就是一个数据类型,指针数据类型,这种数据类型首先是一个变量数据类型,那么它的大小是多少呢?很多同学理所当然地认为整型指针变量和一个字符指针变量的大小是不一样的,这种认识是错的。指针变量也是一个变量,它是一个用来存储其他变量的内存地址的,更准确地说,指针变量时用来存储其他变量的内存首地址的,因为不同的数据类型所占的内存大小不一样。举个例子,在32位机器上,假如a是int型变量,pa是指向a的指针变量,b是一个double型变量,pb是指向b的指针变量,那么a在内存中占四个字节,b在内存中占8个字节,假如a在内存中分布是从0x11111110~0x11111113,而b在内存中分布是0x11112221至0x11112228,那么指针变量pa中存储的内容是0x11111110,而pb中存储就是0x11112221,看到了吧,也就是说,pa和pb中存储的都是地址,而且都是32位的二进制地址;再者,因为存储这样的地址需要4个字节,所以无论是int型指针变量pa或者是double型指针变量pb,它们所占的内存大小都是四个字节,从这点来说,不管什么类型的指针都是一样的,所以不论按整型指针变量、字符型指针变量、浮点型指针变量等等来区分指针变量。总结起来,指针变量和int、float、char等类型一样同属变量类型,指针变量类型占四个字节(32位机器下),存储的是32位的内存地址。下面的代码证明这一点: 上面介绍的是指针变量的一个方面,指针变量还有另外一层含义:在C/C++中星号(*)被定义成取内容符号,虽然所有指针变量占的内存大小和存储的内存地址大小都是一样的,但是由于存储的只是数据的内存首地址,所以指针变量存储的内存地址所指向的数据类型决定着如何解析这个首地址,也就是说对于int型指针变量,我们需要从该指针变量存储的(首)地址开始向后一直搜索4个字节的内存空间,以图中的变量a为例就是从0x12ff60~0x12ff63,对于变量b就是0x12ff44~0x12ff4b。所以从这个意义来上讲,当我们使用*pa,必须先知道pa是一个整型的指针,这里强调“整型”,而a的值1也就存储在从0x12ff60~0x12ff63这四个字节里面,当我们使用*pb,必须先知道pb是一个double型指针,这里强调"double",也就是说值2.0000存储在0x12ff44~0x12ff4b这八个字节里面。因此,我们对指针变量进行算术运算,比如pa + 2,pb + +之类的操作,是以数据类型大小为单位的,也就是说pa + 2,相当于0x12ff60 + sizeof(int) * 2 = 0x12ff60 + 4 * 2 = 0x12ff68,不是0x12ff60 + 2哦;而pb - -相当于0x12ff44 + sizeof(double) * 1 = 0x12ff44 + 8 * 1 = 0x12ff4c。理解这一点很重要。 同理&a + 2和&b - 1也是一样(注意由于&b是一个指针常量,所以写成&b - -是错误的)。 指针变量和指针常量 指针变量首先是一个变量,由于指针变量存储了某个变量的内存首地址,我们通常认为**”指针变量指向了该变量“,但是在这个时刻指针变量pa指向变量a,下个时候可能不存储变量a的首地址,而是存储变量c的首地址,那么我们可以认为这个时候,pa不再指向a,而是指向c。请别嫌我啰嗦,为了帮助你理解,我是故意说得这么细的,后面我们讨论高级主题的时候,当你觉得迷糊,请回来反复咀嚼一下这段话。也就是说指针变量是一个变量,它的值可以变动**的。 相反,指针常量可通俗地理解为存储固定的内存单元地址编号的”量“,它一旦存储了某个内存地址以后,不可再改存储其他的内存地址了。所以指针常量是坚韧,因为它”咬定青山不放松“;说是”痴情“,因为它”曾经沧海难为水“。我这里讲的指针常量对应的是const关键字定义的量,而不是指针字面量。像&a, &b, &a + 2等是指针字面量,而const int *p = &a;中的p才算是真正的指针常量,指针常量一般用在函数的参数中,表示该函数不可改变实参的内容。来看一个例子吧: 上面的函数由于修改了一个常指针(多数情况下等同指针常量),因而会编译出错:error C3892: “x”: 不能给常量赋值。 指针变量与数组 记得多年以前,我在学生会给电子技术部和地理信息系统专业的同学进行C语言培训时,这是一个最让他们头疼和感到一头雾水的话题,尤其是指针变量与二维数组的结合,我永远忘不了胡永月那一脸迷惑与无助的表情。今天我这里给大家深入地分析一下。先看一个例子: 如果你能得出下面这样的结果,说明你已经基本上对数组与指针的概念理解清楚了: 通过上图,我们可以知道*(a + 1) = 2, *(ptr - 1) = 5。 且不说很多同学根本得不到这样的结果,他们看到int ptr = (int)(&a+1);这样的语句就已经懵了,首先,我们知道C语言中规定数组名表示这个数组的首地址,而这里竟然出现了&a这样的符号,本来a就是一个指针常量了,这里对&a再次取地址难道不是非法操作吗?哈哈,当你有这样的疑问的时候,说明你对二维数组相关知识理解不深入。我这里先给你补充下知识点吧:...

January 11, 2021

游戏开发专题

游戏开发专题 1 游戏服务器开发的基本体系与服务器端开发的一些建议 2 网络游戏服务器开发框架设计介绍 3 游戏后端开发需要掌握的知识 4 关于游戏服务端架构的整理 5 各类游戏对应的服务端架构 6 从腾讯QQgame高性能服务器集群架构看“分而治之”与“自治”等分布式架构设计原则 7 QQ游戏百万人同时在线服务器架构实现 8 大型多人在线游戏服务器架构设计 9 百万用户级游戏服务器架构设计 10 十万在线的WebGame的数据库设计思路 11 一种高性能网络游戏服务器架构设计 12 经典游戏服务器端架构概述 13 游戏跨服架构进化之路

January 11, 2021

用Visual Studio调试Linux程序

用Visual Studio调试Linux程序 用Visual Studio调试Linux程序?你真的没看错,这个是真的,不是标题党。当然如果你说VS2015及以上版本自带的Linux调试插件,那就算了。这些自带的插件调试一个有简单的main函数程序还凑合,稍微复杂点的程序,根本无法编译调试。 而本文介绍的主角是VS的另外一款插件Visual GDB,让我们欢迎主角登场,下面是正文。 使用Visual Studio+VisualGDB调试远程Linux程序 需要工具: Visual Studio 2013或以上版本(以下简称VS) VisualGDB(一款VS插件,官网为:http://visualgdb.com/) 含有调试符号的Linux程序文件(该程序文件为调试目标) Visual Assistant(番茄助手,另外一款VS插件) 在VS上安装完VisualGDB插件以后,有如下几种方式来对远程Linux机器上的程序进行调试: **方法一、**如果该程序已经启动,则可以使用VS菜单【Debug】->【Attach to Process…】。 这种方法有个缺点是,不能从开始启动的main函数处添加断点,自始至终地调试程序,查看完整程序运行脉络,所以下面推荐方法二。 方法二、利用VS启动远程Linux机器上一个Linux程序文件进行调试。选择VS菜单【Debug】 ->【Quick Debugwith GDB】。 需要注意的地方,已经在上图中标红框。这里简单地解释一下: 如果你安装了交叉编译环境Target可以选择MinGW/Cygwin,否则就选择远程Linux系统。这里如果不存在一个ssh连接,则需要创建一个。 Debugged program是需要设置的被调试程序的路径,位于远程Linux机器上。 Arguments是该调试程序需要设置的命令行参数,如果被调试程序不需要命令行参数可以不设置。 Working directory是被调试程序运行的工作目录。 另外建议勾选上Initial breakpoint in main,这样启动调试时,程序就会停在程序入口处。 这样,我们就可以利用VS强大的功能去查看程序的各种状态了,常用的面板,如【内存】【线程】【观察】【堆栈】【GDB Session】【断点】等窗口位于VS 菜单【Debug】->【Windows】菜单下,注意,有些窗口只有在调试状态下才可见。这里有两个值得强调一下的功能是: GDB Session****窗口,在这个窗口里面可以像原来直接使用gdb调试一样输入gdb指令来进行调试。 SSH console****窗口,这个窗口类似一个远程操作Linux系统的应用程序如xshell、SecureCRT。 现在还剩下一个问题,就是我们虽然在调试时可视化地远程查看一个Linux进程的状态信息,但很多类型的定义和什么却无法看到。解决这个问题的方法就是你可以先在VS里面建立一个工程,导入你要调试的程序的源代码目录。然后利用方法一或者方法二去启动调试程序。这个时候你想查看某个类型的定义或什么只要利用Visual Assit的查看源码功能即可,快捷键是Alt + G。 需要注意的时:同时安装了Visual Assist和VisualGDB后,后者也会提供一个go按钮去查找源码定义,但这个功能远不如Visual Assist按钮好用,我们可以禁用掉它来使用Visual Assist的Go功能。禁用方法,打开菜单:【Tools】->【Option…】: 然后重启VS即可。 到这里,既可以查看源码,也可以调试程序了。 VisualGDB 下载地址: 链接:https://share.weiyun.com/57aGHLM 密码:kj9ahs

January 11, 2021