beego restful path variable

beego的url路径有多个变量的设置和获取方法。 在 router.go 注册 url beego.Router("/type/:type/?:page", &controllers.WebController{}, "get:Type") 获取变量 typeID, err := self.GetInt(":type") func (c *Controller) GetInt(key string, def ...int) (int, error) { strv := c.Ctx.Input.Query(key) if len(strv) == 0 && len(def) > 0 { return def[0], nil } return strconv.Atoi(strv) }

2021-01-22

2020 年好好读一读开源代码吧

2020 年好好读一读开源代码吧 2019 年就这么结束了,2020 年也来临了,虽然我曾对过去 2019 年做了一份总结,但是认真的来说,其实我对自己的 2019 年的收获并不太满意,一个主要的原因是计划好好研读的几个开源项目的源码都没有去做。好在,2020 新的一年,不再像 2019 年创业一般忙碌,终于可以静下心来认真去把这些未完成的计划好好做完。 其实,我一直想找个机会和我的读者,好好讨论一下阅读开源项目源码这个话题的,我这里观点无任何含糊或者模棱两可,我旗帜鲜明的亮出我的观点——想在技术上有所造诣或者想成为某一技术领域的专家的同学一定要认认真真的研读几个开源软件的源码。下面我会具体来展开说下这个问题。 知识付费与阅读源码 大家都知道,时下"知识付费"这个词非常火热,各大平台各个领域都推出了许多基于知识付费的课程,有图文版、语音版和视频版(包括在线实时教育直播)。当然,知识付费是一个好东西。众所周知,互联网信息的特点是信息量大、有用信息少、信息质量良莠不齐,各大平台推出的各种付费课程,精心制作,用心分类和梳理,读者只要花费一定的费用,就能省去大量搜索、查找和遴选信息的时间,直接专注于获得相关知识本身。 在各类知识付费课程中,有一类课程是介绍业界或者大家平常工作中用到的一些开源软件的原理的,进一步说,有的是分析这类软件的源码的,如 nginx、netty、Spring Boot。 我个人觉得,虽然你可以购买一些这样那样的开源软件的教程或者图书(包括电子书)去学习,但一定不要以这些学习材料为主要的学习这些开源软件的方法和途径,有机会的话,或者说如果你要学习的开源软件所使用的开发语言正好是你熟悉或者使用的编程语言,那么你应该尽量多去以阅读这些开源项目的源码本身为主。举个例子,如果你是 C/C++ 后端开发者,那么像 redis、nginx(它们都是使用 C 编写的)这样的开源项目的源码你应该认真的去研读一下;如果你是做 Windows C/C++ 客户端或者一名 QT 客户端开发人员,那么像 MFC、DUILIB、金山卫士等源码,你可以拿来读一读;如果你是 Java 程序员,netty、Spring 等源码是你进阶路上必须迈过去的一关。 为什么建议以阅读相关源码为主,而不是其他相关教程呢? 首先,任何其他相关教程介绍的内容都是基于这个软件的源码实现创作出来的,虽然能帮助你快速理解一些东西,但是不同的教程作者在阅读同样一份代码时的注意点和侧重点不一样,加上如果作者在某些地方有理解偏差的,这种偏差会被引入你所学习的教程或者图书里面,也就是说,你学习的这些东西其实不是第一手的,而是经过别人加工或者理解意译过的,在这个过程中如果别人理解有偏差,那么你或多或少的会受一点影响。所以,为了"不受制于人”,亲自去阅读一些源码时非常有必要的。 其次,如果你按照别人的教程大纲,那么你的学习该软件的开源项目时,可能会受限于别人的视野和侧重点,通俗的说,假设一个开源项目其可以学习和借鉴的内容有 A、B、C、D、E 五个大的点,别人的教程可能只写了 A、B、C、D 四个点,如果你只局限于别人的教程,你就错过 E 这个点了。 这里可以给读者讲一个具体的例子。我最初开始走上工作岗位时做的是 C/C++ 客户端开发,我无意中找到了一份完整的电驴源码,但是开始阅读这份代码比较吃力,于是我就在网上找相关的电驴源码分析教程来看。但是呢,网上的这方面的教程都是关于电驴的网络通信模块和通信协议介绍的,很多做客户端的读者是知道的,做客户端开发很大一部分工作是在开发 UI 界面方面的逻辑和布局,其实电驴源码中关于界面设计逻辑写的也是很精彩的,也非常值得当时的我去借鉴和学习。如果我只按照网上的教程去学习,那么就错过这方面的学习了。也就是同样一份电驴源码,不同的学习者汲取的其源码中的营养成分是不一样的。需要电驴源码的同学可以在公众号后台回复关键字【电驴源码】获取下载链接。 如何去阅读源码呢? 这应该是很多读者想知道的问题,先讨论几种老生常谈的阅读源码的方式。 第一种方式就是所谓的精读和粗读。很多读者应该听说过这种所谓的阅读源代码的方式,有些人认为有些源码只需要搞清楚其主要结构和流程就可以了,而另外一些源码需要逐行认真去研读其某个或者某几个模块的源码。或者,只阅读自己感兴趣或者需要的模块。 第二种方式,说的是先熟悉代码的整体结构,再去依次搞清楚各个模块的代码细节。 第三种方式是所谓的调试法,通过开源项目的一个或几个典型的流程,去调试跟踪信息流,然后逐步搞清楚整个项目的结构。 以上三种方式都是不错的阅读源码的方式,读者可以根据自己的水平、目的和阶段去使用。但是,我这里想说的并不是这些东西。 我个人觉得,一个技术人员如果想通过源码去提高自己,应该以一种"闲登小阁看新晴"的心境去阅读源码,这也许是在某个节假日的清晨,某个下过雨的午后,某个夜黑人静的深夜。看源码尤其是看高质量源码本来就是一种享受,像品茗。闲暇时间去细细品味一些开源软件的源码,和锻炼身体一样,都是人生中重要不紧急的事情,这类事情做的越多,坚持的越久,越能提高你的人生厚度。虽然阅读源码的最终目的是功利性的,但是阅读源码的心态不建议是功利性的,喜欢做一件事本身的过程,比把这件事做好的目标更快乐。 我从学生时代开始,就喜欢看一些开源软件的源码,当然,从现在的标准来看,看的很多源码都不是"高质量"的,择其善者而从之其不善者而改之,不是吗?有些源码可以学习其架构、结构设计,有些源码则可以学习其细节设计(如变量命名、编码风格等)。 看过的这些源码对我的技术视野影响很大。我上大学的时候,迷恋 Flash 编程,当时非常崇拜 Flash 界的两位前辈——鼠标炸弹(https://mousebomb.org/)和寂寞火山(现在已成币圈有名的大佬),另外还有淘沙网的沙子。多年后再看他们的代码可能质量没有那么高,但是我从他们开源出来的代码中学到了很多东西。举个例子,我喜欢在一些成对结束的花括号后面加上明显的成对结束的注释就是从沙子的代码那里学来的。虽然,现在的 IDE 会清楚的标示出来各个花括号的范围,但是这种注释风格在某些时候大大方便了代码阅读和 review。 //实例 class A { public: void someFunc() { for (int i = 0; i < n; ++i) { for (int j = 0; j < m; ++j) { //some codes... }// end inner-for-loop }// end outer-for-loop }// end method someFunc }; // end class A 给大家阅读源码的一些建议 很多人阅读源码存在以下不当的习惯或者认知方式: ...

