上個周,老張寫了一篇文章《吃透FTP》(沒看過的同學可以先點擊瀏覽一下)。文章主要介紹了FTP的工作原理,寫完之後覺得不過癮,自己動手實現了一個玩具版的FTP服務。
當然,如果實現一個完整穩定的FTP服務,工作量還是相當龐大的。所以老張選擇了利用Python實現一個玩具版來過過癮,寫完發現僅有200行代碼。
所謂玩具版,就是說:
-
用戶登錄。使用預製的賬號root,並沒有使用系統賬號
-
僅支持主動模式。
-
僅支持Binary模式。
-
僅支持文件的上傳和下載。
-
單線程。
Talk is cheap,直接看代碼。
#coding=utf-8
# FtpServer.py
# 一個玩具版的Ftp服務
# by 魔笛手CTO
import socket
import os
import six
END_FLAG = "\r\n"
ASCII_MODE = "II"
BINARY_MODE = "I"
def dump(string):
"""將字符串消息dump爲網絡序的字節"""
if six.PY2:
return string
return bytes(string, "utf-8")
def load(byte):
"""將字節消息load爲字符串"""
if six.PY2:
return byte
return str(byte, "utf-8")
class FtpServer():
def __init__(self):
self.cmd_socket = None
self.ftp_users = {"root": "root"} # 允許登錄的ftp賬號密碼
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
"""確保服務關閉"""
try:
self.cmd_socket.close()
print("socket is closed")
except:
pass
def run(self):
"""啓動服務,開啓21端口監聽"""
print("starting server on port 21...")
self.cmd_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # TCP
self.cmd_socket.bind(("0.0.0.0", 21))
self.cmd_socket.listen(1)
while True:
conn, addr = self.cmd_socket.accept()
self._handle(conn, addr)
def _close_conn(self, conn):
"""關閉指定連接"""
conn.close()
def _handle(self, conn, addr):
"""一旦同客戶端建立連接,將有handle負責處理交互"""
user = None
password = None
authed = False
client_data_addr = None
self._say_hello(conn)
while True:
req = self._read_req(conn)
# 返回空字符串時,關閉連接,準備響應下一個連接
if req == "":
return self._close_conn(conn)
# 解析並響應客戶端命令
cmd, arg = self._parse(req)
if cmd == "USER":
if not authed:
user = arg
resp = "331 Please specify the password"
else:
resp = "500 User has authed!"
elif cmd == "PASS":
if user and not authed:
password = arg
if self._auth(user, password):
authed = True
resp = "230 Login successful"
else:
resp = "500 Auth error"
else:
resp = "500 User is not specified or has login"
# binary模式和ascii模式, 當前僅支持binary模式
elif cmd == "TYPE":
if arg == ASCII_MODE:
resp = "500 Only support binary mode"
elif arg == BINARY_MODE:
resp = "200 Switching to binary mode"
# 主動模式下客戶端的端口號
elif cmd == "PORT":
if not authed:
resp = "530 Not login"
else:
client_data_addr = self._parse_addr(arg)
resp = "200 PORT command successful"
# 上傳文件
elif cmd == "STOR":
if not authed:
resp = "530 Not login"
else:
resp = "150 Ok to send data"
self._send_resp(conn, resp)
self._save_file(arg, client_data_addr)
resp = "226 Transfer complete"
# 下載文件
elif cmd == "RETR":
if not authed:
resp = "530 Not login"
else:
if not os.path.exists(arg):
resp = "550 File not exist"
else:
resp = "150 Ok to send data"
self._send_resp(conn, resp)
self._send_file(arg, client_data_addr)
resp = "226 Transfer complete"
else:
print("500 Unknown command")
# 發送響應
self._send_resp(conn, resp)
def _create_data_conn(self, host, port):
"""主動模式下建立數據通道"""
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(("0.0.0.0", 20))
sock.connect((host, port))
return sock
def _send_file(self, filename, client_data_add):
"""傳輸指定文件至客戶端"""
conn = self._create_data_conn(*client_data_add)
with open(filename, "rb") as f:
conn.sendall(f.read())
conn.close()
def _save_file(self, filename, client_data_addr):
"""保存客戶端上傳的文件"""
conn = self._create_data_conn(*client_data_addr)
with open(filename, "wb") as f:
while True:
body = conn.recv(1)
if body == b'':
break
f.write(body)
conn.close()
def _read_req(self, conn):
"""讀取請求消息"""
print("reading msg...")
msg = ""
while True:
body = load(conn.recv(1))
# 當客戶端關閉連接時,body爲空字符串
if body == "":
return body
msg += body
if msg.endswith(END_FLAG):
break
return msg
def _send_resp(self, conn, msg):
"""發送命令響應"""
print("ready to response:%s" % msg)
if not msg.endswith(END_FLAG):
msg += END_FLAG
conn.sendall(dump(msg))
def _auth(self, user, password):
"""登錄用戶認證"""
if user and self.ftp_users.get(user) and self.ftp_users.get(user) == password:
return True
return False
def _say_hello(self, conn):
"""發送歡迎語"""
self._send_resp(conn, "220 Hello!")
def _parse(self, msg):
"""解析客戶端消息, 返回命令和參數"""
print("receive msg:%s" % msg)
msg = msg.strip()
args = msg.split(" ")
if len(args) == 2:
cmd, arg = args
return cmd, arg
return None, None
def _parse_addr(self, addr):
"""解析ip和端口號"""
args = addr.strip().split(",")
host = ".".join(args[:4])
port = int(args[4]) * 256 + int(args[5])
return host, port
if __name__ == "__main__":
with FtpServer() as server:
server.run()
然後爲了驗證程序是否能夠正常工作,老張使用Python自帶的ftplib來測試服務是否可用。
#coding=utf-8
import ftplib
# 登錄FTP服務,使用主動模式
ftp = ftplib.FTP()
ftp.connect("127.0.0.1", 21)
ftp.login("root", "root")
ftp.set_pasv(False)
# 將本地文件上傳至服務器
with open("client" , 'rb') as f:
ftp.storbinary("STOR upload_from_client", f, 1024)
# 下載服務器文件
with open("download_from_server", "wb") as f:
ftp.retrbinary("RETR server", f.write)
最後,如果有同學對老張的玩具版FTP感興趣,老張把代碼上傳到GitHub了,可以點擊https://github.com/niujiacun/FtpServer