GoのTCP / IPプロキシ

私は新しい言語を学ぶための私のお気に入りのタスクに戻りました。 Goブログのエンジンを書いたもう一度指を伸ばしたいと思いました。病気のTCP / IPプロキシ/デバッガーはGoで書かれています。



要するに、TCP / IPプロキシは、接続を受け入れて指定されたアドレスに転送できるプログラムです。 途中で、送信されたデータのログが保持されます。 これは、さまざまな自家製ネットワークプロトコルをデバッグするときに非常に便利です。



機能に関しては、 ErlangバージョンのようなGoバージョンには、双方向の16進ダンプと、リモートホストへの「from」と「to」の両方向のバイナリログの3つのログがあります。 Pythonバージョンはバイナリログを導きません。



もちろん、すべてがマルチスレッドです。 また、GoプログラミングはGoで非常にシンプル(かつ安全)であるため、各接続の並列アクティビティの数はErlangバージョンよりもさらに多くなります。



Erlangでは、次の4つのフローが各接続で機能しました。



Goのバージョンは少し異なります。



合計5。



どちらの場合も、メッセージをロガーストリームに送信して、ストリームログデータを読み取ります。 もちろん、ミューテックスや条件変数のような愚かなことはありません。 一致する問題は、Goチャンネルを通じてエレガントに解決されます。



以下はソースコードです。 リポジトリ内のコメントとは異なり、コメントが豊富にあります。 Goにあまり詳しくない人にとっては、興味深い点がいくつかあります。



package main import ( "flag" "fmt" "net" "os" "strings" "time" "encoding/hex" "runtime" ) var ( host *string = flag.String("host", "", "target host or address") port *string = flag.String("port", "0", "target port") listen_port *string = flag.String("listen_port", "0", "listen port") ) func die(format string, v ...interface{}) { os.Stderr.WriteString(fmt.Sprintf(format+"\n", v...)) os.Exit(1) } //       . func connection_logger(data chan []byte, conn_n int, local_info, remote_info string) { log_name := fmt.Sprintf("log-%s-%04d-%s-%s.log", format_time(time.Now()), conn_n, local_info, remote_info) logger_loop(data, log_name) } //     . func binary_logger(data chan []byte, conn_n int, peer string) { log_name := fmt.Sprintf("log-binary-%s-%04d-%s.log", format_time(time.Now()), conn_n, peer) logger_loop(data, log_name) } //     .  -   //  .   -      //  .     - . // func logger_loop(data chan []byte, log_name string) { f, err := os.Create(log_name) if err != nil { die("Unable to create file %s, %v\n", log_name, err) } defer f.Close() //      . for { b := <-data if len(b) == 0 { break } f.Write(b) f.Sync() //    flush'. } } func format_time(t time.Time) string { return t.Format("2006.01.02-15.04.05") } func printable_addr(a net.Addr) string { return strings.Replace(a.String(), ":", "-", -1) } // ,     . ,  //    . type Channel struct { from, to net.Conn logger, binary_logger chan []byte ack chan bool } // , ""         . //   . func pass_through(c *Channel) { from_peer := printable_addr(c.from.LocalAddr()) to_peer := printable_addr(c.to.LocalAddr()) b := make([]byte, 10240) offset := 0 packet_n := 0 for { n, err := c.from.Read(b) if err != nil { c.logger <- []byte(fmt.Sprintf("Disconnected from %s\n", from_peer)) break } if n > 0 { //  - ,      . c.logger <- []byte(fmt.Sprintf("Received (#%d, %08X) %d bytes from %s\n", packet_n, offset, n, from_peer)) //  ,      hex-. ,   ? c.logger <- []byte(hex.Dump(b[:n])) c.binary_logger <- b[:n] c.to.Write(b[:n]) c.logger <- []byte(fmt.Sprintf("Sent (#%d) to %s\n", packet_n, to_peer)) offset += n packet_n += 1 } } c.from.Close() c.to.Close() c.ack <- true //     ,   . } //    .      //  . func process_connection(local net.Conn, conn_n int, target string) { //    ,    . remote, err := net.Dial("tcp", target) if err != nil { fmt.Printf("Unable to connect to %s, %v\n", target, err) } local_info := printable_addr(remote.LocalAddr()) remote_info := printable_addr(remote.RemoteAddr()) //   . started := time.Now() //      . logger := make(chan []byte) from_logger := make(chan []byte) to_logger := make(chan []byte) //       . ack := make(chan bool) //  . go connection_logger(logger, conn_n, local_info, remote_info) go binary_logger(from_logger, conn_n, local_info) go binary_logger(to_logger, conn_n, remote_info) logger <- []byte(fmt.Sprintf("Connected to %s at %s\n", target, format_time(started))) //   . go pass_through(&Channel{remote, local, logger, to_logger, ack}) go pass_through(&Channel{local, remote, logger, from_logger, ack}) //     . <-ack <-ack //   . finished := time.Now() duration := finished.Sub(started) logger <- []byte(fmt.Sprintf("Finished at %s, duration %s\n", format_time(started), duration.String())) //    .       // ,         ,    //   . logger <- []byte{} from_logger <- []byte{} to_logger <- []byte{} } func main() { //  Go      . runtime.GOMAXPROCS(runtime.NumCPU()) //    (,   ?) flag.Parse() if flag.NFlag() != 3 { fmt.Printf("usage: gotcpspy -host target_host -port target_port -listen_post=local_port\n") flag.PrintDefaults() os.Exit(1) } target := net.JoinHostPort(*host, *port) fmt.Printf("Start listening on port %s and forwarding data to %s\n", *listen_port, target) ln, err := net.Listen("tcp", ":"+*listen_port) if err != nil { fmt.Printf("Unable to start listener, %v\n", err) os.Exit(1) } conn_n := 1 for { //   . if conn, err := ln.Accept(); err == nil { //    . go process_connection(conn, conn_n, target) conn_n += 1 } else { fmt.Printf("Accept failed, %v\n", err) } } }
      
      





