状态模式:一个Epoll边缘触发的代理服务器设计

设计模式是一门热门的知识,但是何时应该用哪个,却往往不容易掌握,本文以一个Socks5代理服务器的设计为例,介绍状态模式的实践用法。

软件的功能介绍

  1. 提供Socks5代理功能,同时支持TCP和UDP
  2. 可以转发代理连接,把代理服务器连接成代理链条
  3. 使用Linux epoll边缘触发API用法
  4. 期望能加入更多的非标准Socks5代理握手步骤或握手数据

代理网络拓扑模型

Socks5代理协议

Socke5代理协议同时支持TCP和UDP。因此代理握手交互也由2个不同协议承载。其TCP握手阶段包括:

  1. 客户端提出代理请求
  2. 服务器回应代理请求,并返回用户认证方式
  3. 客户端根据认证方式发送认证消息
  4. 服务器鉴权
  5. 客户端发起TCP代理请求,提交目标地址和端口
  6. 服务器回应是否对目标地址连接成功TCP
  7. 客户端、服务器开始转发TCP流数据

UDP协议握手也是通过TCP连接运行,并且从这个TCP连接的状态来表示客户端是否还需要用此UDP代理通道。由于UDP是无连接状态的,所以代理的使用状态就只好使用一个TCP连接了:

  1. 握手的前4个步骤和TCP是一样的。
  2. 客户端发起UDP代理请求,提交客户端自己的UDP端口
  3. 服务器回复UDP代理请求,新建一个给客户端发数据的UDP端口,并返回给客户端
  4. 客户端通过以上端口,改用UDP协议发送数据 a) 客户端发送的每个UDP包都会加上一个10字节的头部,包含了目标服务器的地址 b) 服务器回复的每个UDP包也会加上10字节头部,包含了来源服务器的地址 c) 客户端和代理服务器间TCP连接如果中断,此次代理服务关闭,服务器将关闭所有UDP端口

可以看到,Socks5代理协议的握手其实是比较复杂。如果需要加上代理转发的功能,这个服务器软件处理实现服务器端协议,还需要实现客户端协议。这样整个系统的代理握手阶段的代码就有至少十个以上的过程。

另外,由于我们期望使用“边缘触发”的Epoll API来转发网络数据,所以我们必须要记录网络中的各种异步状态。由于“边缘触发”只会在发生事件时发起一次事件,而一个代理连接有两个方向,两个对端都可能存在堵塞和畅通。所以每个代理连接在握手完成后,还是要处理各种网络堵塞状态的过程。

虽然问题比较繁琐,但是如果我们把系统看成是一个管道的模型,然后把这些过程归纳成状态,问题就很好解决了。因为每个管道,无非只有两种动作:读数据、写数据。而读写操作的目标端也只会有2个:客户端、目标服务端

状态流转

一旦我们确定了管道的模型和状态的想法,我们就能开始列举各种状态。在等待客户端连接的握手阶段,我们有“等待验证方法”“等待登录”“等待代理目标”三个状态。然后进入“等待转发连接”状态,根据客户端的需求,进入转发代理、TCP或者UDP三个不同的阶段。

转发代理阶段,就是实现客户端握手的过程,因此有“握手完成”“验证方法商定”“登录完成”三种状态,最后根据代理的需求,重新回到TCP和UDP两个阶段。

TCP阶段首先就是进入“TCP直连就绪”状态,或者“TCP转发就绪”状态,根据从上面不同的状态而来,然后就进入了管道流程的“双向畅通”“单向堵塞”“双向堵塞”三种流式状态。

UDP阶段比较简单,就是进入“UDP直连就绪”后开始转发数据就好了。

最后,都会进入一个“关闭”的状态。

状态模式的实现

至此我们的实现已经很清楚了,就是先建立一个“会话”类型代表一个通道,而这个通道一共可能有15个状态,因此要定义15个类;每个“状态”的类型,都有“读”“写”两个接口需要实现;每个状态都可以跳转到其他状态

在编写每个状态的读写代码的时候,只要关注当前的读写数据应该如何解析和处理就好了,然后适当的跳转到其他状态就好了。比如在登录验证状态中,WaitingAuth::OnRead()方法就是解码出网络中的数据,然后检查用户名、密码,最后进入新的状态WatingCmd。

在已经建立好TCP通道的状态代码里,根据读写数据read()和write()的系统调用的返回值,判断应该如何修改会话的状态。而在单向堵塞的状态里,又可以根据不同端的数据,以不同的方式来继续处理读写。

总结状态模式

