心跳包机制设计详解

心跳包机制设计详解 存在下面两种情形: 情形一:一个客户端连接服务器以后,如果长期没有和服务器有数据来往,可能会被防火墙程序关闭连接,有时候我们并不想要被关闭连接。例如,对于一个即时通讯软件,如果服务器没有消息时,我们确实不会和服务器有任何数据交换,但是如果连接被关闭了,有新消息来时,我们再也没法收到了,这就违背了“即时通讯”的设计要求。 情形二:通常情况下,服务器与某个客户端一般不是位于同一个网络,其之间可能经过数个路由器和交换机,如果其中某个必经路由器或者交换器出现了故障,并且一段时间内没有恢复,导致这之间的链路不再畅通,而此时服务器与客户端之间也没有数据进行交换,由于 TCP 连接是状态机,对于这种情况,无论是客户端或者服务器都无法感知与对方的连接是否正常,这类连接我们一般称之为“死链”。 情形一中的应用场景要求必须保持客户端与服务器之间的连接正常,就是我们通常所说的“保活“。如上文所述,当服务器与客户端一定时间内没有有效业务数据来往时,我们只需要给对端发送心跳包即可实现保活。 情形二中的死链,只要我们此时任意一端给对端发送一个数据包即可检测链路是否正常,这类数据包我们也称之为”心跳包”,这种操作我们称之为“心跳检测”。顾名思义,如果一个人没有心跳了,可能已经死亡了;一个连接长时间没有正常数据来往,也没有心跳包来往,就可以认为这个连接已经不存在,为了节约服务器连接资源,我们可以通过关闭 socket,回收连接资源。 根据上面的分析,让我再强调一下,心跳检测一般有两个作用: 保活 检测死链 TCP keepalive 选项 操作系统的 TCP/IP 协议栈其实提供了这个的功能,即 keepalive 选项。在 Linux 操作系统中,我们可以通过代码启用一个 socket 的心跳检测(即每隔一定时间间隔发送一个心跳检测包给对端),代码如下: //on 是 1 表示打开 keepalive 选项,为 0 表示关闭,0 是默认值 int on = 1; setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &on, sizeof(on)); 但是,即使开启了这个选项,这个选项默认发送心跳检测数据包的时间间隔是 7200 秒(2 小时),这时间间隔实在是太长了,不具有实用性。 我们可以通过继续设置 keepalive 相关的三个选项来改变这个时间间隔,它们分别是 TCP_KEEPIDLE、TCP_KEEPINTVL 和 TCP_KEEPCNT,示例代码如下: //发送 keepalive 报文的时间间隔 int val = 7200; setsockopt(fd, IPPROTO_TCP, TCP_KEEPIDLE, &val, sizeof(val)); //两次重试报文的时间间隔 int interval = 75; setsockopt(fd, IPPROTO_TCP, TCP_KEEPINTVL, &interval, sizeof(interval)); int cnt = 9; setsockopt(fd, IPPROTO_TCP, TCP_KEEPCNT, &cnt, sizeof(cnt)); TCP_KEEPIDLE 选项设置了发送 keepalive 报文的时间间隔,发送时如果对端回复 ACK。则本端 TCP 协议栈认为该连接依然存活,继续等 7200 秒后再发送 keepalive 报文;如果对端回复 RESET,说明对端进程已经重启,本端的应用程序应该关闭该连接。 如果对端没有任何回复,则本端做重试,如果重试 9 次(TCP_KEEPCNT 值)(前后重试间隔为 75 秒(TCP_KEEPINTVL 值))仍然不可达,则向应用程序返回 ETIMEOUT(无任何应答)或 EHOST 错误信息。...

January 11, 2021

日志系统的设计

