はじめに
以前、UDPのキャプチャ&リプレイを試したので、今度はTCPのキャプチャ&リプレイを実装しました。UDPはコネクションレスであり、TCPはコネクション方式なので、その部分だけ書き直しています。
なんで私がキャプチャ&リプレイについて調査しているのかというと、スタブを制作したいからです。TCPやUDPの開発をする際に、理屈としてはサーバ側がなくても仕様書があれば開発できるはずですが、実際には動作確認しながら使いたいもの。ただ、いつもサーバが近くにあれば良いのですが、ハードウェアなどに依存していることがあり、その借用期間が限られていることから、借用期間内に応答値をキャプチャしておき、返却後もキャプチャしたログを使って開発を続けることを想定しています。
環境
OS X El Capitan 10.11
Python 2.7.11
動作イメージ
+-----------+ +-----------------+ +-----------+
|HTTP Client|---->|Capture_Replay.py|---->|HTTP Server|
| |<----| |<----| |
+-----------+ +--------|--------+ +-----------+
v
+---------------+
|capture_tcp.log|
+---------------+
+-----------+ +-----------------+
|HTTP Client|---->|Capture_Replay.py|
| |<----| |
+-----------+ +-----------------+
^
+-------|-------+
|capture_tcp.log|
+---------------+
+-----------+ +-----------+ +-----------+
|HTTP Client| |Capture & | |HTTP Server|
| | |Replay | | |
+-----+-----+ +-----+-----+ +-----+-----+
| | |
| | | start server
| | | ----+
| | | <---+
| | bind |
| |----+ |
| |<---+ |
| | listen |
| |----+ |
| |<---+ |
| connect | |
|--------------->| |
| accept | |
|<---------------| |
| send/recv | |
|--------------->| |
| | connect |
| |--------------->|
| | accept |
| |<---------------|
| | send/recv |
| |--------------->|
| | recv/send |
| |<---------------|
| | close |
| |--------------->|
| recv/send | |
|<---------------| |
| close | |
|<---------------| |
| | |
| | |
実装
import socket
import urllib
stub_mode = 'replay'
#stub_mode = 'capture'
TCP_IP_LISTEN = "localhost"
TCP_PORT_LISTEN = 60001
TCP_IP_TO = "localhost"
TCP_PORT_TO = 60000
BUFFER_SIZE = 4096
CAPTURE_LOG_FILENAME = 'capture_tcp.log'
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # TCP
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind((TCP_IP_LISTEN, TCP_PORT_LISTEN))
sock.listen(5)
cache = []
captured_data = []
captured_data_dic = {}
if stub_mode == 'capture':
f = open(CAPTURE_LOG_FILENAME, 'a')
elif stub_mode == 'replay':
f = open(CAPTURE_LOG_FILENAME, 'r')
captured_data = f.readlines()
for i in range(len(captured_data)):
if 'Requested' in captured_data[i]:
key = urllib.unquote(captured_data[i+3].rstrip())
value = urllib.unquote(captured_data[i+3+4].rstrip())
captured_data_dic[key] = value
while True:
if stub_mode == 'capture':
# send to remote server
conn, (host_request, port_request) = sock.accept()
data_request = ''
while True:
tmp = conn.recv(BUFFER_SIZE)
data_request += tmp
if b'\r\n\r\n' in tmp or tmp == b'\r\n' or tmp == '':
break
print 'Requested:'
print data_request, host_request, port_request
print urllib.quote(data_request), host_request, port_request
print
f.write(urllib.quote('Requested data') + '\n')
f.write(urllib.quote(host_request) + '\n')
f.write(urllib.quote(str(port_request)) + '\n')
f.write(urllib.quote(data_request) + '\n')
sock_remote = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # TCP
sock_remote.connect((TCP_IP_TO, TCP_PORT_TO))
sock_remote.send(data_request)
# receive from remote server
data_response = ''
while True:
tmp = sock_remote.recv(BUFFER_SIZE)
data_response += tmp
if b'\r\n\r\n' in tmp or tmp == b'\r\n' or tmp == '':
break
sock_remote.close()
f.write(urllib.quote('Responsed data') + '\n')
f.write(urllib.quote(TCP_IP_TO) + '\n')
f.write(urllib.quote(str(TCP_PORT_TO)) + '\n')
f.write(urllib.quote(data_response) + '\n')
f.write('\n')
conn.send(data_response)
print 'responsed:'
print data_response, TCP_IP_LISTEN, port_request
print urllib.quote(data_response), TCP_IP_LISTEN, port_request
print '---------------------------'
conn.close()
elif stub_mode == 'replay':
conn, (host_request, port_request) = sock.accept()
data_request = ''
while True:
tmp = conn.recv(BUFFER_SIZE)
data_request += tmp
if b'\r\n\r\n' in tmp or tmp == b'\r\n' or tmp == '':
break
print 'Requested:'
print data_request, host_request, port_request
print urllib.quote(data_request), host_request, port_request
print
try:
ret_data = captured_data_dic[data_request]
conn.send(ret_data)
print 'responsed:'
print ret_data, TCP_IP_LISTEN, port_request
print urllib.quote(ret_data), TCP_IP_LISTEN, port_request
print '---------------------------'
except:
ret_data = 'not found'
print 'ERROR: Key error'
finally:
conn.close()
else:
print 'error'
break
解説1
socket.SO_REUSEADDRは、多重でbindするための設定。
通常はソケット通信を終わらせる際にきちんとcloseさせるのですが、デバッグ中などは例外で終了してしまいきちんとcloseできないケースが多いので、このフラグをオンにしています。
http://stackoverflow.com/questions/29217502/socket-error-address-already-in-use
解説2
conn.recvは、リクエストの終端が\r\n\r\nであることから、それを受けるまでは受信を続けるようにしています。ただ、クライアントによっては、1回ですべての受信できる場合(通常のブラウザ)や、1行ずつ受信(Telnet)する場合があるので、クアイアントからの送信形態に合わせて、終端を検出するようにしています。
ちなみに、毎回全データの終端をチェックすれば検出パターンは\r\n\r\nだけで済みます。
解説3
HTTPクライアントがResponseを受信できたらコネクションをクローズするようにしています。コネクションを維持させるばあいは、サーバ側から切断される場合もあるので、そのための考慮が必要です。
解説4
TCPはUDPと異なり、コネクションをクローズさせたら、再度ソケットからの作成が必要になります。connectからではダメなようです。
準備
PythonのSimpleHTTPServerを使って動作確認をします。
表示するHTMLを準備。
<HTML><HEAD></HEAD><BODY>TEST</BODY></HTML>
SimpleHTTPSeverを起動。
$ python -m SimpleHTTPServer 60000
Serving HTTP on 0.0.0.0 port 60000 ...
キャプチャ&リプレイを起動。
$ sudo python tcp_capture_replay.py
動作確認
適当なHTTPクライアントを用意するのが面倒だったので、TelnetでHTTPリクエストを送信しました。
Captureモード
$ telnet 127.0.0.1 60001
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET /
<HTML><HEAD></HEAD><BODY>TEST</BODY></HTML>
Connection closed by foreign host.
$ sudo python tcp_capture_replay.py
Requested:
GET /
127.0.0.1 56607
GET%20/%0D%0A%0D%0A 127.0.0.1 56607
responsed:
<HTML><HEAD></HEAD><BODY>TEST</BODY></HTML>
localhost 56607
%3CHTML%3E%3CHEAD%3E%3C/HEAD%3E%3CBODY%3ETEST%3C/BODY%3E%3C/HTML%3E%0A localhost 56607
Replayモード
$ telnet 127.0.0.1 60001
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
GET /
<HTML><HEAD></HEAD><BODY>TEST</BODY></HTML>
Connection closed by foreign host.
$ sudo python tcp_capture_replay.py
Requested:
GET /
127.0.0.1 56611
GET%20/%0D%0A%0D%0A 127.0.0.1 56611
responsed:
<HTML><HEAD></HEAD><BODY>TEST</BODY></HTML>
localhost 56611
%3CHTML%3E%3CHEAD%3E%3C/HEAD%3E%3CBODY%3ETEST%3C/BODY%3E%3C/HTML%3E%0A localhost 56611
今後
この方法は、SSL上でも使えますし、SOAPやRESTでも使える方法なので、応用していきたいと思います。
参考
https://docs.python.org/2/howto/sockets.html
http://qiita.com/higuma/items/b23ca9d96dac49999ab9
http://stackoverflow.com/questions/29217502/socket-error-address-already-in-use
http://memo.saitodev.com/home/python_network_programing/