优点:

  • 代码分拆比较自然,易于理解。经过几个人接手此系统,都能快速修改代码。
  • 扩展性好,可以用于改造其他协议。已经在socks5上添加了很多新内容
  • 内存管理比较简单,状态变量和执行代码完全分开。状态变量Session类以池化管理。
  • 很好的支持了epoll的边缘触发。能提高一点点性能。

缺点

  • 状态之间由代码随意跳转,除了TCP管道有带状态流转控制外,其他代码都没有实现状态图约束。
  • 状态跳转上下文缺失,依赖Session对象,导致其结构复杂
  • “UDP就绪状态”职责过多,没有继续细分状态,出过不少BUG,应该对转发还是直连划分更多的状态。由于还需要支持P2P功能,所以可能还需要增加P2P的状态。

最后,总结一下状态模式的思考:

  • 状态模式适合行为数量少,但由于状态复杂导致的代码复杂的场景。比如代理服务器本身的行为就是读和写数据。但是因为其内在状态很复杂,导致读写的数据内容千差万别,这种就比较适合状态模式。
  • 状态模式的标准描述,除了把状态定义成类,行为定位成方法外,还需要对附着状态的主体模型,定义各个状态的流转约束。如果能把这种流转图以数据形式抽取出来,则更容易调整复杂的状态流程,甚至不需要重新编译代码。
  • 状态模式的主要优点是能描述在复杂状态下,固定行为的不同内容。非常方便接手程序员以业务状态来理解代码。甚至无需完全理解代码都能添加、修改功能。
  • 状态模式的缺点也比较明确,就是状态对象本身的划分,如果太细会导致代码非常复杂。而且不适合复杂的行为,比如一个系统有很多种行为,在某些状态下可以用,有些状态又不可以有,这样在状态对象中的编码就会额外增加出很多复杂性。——这种情况往往用策略模式更方便。

感谢大家的阅读,如觉得此文对你有那么一丁点的作用,麻烦动动手指转发或分享至朋友圈。如有不同意见,欢迎后台留言探讨。

原文发布于微信公众号 - 韩大(handa1740168)

原文发表时间:2015-12-30

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏移动端开发

Socket学习总结系列(一) -- IM & Socket

Socket通讯在iOS中也是很常见,自己最近也一直在学习Telegram这个开源项目,Telegram就是在Socket的基础上做的即时通讯,这个相信了解这...

2865
来自专栏SDNLAB

数据中心网络虚拟化 隧道技术

如何实现不同租户和应用间的地址空间和数据流量的隔离是实现数据中心网络虚拟化首先需要解决的几个问题之一。所谓地址空间的隔离是指不同租户和应用之间的网络(ip)地址...

3635
来自专栏程序人生

撰写合格的REST API

两周前因为公司一次裁人,好几个人的活都被按在了我头上,这其中的一大部分是一系列REST API,撰写者号称基本完成,我测试了一下,发现尽管从功能的角度来说,这些...

3185
来自专栏Laoqi's Linux运维专列

LVS负载均衡中arp_ignore和arp_annonuce参数配置的含义

743
来自专栏Python中文社区

Python写TCP端口扫描工具之IP协议的讲解

專 欄 ❈exploit,Python中文社区专栏作者,入坑Python一年。希望与作者交流或者对文章有任何疑问的可以与作者联系: QQ:1585173691...

1996
来自专栏Golang语言社区

[Go语言]一种用于网游服务器的支持多路复用的网络协议处理框架

简介: 本文描述了使用Go语言实现的、适应于Go语言并发模型的一种支持多路复用的网络协议处理框架,并提供了框架的代码实现。作者将这种框架用于网络游戏服务器中的协...

3256
来自专栏Golang语言社区

[Go语言]一种用于网游服务器的支持多路复用的网络协议处理框架

简介: 本文描述了使用Go语言实现的、适应于Go语言并发模型的一种支持多路复用的网络协议处理框架,并提供了框架的代码实现。作者将这种框架用于网络游戏服务器中的协...

35210
来自专栏PHP技术

认识webservice

1.Web services 使用 XML 来编解码数据,并使用 SOAP 来传输数据。 基础的 Web Services 平台是 XML + HTTP。 HT...

33510
来自专栏SDNLAB

EBGP vs IBGP

在上一篇(BGP漫谈)介绍了BGP的一些基本概念。我们知道了BGP分为EBGP和IBGP。这次再进一步看看EBGP和IBGP有什么区别。 ? 应用场景 从应用...

2948
来自专栏刘君君

Rest Notes-将REST应用于HTTP

1663

扫码关注云+社区