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

聊聊技术人员的常见的职业问题 由于时间有限,很多读者提出的问题,不能一一解答,因此这篇文章,来系统地就各类型的读者遇到的一些常见职业问题回答一下: 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 年终奖还没发,我跳槽是不是就没有年终奖了? ...

2021-01-11

日志系统的设计

日志系统的设计 为什么需要日志 实际的软件项目产出都有一个流程,即先开发、测试,再发布生产,由于人的因素,既然是软件产品就不可能百分百没有 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, ...); //输出线程ID号和所在函数签名、行号 static bool Log(long nLevel, PCSTR pszFunctionSig, int nLineNo, PCTSTR pszFmt, ...); //注意:pszFunctionSig参数为Ansic版本 static bool Log(long nLevel, PCSTR pszFunctionSig, int nLineNo, PCSTR pszFmt, ...); private: CIULog() = delete; ~CIULog() = delete; CIULog(const CIULog& rhs) = delete; CIULog& operator=(const CIULog& rhs) = delete; static void GetTime(char* pszTime, int nTimeStrLength); private: static bool m_bToFile; //日志写入文件还是写到控制台 static HANDLE m_hLogFile; static bool m_bTruncateLongLog; //长日志是否截断 static LOG_LEVEL m_nLogLevel; //日志级别 }; #endif // !__LOG_H__ 日志的 cpp 文件 ...

2021-01-11

如何设计断线自动重连机制

如何设计断线自动重连机制 在有连接依赖关系的服务与服务之间,或客户端与服务器之间,无论是出于方便使用、降低运维成本、提高工作效率(服务与服务之间),还是优化用户体验(客户端与服务器之间)自动重连机制通常都是一个非常重要的功能。 情景一 对于一组服务之间,如果其中一些服务(主动连接方,下文以 A 代称)需要与另外一些服务(被连接方,下文以 B 代称)建立 TCP 长连接,如果 A 没有自动连接 B 的功能,那么在部署或者测试这些服务的时候,必须先启动 B,再启动 A,因为一旦先启动 A,A 此时去尝试连接 B(由于 B 还没有启动)会失败,之后 A 再也不会去连接 B了(即使随后 B 被启动了),从而导致整个系统不能正常工作。这是缺点一。 情景二 即使部署或测试的时候,先启动了 B,再启动 A,A 与 B 之间的连接在运行期间内,可能由于网络波动等原因导致 A 与 B 之间连接断开,之后整个系统也不能再正常工作了。这是缺点二。 情景三 如果我们想升级 B,更新完程序后,重启 B,也必须重启 A。如果这种依赖链比较长(例如 A 连接 B,B 连接 C,C 连接 D,D 连接 E,等等),那么更新某个程序的效率和成本会非常高。这是缺点三。 情景四 对于客户端软件来说,如果因为用户的网络短暂故障导致客户端与服务器失去连接,等网络恢复后,较好的用户体验是客户端能检测到用户网络变化后,自动与服务器重连,以便用户能及时收到最新的消息。 以上四个情景说明了断线自动重连功能的重要性,那如何去设计好的断线重连机制呢? 重连本身的功能开发很简单,其实就是调用 socket 函数 connect 函数,不断去“重试”。这里的“重试”我使用了双引号,是为了说明重试的技巧非常有讲究: 对于服务器端程序,例如 A 连接 B,如果连接不上,整个系统将无法工作,那么我们开发 A 服务时,重连的逻辑可以很简单,即 A 一旦发现与 B 断开了连接,就立即尝试与 B 重新连接,如果连接不上,隔一段时间再重试(一般设置为 3 秒或 5 秒即可),一直到连接成功为止。当然,期间可以不断发送报警邮件或者持续输出错误日志,来引起开发或者运维人员的尽快干预,以便尽早排查和解决连接不上的原因。 对于客户端软件,以上做法也是可以的,但是不是最优的。客户端所处的网络环境比服务器程序所处的网络环境一般要恶劣的多,等间隔的定时去重连,一般作用不大(例如用户拔掉了网线)。因此,对于客户端软件,一般出现断线,会尝试去重连,如果连接不上,会隔个比前一次时间更长的时间间隔去重连,例如这个时间间隔可以是 2 秒、4 秒、8 秒、16秒等等。但是,这样也存在一个问题,随着重连次数的变多,重连的时间间隔会越来越大(当然,你也可以设置一个最大重连时间间隔,之后恢复到之前较小的时间间隔)。如果网络此时已经恢复(例如用户重新插上网线),我们的程序需要等待一个很长的时间间隔(如 16 秒)才能恢复连接,这同样不利于用户体验。一般情况下,如果网络发生波动,我们的程序可以检测网络状态,如果网络状态恢复正常此时应该立即进行一次重连,而不是一成不变地按照设置的时间间隔去重连。 操作系统提供了检测网络状态变化的 API 函数,例如对于 Windows 可以使用 IsNetworkAlive() 函数去检测,对于 Android,网络变化时会发送消息类型是 WifiManager.NETWORK_STATE_CHANGED_ACTION 的广播通知。 另外,还需要注意的是,如果客户端网络断开,一般会在界面某个地方显式地告诉用户当前连接状态,并提醒当前正在进行断线重连,且应该有一个可以让用户放弃断线重连或者立即进行一次断线重连的功能。 综上所述,总结起来,对于服务器程序之间的重连可以设计成等时间间隔的定时重连,对于客户端程序要结合依次放大重连时间间隔、网络状态变化立即重连或用户主动发起重连这三个因素来设计。 不需要重连的情形 不需要重连一般有以下情形: 用户使用客户端主动放弃重连; 因为一些业务上的规定,禁止客户端重连; 举个例子,如果某个系统同一时刻同一个账户只允许登陆一个,某个账户在机器 A 上登陆,此时接着又在机器 B 上登陆,此时 A 将被服务器踢下线,那么此时 A 客户端的逻辑就应该禁止自动重连。 ...