日志系统的设计 为什么需要日志 实际的软件项目产出都有一个流程,即先开发、测试,再发布生产,由于人的因素,既然是软件产品就不可能百分百没有 bug 或者逻辑错误,对于已经发布到生产的项目,一旦某个时刻产生非预期的结果,我们就需要去定位和排查问题。但是一般正式的生产环境的服务器或者产品是不允许开发人员通过附加调试器去排查问题的,主要有如下可能原因: 在很多互联网企业,开发部门、测试部分和产品运维部门是分工明确的,软件产品一旦发布到生产环境以后,将全部交由运维部门人员去管理和维护,而原来开发此产品的开发人员不再拥有相关的操作程序的权限。 对于已经上了生产环境的服务,其数据和程序稳定性是公司的核心产值所在,一般不敢或不允许被开发人员随意调试或者操作,以免造成损失。 发布到生产环境的服务,一般为了让程序执行效率更高、文件体积更小,都是去掉调试符号后的版本,不方便也不利于调试。 既然我们无法通过调试器去调试,这个时候为了跟踪和回忆当时的程序行为进而定位问题,我们就需要日志系统。 退一步说,即使在开发或者测试环境,我们可以把程序附加到调试器上去调试,但是对于一些特定的程序行为,我们无法通过设置断点,让程序在某个时刻暂停下来进行调试。例如,对于某些网络通信功能,如果暂停时间过长(相对于某些程序逻辑来说),通信的对端可能由于彼端没有在规定时间内响应而断开连接,导致程序逻辑无法进入我们想要的执行流中去;再例如,对于一些高频操作(如心跳包、定时器、界面绘制下的某些高频重复行为),可能在少量次数下无法触发我们想要的行为,而通过断点的暂停方式,我们不得不重复操作几十次、上百次甚至更多,这样排查问题效率是非常低下的。对于这类操作,我们可以通过打印日志,将当时的程序行为上下文现场记录下来,然后从日志系统中找到某次不正常的行为的上下文信息。这也是日志的另外一个作用。 本文将从技术和业务上两个方面来介绍日志系统相关的设计与开发,所谓技术上,就是如何从程序开发的角度设计一款功能强大、性能优越、使用方便的日志系统;而业务上,是指我们在使用日志系统时,应该去记录哪些行为和数据,既简洁、不啰嗦,又方便需要时快速准确地定位到问题。 日志系统的技术上的实现 日志的最初的原型即将程序运行的状态打印出来,对于 C/C++ 这门语言来说,即可以利用 printf、std::cout 等控制台输出函数,将日志信息输出到控制台,这类简单的情形我们不在此过多赘述。 对于商业项目,为了方便排查问题,我们一般不将日志写到控制台,而是输出到文件或者数据库系统。不管哪一种,其思路基本上一致,我们这里以写文件为例来详细介绍。 同步写日志 所谓同步写日志,指的是在输出日志的地方,将日志即时写入到文件中去。根据笔者的经验,这种设计广泛地用于相当多的的客户端软件。笔者曾从事过数年的客户端开发(包括 PC、安卓软件),设计过一些功能复杂的金融客户端产品,在这些系统中采用的就是这种同步写日志的方式。之所以使用这种方式其主要原因就是设计简单,而又不会影响用户使用体验。说到这里读者可能有这样一个疑问:一般的客户端软件,一般存在界面,而界面部分所属的逻辑就是程序的主线程,如果采取这种同步写日志的方式,当写日志时,写文件是磁盘 IO 操作,相比较程序其他部分是 CPU 操作,前者要慢很多,这样势必造成CPU等待,进而导致主线程“卡”在写文件处,进而造成界面卡顿,从而导致用户使用软件的体验不好。读者的这种顾虑确实是存在的。但是,很多时候我们不用担心这种问题,主要有两个原因: 对于客户端程序,即使在主线程(UI 线程)中同步写文件,其单次或者几次磁盘操作累加时间,与人(用户)的可感知时间相比,也是非常小的,也就是说用户根本感觉不到这种同步写文件造成的延迟。当然,这里也给您一个提醒就是,如果在 UI 线程里面写日志,尤其是在一些高频操作中(如 Windows 的界面绘制消息 WM_PAINT 处理逻辑中),一定要控制写日志的长度和次数,否则就会因频繁写文件或一次写入数据过大而对界面造成卡顿。 客户端程序除了 UI 线程,还有其他与界面无关的工作线程,在这些线程中直接写文件,一般不会对用户的体验产生什么影响。 说了这么多,我们给出一个具体的例子。 日志类的 .h 文件 /** *@desc: IULog.h *@author: zhangyl *@date: 2014.12.25 */ #ifndef __LOG_H__ #define __LOG_H__ enum LOG_LEVEL { LOG_LEVEL_INFO, LOG_LEVEL_WARNING, LOG_LEVEL_ERROR }; //注意:如果打印的日志信息中有中文,则格式化字符串要用_T()宏包裹起来, #define LOG_INFO(...) CIULog::Log(LOG_LEVEL_INFO, __FUNCSIG__,__LINE__, __VA_ARGS__) #define LOG_WARNING(...) CIULog::Log(LOG_LEVEL_WARNING, __FUNCSIG__, __LINE__,__VA_ARGS__) #define LOG_ERROR(...) CIULog::Log(LOG_LEVEL_ERROR, __FUNCSIG__,__LINE__, __VA_ARGS__) class CIULog { public: static bool Init(bool bToFile, bool bTruncateLongLog, PCTSTR pszLogFileName); static void Uninit(); static void SetLevel(LOG_LEVEL nLevel); //不输出线程ID号和所在函数签名、行号 static bool Log(long nLevel, PCTSTR pszFmt, ....

