IM服务器:编写一个健壮的服务器程序需要考虑哪些问题

时间:2023-02-18 15:49:14

如果是编写一个服务器demo,比较简单,只要会socket编程就能实现一个简单C/S程序,但如果是实现一个健壮可靠的服务器则需要考虑很多问题。下面我们看看需要考虑哪些问题。

一、维持心跳

为何要维持心跳,TCP难道不是一个安全可靠的连接么?正常情况下,C端和S端无论是谁掉线,对方都能感知到。从而进行后续处理,比如释放维持的资源并通知业务层进行相应的业务处理。

如果TCP通道非常繁忙,C端和S端都能通过正常的业务通信感知到对方的存在与否。但如果TCP通道长时间无数据往来,这种感知就无法主动获取到,这时就需要通过心跳包来进行检测。 看看下面的情况:

1.1、突然死亡

客户端突然断电、死机等,这种情况下对方都来不及跟你道别就驾鹤西游了,只留下服务器搁那傻等。

1.2、突然失联

比如:网线突然脱落或防火墙强行关闭TCP通道。防火墙为何会关闭TCP通道呢

防火墙认为C端和S端长时间没通信,可能感情破裂了,因此继续维持两者之间的联系毫无意义,所以单方面宣布两者离婚,强制执行,立即生效。

上面是我猜的,实际情况是防火墙出于对服务器的爱,防火墙时刻监视着所有连接到服务器上的TCP通道,如果有长期占着茅坑不拉屎的连接,防火墙就会认为该连接是恶意的,是在对服务器耍流氓,因此有必要立即断开连接。

此时C端和S端虽然都活着,但两者之间已经阴阳两地,不可能在碰面了。

上述情况下,如果不进行心跳检测,服务器长期运行后,可能存在大量的“僵尸”连接,从而过多的占用系统资源。对于业务层来说如果不及时处理这些“僵尸” 可能造成业务处理的混乱。

二、处理超时

为何要处理超时? 我们通常理解的超时处理,大部分是基于套接字(socket)这层,超时有可能是网络拥塞导致,也有可能是上述的突然死亡突然失联导致。

如果send或recv长期无法完成,则有可能是TCP通道失效或对方已不在服务区,因此服务器端有必要主动进行关闭操作。对于超时,你可以粗暴的直接关闭连接,也可以在尝试N次发送或接收都超时后进行关闭

对于这种socket超时,我们只需要通过setsockopt函数在网络层进行超时设置。对于阻塞套接字而言,这种方式是可行的,但对于异步模型,这种方式则无法采用,比如IOCP模型。在IOCP模型下,所有投递的读、写操作都需要业务层进行超时判断。

上面的超时大家都比较清楚,其实超时处理最重要的作用是防止恶意连接,从而增强服务器的健壮性。

以HTTP协议为例,服务器需要读取HTTP请求头,这个请求头会以两个连续的回车换行(\r\n)来标记结束。

服务器只有读取完请求头后才能进行下一步的解析和业务处理工作。如果请求方在发送一半请求头后,迟迟不发送结束标记,就会导致服务器傻等,因为服务器会认为一次完成会话(HTTP Sesstion)并没有结束。

或者,对方在content-length字段中指明长度为100字节,却只给服务器发送了99字节后跑路。如果没有超时,服务器会一直痴痴的等着这最后一个字节的到来。

因此有必要在超时后进行会话关闭,否则这种恶意连接会很轻松的耗尽服务器有限的连接资源。

因此处理超时,不仅能解决网络层的意外问题,也能有效解决业务层的耍流氓行为。 当然超时也可能导致误伤,但相较于整体安全而言,这点误伤是可以理解的,大不了重联,重新培养感情。

三、实现定时器

这个好理解,上面的心跳检测,需要定时器来周期性的发起(如果你的超时判断不是依赖socket自带实现机制,即通过setsockopt函数设置KEEP_ALIVE参数来实现的话)。ngnix、redis、libuv(nodejs使用的底层库)等服务器都有自己的定时器实现逻辑,设计一个好的定时器有助于减少不必要的资源浪费

定时器可以帮服务器维持心跳检测,同时也能帮服务器做一些自身维护方面的工作,比如定期检查内存、CPU使用情况,定期同步(保存)数据等。

此外,处理超时也需要定时器来进行检测,对于IOCP模型,无法通过setsockopt函数来设置套接字层的超时,只能通过业务层来自己实现,也就是对于每个发出的IO请求(读写操作)记录时间,并在IO请求完毕后更新时间。定时器要周期性的检查所有IO请求是否完成,或者是否超时。比如投递一个写操作,如果长时间没有写完毕,则需要进行超时处理。

对于上万连接,该如何设计自己的定时器? 如果对每个连接socket(TCP连接)都启动一个定时器进行超时或心跳检测,则定时器本身就会消耗大量的系统资源,显然这种方式是不明智的。