2021-01-11

实例:一个服务器程序的架构介绍

实例:一个服务器程序的架构介绍 本文将介绍我曾经做过的一个项目的服务器架构和服务器编程的一些重要细节。 一、程序运行环境 操作系统:Centos 7.0 编译器:gcc/g++ 4.8.3、cmake 2.8.11 mysql数据库:5.5.47 项目代码管理工具:Visual Studio 2013 一、程序结构 该程序总共有 17 个线程,其中分为 9 个数据库工作线程 D 和一个日志线程 L,6 个普通工作线程 W,一个主线程 M。(以下会用这些字母来代指这些线程) (一)、数据库工作线程的用途 9 个数据库工作线程在线程启动之初,与 mysql 建立连接,也就是说每个线程都与 mysql 保持一路连接,共 9 个数据库连接。 每个数据库工作线程同时存在两个任务队列,第一个队列 A 存放需要执行数据库增删查改操作的任务 sqlTask,第二个队列 B 存放 sqlTask 执行完成后的结果。sqlTask 执行完成后立即放入结果队列中,因而结果队列中任务也是一个个的需要执行的任务。大致伪代码如下: void db_thread_func() { while (!m_bExit) { if (NULL != (pTask = m_sqlTask.Pop())) { //从m_sqlTask中取出的任务先执行完成后,pTask将携带结果数据 pTask->Execute(); //得到结果后,立刻将该任务放入结果任务队列 m_resultTask.Push(pTask); continue; } sleep(1000); }//end while-loop } 现在的问题来了: 任务队列 A 中的任务从何而来,目前只有消费者,没有生产者,那么生产者是谁? 任务队列 B 中的任务将去何方,目前只有生产者没有消费者。 这两个问题先放一会儿,等到后面我再来回答。 (二)工作线程和主线程 在介绍主线程和工作线程具体做什么时,我们介绍下服务器编程中常常抽象出来的几个概念(这里以 tcp 连接为例): TcpServer 即 Tcp 服务,服务器需要绑定ip地址和端口号,并在该端口号上侦听客户端的连接(往往由一个成员变量 TcpListener 来管理侦听细节)。所以一个 TcpServer 要做的就是这些工作。除此之外,每当有新连接到来时,TcpServer 需要接收新连接,当多个新连接存在时,TcpServer 需要有条不紊地管理这些连接:连接的建立、断开等,即产生和管理下文中说的 TcpConnection 对象。 一个连接对应一个 TcpConnection 对象,TcpConnection 对象管理着这个连接的一些信息:如连接状态、本端和对端的 ip 地址和端口号等。 数据通道对象 Channel,Channel 记录了 socket 的句柄,因而是一个连接上执行数据收发的真正执行者,Channel 对象一般作为 TcpConnection 的成员变量。 TcpSession 对象,是将 Channel 收取的数据进行解包,或者对准备好的数据进行装包,并传给 Channel 发送。 归纳起来:一个 TcpServer 依靠 TcpListener 对新连接的侦听和处理,依靠 TcpConnection 对象对连接上的数据进行管理,TcpConnection 实际依靠 Channel 对数据进行收发,依靠 TcpSession 对数据进行装包和解包。也就是说一个 TcpServer 存在一个 TcpListener,对应多个 TcpConnection,有几个TcpConnection 就有几个 TcpSession,同时也就有几个 Channel。 ...

2021-01-11

写给那些傻傻想做服务器开发的朋友

写给那些傻傻想做服务器开发的朋友 很久以前看过一篇标题为《写给那些傻傻的,想做服务器开发的应届生》文章,无意中看到知乎上也对这篇文章进行了激烈的讨论。下面谈谈我的看法。 写在前面的话 我在七八年前就看过这篇文章,那个时候我还是一名学生,它深深地影响了我学生时代以及后来的人生轨迹。(所以原文绝对不是首次发表于2015年,我猜想可能是后来的作者2015年修改了原作者的一些内容,并增加了一些自己的东西,让它"与时俱进")。我学生时代深受这篇文章的影响,以至于我印象中的服务器开发的样子和地位就是这篇文章中所描述的。 我的工作经历 我毕业的时候,一心想做出Windows C/C++客户端开发,当时为了做这个开发放弃了我熟悉的flash编程和web开发,当然薪资也是比较低的。做了几年Windows客户端后,我毅然以一定的代价转到了linux服务器开发。到今天为止,大致做过股票资讯、交易系统、游戏服务器、即时通讯系统和直播类型的服务器,架构的能力也由最初的千人到后来的百万在线。我从不后悔我当初转行服务器开发,甚至很庆幸当初的抉择,然而我可能更喜欢的还是客户端开发。 《写给那些傻傻的,想做服务器开发的应届生》一文中的有些观点,根据我的经历,我不敢赞同,或者说我的感受与之大相径庭。 加班的情况 首先说下加班的情况,不管是大公司还是小公司,由于现在的各种测试、预警机制、监控策略和公司发布流程的不断完善,一个月内经常为各种服务器bug、和应急的情况加班的现状已经大为改善不少,当然偶尔发版或者赶项目加班还是有的,不过一个月的频率也就那么一两次。如果你们团队频繁地为了修正紧急bug、解决服务器稳定性问题,那么你们真要好好考虑你们的方法是不是有问题了。 服务器开发与轮子 其次,服务器开发,不仅仅如文中所说的,利用或者组装各种轮子。一个稳定的服务器架构,必须是建立在设计师良好的基础知识和见多识广的经验基础上,即使是使用现有的轮子,也是对这个轮子足够熟悉的基础上,才能让轮子最大地适用自己的公司的业务。也就是说,服务器核心项目人员虽然不一定要造轮子,但一定要具备造轮子的能力。开源的东西好用是好用,但是要么不出问题,一旦出问题往往很难修改。我们去年做类似“冲顶大会”、“百万英雄”这类直播答题应用,由于这类游戏是从美国HQ刮过来的风,国内各大公司为了迅速抢占市场与用户,都想着要比别人早点做出来上线,所以我们公司当时deadline压得比较紧。我们那个时候,最不想看到的人就是项目经理,天天跟着我们后面催项目的进度。项目进度紧不说,另外还有一个技术挑战,由于节目比较火热,同一个房间里面可能会达到百万人同时在线,而这百万人可能同时都会发弹幕消息。假设某个时刻,房间里面有n个人,某个人发一条消息,其他n-1个人收到,服务器需要推送n-1次。如果n个人同时发消息,那么服务器同一时间就要推送n*n,如果n等于1百万的时候,那么单秒的数据量将非常恐怖,这个是我们需要解决的一个技术难题,解决目标是最少延迟的情况下,弹幕最多的送达率;另外一个难题就是,保证出题和答案不能有太多的延时(小于1秒),并在用户给出答案后,服务器能够迅速统计出答案结果并应答客户端。(没办法,所以此时主持人的作用就发挥了,万一延迟太厉害,主持人可以和观众各种唠嗑,当然这是下下策,如果频繁出现这种情况,领导的脸色肯定也不好看,我们做技术的脸上也没有光彩。)那段时间基本上是周六周日都要加班,甚至连周末都可能要到凌晨才能回去。注意:我把这段经历并没有放在上面的关于服务器开发是否频繁地加班的栏目下,这里我想说明的并不是服务器开发要经常加班,我想说的是,如果你平常只会用轮子,而不注重基础内功的修养,这种场景你是很难应对的,首先是单机服务性能要做到极致,其次是多个服务之间的高效配合。很多人可能觉得这种场景也不难,甚至有的人号称单机服务就能解决,这些都是站着说话不腰疼了。像熊猫tv的“冲顶大会”和西瓜视频的“百万英雄”前几次的答题活动中,也出现了服务中断或者题目延迟厉害,甚至“百万英雄”还出现过一次因技术问题答题活动被迫延期的事故。 技术与产品思维 接着说下,技术和产品方面的,服务器开发与客户端开发的思维方式和理念其实是不一样的,如果说客户端产品是一个产品的脸面,那么服务器端就是产品的灵魂。这里可能比喻有点不恰当,与客户端开发相比,优秀的服务器开发应该尽量在单机服务上的性能做到极致,必须尽量利用少的资源给尽可能多的客户端服务(在资源总量有限的情况下,你为单个客户端服务使用的资源越少,你才可能为越多的客户服务)。而服务器开发必须有条不紊地处理与每个客户端的交互,不能纠结或把资源花费在某一个客户端上。但是客户端不一样,客户端只需要管理好自己的一亩三分地就可以了,而且客户端的大多数逻辑和细节在界面(UI)逻辑上。但是我不赞成文中作者所说的客户端代码比服务器代码少很多,相反,我经历过的项目,都是客户度代码比服务器代码多很多。因为客户端代码往往有大量的界面逻辑,如果服务器端没有UI的话,其核心除了网路通信部分,剩下的就是各种业务逻辑(包括存储逻辑,也就是业务逻辑服务器和客户端都有,但是客户端还有界面逻辑)。而从开发团队的人数配比上来说,一般单个端(比如pc、安卓、ios中的一端)的人数要小于服务器开发人员的数量,因为一般一个高级客户端开发,往往可以一个人搞定一个客户端,但是一般很少有一个高级服务器开发可以单独搞定一套服务开发的。(说的是通常情形,请不要走极端)。服务器开发的核心字眼体现在“服务”上,如何为客户端提供稳定的、高效的服务,这是关键的地方。这里“稳定”也包括容灾容错。大凡有一定规模的用户群体的产品,如果服务器不稳定,那后果将是灾难性的,试想QQ或者微信服务器中断一两个小时,后果会怎样?而客户端更侧重的就是产品的细节、用户的体验,当然尽管有些用户体验可能是由服务器端决定的,但是最终还是由客户端反映出来。我不赞同文章中说,客户端更能积累除了技术以外的其他知识,服务器开发也一样的,不管是客户端还是服务器,只有具有产品思维的开发才是好的开发,而功能的设计与规划服务器端的开发在时间点上一般先于客户端开发的。而具体的功能点,也是需要服务器开发人员与产品人员乃至客户沟通的。 薪资方面 最后说下,薪资方面。一般大于两年且同样的工作年限的服务器开发人员要比客户端开发人员高至少三分之一左右。当然不排除一些非常优秀的客户端开发人员可能不在这个规则内。 结语 总结起来,选择了哪条路就选择了什么样的生活。做服务器开发的可以在高并发、高可用方向进一步努力,而做客户端开发可以在用户体验、设计细节方面下功夫。不管怎样,都是我们想要的生活,那里倾洒了我们的汗水,也收获了我们自己的成就感。

2021-01-11

心跳包机制设计详解

心跳包机制设计详解 存在下面两种情形: 情形一:一个客户端连接服务器以后,如果长期没有和服务器有数据来往,可能会被防火墙程序关闭连接,有时候我们并不想要被关闭连接。例如,对于一个即时通讯软件,如果服务器没有消息时,我们确实不会和服务器有任何数据交换,但是如果连接被关闭了,有新消息来时,我们再也没法收到了,这就违背了“即时通讯”的设计要求。 情形二:通常情况下,服务器与某个客户端一般不是位于同一个网络,其之间可能经过数个路由器和交换机,如果其中某个必经路由器或者交换器出现了故障,并且一段时间内没有恢复,导致这之间的链路不再畅通,而此时服务器与客户端之间也没有数据进行交换,由于 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 错误信息。 ...

2021-01-11

业务数据处理一定要单独开线程吗

业务数据处理一定要单独开线程吗 在 《one thread one loop 思想》一文我们介绍了一个 loop 的主要结构一般如下所示: while (!m_bQuitFlag) { epoll_or_select_func(); handle_io_events(); handle_other_things(); } 对于一些业务逻辑处理比较简单、不会太耗时的应用来说,handle_io_events() 方法除了收发数据也可以直接用来直接做业务的处理,即其结构如下: void handle_io_events() { //收发数据 recv_or_send_data(); //解包并处理数据 decode_packages_and_process(); } 其中 recv_or_send_data() 方法中调用 send/recv API 进行实际的网络数据收发。以收数据为例,收完数据存入接收缓冲区后,接下来进行解包处理,然后进行业务处理,例如一个登陆数据包,其业务就是验证登陆的账户密码是否正确、记录其登陆行为等等。从程序函数调用堆栈来看,这些业务处理逻辑其实是直接在网络收发数据线程中处理的。我的意思是:网络线程调用 handle_io_events() 方法,handle_io_events() 方法调用 decode_packages_and_process() 方法,decode_packages_and_process() 方法做具体的业务逻辑处理。 需要注意的是,为了让网络层与业务层脱耦,网络层中通常会提供一些回调函数的接口,这些回调函数我们将其指向具体的业务处理函数。以 libevent 网络库的用法为例: int main(int argc, char **argv) { struct event_base *base; struct evconnlistener *listener; struct event *signal_event; struct sockaddr_in sin; base = event_base_new(); memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = htons(PORT); //listener_cb是我们自定义回调函数 listener = evconnlistener_new_bind(base, listener_cb, (void *)base, LEV_OPT_REUSEABLE|LEV_OPT_CLOSE_ON_FREE, -1, (struct sockaddr*)&sin, sizeof(sin)); if (!listener) { fprintf(stderr, "Could not create a listener!\n"); return 1; } //signal_cb是我们自定义回调函数 signal_event = evsignal_new(base, SIGINT, signal_cb, (void *)base); if (!signal_event || event_add(signal_event, NULL)<0) { fprintf(stderr, "Could not create/add a signal event!\n"); return 1; } //启动loop event_base_dispatch(base); evconnlistener_free(listener); event_free(signal_event); event_base_free(base); printf("done\n"); return 0; } 上述代码根据 libevent 自带的 helloworld 示例修改而来,其中 listener_cb 和 signal_cb 是自定义的回调函数,有相应的事件触发后,libevent 的事件循环会调用我们设置的回调,在这些回调函数中,我们可以编写自己的业务逻辑代码。 ...

2021-01-11

职业规划

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

2021-01-11

主线程与工作线程的分工

主线程与工作线程的分工 服务器端为了能流畅处理多个客户端链接,一般在某个线程A里面accept新的客户端连接并生成新连接的socket fd,然后将这些新连接的socketfd给另外开的数个工作线程B1、B2、B3、B4,这些工作线程处理这些新连接上的网络IO事件(即收发数据),同时,还处理系统中的另外一些事务。这里我们将线程A称为主线程,B1、B2、B3、B4等称为工作线程。工作线程的代码框架一般如下: while (!m_bQuit) { epoll_or_select_func(); handle_io_events(); handle_other_things(); } 在epoll_or_select_func()中通过select()或者poll/epoll()去检测socket fd上的io事件,若存在这些事件则下一步handle_io_events()来处理这些事件(收发数据),做完之后可能还要做一些系统其他的任务,即调用handle_other_things()。 这样做有三个好处: 线程A只需要处理新连接的到来即可,不用处理网络IO事件。由于网络IO事件处理一般相对比较慢,如果在线程A里面既处理新连接又处理网络IO,则可能由于线程忙于处理IO事件,而无法及时处理客户端的新连接,这是很不好的。 线程A接收的新连接,可以根据一定的负载均衡原则将新的socket fd分配给工作线程。常用的算法,比如round robin,即轮询机制,即,假设不考虑中途有连接断开的情况,一个新连接来了分配给B1,又来一个分配给B2,再来一个分配给B3,再来一个分配给B4。如此反复,也就是说线程A记录了各个工作线程上的socket fd数量,这样可以最大化地来平衡资源,避免一些工作线程“忙死”,另外一些工作线程“闲死”的现象。 即使工作线程不满载的情况下,也可以让工作线程做其他的事情。比如现在有四个工作线程,但只有三个连接。那么线程B4就可以在handle_other_thing()做一些其他事情。 下面讨论一个很重要的效率问题: 在上述while循环里面,epoll_or_selec_func()中的epoll_wait/poll/select等函数一般设置了一个超时时间。如果设置超时时间为0,那么在没有任何网络IO时间和其他任务处理的情况下,这些工作线程实际上会空转,白白地浪费cpu时间片。如果设置的超时时间大于0,在没有网络IO时间的情况,epoll_wait/poll/select仍然要挂起指定时间才能返回,导致handle_other_thing()不能及时执行,影响其他任务不能及时处理,也就是说其他任务一旦产生,其处理起来具有一定的延时性。这样也不好。那如何解决该问题呢? 其实我们想达到的效果是,如果没有网络IO时间和其他任务要处理,那么这些工作线程最好直接挂起而不是空转;如果有其他任务要处理,这些工作线程要立刻能处理这些任务而不是在epoll_wait/poll/selec挂起指定时间后才开始处理这些任务。 我们采取如下方法来解决该问题,以linux为例,不管epoll_fd上有没有文件描述符fd,我们都给它绑定一个默认的fd,这个fd被称为唤醒fd。当我们需要处理其他任务的时候,向这个唤醒fd上随便写入1个字节的,这样这个fd立即就变成可读的了,epoll_wait()/poll()/select()函数立即被唤醒,并返回,接下来马上就能执行handle_other_thing(),其他任务得到处理。反之,没有其他任务也没有网络IO事件时,epoll_or_select_func()就挂在那里什么也不做。 这个唤醒fd,在linux平台上可以通过以下几种方法实现: 管道pipe,创建一个管道,将管道绑定到epoll_fd上。需要时,向管道一端写入一个字节,工作线程立即被唤醒。 linux 2.6新增的eventfd: int eventfd(unsigned int initval, int flags); 步骤也是一样,将生成的eventfd绑定到epoll_fd上。需要时,向这个eventfd上写入一个字节,工作线程立即被唤醒。 第三种方法最方便。即linux特有的socketpair,socketpair是一对相互连接的socket,相当于服务器端和客户端的两个端点,每一端都可以读写数据。 int socketpair(int domain, int type, int protocol, int sv[2]); 调用这个函数返回的两个socket句柄就是sv[0],和sv[1],在一个其中任何一个写入字节,在另外一个收取字节。 将收取的字节的socket绑定到epoll_fd上。需要时,向另外一个写入的socket上写入一个字节,工作线程立即被唤醒。如果是使用socketpair,那么domain参数一定要设置成AFX_UNIX。 由于在windows,select函数只支持检测socket这一种fd,所以Windows上一般只能用方法3的原理。而且需要手动创建两个socket,然后一个连接另外一个,将读取的那一段绑定到select的fd上去。这在写跨两个平台代码时,需要注意的地方。

2021-01-11

自我提升与开源代码

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

2021-01-11