January 11, 2021

给工作 4 年迷茫的程序员们的一点建议

给工作 4 年迷茫的程序员们的一点建议 有公众号读者在后台向我提问: JAVA 程序员,4 年了,迷茫了,希望由前辈可以给指出一个技术路线5年左右程序员必须要掌握的知识技能树? 工作了很久了,对于目前自己的技术程度不满意,但是不知道如何梳理。学习一些技术是不知道是否有用。希望前辈可以指点迷津。不以年限轮英雄,希望可以给出您的见解。修改一次。。。。。。项目设计都是我来做。。。数据库设计也是我来做。。。我的意思是。。感觉目前自己的知识储备不足以支撑我架构以及设计。。求个知识树。。。。 以下是我的回答: 先举两个真实的例子。 例子一: 前两天我在给我们部门做服务器网络故障排查经验分享时,我问了一个问题关于 java.io.DataOutputStream 的问题,如果从一个 socket 输出流中读取数据,如果当前流中没有数据,读方法是否会阻塞。我又问,假如阻塞,会阻塞多久?我们如何避免这个问题。很多人回答不上来,更不用说,Java 中的 AIO、NIO 的使用细节了。 例子二: 我归纳一下,情况大致如下: 有不少朋友通过我的公众号『高性能服务器开发』中的『职业指导』模块找到我,来意大致是:做 java 开发工作了三五年了,月收入不到二万,现在因为人到中年,经济压力比较大; 但是工作上只能做做模块,写写业务代码,所以即使跳槽也不会拿到满意的薪资,所以只好维持现状(但又特别苦闷、迷茫)。 我来说一下我的观点,说的现实一点,题主所谓的迷茫其实因知识能力的不足导致的成就感、收入水平与日益增长的工作年限的矛盾。 越是高薪的职位,其对人的要求也越高。诸如上面的例子,工作有几年的 java 开发者,连 jdk 中基本的输入输出流的细节都搞不清楚,一问到就是各种摇头,然后说各种 java 框架,这样的开发者其实并不合格,因为他们离开了框架就啥也做不了,那么在工作安排上这样的人不天天也业务代码,谁来写呢?(核心的技术框架是不能让他们写的,由于基础水平不扎实,写出来的框架稳定性和性能会不好)。说的悲观一点,这样的开发者公司是从来不缺的,铁打的营盘,流水的兵,走了再招一批罢了,这也就是所谓的千军易得一将难求,我们要努力做将才乃至帅才,而不是小兵。 在面试某些 java 开发者时,我问的比较多的一个问题就是,java 多线程之间的同步技术有哪些,然后不少面试者就病急乱投医了,甚至连 ConcurrentHashMap 都说上了。这也是典型的基础概念模糊不清,ConcurrentHashMap 是一个线程安全性容器,但绝不是一个线程同步技术。 再比如问面试者 java.lang.Object 有哪些常用方法时,不少面试者能说出来的也不多。 我举这些例子并不是为了要教大家具体的 java 知识,而是为了说明基础知识的重要性。如果你的java基础足够好(熟悉 jdk 的常用类,知道常用接口的各种坑和注意事项),那么开发一个东西时即使不用框架你也能顺畅地写出来。这样的人才具备进一步发展的潜力。退一步说,不管多么复杂的java框架,都是基于jdk那些类库的。你jdk的基础知识都学不好,我不相信那些上层框架你能搞的透彻。 说一千道一万,核心的还是基础知识不扎实的问题。就和刘备当年成就帝业一样,诸葛亮给的策略就是先谋取荆州,再进军西蜀,最后三分天下。同理jdk的基础知识就是你应该要首先谋取的“荆州”,进一步的各种框架、架构设计是你的“蜀地”。基础不牢,想其他的东西都是好高骛远,不切实际。最后日复一日,年复一年,在恨自己生不逢时,领导不是伯乐的嗟叹中蹉跎了岁月。 对于上面这个注重基础的问题上,实际情形中,我遇到三种人。 第一类:意识不到基础知识的重要性,这类人就不提了。 第二类,意识到基础知识的重要性,但是总是在各种理由和借口中麻痹自己,温水煮青蛙把自己“煮死”。很多咨询我的人,也是这种情况,说什么自己工作忙,家庭琐事多。我其实不想多说啥,为失败找借口的人太多,为成功找方法的人太少。你工作五年了,每个月抽一天时间来补一下基础,你现在都不是这样了,这个时间也抽不出来?自我麻痹而已。这类人其实是有想法没啥行动。 第三类,意识到基础的重要性,同时在各种闲暇时间去补充,去积累。这样的人学的最快,最后达到的高度也很高(当然收入也不菲)。 扎实的基础知识 + 见多识广的框架经验,让你在职场上变得无可替代,这才是你的核心竞争力。答案可能有点跑题了,但是我觉得先解决思想上的问题,行动上就容易许多了。 如果你想和我聊聊职业上的困惑,可以在『高性能服务器开发』公众号后台回复关键字『职业指导』,我们可以针对性地聊一聊。