如果只启动一个定时器,去检测成千上万连接,则需要考虑如何在CPU空闲或IO空闲时的去做这些事。比如当服务器准备向某个TCP通道发送心跳包时,该通道正在进行正常业务会话,此时心跳包可能会干扰正常的业务数据。比如CPU很繁忙的时候,如何让你的定时器进行错峰检测。

也就是说,你的定时器要根据你的服务器业务特点亲自实现,并融合到整体的IO调度中。

四、有罪推论

这个和现实中的无罪推论相反。服务器设计上,一定要假设所有请求可能都是非法的,要做有罪推论。 我们不能想当然的认为每个连接请求都会按照标准的协议与服务器通信。

大部分协议都是通过特定的结束标记(\r\n)来表示一次完整的请求或数据响应的完成。比如HTTP、FTP、TELNET、POP3、SMTP等协议。上古时期,早期操作系统UNIX(或DOS),用户操作界面就是控制台,控制台的输入输出方式就决定了用户只能通过敲击键盘将协议命令输入到网络,这也就导致了回车换行"\r\n"会作为一次命令结束的标识。 比如HTTP协议,与主机建立连接后,输入"GET / HTTP/1.1\r\n"即可获取网站的主页。

还是以HTTP协议为例,HTTP请求头是以两个回车换行(\r\n\r\n)来标记结束。如果对方一直发送数据,而不发送结束标记该如何处理? 假设我们开辟一个4K(4096)字节的缓冲区用于接收HTTP请求头,对方发送的请求头超过4K怎么办,当然你可以remalloc内存继续接收,但如果是恶意请求呢?比如对方一直发送数据,直到把你的服务器内存消耗殆尽。这时候就需要我们设置一个阀值,超过该值时要立即断开连接。

这种方式可以理解为对讲机模式,一句话讲完后必须要带上一句over,属于后付费 。对方在你没有发送over之前无法知道最终数据有多长。这种后付费方式容易让对方吃霸王餐,比如吃完之后没说over(没付钱)就跑了。。。。

还有一种协议不是以“over”标记符来表示请求的完整性。而是通过请求头中的“长度字段”来表示后续数据的大小。这种方式可以理解为报文方式。 属于预付费 ,就是一开始就告诉对方自己要发送数据的大小,或者告诉对方自己有多少钱,可以消费多少,让对方提前准备好缓冲区。

这种方式下会有一个固定大小的报文头,报文头的字段有严格的定义,用于指示后续数据的实际情况或者意义。

后付费能吃霸王餐,预付费也是可以的,也就是数据长度可能是假的,长度字段虽然是1000个字节,但最后给你2000个怎么办?或者只给你500个怎么办?

以websocket协议为例,虽然websocket协议是基于HTTP协议,但这仅限于建立会话阶段。一旦会话建议,websocket就会通过固定格式的报文来进行数据交流。这种情况下我们要严格检验报文的格式,比如长度是否合法。

此外,对于所有recv来说,一次接收的数据不一定是你想要的结果,不是缓冲区开辟了多大,对方就一次性发给你多大。极端情况下,对方可以一个字节一个字节的发送数据,这时候你就要进行数据的封装和实时校验。

上述情况都会涉及到内存的分配和访问,一旦处理不当就可能造成系统资源耗尽或这服务器的直接coredown。

五、使用内存池

从上面我们可以看到,内存的分配和销毁是频繁发生的事,服务器长期运行就会导致内存碎片的产生。我的这篇文章

【超值分享】为何写服务器程序需要自己管理内存,从改造std::string字符串操作说起

写累了,到此为止吧,考虑的问题还有很多,比如你的上层业务是IO密集型还是CPU密集型,这就会对你程序架构产生影响,比如是否考虑使用线程池?这就是为何redis采用单线程,nginix采用多线程的原因之一。