繰り返しますが、各接続は5つのスレッドによって提供されます。 そして、私は楽しみのためにそれをしませんでした。 論理的には、並行して実行することが論理的に独立したサブタスクが論理的に存在するように思えました。 私がすべてをC ++ / boostで書いた場合、私はおそらくすべての接続ごとに1つのスレッドですべてを泥だらけにしたでしょう(またはおそらく、プログラム全体がいくつかの洗練された多重化ライブラリを介して純粋にシングルスレッドになるでしょう)、そして最後にC ++でまた、スレッドが1つでも高速に動作します。 しかし、これは私が言いたいことではありません。 マルチスレッドプログラミングのプッシュを実行します(C ++などのプッシュではなく、新しい標準のステロイドでも)。 何らかの方法で、便利なマルチスレッドが重要な要素になるタスクがあります。



次のように実行できます(少なくともGoリリース1が必要です):



 go run gotcpspy.go -host pop.yandex.ru -port 110 -local_port 8080
      
      





表示されます:



 Start listening on port 8080 and forwarding data to pop.yandex.ru:110
      
      





次に、別のウィンドウに入力する場合:



 telnet localhost 8080
      
      





たとえば、「 USER test



」「 ENTER



」および「 PASS none



」「 ENTER



」と入力すると、3つのログが作成されます(名前の日付はもちろん異なります)。



一般的なログはlog-2012.04.20-19.55.17-0001-192.168.1.41-49544-213.180.204.37-110.log



