前两天在B站上看到一个小伙纸100元组装个电脑打LOL画质流畅,突发奇想100行代码能(简单)实现个啥好玩的。我主要是做php开发的,于是就有了本文。
当然,由于php(不算swoole扩展)本身不擅长做网络服务端编程,所以这个代理,只是个玩具,离日常使用有点距离。如果想使用稳定可靠的加密(所以能禾斗学上网)代理,可以用这个:https://github.com/momaer/asocks-go也是100来行代码使用go实现。
写的过程中发现php多线程还是难的。比如我开始想每个连接新建一个线程。但这个线程得保存起来(比如保存到数组),比如官方例子中的这个:https://github.com/krakjoe/pthreads/blob/master/examples/SocketServer.php 要放到$clients这个数组里,不然,你试试(curl -L一个要301的地址)就知道出现什么情况了。
这个例子说了in the real world, do something here to ensure clients not running are destroyed 但是,如何把不再运行的连接销毁却没有讲。恩。我试了把$clients放到一个类里,把类传给线程类,然后在线程类要结束时把$clients里对应的连接给unset掉,无果。
那,以下就是使用线程池来实现的代理,按道理讲,退出时池要shutdown(),监听socket也要shutdown的,但百行代码,就不勉强了,随着ctrl + c,就让操作系统来回收资源吧。
php不擅长网络编程体现在哪里呢?首先我用的是stream_socket_XXX相关的函数,为啥不用socket扩展呢?因为socket扩展有问题,参见:https://github.com/krakjoe/pthreads/issues/581 而stream_set_timeout对stream_socket_recvfrom这些高级操作,不起作用,参见:http://php.net/manual/en/function.stream-set-timeout.php 而这些,在写代理时都需要考虑的。比如连接远程目标服务器时,没有超时控制,很容易就线程池跑满了。
测试的话,使用curl即可,对了,目前只支持远程dns解析,为啥呢?因为这个玩具后期可是要实现禾斗学上网的哟: 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 '协议不正确.' , 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 '请使用远程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 ));
}
|