前兩天在B站上看到一個(gè)小伙紙100元組裝個(gè)電腦打LOL畫質(zhì)流暢,突發(fā)奇想100行代碼能(簡單)實(shí)現(xiàn)個(gè)啥好玩的。我主要是做php開發(fā)的,于是就有了本文。
當(dāng)然,由于php(不算swoole擴(kuò)展)本身不擅長做網(wǎng)絡(luò)服務(wù)端編程,所以這個(gè)代理,只是個(gè)玩具,離日常使用有點(diǎn)距離。如果想使用穩(wěn)定可靠的加密(所以能禾斗學(xué)上網(wǎng))代理,可以用這個(gè):https://github.com/momaer/asocks-go也是100來行代碼使用go實(shí)現(xiàn)。
寫的過程中發(fā)現(xiàn)php多線程還是難的。比如我開始想每個(gè)連接新建一個(gè)線程。但這個(gè)線程得保存起來(比如保存到數(shù)組),比如官方例子中的這個(gè):https://github.com/krakjoe/pthreads/blob/master/examples/SocketServer.php 要放到$clients這個(gè)數(shù)組里,不然,你試試(curl -L一個(gè)要301的地址)就知道出現(xiàn)什么情況了。
這個(gè)例子說了in the real world, do something here to ensure clients not running are destroyed 但是,如何把不再運(yùn)行的連接銷毀卻沒有講。恩。我試了把$clients放到一個(gè)類里,把類傳給線程類,然后在線程類要結(jié)束時(shí)把$clients里對應(yīng)的連接給unset掉,無果。
那,以下就是使用線程池來實(shí)現(xiàn)的代理,按道理講,退出時(shí)池要shutdown(),監(jiān)聽socket也要shutdown的,但百行代碼,就不勉強(qiáng)了,隨著ctrl + c,就讓操作系統(tǒng)來回收資源吧。
php不擅長網(wǎng)絡(luò)編程體現(xiàn)在哪里呢?首先我用的是stream_socket_XXX相關(guān)的函數(shù),為啥不用socket擴(kuò)展呢?因?yàn)閟ocket擴(kuò)展有問題,參見:https://github.com/krakjoe/pthreads/issues/581 而stream_set_timeout對stream_socket_recvfrom這些高級操作,不起作用,參見:http://php.net/manual/en/function.stream-set-timeout.php 而這些,在寫代理時(shí)都需要考慮的。比如連接遠(yuǎn)程目標(biāo)服務(wù)器時(shí),沒有超時(shí)控制,很容易就線程池跑滿了。
測試的話,使用curl即可,對了,目前只支持遠(yuǎn)程dns解析,為啥呢?因?yàn)檫@個(gè)玩具后期可是要實(shí)現(xiàn)禾斗學(xué)上網(wǎng)的喲: curl --socks5-hostname 127.0.0.1:1080 http://ip.cn
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
Class Pipe extends Threaded { private $client ; private $remote ; public function __construct( $client , $remote ) { $this ->client = $client ; $this ->remote = $remote ; } public function run() { for ( ; ; ) { $data = stream_socket_recvfrom( $this ->client, 4096); if ( $data === false || strlen ( $data ) === 0) { break ; } $sendBytes = stream_socket_sendto( $this ->remote, $data ); if ( $sendBytes <= 0) { break ; } } stream_socket_shutdown( $this ->client, STREAM_SHUT_RD); stream_socket_shutdown( $this ->remote, STREAM_SHUT_WR); } } Class Client extends Threaded { public $fd ; public function __construct( $fd ) { $this ->fd = $fd ; } public function run() { $data = stream_socket_recvfrom( $this ->fd, 2); $data = unpack( 'c*' , $data ); if ( $data [1] !== 0x05) { stream_socket_shutdown( $this ->fd, STREAM_SHUT_RDWR); echo '協(xié)議不正確.' , PHP_EOL; return ; } $nmethods = $data [2]; $data = stream_socket_recvfrom( $this ->fd, $nmethods ); stream_socket_sendto( $this ->fd, "\x05\x00" ); $data = stream_socket_recvfrom( $this ->fd, 4); $data = unpack( 'c*' , $data ); $addressType = $data [4]; if ( $addressType === 0x03) { // domain $domainLength = unpack( 'c' , stream_socket_recvfrom( $this ->fd, 1))[1]; $data = stream_socket_recvfrom( $this ->fd, $domainLength + 2); $domain = substr ( $data , 0, $domainLength ); $port = unpack( "n" , substr ( $data , -2))[1]; } else { stream_socket_shutdown( $this ->fd, STREAM_SHUT_RDWR); echo '請使用遠(yuǎn)程dns解析.' , PHP_EOL; } stream_socket_sendto( $this ->fd, "\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00" ); echo "{$domain}:{$port}" , PHP_EOL; $remote = stream_socket_client( "tcp://{$domain}:{$port}" ); if ( $remote === false) { stream_socket_shutdown( $this ->fd, STREAM_SHUT_RDWR); return ; } $pool = $this ->worker->pipePool; $pipe1 = new Pipe( $remote , $this ->fd); $pipe2 = new Pipe( $this ->fd, $remote ); $pool ->submit( $pipe1 ); $pool ->submit( $pipe2 ); } } class ProxyWorker extends Worker { public $pipePool ; public function __construct( $pipePool ) { $this ->pipePool = $pipePool ; } } $server = stream_socket_server( 'tcp://0.0.0.0:1080' , $errno , $errstr ); if ( $server === false) exit ( $errstr ); $pipePool = new Pool(200, Worker:: class ); $pool = new Pool(50, 'ProxyWorker' , [ $pipePool ]); for ( ; ; ) { $fd = @stream_socket_accept( $server , 60); if ( $fd === false) continue ; $pool ->submit( new Client( $fd )); } |