です:



  Connected to pop.yandex.ru:110 at 2012.04.20-19.55.17 Received (#0, 00000000) 38 bytes from 192.168.1.41-49544 00000000 2b 4f 4b 20 50 4f 50 20 59 61 21 20 76 31 2e 30 |+OK POP Ya! v1.0| 00000010 2e 30 6e 61 40 32 36 20 48 74 6a 4a 69 74 63 50 |.0na@26 HtjJitcP| 00000020 52 75 51 31 0d 0a |RuQ1..| Sent (#0) to [--1]-8080 Received (#0, 00000000) 11 bytes from [--1]-8080 00000000 55 53 45 52 20 74 65 73 74 0d 0a |USER test..| Sent (#0) to 192.168.1.41-49544 Received (#1, 00000026) 23 bytes from 192.168.1.41-49544 00000000 2b 4f 4b 20 70 61 73 73 77 6f 72 64 2c 20 70 6c |+OK password, pl| 00000010 65 61 73 65 2e 0d 0a |ease...| Sent (#1) to [--1]-8080 Received (#1, 0000000B) 11 bytes from [--1]-8080 00000000 50 41 53 53 20 6e 6f 6e 65 0d 0a |PASS none..| Sent (#1) to 192.168.1.41-49544 Received (#2, 0000003D) 72 bytes from 192.168.1.41-49544 00000000 2d 45 52 52 20 5b 41 55 54 48 5d 20 6c 6f 67 69 |-ERR [AUTH] logi| 00000010 6e 20 66 61 69 6c 75 72 65 20 6f 72 20 50 4f 50 |n failure or POP| 00000020 33 20 64 69 73 61 62 6c 65 64 2c 20 74 72 79 20 |3 disabled, try | 00000030 6c 61 74 65 72 2e 20 73 63 3d 48 74 6a 4a 69 74 |later. sc=HtjJit| 00000040 63 50 52 75 51 31 0d 0a |cPRuQ1..| Sent (#2) to [--1]-8080 Disconnected from 192.168.1.41-49544 Disconnected from [--1]-8080 Finished at 2012.04.20-19.55.17, duration 5.253979s
      
      





発信バイナリログlog-binary-2012.04.20-19.55.17-0001-192.168.1.41-49544.log







  USER test PASS none
      
      





バイナリ入力log-binary-2012.04.20-19.55.17-0001-213.180.204.37-110.log







  +OK POP Ya! v1.0.0na@26 HtjJitcPRuQ1 +OK password, please. -ERR [AUTH] login failure or POP3 disabled, try later. sc=HtjJitcPRuQ1
      
      





次に、パフォーマンスを測定します。 ファイルを直接ポンプし、このプログラムを介してポンプします。



直接ダウンロード(ファイルサイズは約72MB):



  time wget http://www.erlang.org/download/otp_src_R15B01.tar.gz ... Saving to: `otp_src_R15B01.tar.gz' ... real 1m2.819s
      
      





プログラムを実行して、事前に実行します。



  go run gotcpspy.go -host=www.erlang.org -port=80 -listen_port=8080
      
      





ダウンロード:



  time wget http://localhost:8080/download/otp_src_R15B01.tar.gz ... Saving to: `otp_src_R15B01.tar.gz.1' ... real 0m56.209s
      
      





念のため、結果を比較できます。



  diff otp_src_R15B01.tar.gz otp_src_R15B01.tar.gz.1


私のファイルは同じなので、すべてが正しく機能します。



今がその時です。 この実験を(Mac Airで)数回繰り返しましたが、驚くべきことに、プログラムを介したダウンロードは常にそれほど遅くなく、少し速くさえありました。 たとえば、直接-1m2.819s、プログラムを通じて-0m56.209s。 唯一の説明は、wgetはおそらく1つのスレッドで動作し、プログラムは2つのスレッドでローカルソケットとリモートソケットからデータを受信するため、わずかな加速が得られるということです。 ただし、違いはまだ最小限であり、別のマシンまたはネットワークでは見えない場合がありますが、主なことは、転送プロセス中に非常に大規模なログが作成されるにもかかわらず、少なくとも直接的な速度で動作することです。



そのため、このようなプログラムの3つのオプションのうち、Python、Erlang、Goでは、後者が最も気に入っています。



Goでの並列処理の良い実験のように思えました。



関連する投稿





リポジトリリンク





PS



ちなみに、Javistsの1人が同様のプログラム(可能であれば、Eclipse / IDEA / ant / maven / spring / log4j / ivyなどを必要としない)を恥ずかしく思うなら、比較するのは非常に興味深いでしょう。 そして、効率とスピードの面ではなく、美しさ、優雅さの面で。



All Articles