January 11, 2021

聊聊技术人员的常见的职业问题

聊聊技术人员的常见的职业问题 由于时间有限,很多读者提出的问题,不能一一解答,因此这篇文章,来系统地就各类型的读者遇到的一些常见职业问题回答一下: Q1 应届生如何选择自己的第一份工作? Q2 作为一个程序员,是进入大厂好,还是进入创业公司好? Q3 我专科(或二本)毕业,学历不行,如何进大厂工作? Q4 我非科班出身,如何进大厂工作? Q5 有没有人能分享一下大厂的面经? A1 答案点 这里 和 这里。 Q6 我工作了几年,技术不行,如何提高? Q7 我非科班出身,应该看哪些书才能补上计算机专业的基础? Q8 我想成为一名技术高手,应该如何提高? Q9 天天写业务代码,如何能有机会做一些底层的设计和开发? A2 答案点 这里。 Q10 服务器端开发与前端开发有什么差别?哪个发展潜力好一点?哪个薪资高一点? A3 答案点 这里。 Q11 我想成为一名 C++ 程序员,该如何入门、进阶以及升华? Q12 C++ 后端开发需要掌握哪些东西? Q13 C++ 面试应该准备哪些东西? A4 答案点 这里 和 这里 以及 这里。 Q15 我是一名 Java 程序员,天天增删改查数据库,我如何实质性的提高自己? Q16 Java 技术栈的所谓的基础在哪里? A5 答案点 这里。 Q17 程序员真的很难找女朋友吗? Q18 大厂加班严重,在大厂上班的程序员真的没有女朋友吗? A6 这是一个忧伤的话题,答案戳 这里 和 这里 以及 **这里****。 Q20 如何通过技术面试来确定面试官的职级?如何确定自己面试职位所对应的职级? A7 答案看 这里。 Q21 技术面试中,面试官问我薪资,我该不该告诉他? Q22 技术面试过了,如何和 HR 谈薪水? Q23 我报了一个薪水之后,HR 爽快的答应了,我是不是报低了?我能不能再找他们提高一点? A8 答案看 这里。 Q24 年薪五十万的技术岗位做些什么工作? Q25 做技术岗位如何年薪五十万呢? Q26 年薪五十万的程序员是不是真的头发很少? A9 别害怕,答案戳 这里。 Q26 年终奖是如何发的?什么时候发? Q27 年终奖还没发,我跳槽是不是就没有年终奖了?...

January 11, 2021

职业规划

职业规划 给工作 4 年迷茫的程序员们的一点建议 聊聊技术人员的常见的职业问题 写给那些傻傻想做服务器开发的朋友

January 11, 2021

自我提升与开源代码

自我提升与开源代码 2020 年好好读一读开源代码吧

January 11, 2021

错误码系统的设计