2021-01-11

C++ 高性能服务器网络框架设计细节

C++ 高性能服务器网络框架设计细节 这篇文章我们将介绍服务器的开发,并从多个方面探究如何开发一款高性能高并发的服务器程序。需要注意的是一般大型服务器,其复杂程度在于其业务,而不是在于其代码工程的基本框架。大型服务器一般有多个服务组成,可能会支持CDN,或者支持所谓的“分布式”等,这篇文章不会介绍这些东西,因为不管结构多么复杂的服务器,都是由单个服务器组成的。所以这篇文章的侧重点是讨论单个服务程序的结构,而且这里的结构指的也是单个服务器的网络通信层结构,如果你能真正地理解了我所说的,那么在这个基础的结构上面开展任何业务都是可以的,也可以将这种结构扩展成复杂的多个服务器组,例如“分布式”服务。文中的代码示例虽然是以C++为例,但同样适合Java(我本人也是Java开发者),原理都是一样的,只不过Java可能在基本的操作系统网络通信API的基础上用虚拟机包裹了一层接口而已(Java甚至可能基于一些常用的网络通信框架思想提供了一些现成的API,例如NIO)。有鉴于此,这篇文章不讨论那些大而空、泛泛而谈的技术术语,而是讲的是实实在在的能指导读者在实际工作中实践的编码方案或优化已有编码的方法。另外这里讨论的技术同时涉及windows和linux两个平台。 所谓高性能就是服务器能流畅地处理各个客户端的连接并尽量低延迟地应答客户端的请求;所谓高并发,不仅指的是服务器可以同时支持多的客户端连接,而且这些客户端在连接期间内会不断与服务器有数据来往。网络上经常有各种网络库号称单个服务能同时支持百万甚至千万的并发,然后我实际去看了下,结果发现只是能同时支持很多的连接而已。如果一个服务器能单纯地接受n个连接(n可能很大),但是不能有条不紊地处理与这些连接之间的数据来往也没有任何意义,这种服务器框架只是“玩具型”的,对实际生产和应用没有任何意义。 这篇文章将从两个方面来介绍,一个是服务器中的基础的网络通信部件;另外一个是,如何利用这些基础通信部件整合成一个完整的高效的服务器框架。注意:本文以下内容中的客户端是相对概念,指的是连接到当前讨论的服务程序的终端,所以这里的客户端既可能是我们传统意义上的客户端程序,也可能是连接该服务的其他服务器程序。 一、网络通信部件 按上面介绍的思路,我们先从服务程序的网络通信部件开始介绍。 (一)、需要解决的问题 既然是服务器程序肯定会涉及到网络通信部分,那么服务器程序的网络通信模块要解决哪些问题?目前,网络上有很多网络通信框架,如libevent、boost asio、ACE,但都网络通信的常见的技术手段都大同小异,至少要解决以下问题: 如何检测有新客户端连接? 如何接受客户端连接? 如何检测客户端是否有数据发来? 如何收取客户端发来的数据? 如何检测连接异常?发现连接异常之后,如何处理? 如何给客户端发送数据? 如何在给客户端发完数据后关闭连接? 稍微有点网络基础的人,都能回答上面说的其中几个问题,比如接收客户端连接用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会表明当前已经没有数据了。示例: bool CIUSocket::Recv() { int nRet = 0; while(true) { char buff[512]; nRet = ::recv(m_hSocket, buff, 512, 0); if(nRet == SOCKET_ERROR) { if (::WSAGetLastError() == WSAEWOULDBLOCK) break; else return false; } else if(nRet < 1) return false; m_strRecvBuf.append(buff, nRet); ::Sleep(1); } return true; } 如何检测连接异常?发现连接异常之后,如何处理? 同样当我们收到异常事件后例如EPOLLERR或关闭事件FD_CLOSE,我们就知道了有异常产生,我们对异常的处理一般就是关闭对应的socket。另外,如果send/recv或者read/write函数对一个socket进行操作时,如果返回0,那说明对端已经关闭了socket,此时这路连接也没必要存在了,我们也可以关闭对应的socket。 如何给客户端发送数据? 这也是一道常见的网络通信面试题,某一年的腾讯后台开发职位就问到过这样的问题。给客户端发送数据,比收数据要稍微麻烦一点,也是需要讲点技巧的。首先我们不能像注册检测数据可读事件一样一开始就注册检测数据可写事件,因为如果检测可写的话,一般情况下只要对端正常收取数据,我们的socket就都是可写的,如果我们设置监听可写事件,会导致频繁地触发可写事件,但是我们此时并不一定有数据需要发送。所以正确的做法是:如果有数据要发送,则先尝试着去发送,如果发送不了或者只发送出去部分,剩下的我们需要将其缓存起来,然后再设置检测该socket上可写事件,下次可写事件产生时,再继续发送,如果还是不能完全发出去,则继续设置侦听可写事件,如此往复,一直到所有数据都发出去为止。一旦所有数据都发出去以后,我们要移除侦听可写事件,避免无用的可写事件通知。不知道你注意到没有,如果某次只发出去部分数据,剩下的数据应该暂且存起来,这个时候我们就需要一个缓冲区来存放这部分数据,这个缓冲区我们称为“发送缓冲区”。发送缓冲区不仅存放本次没有发完的数据,还用来存放在发送过程中,上层又传来的新的需要发送的数据。为了保证顺序,新的数据应该追加在当前剩下的数据的后面,发送的时候从发送缓冲区的头部开始发送。也就是说先来的先发送,后来的后发送。 如何在给客户端发完数据后关闭连接? 这个问题比较难处理,因为这里的“发送完”不一定是真正的发送完,我们调用send或者write函数即使成功,也只是向操作系统的协议栈里面成功写入数据,至于能否被发出去、何时被发出去很难判断,发出去对方是否收到就更难判断了。所以,我们目前只能简单地认为send或者write返回我们发出数据的字节数大小,我们就认为“发完数据”了。然后调用close等socket API关闭连接。当然,你也可以调用shutdown函数来实现所谓的“半关闭”。关于关闭连接的话题,我们再单独开一个小的标题来专门讨论一下。 (四)被动关闭连接和主动关闭连接 在实际的应用中,被动关闭连接是由于我们检测到了连接的异常事件,比如EPOLLERR,或者对端关闭连接,send或recv返回0,这个时候这路连接已经没有存在必要的意义了,我们被迫关闭连接。 ...

