加载中...

5. 用Rust手把手编写一个Proxy(代理), 通讯协议建立, 为内网穿透做准备

1.用Rust手把手编写一个Proxy(代理), 动工2.用Rust手把手编写一个Proxy(代理), UDP绑定篇
3.5. 用Rust手把手编写一个Proxy(代理), 通讯协议建立, 为内网穿透做准备
4.6. 用Rust手把手编写一个wmproxy(代理,内网穿透等), 通讯协议源码解读篇5.7. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP及TCP内网穿透原理及运行篇6.8. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP改造篇之HPACK原理7. 用Rust手把手编写一个Proxy(代理), 准备篇, 动手造轮子8.9. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP2改造篇之HPACK示例, 了解http2头信息如何处理9.10. 用Rust手把手编写一个wmproxy(代理,内网穿透等), HTTP内网穿透支持修改头信息10.11. 用Rust手把手编写一个wmproxy(代理,内网穿透等), 实现健康检查11.12. 用Rust手把手编写一个wmproxy(代理,内网穿透等), TLS的双向认证信息及token验证12.13. 从零开始编写一个类nginx工具, HTTP中的压缩gzip,deflate,brotli算法13.14. 从零开始编写一个类nginx工具, HTTP文件服务器的实现过程及参数14.15. 从零开始编写一个类nginx工具, 如果将nginx.conf转成yaml,toml,json会怎么样15.16. 从零开始编写一个类nginx工具, 反向代理upstream源码实现16.17. 从零开始编写一个类nginx工具, Rust中一些功能的实现17.18. 从零开始编写一个类nginx工具, 主动式健康检查源码实现18.19. 从零开始编写一个类nginx工具, 配置数据的热更新原理及实现19.25. 干货系列从零用Rust编写正反向代理,序列化之serde是如何工作的20.27. 干货系列从零用Rust编写正反向代理,Rust中日志库的应用基础准备21.28. 干货系列从零用Rust编写正反向代理,项目日志的源码实现22.30. 干货系列从零用Rust编写正反向代理,HTTP的组装之旅(中间件)23.29. 干货系列从零用Rust编写正反向代理,异步回调(async trait)的使用24.26. 干货系列从零用Rust编写正反向代理,如何发布Rust项目到Docker25.32. 干货系列从零用Rust编写正反向代理,关于堆和栈以及如何解决stack overflow26.33. 干货系列从零用Rust编写正反向代理,关于HTTP客户端代理的源码实现27.34. 干货系列从零用Rust编写负载均衡及代理,异步测试在Rust中的实现28.35. 干货系列从零用Rust编写负载均衡及代理,代理服务器的源码升级改造29.36. 干货系列从零用Rust编写负载均衡及代理,内网穿透中内网代理的实现30.37. 干货系列从零用Rust编写负载均衡及代理,负载均衡中try_files实现31.38. 干货系列从零用Rust编写负载均衡及代理,负载均衡中ip通行与禁止32.39. 干货系列从零用Rust编写负载均衡及代理,正则及格式替换33.40. 干货系列从零用Rust编写负载均衡及代理,websocket的实现34.41. 干货系列从零用Rust编写负载均衡及代理,websocket与tcp的映射,WS与TCP互转35.42 干货系列从零用Rust编写负载均衡及代理,wmproxy中配置tcp转websocket36.43 干货系列从零用Rust编写负载均衡及代理,内网穿透方案完整部署37.44从零开始用Rust编写nginx,命令行参数的设计与解析及说明38.45从零开始用Rust编写nginx,静态文件服务器竟然还有这些细节39.46从零开始用Rust编写nginx,数据还能这么传,多层代理(IP多级代理)搭建40.47从零开始用Rust编写nginx,配对还有这么多要求!负载均衡中的路径匹配41.48从零开始用Rust编写nginx,搭建一个简单又好看官方网站42.49从零开始用Rust编写nginx,我竟然在同一个端口上绑定了多少IP43.50从零开始用Rust编写nginx,原来TLS证书还可以这么申请44.51从零开始用Rust编写nginx,江湖救急,TLS证书快过期了
收起

用Rust手把手编写一个Proxy(代理), 通讯协议建立, 为内网穿透做准备

项目 ++wmproxy++

gite: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

什么是通讯协议?

在tcp的流传输过程中,可以看做是一堆的字节的集合体,是一种“流”式协议,就像河里的水,中间没有边界。或者好比不懂汉语的来看古文,因为古文里没有任何的句读,不知何时另起一行。那我们如何正确的做到拆包解包,保证数据格式的正确呢?

以下是客户端发送两个30字节的包(P1及P2),服务端读取数据可能读出来的可能