错误码系统的设计 本文介绍服务器开发中一组服务中错误码系统的设计理念与实践,如果读者从来没想过或者没接触过这种设计理念,建议认真体会一下这种设计思路的优点。 错误码的作用 读者如果有使用过中国电信的宽带账号上网的经历,如果我们登陆不成功,一般服务器会返回一个错误码,如651、678。然后,我们打中国电信的客服电话,客服会询问我们错误码是多少,通过错误码他们的技术人员就大致知道了错误原因;并且通过错误码,他们就知道到底是电信的服务器问题还是宽带用户自己的设备或者操作问题,如果是用户自己的问题,他们一般会尝试教用户如何操作,而不是冒然就派遣维修人员上门,这样不仅能尽早解决问题同时也节约了人力成本。 再举另外一个例子,我们日常浏览网页,当Web服务器正常返回页面时,状态码一般是200(OK),而当页面不存在时,错误码一般是404,另外像503等错误都是比较常见的。 通过以上两个例子,读者应该能明白,对于服务器系统来说,设计一套好的错误码是非常有必要的,可以在用户请求出问题时迅速定位并解决问题。具体包括两个方面: 可以迅速定位是用户“输入”问题还是服务器自身的问题。 所谓的用户“输入”问题,是指用户的不当操作,这里的“用户的不当操作”可能是因为客户端软件本身的逻辑错误或漏洞,也可能是使用客户端的人的非法操作,而客户端软件在设计上因为考虑不周而缺乏有效性校验,这两类情形都可能会产生非法的数据,并且直接发给服务器。一个好的服务端系统不能假设客户端的请求数据一定是合法的,必须对传过来的数据做有效性校验。服务器没有义务一定给非法的请求做出应答,因此请求的最终结果是服务器不应答或给客户端不想要的应答。 以上面的例子为例,宽带用户输入了无效的用户名或者密码造成服务器拒绝访问;用户在浏览器中输入了一个无效的网址等。这类错误,都是需要用户自己解决或者用户可以自己解决的。如果错误码可以反映出这类错误,那么在实际服务器运维的过程中,当用户反馈这一类故障时,我们通过服务器内部产生的错误码或者应答给客户端的错误码,准确快速地确定问题原因。如果是用户非法请求造成的,可以让用户自行解决。注意,这里的“用户”,可以代指人,也可以代指使用某个服务器的所有下游服务和客户端。 可以快速定位哪个步骤或哪个服务出了问题。 对于单个服务,假设收到某个“客户端”请求时,需要经历多个步骤才能完成,而这中间任何一个步骤都可能出问题,在不同步骤出错时返回不同的错误码,那么就可以知道是哪个步骤出了问题。 其次,一般稍微复杂一点的系统,都不是单个服务,往往是由一组服务构成。如果将错误码分段,每个服务的错误码都有各自的范围,那么通过错误码,我们也能准确地知道是哪个服务出了问题。 错误码系统设计实践 前面介绍了太多的理论知识,我们来看一个具体的例子。假设如下一个“智能邮件系统”,其结构如下所示: 上图中的服务**“智能邮件坐席站点”和“配置站点”是客户端,”智能邮件操作综合接口“和”邮件配置服务“**是对客户端提供服务的前置服务,这两个前置服务后面还依赖后面的数个服务。由于这里我们要说明的是技术问题,而不是业务问题,所以具体每个服务作何用途这里就不一一介绍了。在这个系统中,当客户端得到前置服务某个不正确应答时,会得到一个错误码,我们按以下规则来设计错误码: 服务名称 正值错误码范围 负值错误码范围 智能邮件综合操作接口 100~199 -100~-199 ES数据同步服务 200~299 -200~-299 邮件配置服务 300~399 -300~-399 邮件基础服务 400~499 -400~-499 我们在设计这套系统时,做如下规定: 所有的正值错误码表示所在服务的上游服务发来的请求不满足业务要求。举个例子,假设某次智能邮件坐席站点客户端得到了一个错误码101,我们可以先确定错误产生的服务器是智能邮件综合操作接口服务;其次,产生该错误的原因是智能邮件坐席站点客户端发送给智能邮件综合操作接口服务的请求不满足要求,通过这个错误码我们甚至可以进一步确定发送的请求哪里不符合要求。如我们可以这样定义: 100 用户名不存在 101 密码无效 102 发送的邮件收件人非法 103 邮件正文含有非法字符 其他从略,此处就不一一列举了。 所有的负值错误码表示程序内部错误。如: -100 数据库操作错误 -101 网络错误 -102 内存分配失败 -103 ES数据同步服务连接不上 其他从略,此处就不一一列举了。 对负值错误码的特殊处理 通过前面的介绍,读者应该能看出正值错误码与负值错误码的区别,即正值错误码一般是由请求服务的客户产生,如果出现这样的错误,应该由客户自己去解决问题;而负值错误码,则一般是服务内部产生的错误。因此,如果是正值错误码,错误码和错误信息一般可以直接返回给客户端;而对于负值错误,我们一般只将错误码返回给客户端,而不带上具体的错误信息,这也是读者在使用很多软件产品时,经常会得到“网络错误”这类万能错误提示。也就是说对于负值错误码的错误信息,我们可以统一显示成“网络错误”或者其他比较友好的错误提示。 这样做的原因有二: 客户端即使拿到这样的错误信息,也不能对排查和解决问题提供任何帮助,因为这些错误是程序内部错误或者bug。 这类错误有可能是企业内部的设计缺陷,直接暴露给客户,除了让客户对企业的技术实力产生质疑以外,没有任何其他正面效应。 而之所以带上错误码,是为了方便内部排查和定位问题。当然,现在的企业服务,内部也有大量监控系统,可能也不会再暴露这样的错误码了。 扩展 上文介绍了利用错误码的分段来定位问题的技术思想,其实不仅仅是错误码可以分段,我们在开发一组服务时,业务类型也可以通过编号来分段,这样通过业务号就能知道归属哪个服务了。 如果读者以前没接触过这种设计思想,希望可以好好的思考和体会一下。