IM服务器:编写一个健壮的服务器程序需要考虑哪些问题的更多相关文章

  1. 多线程编程学习笔记——编写一个异步的HTTP服务器和客户端

    接上文 多线程编程学习笔记——使用异步IO 二.   编写一个异步的HTTP服务器和客户端 本节展示了如何编写一个简单的异步HTTP服务器. 1.程序代码如下. using System; using ...

  2. 编写一个简单的C++程序

    编写一个简单的C++程序 每个C++程序都包含一个或多个函数(function),其中一个必须命名为main.操作系统通过调用main来运行C++程序.下面是一个非常简单的main函数,它什么也不干, ...

  3. 用C语言编写一个简单的词法分析程序

    问题描述: 用C或C++语言编写一个简单的词法分析程序,扫描C语言小子集的源程序,根据给定的词法规则,识别单词,填写相应的表.如果产生词法错误,则显示错误信息.位置,并试图从错误中恢复.简单的恢复方法 ...

  4. 使用C#来编写一个异步的Socket服务器

    介绍 我最近需要为一个.net项目准备一个内部线程通信机制. 项目有多个使用ASP.NET,Windows 表单和控制台应用程序的服务器和客户端构成. 考虑到实现的可能性,我下定决心要使用原生的soc ...

  5. 如何编写一个稳定的网络程序(TCP)

    本节我们看一下怎样才能编写一个基于TCP稳定的客户端或者服务器程序,主要以试验抓包的方式观察数据包的变化,对网络中出现的多种情况进行分析,分析网络程序中常用的技术及它们出现的原因,在之后的编程中能早一 ...

  6. Java入门篇(一)——如何编写一个简单的Java程序

    最近准备花费很长一段时间写一些关于Java的从入门到进阶再到项目开发的教程,希望对初学Java的朋友们有所帮助,更快的融入Java的学习之中. 主要内容包括JavaSE.JavaEE的基础知识以及如何 ...

  7. 编写一个 Chrome 浏览器扩展程序

    浏览器扩展允许我们编写程序来实现对浏览器元素(书签.导航等)以及对网页元素的交互, 甚至从 web 服务器获取数据,以 Chrome 浏览器扩展为例,扩展文件包括: 一个manifest文件(主文件, ...

  8. 用python编写一个合格的ftp程序,思路是怎样的?

      经验1.一般在比较正规的类中的构造函数.都会有一个verify_args函数,用于验证传入参数.尤其是对于系统传参.2.并且系统传参,其实后面大概都是一个函数名 例如:python server. ...

  9. 使用PyQt5编写一个简单的GUI程序(pyside 有 pyside-uic 把ui文件转成py文件,pyside-rcc 把qrc文件转成 py文件导入就行了)

    我做Python窗口界面编程时,经常使用PyQt进行设计.这里简单叙述一下使用PyQt5制作一个简单的图形界面的流程 PyQt的简介以及开发环境的搭建在此不多赘述. 1.       打开Qt Des ...

随机推荐

  1. MMC不能打开文件D:\Program Files\Microsoft SQL Server\80\Tools\BINN\SQL Server Enterprise Manager.MSC

    以上问题的解决方式如下: 1. 打开windows运行对话框.在对话框输入mmc.打开了如图所示的控制台. 2. 文件---添加/删除管理单元(M). 3. 添加.然后选择Microsoft SQL ...

  2. 免费安卓IOS测试API接口,后续会陆续增加接口

    各位博友好!开发的安卓或者ios的朋友们,经常会遇到想测试但是没有公开的api接口进行进行测试.但自己又不会开发服务端或者没有服务器,这里我免费提供了一整套API接口.欢迎大家调用,目标是方便大家. ...

  3. spring集成mongodb jar包版本问题

    在开发过程中,spring集成mongodb的jar包. 如果需要使用排序功能. spring-data-mongodb-1.4.1.RELEASE.jar 的版本为1.4.1,如果使用如下代码: Q ...

  4. MySQL触发器学习

    简介 MySQL从5.0.2版本开始支持触发器的功能.触发器是与表有关的数据库对象,在满足定义条件时触发,并执行触发器中定义的语句集合. 创建触发器 语法: CREATE TRIGGER trigge ...

  5. linux目录的特点

    1./是所有目录的顶点. 2.目录结构像一颗倒挂的树 3.目录和磁盘分区,默认是木有关联的 4./不同的目录可能会对应不同的分区或磁盘 linux里设备如果不挂载是看不到入口的, 如果希望设备被访问, ...

  6. 用sklearn封装的kmeans库

    由于需要海量的进行聚类,所以将 k-means 算法自我封装成一个方便利用的库,可以直接调用得到最优的 k值 和 中心点: #!/usr/bin/python3.4 # -*- coding: utf ...

  7. JVM读书笔记之内存管理

    对于从事C.C++程序开发人员来说,在内存管理领域,他们既是拥有最高权力的“皇帝”又是从事最基础工作的“劳动人民”--既拥有每一个对象的“所有权”,又负责每一个对象生命开始到终结的维护责任. 对于Ja ...

  8. 20172302 《Java软件结构与数据结构》实验二:树实验报告

    课程:<Java软件结构与数据结构> 班级: 1723 姓名: 侯泽洋 学号:20172302 实验教师:王志强老师 实验日期:2018年11月5日 必修/选修: 必修 实验内容 (1)参 ...

  9. Codeforces777D Cloud of Hashtags 2017-05-04 18&colon;06 67人阅读 评论&lpar;0&rpar; 收藏

    D. Cloud of Hashtags time limit per test 2 seconds memory limit per test 256 megabytes input standar ...

  10. spring 采用编程式事务

    1.getCurrentSession()与openSession()的区别? * 采用getCurrentSession()创建的session会绑定到当前线程中,而采用openSession() ...