2021-01-11

CppGuide

Part IC++ 17 结构化绑定pimpl 惯用法C++必知必会的知识点不定参数函数实现var_arg系列的宏你一定要搞明白的C函数调用方式与栈原理利用 cmake 工具生成 Visual Studio 工程文件如何使用 Visual Studio 管理和阅读开源项目代码如何成为一名合格的 C/C++ 开发者?深入理解C/C++中的指针用Visual Studio调试Linux程序详解 C++ 11 中的智能指针Memcached源码阅读序 服务器资源调整Memcached源码阅读一 初始化参数解析Memcached源码阅读二 网络监听的建立Memcached源码分析三 网络连接建立Memcached源码阅读四 内存初始化Memcached源码阅读五 资源初始化Memcached源码阅读六 get过程Memcached源码阅读七 cas属性Memcached源码阅读八 内存池Memcached源码阅读九 连接队列Memcached源码阅读十 Hash表操作Memcached源码阅读十一 LRU操作Memcached源码阅读十二 set操作Memcached源码阅读十三 do_item_alloc操作Memcached源码阅读十四 item结构Memcached阅读十五 Hash表扩容Memcached源码阅读十六 线程交互Memcached源码阅读十七 状态机Memcached源码分析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客户端源码分析TeamTalk源码解析Leveldb源码分析16leveldb源码分析leveldb源码分析1leveldb源码分析10leveldb源码分析11leveldb源码分析12leveldb源码分析13leveldb源码分析14leveldb源码分析15leveldb源码分析17leveldb源码分析18leveldb源码分析19leveldb源码分析2leveldb源码分析20leveldb源码分析21leveldb源码分析22leveldb源码分析3leveldb源码分析4leveldb源码分析5leveldb源码分析6leveldb源码分析7leveldb源码分析8leveldb源码分析9libevent源码深度剖析libevent源码深度剖析一libevent源码深度剖析02libevent源码深度剖析03libevent源码深度剖析04libevent源码深度剖析05libevent源码深度剖析06libevent源码深度剖析07libevent源码深度剖析08libevent源码深度剖析09libevent源码深度剖析10libevent源码深度剖析11libevent源码深度剖析12libevent源码深度剖析13作者的故事我是如何年薪五十万的我的 2019后端开发相关的书籍后台开发应该读的书多线程后台C++开发你一定要知道的条件变量整型变量赋值是原子操作吗?从零学习开源项目系列(一) 从一款多人联机实时对战游戏开始从零学习开源项目系列(二) 最后一战概况从零学习开源项目系列(三) CSBattleMgr服务源码研究从零学习开源项目系列(四)LogServer源码探究服务器开发案例实战从零实现一个http服务器从零实现一个邮件收发客户端从零实现一款12306刷票软件从零开发一个WebSocket服务器10 十万在线的WebGame的数据库设计思路11 一种高性能网络游戏服务器架构设计12 经典游戏服务器端架构概述13 游戏跨服架构进化之路1 游戏服务器开发的基本体系与服务器端开发的一些建议2 网络游戏服务器开发框架设计介绍3 游戏后端开发需要掌握的知识4 关于游戏服务端架构的整理5 各类游戏对应的服务端架构6 从腾讯QQgame高性能服务器集群架构看“分而治之”与“自治”等分布式架构设计原则7 QQ游戏百万人同时在线服务器架构实现8 大型多人在线游戏服务器架构设计9 百万用户级游戏服务器架构设计游戏开发专题Linux tcpdump 使用介绍Linux 网络故障排查的瑞士军刀程序员必知必会的网络命令从抓包的角度分析connect()函数的连接过程做 Java 或者 C++ 开发都应该知道的 lsof 命令利用 telnet 命令发电子邮件服务器开发中网络数据分析与故障排查经验漫谈程序员的烦心事我是一名程序员,结婚时女友要求我用两年的工资作为彩礼,我该不该答应?拒绝了一家公司的offer后,他们的副总和hr总监同时打电话来询问拒绝原因并极力要求加入,我该不该去?为什么你的简历没人看程序员如何写简历程序员的薪资与年终奖那些事儿技术面试与HR谈薪资技巧聊一聊程序员如何增加收入谈一谈年终奖Linux C/C++后端开发面试问哪些问题程序员面试题精讲我面试后端开发经理的经历网络通信面试题集锦聊聊如何拿大厂的 offer腾讯后台开发实习生技能要求Linux epoll 模型(含LT 模式和 ET 模式详解)网络编程TCP 协议如何解决粘包、半包问题bind 函数重难点解析connect 函数在阻塞和非阻塞模式下的行为select 函数重难点解析socket 的阻塞模式和非阻塞模式服务器开发通信协议设计介绍服务器端发数据时,如果对端一直不收,怎么办?网络通信中收发数据的正确姿势非阻塞模式下 send 和 recv 函数的返回值职业规划写给那些傻傻想做服务器开发的朋友给工作 4 年迷茫的程序员们的一点建议聊聊技术人员的常见的职业问题2020 年好好读一读开源代码吧自我提升与开源代码C++ 高性能服务器网络框架设计细节高性能服务器框架设计Reactor模式业务数据处理一定要单独开线程吗主线程与工作线程的分工如何设计断线自动重连机制实例:一个服务器程序的架构介绍心跳包机制设计详解日志系统的设计错误码系统的设计高性能服务器架构设计总结