January 11, 2021

非阻塞模式下 send 和 recv 函数的返回值

非阻塞模式下 send 和 recv 函数的返回值 我们来总结一下 send 和 recv 函数的各种返回值意义: 返回值 n 返回值含义 大于 0 成功发送 n 个字节 0 对端关闭连接 小于 0( -1) 出错或者被信号中断或者对端 TCP 窗口太小数据发不出去(send)或者当前网卡缓冲区已无数据可收(recv) 我们来逐一介绍下这三种情况: 返回值大于 0 对于 send 和 recv 函数返回值大于 0,表示发送或接收多少字节,需要注意的是,在这种情形下,我们一定要判断下 send 函数的返回值是不是我们期望发送的缓冲区长度,而不是简单判断其返回值大于 0。举个例子: 1int n = send(socket, buf, buf_length, 0); 2if (n > 0) 3{ 4 printf("send data successfully\n"); 5} 很多新手会写出上述代码,虽然返回值 n 大于 0,但是实际情形下,由于对端的 TCP 窗口可能因为缺少一部分字节就满了,所以返回值 n 的值可能在 (0, buf_length] 之间,当 0 < n < buf_length 时,虽然此时 send 函数是调用成功了,但是业务上并不算正确,因为有部分数据并没发出去。你可能在一次测试中测不出 n 不等于 buf_length 的情况,但是不代表实际中不存在。所以,建议要么认为返回值 n 等于 buf_length 才认为正确,要么在一个循环中调用 send 函数,如果数据一次性发不完,记录偏移量,下一次从偏移量处接着发,直到全部发送完为止。 1 //推荐的方式一 2 int n = send(socket, buf, buf_length, 0); 3 if (n == buf_length) 4 { 5 printf("send data successfully\n"); 6 } 1//推荐的方式二:在一个循环里面根据偏移量发送数据 2bool SendData(const char* buf , int buf_length) 3{ 4 //已发送的字节数目 5 int sent_bytes = 0; 6 int ret = 0; 7 while (true) 8 { 9 ret = send(m_hSocket, buf + sent_bytes, buf_length - sent_bytes, 0); 10 if (nRet == -1) 11 { 12 if (errno == EWOULDBLOCK) 13 { 14 //严谨的做法,这里如果发不出去,应该缓存尚未发出去的数据,后面介绍 15 break; 16 } 17 else if (errno == EINTR) 18 continue; 19 else 20 return false; 21 } 22 else if (nRet == 0) 23 { 24 return false; 25 } 26 27 sent_bytes += ret; 28 if (sent_bytes == buf_length) 29 break; 30 31 //稍稍降低 CPU 的使用率 32 usleep(1); 33 } 34 35 return true; 36} 返回值等于 0...

