时间:2023-02-10 20:03:01 | 来源:建站知识
时间:2023-02-10 20:03:01 来源:建站知识
浏览器<--->代理服务器<--->饿了么网站浏览器在开启代理模式下,当打开一个网站时,会先连上代理服务器,并进行必要的握手步骤。 Socks5 的握手分两步。
1. 第一步握手是无聊且几乎固定的问答,俗称「对暗号」。到现在,握手完成。浏览器会发送数据给代理服务器,代理服务器将 TCP 报文原封不动发送给饿了么网站, 再将饿了么网站返回的数据也原封不动返回给浏览器。浏览器最终将页面渲染并显示在屏幕上。代理服务器甚至感知不到 HTTP 协议的存在,因为它只做 TCP 字节流的转发。
2. 第二步握手, 浏览器发给代理服务器的报文中会包含需要代访问的 IP 地址(或者域名)。代理服务器连接(connect)上饿了么网站服务器后,会发送响应报文给浏览器,告诉他成功了 。
浏览器<--->SSlocal<--->SSServer<--->网站Shadowsocks 也有握手过程,和 Socks5 的协议比较像,这节不做重点介绍。在握手成功后,代理会执行转发任务。SSlocal 会将浏览器发来的数据加密,发送给 SSServer , SSServer 把数据解密后发给网站。返回来的数据也是同理,都会有个加密解密的过程。一般来说比较建议用 AES-256-CFB 这种比较安全的加密方式。
// go 伪代码func main() { while (true) { conn = acceptor.accept() go handleConn(conn) }}func handleConn(conn) { // 第一次握手 err = handshake(conn) checkError(err) addr, port = getAddr(conn) // 尝试连接客户端发来的 IP 地址 server, err = connect(addr, port) checkError(err) // 成功连上要通知客户端,这里省略代码 ... // 将客户端发来的消息发送至远程服务器 go io.Copy(server, conn) // 将服务端发来的消息转发至客户端 io.Copy(conn, server)}
说明:通常主函数就是一个大循环,有新连接就开个 Goroutine 处理这个客户端连接。客户端连接先进行握手后会发送想要访问的目的服务器地址,代理服务器先尝试 connect ,成功连接上则通知客户端,客户端开始发送真正的数据。这时候做一下数据的转发就可以了。由于 TCP 是全双工的协议,收发独立,再加上 Goroutine 已经相当廉价了,所以可以开启两个 Goroutine, 一个负责收,一个负责发,互相不影响。不可以开启多个线程(Goroutine)去对同一个 TCP 连接并行地发送数据,因为这样发送的数据是交错在一起的,是错误的。//c++伪代码int main() { while (true) { err, events = poller.wait(interval) processTimerTask() //处理定时器任务 if (err) { //处理错误 continue } for (event : events) { if (event.isReadable()) { //处理读事件 handleRead(event) } if (event.isWriteable()) { //处理写事件 } if (event.isClosed()) { //处理关闭套接字事件 } if (event.hasError()) { //处理错误事件 } } }}
poller 底层一般有 select,epoll 等。通常情况下使用 epoll 性能最好。单线程可以很容易支撑好几万并发。//c++伪代码struct ConnContext { Buffer buffer; // 接收的消息(可能还不是一个完整的消息) State state; // 状态}
read 函数一次性读的字节数也是不确定的,有时需要多次调用 read 才能接受完完整的数据。由于数据不完整,并不能执行接下来的流程,所以要先把数据缓存在一个地方,然后无奈返回。等数据接收完整了,才能进入下一个处理程序。一般每个连接都有一个上下文,由 map 保存对应关系。有些协议实现起来状态比较多,比如有好几次握手,必要时还需要使用状态机保存状态。每次有读事件的时候,都会调用 handleRead 函数,这时候根据之前保存的状态,很容易恢复到之前执行的函数的位置(通过switch case分发)。// c++ 伪代码void handleRead(conn) { context = contexts[conn] switch (context.state) { case eSTATE_HANDSHAKE1: handshake1(conn, context) break case eSTATE_HANDSHAKE2: handshake2(conn, context) break // 省略 // ... }}void handshake1(conn, context) { data = read_some(conn) append(context.buffer, data) if (context.buffer 不是一个完整的数据) { return } //发送一些东西 send_some(...) // 将 context.buffer 处理过的数据清理掉 // 现在handleshake1状态结束了,更改为下一个状态 context.state = eSTATE_HANDSHAKE2 // 下一次读事件将会调用 handshake2 函数}
3.2.2 gethostbyname 是阻塞的浏览器<--->代理服务器<--->饿了么网站考虑一下这个情况,浏览器和代理服务器连通速度很好,收发很快;而代理服务器和饿了么站点收发很慢。这样一个收发速度不相等的情况,会出现怎样的问题?
1. 对于阻塞同步模型,基本上不用考虑这个问题。因为他的收和发是串行的,这意味着它会自动调整滑动窗口大小。当代理服务器收到了浏览器的10Kib数据,代理服务器就会慢慢发送这10Kib数据给饿了么网站,这时候如果浏览器还想发数据给代理服务器,只会保存在代理服务器的内核缓冲区里,由于代理服务器程序在执行发送的任务(顾不上收数据),并没有从缓冲区取数据,缓冲区的数据会越来越多,剩余空间越来越小,在TCP层面,就会通知调整滑动窗口大小。当读缓冲区满了以后,通知滑动窗口为0,客户端就会停止发送数据。等代理服务器发送完数据,开始从缓冲区取浏览器的数据,浏览器到代理服务器的发送窗口又会从0变大,浏览器又可以发送数据了。4.2 TCP 可靠性
2. 对于非阻塞模型,这是一个大问题。由于没有阻塞功能,代理服务器会一个劲儿的收下浏览器的所有数据,读取内核缓冲区的数据,再转发数据,并保存在自己的某个缓冲区中(sendBuffer)。有点类似于生产者消费者模型,生产得快,消费得慢,内存会一直膨胀下去。其实我们很容易做1情况的模拟,只要发现 sendBuffer 过大就停止读缓冲区的数据,等 sendBuffer 消下去了再开始读就行了。
std::vector<char> msg {5, 'h', 'e', 'l', 'l', 'o'}
第一字节表示长度(这里是 5 ),后面跟上这个长度的字节流。接收者先收一字节,然后动态开辟这个长度的缓冲区,把剩下的收完。uint16_t a = 0x01;
那么在有些机器上,a里存的是0000000000000001, 有些机器是0000000100000000。如果直接就把字节流传给对方,说不定对方不是和自己一种字节序,就会把数据认错。所以需要统一规定大小端顺序,传到网络上统一用一种端序,从网络到本机再转换到本机的端序。union Uint16 { uint16_t u; char c[2];}Uint16 foo;foo.u = 0x01;std::cout<<static_cast<unsigned int>(foo.c[0]);std::cout<<static_cast<unsigned int>(foo.c[1]);
根据机器不同,有可能输出01,有可能输出10。u_long htonl(u_long hostlongvalue);u_short htons(u_short hostshortvalue);u_long ntohl(u_long netlongvalue);u_short hotns(u_short netshortvalue);
发送方代码如下:// go 伪代码// 协议格式 两个字节的长度 + 不定长数据sendmsg = "hello sunfish gao!";// 将本地序转换成网络序 len = htons(sendmsg.size());conn.write([]byte(len))conn.write([]byte(sendmsg))
接收方伪代码如下:// go 伪代码// 读两个字节,得到接下来的数据总长度len = conn.read(2)// 网络序转主机序len = ntons(len) // 再读剩下的字节数data = conn.read(len)
5.1.2 以特殊符号分割put key value/r/n当客户端向服务端分开发送如下两条命令:
get key/r/n
“put key value/r/n”极有可能在服务端收到一条粘起来的数据:
“get key/r/n”
“put key value/r/nget key/r/n”甚至是分两次收到奇怪的分割的数据:
“put key value/r/nget k”其实各种可能都有,因为 TCP 是字节流协议,所以 read 函数每次读的长度不是确定的,他不像 WebSocket 能不用担心「粘包」问题。如果数据中存在「空格」、「换行符」这样的字符,则需要进行字符串替换,也就是「转义」。
“ey/r/n”
关键词:服务,代理