2021-01-11

Reactor模式

Reactor模式 最近一直在看游双的《高性能Linux服务器编程》一书,下载链接: http://download.csdn.net/detail/analogous_love/9673008 书上是这么介绍Reactor模式的: 按照这个思路,我写个简单的练习: /** *@desc: 用reactor模式练习服务器程序,main.cpp *@author: zhangyl *@date: 2016.11.23 */ #include <iostream> #include <string.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> //for htonl() and htons() #include <unistd.h> #include <fcntl.h> #include <sys/epoll.h> #include <signal.h> //for signal() #include <pthread.h> #include <semaphore.h> #include <list> #include <errno.h> #include <time.h> #include <sstream> #include <iomanip> //for std::setw()/setfill() #include <stdlib.h> #define WORKER_THREAD_NUM 5 #define min(a, b) ((a <= b) ? (a) : (b)) int g_epollfd = 0; bool g_bStop = false; int g_listenfd = 0; pthread_t g_acceptthreadid = 0; pthread_t g_threadid[WORKER_THREAD_NUM] = { 0 }; pthread_cond_t g_acceptcond; pthread_mutex_t g_acceptmutex; pthread_cond_t g_cond /*= PTHREAD_COND_INITIALIZER*/; pthread_mutex_t g_mutex /*= PTHREAD_MUTEX_INITIALIZER*/; pthread_mutex_t g_clientmutex; std::list<int> g_listClients; void prog_exit(int signo) { ::signal(SIGINT, SIG_IGN); //::signal(SIGKILL, SIG_IGN);//该信号不能被阻塞、处理或者忽略 ::signal(SIGTERM, SIG_IGN); std::cout << "program recv signal " << signo << " to exit." << std::endl; g_bStop = true; ::epoll_ctl(g_epollfd, EPOLL_CTL_DEL, g_listenfd, NULL); //TODO: 是否需要先调用shutdown()一下? ::shutdown(g_listenfd, SHUT_RDWR); ::close(g_listenfd); ::close(g_epollfd); ::pthread_cond_destroy(&g_acceptcond); ::pthread_mutex_destroy(&g_acceptmutex); ::pthread_cond_destroy(&g_cond); ::pthread_mutex_destroy(&g_mutex); ::pthread_mutex_destroy(&g_clientmutex); } bool create_server_listener(const char* ip, short port) { g_listenfd = ::socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0); if (g_listenfd == -1) return false; int on = 1; ::setsockopt(g_listenfd, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on)); ::setsockopt(g_listenfd, SOL_SOCKET, SO_REUSEPORT, (char *)&on, sizeof(on)); struct sockaddr_in servaddr; memset(&servaddr, 0, sizeof(servaddr)); servaddr.sin_family = AF_INET; servaddr.sin_addr.s_addr = inet_addr(ip); servaddr.sin_port = htons(port); if (::bind(g_listenfd, (sockaddr *)&servaddr, sizeof(servaddr)) == -1) return false; if (::listen(g_listenfd, 50) == -1) return false; g_epollfd = ::epoll_create(1); if (g_epollfd == -1) return false; struct epoll_event e; memset(&e, 0, sizeof(e)); e.events = EPOLLIN | EPOLLRDHUP; e.data.fd = g_listenfd; if (::epoll_ctl(g_epollfd, EPOLL_CTL_ADD, g_listenfd, &e) == -1) return false; return true; } void release_client(int clientfd) { if (::epoll_ctl(g_epollfd, EPOLL_CTL_DEL, clientfd, NULL) == -1) std::cout << "release client socket failed as call epoll_ctl failed" << std::endl; ::close(clientfd); } void* accept_thread_func(void* arg) { while (!g_bStop) { ::pthread_mutex_lock(&g_acceptmutex); ::pthread_cond_wait(&g_acceptcond, &g_acceptmutex); //::pthread_mutex_lock(&g_acceptmutex); //std::cout << "run loop in accept_thread_func" << std::endl; struct sockaddr_in clientaddr; socklen_t addrlen; int newfd = ::accept(g_listenfd, (struct sockaddr *)&clientaddr, &addrlen); ::pthread_mutex_unlock(&g_acceptmutex); if (newfd == -1) continue; std::cout << "new client connected: " << ::inet_ntoa(clientaddr.sin_addr) << ":" << ::ntohs(clientaddr.sin_port) << std::endl; //将新socket设置为non-blocking int oldflag = ::fcntl(newfd, F_GETFL, 0); int newflag = oldflag | O_NONBLOCK; if (::fcntl(newfd, F_SETFL, newflag) == -1) { std::cout << "fcntl error, oldflag =" << oldflag << ", newflag = " << newflag << std::endl; continue; } struct epoll_event e; memset(&e, 0, sizeof(e)); e.events = EPOLLIN | EPOLLRDHUP | EPOLLET; e.data.fd = newfd; if (::epoll_ctl(g_epollfd, EPOLL_CTL_ADD, newfd, &e) == -1) { std::cout << "epoll_ctl error, fd =" << newfd << std::endl; } } return NULL; } void* worker_thread_func(void* arg) { while (!g_bStop) { int clientfd; ::pthread_mutex_lock(&g_clientmutex); while (g_listClients.empty()) ::pthread_cond_wait(&g_cond, &g_clientmutex); clientfd = g_listClients.front(); g_listClients.pop_front(); pthread_mutex_unlock(&g_clientmutex); //gdb调试时不能实时刷新标准输出,用这个函数刷新标准输出,使信息在屏幕上实时显示出来 std::cout << std::endl; std::string strclientmsg; char buff[256]; bool bError = false; while (true) { memset(buff, 0, sizeof(buff)); int nRecv = ::recv(clientfd, buff, 256, 0); if (nRecv == -1) { if (errno == EWOULDBLOCK) break; else { std::cout << "recv error, client disconnected, fd = " << clientfd << std::endl; release_client(clientfd); bError = true; break; } } //对端关闭了socket,这端也关闭。 else if (nRecv == 0) { std::cout << "peer closed, client disconnected, fd = " << clientfd << std::endl; release_client(clientfd); bError = true; break; } strclientmsg += buff; } //出错了,就不要再继续往下执行了 if (bError) continue; std::cout << "client msg: " << strclientmsg; //将消息加上时间标签后发回 time_t now = time(NULL); struct tm* nowstr = localtime(&now); std::ostringstream ostimestr; ostimestr << "[" << nowstr->tm_year + 1900 << "-" << std::setw(2) << std::setfill('0') << nowstr->tm_mon + 1 << "-" << std::setw(2) << std::setfill('0') << nowstr->tm_mday << " " << std::setw(2) << std::setfill('0') << nowstr->tm_hour << ":" << std::setw(2) << std::setfill('0') << nowstr->tm_min << ":" << std::setw(2) << std::setfill('0') << nowstr->tm_sec << "]server reply: "; strclientmsg.insert(0, ostimestr.str()); while (true) { int nSent = ::send(clientfd, strclientmsg.c_str(), strclientmsg.length(), 0); if (nSent == -1) { if (errno == EWOULDBLOCK) { ::sleep(10); continue; } else { std::cout << "send error, fd = " << clientfd << std::endl; release_client(clientfd); break; } } std::cout << "send: " << strclientmsg; strclientmsg.erase(0, nSent); if (strclientmsg.empty()) break; } } return NULL; } void daemon_run() { int pid; signal(SIGCHLD, SIG_IGN); //1)在父进程中,fork返回新创建子进程的进程ID; //2)在子进程中,fork返回0; //3)如果出现错误,fork返回一个负值; pid = fork(); if (pid < 0) { std:: cout << "fork error" << std::endl; exit(-1); } //父进程退出,子进程独立运行 else if (pid > 0) { exit(0); } //之前parent和child运行在同一个session里,parent是会话(session)的领头进程, //parent进程作为会话的领头进程,如果exit结束执行的话,那么子进程会成为孤儿进程,并被init收养。 //执行setsid()之后,child将重新获得一个新的会话(session)id。 //这时parent退出之后,将不会影响到child了。 setsid(); int fd; fd = open("/dev/null", O_RDWR, 0); if (fd != -1) { dup2(fd, STDIN_FILENO); dup2(fd, STDOUT_FILENO); dup2(fd, STDERR_FILENO); } if (fd > 2) close(fd); } int main(int argc, char* argv[]) { short port = 0; int ch; bool bdaemon = false; while ((ch = getopt(argc, argv, "p:d")) != -1) { switch (ch) { case 'd': bdaemon = true; break; case 'p': port = atol(optarg); break; } } if (bdaemon) daemon_run(); if (port == 0) port = 12345; if (!create_server_listener("0.0.0.0", port)) { std::cout << "Unable to create listen server: ip=0.0.0.0, port=" << port << "." << std::endl; return -1; } //设置信号处理 signal(SIGCHLD, SIG_DFL); signal(SIGPIPE, SIG_IGN); signal(SIGINT, prog_exit); //signal(SIGKILL, prog_exit);//该信号不能被阻塞、处理或者忽略 signal(SIGTERM, prog_exit); ::pthread_cond_init(&g_acceptcond, NULL); ::pthread_mutex_init(&g_acceptmutex, NULL); ::pthread_cond_init(&g_cond, NULL); ::pthread_mutex_init(&g_mutex, NULL); ::pthread_mutex_init(&g_clientmutex, NULL); ::pthread_create(&g_acceptthreadid, NULL, accept_thread_func, NULL); //启动工作线程 for (int i = 0; i < WORKER_THREAD_NUM; ++i) { ::pthread_create(&g_threadid[i], NULL, worker_thread_func, NULL); } while (!g_bStop) { struct epoll_event ev[1024]; int n = ::epoll_wait(g_epollfd, ev, 1024, 10); if (n == 0) continue; else if (n < 0) { std::cout << "epoll_wait error" << std::endl; continue; } int m = min(n, 1024); for (int i = 0; i < m; ++i) { //通知接收连接线程接收新连接 if (ev[i].data.fd == g_listenfd) pthread_cond_signal(&g_acceptcond); //通知普通工作线程接收数据 else { pthread_mutex_lock(&g_clientmutex); g_listClients.push_back(ev[i].data.fd); pthread_mutex_unlock(&g_clientmutex); pthread_cond_signal(&g_cond); //std::cout << "signal" << std::endl; } } } return 0; } 程序的功能一个简单的echo服务:客户端连接上服务器之后,给服务器发送信息,服务器加上时间戳等信息后返回给客户端。 ...

