時間:2023-02-10 20:03:01 | 來源:建站知識
時間:2023-02-10 20:03:01 來源:建站知識
瀏覽器<--->代理服務(wù)器<--->餓了么網(wǎng)站瀏覽器在開啟代理模式下,當打開一個網(wǎng)站時,會先連上代理服務(wù)器,并進行必要的握手步驟。 Socks5 的握手分兩步。
1. 第一步握手是無聊且?guī)缀豕潭ǖ膯柎?,俗稱「對暗號」。到現(xiàn)在,握手完成。瀏覽器會發(fā)送數(shù)據(jù)給代理服務(wù)器,代理服務(wù)器將 TCP 報文原封不動發(fā)送給餓了么網(wǎng)站, 再將餓了么網(wǎng)站返回的數(shù)據(jù)也原封不動返回給瀏覽器。瀏覽器最終將頁面渲染并顯示在屏幕上。代理服務(wù)器甚至感知不到 HTTP 協(xié)議的存在,因為它只做 TCP 字節(jié)流的轉(zhuǎn)發(fā)。
2. 第二步握手, 瀏覽器發(fā)給代理服務(wù)器的報文中會包含需要代訪問的 IP 地址(或者域名)。代理服務(wù)器連接(connect)上餓了么網(wǎng)站服務(wù)器后,會發(fā)送響應報文給瀏覽器,告訴他成功了 。
瀏覽器<--->SSlocal<--->SSServer<--->網(wǎng)站Shadowsocks 也有握手過程,和 Socks5 的協(xié)議比較像,這節(jié)不做重點介紹。在握手成功后,代理會執(zhí)行轉(zhuǎn)發(fā)任務(wù)。SSlocal 會將瀏覽器發(fā)來的數(shù)據(jù)加密,發(fā)送給 SSServer , SSServer 把數(shù)據(jù)解密后發(fā)給網(wǎng)站。返回來的數(shù)據(jù)也是同理,都會有個加密解密的過程。一般來說比較建議用 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) // 嘗試連接客戶端發(fā)來的 IP 地址 server, err = connect(addr, port) checkError(err) // 成功連上要通知客戶端,這里省略代碼 ... // 將客戶端發(fā)來的消息發(fā)送至遠程服務(wù)器 go io.Copy(server, conn) // 將服務(wù)端發(fā)來的消息轉(zhuǎn)發(fā)至客戶端 io.Copy(conn, server)}
說明:通常主函數(shù)就是一個大循環(huán),有新連接就開個 Goroutine 處理這個客戶端連接??蛻舳诉B接先進行握手后會發(fā)送想要訪問的目的服務(wù)器地址,代理服務(wù)器先嘗試 connect ,成功連接上則通知客戶端,客戶端開始發(fā)送真正的數(shù)據(jù)。這時候做一下數(shù)據(jù)的轉(zhuǎn)發(fā)就可以了。由于 TCP 是全雙工的協(xié)議,收發(fā)獨立,再加上 Goroutine 已經(jīng)相當廉價了,所以可以開啟兩個 Goroutine, 一個負責收,一個負責發(fā),互相不影響。不可以開啟多個線程(Goroutine)去對同一個 TCP 連接并行地發(fā)送數(shù)據(jù),因為這樣發(fā)送的數(shù)據(jù)是交錯在一起的,是錯誤的。//c++偽代碼int main() { while (true) { err, events = poller.wait(interval) processTimerTask() //處理定時器任務(wù) if (err) { //處理錯誤 continue } for (event : events) { if (event.isReadable()) { //處理讀事件 handleRead(event) } if (event.isWriteable()) { //處理寫事件 } if (event.isClosed()) { //處理關(guān)閉套接字事件 } if (event.hasError()) { //處理錯誤事件 } } }}
poller 底層一般有 select,epoll 等。通常情況下使用 epoll 性能最好。單線程可以很容易支撐好幾萬并發(fā)。//c++偽代碼struct ConnContext { Buffer buffer; // 接收的消息(可能還不是一個完整的消息) State state; // 狀態(tài)}
read 函數(shù)一次性讀的字節(jié)數(shù)也是不確定的,有時需要多次調(diào)用 read 才能接受完完整的數(shù)據(jù)。由于數(shù)據(jù)不完整,并不能執(zhí)行接下來的流程,所以要先把數(shù)據(jù)緩存在一個地方,然后無奈返回。等數(shù)據(jù)接收完整了,才能進入下一個處理程序。一般每個連接都有一個上下文,由 map 保存對應關(guān)系。有些協(xié)議實現(xiàn)起來狀態(tài)比較多,比如有好幾次握手,必要時還需要使用狀態(tài)機保存狀態(tài)。每次有讀事件的時候,都會調(diào)用 handleRead 函數(shù),這時候根據(jù)之前保存的狀態(tài),很容易恢復到之前執(zhí)行的函數(shù)的位置(通過switch case分發(fā))。// 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 不是一個完整的數(shù)據(jù)) { return } //發(fā)送一些東西 send_some(...) // 將 context.buffer 處理過的數(shù)據(jù)清理掉 // 現(xiàn)在handleshake1狀態(tài)結(jié)束了,更改為下一個狀態(tài) context.state = eSTATE_HANDSHAKE2 // 下一次讀事件將會調(diào)用 handshake2 函數(shù)}
3.2.2 gethostbyname 是阻塞的瀏覽器<--->代理服務(wù)器<--->餓了么網(wǎng)站考慮一下這個情況,瀏覽器和代理服務(wù)器連通速度很好,收發(fā)很快;而代理服務(wù)器和餓了么站點收發(fā)很慢。這樣一個收發(fā)速度不相等的情況,會出現(xiàn)怎樣的問題?
1. 對于阻塞同步模型,基本上不用考慮這個問題。因為他的收和發(fā)是串行的,這意味著它會自動調(diào)整滑動窗口大小。當代理服務(wù)器收到了瀏覽器的10Kib數(shù)據(jù),代理服務(wù)器就會慢慢發(fā)送這10Kib數(shù)據(jù)給餓了么網(wǎng)站,這時候如果瀏覽器還想發(fā)數(shù)據(jù)給代理服務(wù)器,只會保存在代理服務(wù)器的內(nèi)核緩沖區(qū)里,由于代理服務(wù)器程序在執(zhí)行發(fā)送的任務(wù)(顧不上收數(shù)據(jù)),并沒有從緩沖區(qū)取數(shù)據(jù),緩沖區(qū)的數(shù)據(jù)會越來越多,剩余空間越來越小,在TCP層面,就會通知調(diào)整滑動窗口大小。當讀緩沖區(qū)滿了以后,通知滑動窗口為0,客戶端就會停止發(fā)送數(shù)據(jù)。等代理服務(wù)器發(fā)送完數(shù)據(jù),開始從緩沖區(qū)取瀏覽器的數(shù)據(jù),瀏覽器到代理服務(wù)器的發(fā)送窗口又會從0變大,瀏覽器又可以發(fā)送數(shù)據(jù)了。4.2 TCP 可靠性
2. 對于非阻塞模型,這是一個大問題。由于沒有阻塞功能,代理服務(wù)器會一個勁兒的收下瀏覽器的所有數(shù)據(jù),讀取內(nèi)核緩沖區(qū)的數(shù)據(jù),再轉(zhuǎn)發(fā)數(shù)據(jù),并保存在自己的某個緩沖區(qū)中(sendBuffer)。有點類似于生產(chǎn)者消費者模型,生產(chǎn)得快,消費得慢,內(nèi)存會一直膨脹下去。其實我們很容易做1情況的模擬,只要發(fā)現(xiàn) sendBuffer 過大就停止讀緩沖區(qū)的數(shù)據(jù),等 sendBuffer 消下去了再開始讀就行了。
std::vector<char> msg {5, 'h', 'e', 'l', 'l', 'o'}
第一字節(jié)表示長度(這里是 5 ),后面跟上這個長度的字節(jié)流。接收者先收一字節(jié),然后動態(tài)開辟這個長度的緩沖區(qū),把剩下的收完。uint16_t a = 0x01;
那么在有些機器上,a里存的是0000000000000001, 有些機器是0000000100000000。如果直接就把字節(jié)流傳給對方,說不定對方不是和自己一種字節(jié)序,就會把數(shù)據(jù)認錯。所以需要統(tǒng)一規(guī)定大小端順序,傳到網(wǎng)絡(luò)上統(tǒng)一用一種端序,從網(wǎng)絡(luò)到本機再轉(zhuǎn)換到本機的端序。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]);
根據(jù)機器不同,有可能輸出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);
發(fā)送方代碼如下:// go 偽代碼// 協(xié)議格式 兩個字節(jié)的長度 + 不定長數(shù)據(jù)sendmsg = "hello sunfish gao!";// 將本地序轉(zhuǎn)換成網(wǎng)絡(luò)序 len = htons(sendmsg.size());conn.write([]byte(len))conn.write([]byte(sendmsg))
接收方偽代碼如下:// go 偽代碼// 讀兩個字節(jié),得到接下來的數(shù)據(jù)總長度len = conn.read(2)// 網(wǎng)絡(luò)序轉(zhuǎn)主機序len = ntons(len) // 再讀剩下的字節(jié)數(shù)data = conn.read(len)
5.1.2 以特殊符號分割put key value/r/n當客戶端向服務(wù)端分開發(fā)送如下兩條命令:
get key/r/n
“put key value/r/n”極有可能在服務(wù)端收到一條粘起來的數(shù)據(jù):
“get key/r/n”
“put key value/r/nget key/r/n”甚至是分兩次收到奇怪的分割的數(shù)據(jù):
“put key value/r/nget k”其實各種可能都有,因為 TCP 是字節(jié)流協(xié)議,所以 read 函數(shù)每次讀的長度不是確定的,他不像 WebSocket 能不用擔心「粘包」問題。如果數(shù)據(jù)中存在「空格」、「換行符」這樣的字符,則需要進行字符串替換,也就是「轉(zhuǎn)義」。
“ey/r/n”
關(guān)鍵詞:服務(wù),代理
微信公眾號
版權(quán)所有? 億企邦 1997-2025 保留一切法律許可權(quán)利。