January 11, 2021

高性能服务器架构设计总结

高性能服务器架构设计总结 系列目录 第01篇 主线程与工作线程的分工 第02篇 Reactor模式 第03篇 一个服务器程序的架构介绍 第04篇 如何将socket设置为非阻塞模式 第05篇 如何编写高性能日志 第06篇 关于网络编程的一些实用技巧和细节 第07篇 开源一款即时通讯软件的源码 第08篇 高性能服务器架构设计总结1 第09篇 高性能服务器架构设计总结2 第10篇 高性能服务器架构设计总结3 第11篇 高性能服务器架构设计总结4 这篇文章算是对这个系列的一个系统性地总结。我们将介绍服务器的开发,并从多个方面探究如何开发一款高性能高并发的服务器程序。 所谓高性能就是服务器能流畅地处理各个客户端的连接并尽量低延迟地应答客户端的请求;所谓高并发,指的是服务器可以同时支持多的客户端连接,且这些客户端在连接期间内会不断与服务器有数据来往。 这篇文章将从两个方面来介绍,一个是服务器的框架,即单个服务器程序的代码组织结构;另外一个是一组服务程序的如何组织与交互,即架构。注意:本文以下内容中的客户端是相对概念,指的是连接到当前讨论的服务程序的终端,所以这里的客户端既可能是我们传统意义上的客户端程序,也可能是连接该服务的其他服务器程序。 一、框架篇 按上面介绍的思路,我们先从单个服务程序的组织结构开始介绍。 (一)、网络通信 既然是服务器程序肯定会涉及到网络通信部分,那么服务器程序的网络通信模块要解决哪些问题? 笔者认为至少要解决以下问题: 如何检测有新客户端连接? 如何接受客户端连接? 如何检测客户端是否有数据发来? 如何收取客户端发来的数据? 如何检测连接异常?发现连接异常之后,如何处理? 如何给客户端发送数据? 如何在给客户端发完数据后关闭连接? 稍微有点网络基础的人,都能回答上面说的其中几个问题,比如接收客户端连接用socket API的accept函数,收取客户端数据用recv函数,给客户端发送数据用send函数,检测客户端是否有新连接和客户端是否有新数据可以用IO multiplexing技术(IO复用)的select、poll、epoll等socket API。确实是这样的,这些基础的socket API构成了服务器网络通信的地基,不管网络通信框架设计的如何巧妙,都是在这些基础的socket API的基础上构建的。但是如何巧妙地组织这些基础的socket API,才是问题的关键。我们说服务器很高效,支持高并发,实际上只是一个技术实现手段,不管怎样从软件开发的角度来讲无非就是一个程序而已,所以,只要程序能最大可能地满足“尽量减少等待”就是高效。也就是说高效不是“忙的忙死,闲的闲死”,而是大家都可以闲着,但是如果有活要干,大家尽量一起干,而不是一部分忙着依次做事情123456789,另外一部分闲在那里无所事事。说的可能有点抽象,下面我们来举一些例子具体来说明一下。 比如默认recv函数如果没有数据的时候,线程就会阻塞在那里; 默认send函数,如果tcp窗口不是足够大,数据发不出去也会阻塞在那里; connect函数默认连接另外一端的时候,也会阻塞在那里; 又或者是给对端发送一份数据,需要等待对端回答,如果对方一直不应答,当前线程就阻塞在这里。 以上都不是高效服务器的开发思维方式,因为上面的例子都不满足“尽量减少等待”的原则,为什么一定要等待呢?有没用一种方法,这些过程不需要等待,最好是不仅不需要等待,而且这些事情完成之后能通知我。这样在这些本来用于等待的cpu时间片内,我就可以做一些其他的事情。有,也就是我们下文要讨论的IO Multiplexing技术(IO复用技术)。 (二)、几种IO复用机制的比较 目前windows系统支持select、WSAAsyncSelect、WSAEventSelect、完成端口(IOCP),linux系统支持select、poll、epoll。这里我们不具体介绍每个具体的函数的用法,我们来讨论一点深层次的东西,以上列举的API函数可以分为两个层次: 层次一 select和poll 层次二 WSAAsyncSelect、WSAEventSelect、完成端口(IOCP)、epoll 为什么这么分呢?先来介绍第一层次,select和poll函数本质上还是在一定时间内主动去查询socket句柄(可能是一个也可能是多个)上是否有事件,比如可读事件,可写事件或者出错事件,也就是说我们还是需要每隔一段时间内去主动去做这些检测,如果在这段时间内检测出一些事件来,我们这段时间就算没白花,但是倘若这段时间内没有事件呢?我们只能是做无用功了,说白了,还是在浪费时间,因为假如一个服务器有多个连接,在cpu时间片有限的情况下,我们花费了一定的时间检测了一部分socket连接,却发现它们什么事件都没有,而在这段时间内我们却有一些事情需要处理,那我们为什么要花时间去做这个检测呢?把这个时间用在做我们需要做的事情不好吗?所以对于服务器程序来说,要想高效,我们应该尽量避免花费时间主动去查询一些socket是否有事件,而是等这些socket有事件的时候告诉我们去处理。这也就是层次二的各个函数做的事情,它们实际相当于变主动查询是否有事件为当有事件时,系统会告诉我们,此时我们再去处理,也就是“好钢用在刀刃”上了。只不过层次二的函数通知我们的方式是各不相同,比如WSAAsyncSelect是利用windows消息队列的事件机制来通知我们设定的窗口过程函数,IOCP是利用GetQueuedCompletionStatus返回正确的状态,epoll是epoll_wait函数返回而已。 比如connect函数连接另外一端,如果连接socket是异步的,那么connect虽然不能立刻连接完成,但是也是会立刻返回,无需等待,等连接完成之后,WSAAsyncSelect会返回FD_CONNECT事件告诉我们连接成功,epoll会产生EPOLLOUT事件,我们也能知道连接完成。甚至socket有数据可读时,WSAAsyncSelect产生FD_READ事件,epoll产生EPOLLIN事件,等等。 所以有了上面的讨论,我们就可以得到网络通信检测可读可写或者出错事件的正确姿势。这是我这里提出的第二个原则:尽量减少做无用功的时间。这个在服务程序资源够用的情况下可能体现不出来什么优势,但是如果有大量的任务要处理,个人觉得这个可能带来无用。 (三)、检测网络事件的正确姿势 根据上面的介绍,第一,为了避免无意义的等待时间,第二,不采用主动查询各个socket的事件,而是采用等待操作系统通知我们有事件的状态的策略。我们的**socket都要设置成异步的。**在此基础上我们回到栏目(一)中提到的七个问题: 如何检测有新客户端连接? 如何接受客户端连接? 默认accept函数会阻塞在那里,如果epoll检测到侦听socket上有EPOLLIN事件,或者WSAAsyncSelect检测到有FD_ACCEPT事件,那么就表明此时有新连接到来,这个时候调用accept函数,就不会阻塞了。当然产生的新socket你应该也设置成非阻塞的。这样我们就能在新socket上收发数据了。 如何检测客户端是否有数据发来? 如何收取客户端发来的数据? 同理,我们也应该在socket上有可读事件的时候才去收取数据,这样我们调用recv或者read函数时不用等待。 至于一次性收多少数据好呢? 我们可以根据自己的需求来决定,甚至你可以在一个循环里面反复recv或者read,对于非阻塞模式的socket,如果没有数据了,recv或者read也会立刻返回,错误码EWOULDBLOCK会表明当前已经没有数据了。示例: 1bool CIUSocket::Recv() 2{ 3 int nRet = 0; 4 5 while(true) 6 { 7 char buff[512]; 8 nRet = ::recv(m_hSocket, buff, 512, 0); 9 //一旦出现错误就立刻关闭Socket 10 if(nRet == SOCKET_ERROR) 11 { 12 if (::WSAGetLastError() == WSAEWOULDBLOCK) 13 break; 14 else 15 return false; 16 } 17 else if(nRet < 1) 18 return false; 19 20 m_strRecvBuf....

January 11, 2021

高性能服务器框架设计

高性能服务器框架设计 主线程与工作线程的分工 Reactor模式 实例:一个服务器程序的架构介绍 错误码系统的设计 日志系统的设计 如何设计断线自动重连机制 心跳包机制设计详解 业务数据处理一定要单独开线程吗 C++ 高性能服务器网络框架设计细节

January 11, 2021