状态模式:一个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 条评论
登录 后参与评论

相关文章

来自专栏云计算认知升级

【腾讯云的1001种玩法】十分钟轻松搞定云架构 · 负载均衡的最佳实践

视频内容 大家好,这节课,我们来看一看负载均衡的最佳实践。 虽然说我们明白了,负载均衡是什么和能干什么,但是想要真正用好负载均衡还是差一些火候,这里,我们来...

2049
来自专栏运维小白

18.7 LVS介绍

LVS介绍 LVS是由国人章文嵩开发 流行度不亚于apache的httpd,基于TCP/IP做的路由和转发,稳定性和效率很高 LVS最新版本基于Linux内核2...

1788
来自专栏何俊林

移动App 网络优化细节探讨

1336
来自专栏白驹过隙

ZeroMQ - 三种模型的python实现

35614
来自专栏Golang语言社区

转--分布式系统开发里必须要解决的3个技术问题

以前跟开发提过很多次,今天又有人出错了,看样子不经常提不行 这里记录一下,看到的开发人员都注意一下: 这3个问题经常出现,不解决的话,一定会造成经济损失的 1、...

3076
来自专栏超然的博客

简单理解同步与异步

进程同步:就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,这时程序是出于阻塞的,只有接收到返回的值或消息后才往下执行其他的命令。  

872
来自专栏老秦求学

JavaWeb三层结构---课设02

收获总结 1三层架构模式 区分层次的目的即为了“高内聚,低耦合”的思想 ? 分层介绍: Javaweb设计分为三层:数据访问层,业务逻辑层和表示层。 数据访问层...

3469
来自专栏码洞

蚂蚁金服RPC框架结构分析

蚂蚁金服近期开源了研发多年的SOFA一篮子框架,其中就有一个非常核心的RPC框架,它叫SOFA-BOLT。小编今天花了近一天的时间仔细阅读研究它的源码,阅读过程...

471
来自专栏Java编程技术

微服务核心组件 Zuul 网关原理剖析

Zuul 网关是具体核心业务服务的看门神,相比具体实现业务的系统服务来说它是一个边缘服务,主要提供动态路由,监控,弹性,安全性等功能。在分布式的微服务系统中,系...

853
来自专栏cmazxiaoma的架构师之路

FastDFS蛋疼的集群和负载均衡(十二)之浅谈负载均衡

1063

扫描关注云+社区