gantt
title 粘包的可能,例每个包30字节
%% This is a comment
dateFormat X
axisFormat %s
section 示例1
P2 :a1, 1, 30
P1 :after a1, 60
section 示例2
P2,P1 :1,60
section 示例3
P2部分 :a3, 1, 20
P2部分P1全部 :after a3, 60
section 示例4
P2全部P1部分 :a4, 1, 40
P1部分 :after a4, 60

若没有事先约定好格式,在服务端部分无法正确的解析出P1包和P2包,也就意味着无法理解客户端发的内容。若此时我们约定每个包的大小固定为30字节,那么2,3,4三种可能不管收到多少,都必须等待30字节填充完毕后解析出P1,剩余的数据待待60字节接收完毕后解析P2包

粘包拆包常见的解决方案

对于粘包和拆包问题,常见的解决方案有四种:

  • 发送端将每个包都封装成固定的长度,比如512字节大小。如果不足512字节可通过补0或空等进行填充到指定长度;
  • 发送端在每个包的末尾使用固定的分隔符,例如\r\n。如果发生拆包需等待多个包发送过来之后再找到其中的\r\n进行合并;例如,Redis协议,每一行的结尾都是CRLF,在碰到结尾的时候才进行转发;
  • 将消息分为头部和消息体,头部中保存整个消息的长度,只有读取到足够长度的消息之后才算是读到了一个完整的消息,例如HTTP2协议,固定先读3个字节的长度,9个字节的长度头信息;
  • 通过自定义协议进行粘包和拆包的处理。
在此的解决方案

选择了分为头部和消息体方案,头部分为8个字节,然后前3个字节表示包体的长度,单包支持长度为8-167777215也就是16m的大小,足够应对大多数情况。

网络的拓扑图

因为每个链接的处理函数均在不同的协程里,所以这里用了Sender/Receiver来同步数据。

tls加密连接或普通连接
Sender/Receiver
Sender/Receiver
中心客户端/CenterClient
中心服务端/CenterServer
客户端链接
服务端链接

协议的分类

协议相关的类均在prot目录下面,统一对外的为枚举ProtFrame,类的定义如下

pub enum ProtFrame {
/// 收到新的Socket连接
Create(ProtCreate),
/// 收到旧的Socket连接关闭
Close(ProtClose),
/// 收到Socket的相关数据
Data(ProtData),
}

主要涉及类的编码及解析在方法encode,parse,定义如下

/// 把字节流转化成数据对象
pub fn parse<T: Buf>(
header: ProtFrameHeader,
buf: T,
) -> ProxyResult<ProtFrame> {
}
/// 把数据对象转化成字节流
pub fn encode<B: Buf + BufMut>(
self,
buf: &mut B,
) -> ProxyResult<usize> {
}
消息的包头

任何消息优先获取包头信息,从而才能进行相应的类型解析,类为ProtFrameHeader,定义如下,总共8个字节

pub struct ProtFrameHeader {
/// 包体的长度, 3个字节, 最大为16m
pub length: u32,
/// 包体的类型, 如Create, Data等
kind: ProtKind,
/// 包体的标识, 如是否为响应包等
flag: ProtFlag,
/// 3个字节, socket在内存中相应的句柄, 客户端发起为单数, 服务端发起为双数
sock_map: u32,
}
消息类型的定义

暂时目前定义三种类型,Create, Close, Data

  • Socket创建,类为ProtCreate
/// 新的Socket连接请求,
/// 接收方创建一个虚拟链接来对应该Socket的读取写入
#[derive(Debug)]
pub struct ProtCreate {
sock_map: u32,
mode: u8,
domain: Option<String>,
}
  • Socket关闭,类为ProtClose
/// 旧的Socket连接关闭, 接收到则关闭掉当前的连接
#[derive(Debug)]
pub struct ProtClose {
sock_map: u32,
}
  • Socket数据包,类为ProtData
/// Socket的数据消息包
#[derive(Debug)]
pub struct ProtData {
sock_map: u32,
data: Binary,
}

一个数据包的自白

我是一段数据,我要去找服务器获得详细的数据

首先我得和服务器先能沟通上,建立一条可以通讯的线

请求连接建立
把链接交由
生成sock_map如1,并发送ProtCreate
根据ProtCreate创建与sock_map对应的唯一id
根据相应信息连接到服务端
客户端代理
中心客户端
中心服务端
虚拟TCP连接
服务端

此时我已经和服务端构建起了一条通讯渠道,接下来我要和他发送数据了

发送字节数据
读出数据交由
加工成ProtData发送
根据ProtData的sock_map发送给对应
解析成数据流写入
把数据流返回
读出数据交由
根据ProtData的sock_map发送给对应
解析成数据流写入
客户端代理
中心客户端
中心服务端
虚拟TCP连接
服务端

至此一条我与服务端已经可以说悄悄话啦。

内网穿透

内网穿秀本质上从中心服务端反向交由中心客户端构建起一条通讯渠道,如今数据协议已经建立,可由服务端推送数据到客户端进行处理,后续实现请看下篇