2021-01-11

错误码系统的设计

错误码系统的设计 本文介绍服务器开发中一组服务中错误码系统的设计理念与实践,如果读者从来没想过或者没接触过这种设计理念,建议认真体会一下这种设计思路的优点。 错误码的作用 读者如果有使用过中国电信的宽带账号上网的经历,如果我们登陆不成功,一般服务器会返回一个错误码,如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。 这类错误有可能是企业内部的设计缺陷,直接暴露给客户,除了让客户对企业的技术实力产生质疑以外,没有任何其他正面效应。 而之所以带上错误码,是为了方便内部排查和定位问题。当然,现在的企业服务,内部也有大量监控系统,可能也不会再暴露这样的错误码了。 扩展 上文介绍了利用错误码的分段来定位问题的技术思想,其实不仅仅是错误码可以分段,我们在开发一组服务时,业务类型也可以通过编号来分段,这样通过业务号就能知道归属哪个服务了。 如果读者以前没接触过这种设计思想,希望可以好好的思考和体会一下。

2021-01-11

非阻塞模式下 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 通常情况下,如果 send 或者 recv 函数返回 0,我们就认为对端关闭了连接,我们这端也关闭连接即可,这是实际开发时最常见的处理逻辑。 但是,现在还有一种情形就是,假设调用 send 函数传递的数据长度就是 0 呢?send 函数会是什么行为?对端会 recv 到一个 0 字节的数据吗?需要强调的是,在实际开发中,你不应该让你的程序有任何机会去 send 0 字节的数据,这是一种不好的做法。 这里仅仅用于实验性讨论,我们来通过一个例子,来看下 send 一个长度为 0 的数据,send 函数的返回值是什么?对端会 recv 到 0 字节的数据吗? server 端代码: ...

