如果是编写一个服务器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. 手写tomcat——编写一个echo http服务器

    核心代码如下: public class DiyTomcat1 { public void run() throws IOException { ServerSocket serverSocket = ...

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

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

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

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

随机推荐

  1. 贪心/构造/DP 杂题选做Ⅲ

    颓!颓!颓!(bushi 前传: 贪心/构造/DP 杂题选做 贪心/构造/DP 杂题选做Ⅱ 51. CF758E Broken Tree 讲个笑话,这道题是 11.3 模拟赛的 T2,模拟赛里那道题的 ...

  2. Codeforces 1264D - Beautiful Bracket Sequence(组合数学)

    Codeforces 题面传送门 & 洛谷题面传送门 首先对于这样的题目,我们应先考虑如何计算一个括号序列 \(s\) 的权值.一件非常显然的事情是,在深度最深的.是原括号序列的子序列的括号序 ...

  3. Atcoder Regular Contest 058 D - 文字列大好きいろはちゃん / Iroha Loves Strings(单调栈+Z 函数)

    洛谷题面传送门 & Atcoder 题面传送门 神仙题. mol 一发现场(bushi)独立切掉此题的 ycx %%%%%%% 首先咱们可以想到一个非常 naive 的 DP,\(dp_{i, ...

  4. 洛谷 P5406 - [THUPC2019]找树(FWT+矩阵树定理)

    题面传送门 首先看到这道题你必须要有一个很清楚的认识:这题新定义的 \(\oplus\) 符号非常奇怪,也没有什么性质而言,因此无法通过解决最优化问题的思路来解决这个问题,只好按照计数题的思路来解决, ...

  5. 【机器学习与R语言】8- 神经网络

    目录 1.理解神经网络 1)基本概念 2)激活函数 3)网络拓扑 4)训练算法 2.神经网络应用示例 1)收集数据 2)探索和准备数据 3)训练数据 4)评估模型 5)提高性能 1.理解神经网络 1) ...

  6. shell编程100列

    1.编写hello world脚本 #!/bin/bash# 编写hello world脚本 echo "Hello World!"2.通过位置变量创建 Linux 系统账户及密码 ...

  7. Python基础之流程控制if判断

    目录 1. 语法 1.1 if语句 1.2 if...else 1.3 if...elif...else 2. if的嵌套 3. if...else语句的练习 1. 语法 1.1 if语句 最简单的i ...

  8. javaSE中级篇2 — 工具类篇 — 更新完毕

    1.工具类(也叫常用类)-- 指的是别人已经写好了的,我们只需要拿来用就行了 官网网址:Overview (Java Platform SE 8 ) (oracle.com) ---- 但是这个是英文 ...

  9. go 函数进阶

    目录 回调函数和闭包 高阶函数示例 回调函数(sort.SliceStable) 闭包 最佳闭包实例 回调函数和闭包 当函数具备以下两种特性的时候,就可以称之为高阶函数(high order func ...

  10. 最长公共子序列问题(LCS) 洛谷 P1439

    题目:P1439 [模板]最长公共子序列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn) 关于LCS问题,可以通过离散化转换为LIS问题,于是就可以使用STL二分的方法O(nlogn ...