2021-01-11

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

高性能服务器架构设计总结 系列目录 第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.append(buff, nRet); 21 22 ::Sleep(1); 23 } 24 25 return true; 26} 如何检测连接异常?发现连接异常之后,如何处理? 同样当我们收到异常事件后例如EPOLLERR或关闭事件FD_CLOSE,我们就知道了有异常产生,我们对异常的处理一般就是关闭对应的socket。另外,如果send/recv或者read/write函数对一个socket进行操作时,如果返回0,那说明对端已经关闭了socket,此时这路连接也没必要存在了,我们也可以关闭对应的socket。 如何给客户端发送数据? 给客户端发送数据,比收数据要稍微麻烦一点,也是需要讲点技巧的。首先我们不能像检测数据可读一样检测数据可写,因为如果检测可写的话,一般情况下只要对端正常收取数据,我们的socket就都是可写的,如果我们设置监听可写事件,会导致频繁地触发可写事件,但是我们此时并不一定有数据需要发送。所以正确的做法是:如果有数据要发送,则先尝试着去发送,如果发送不了或者只发送出去部分,剩下的我们需要将其缓存起来,然后设置检测该socket上可写事件,下次可写事件产生时,再继续发送,如果还是不能完全发出去,则继续设置侦听可写事件,如此往复,一直到所有数据都发出去为止。一旦所有数据都发出去以后,我们要移除侦听可写事件,避免无用的可写事件通知。不知道你注意到没有,如果某次只发出去部分数据,剩下的数据应该暂且存起来,这个时候我们就需要一个缓冲区来存放这部分数据,这个缓冲区我们称为“发送缓冲区”。发送缓冲区不仅存放本次没有发完的数据,还用来存放在发送过程中,上层又传来的新的需要发送的数据。为了保证顺序,新的数据应该追加在当前剩下的数据的后面,发送的时候从发送缓冲区的头部开始发送。也就是说先来的先发送,后来的后发送。 如何在给客户端发完数据后关闭连接? 这个问题比较难处理,因为这里的“发送完”不一定是真正的发送完,我们调用send或者write函数即使成功,也只是向操作系统的协议栈里面成功写入数据,至于能否被发出去、何时被发出去很难判断,发出去对方是否收到就更难判断了。所以,我们目前只能简单地认为send或者write返回我们发出数据的字节数大小,我们就认为“发完数据”了。然后调用close等socket API关闭连接。关闭连接的话题,我们再单独开一个小的标题来专门讨论一下。 ...

2021-01-11

高性能服务器框架设计

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

2021-01-11

给工作 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的基础知识就是你应该要首先谋取的“荆州”,进一步的各种框架、架构设计是你的“蜀地”。基础不牢,想其他的东西都是好高骛远,不切实际。最后日复一日,年复一年,在恨自己生不逢时,领导不是伯乐的嗟叹中蹉跎了岁月。 对于上面这个注重基础的问题上,实际情形中,我遇到三种人。 第一类:意识不到基础知识的重要性,这类人就不提了。 第二类,意识到基础知识的重要性,但是总是在各种理由和借口中麻痹自己,温水煮青蛙把自己“煮死”。很多咨询我的人,也是这种情况,说什么自己工作忙,家庭琐事多。我其实不想多说啥,为失败找借口的人太多,为成功找方法的人太少。你工作五年了,每个月抽一天时间来补一下基础,你现在都不是这样了,这个时间也抽不出来?自我麻痹而已。这类人其实是有想法没啥行动。 第三类,意识到基础的重要性,同时在各种闲暇时间去补充,去积累。这样的人学的最快,最后达到的高度也很高(当然收入也不菲)。 扎实的基础知识 + 见多识广的框架经验,让你在职场上变得无可替代,这才是你的核心竞争力。答案可能有点跑题了,但是我觉得先解决思想上的问题,行动上就容易许多了。 如果你想和我聊聊职业上的困惑,可以在『高性能服务器开发』公众号后台回复关键字『职业指导』,我们可以针对性地聊一聊。

2021-01-11