07-2. SFTP 協定:安全的檔案傳輸
基於 SSH 的加密檔案傳輸協定
目錄
🔐 SFTP 協定:安全的檔案傳輸
SFTP 在網路模型中的位置
┌──────────────────────────────────────────────────────────┐
│ OSI 七層模型 TCP/IP 四層模型 │
├──────────────────────────────────────────────────────────┤
│ 7. 應用層 (Application) │
│ ├─ SFTP ───────────────┐ 應用層 (Application) │
│ │ (SFTP, HTTP, FTP...) │
├─────────────────────────────┤ │
│ 6. 表現層 (Presentation) │ │
│ ├─ SSH 加密 │ │
├─────────────────────────────┤ │
│ 5. 會話層 (Session) │ │
│ ├─ SSH Session │ │
├─────────────────────────────┼─────────────────────────────┤
│ 4. 傳輸層 (Transport) │ 傳輸層 (Transport) │
│ └─ TCP ─────────────────┘ (TCP) │
├─────────────────────────────┼─────────────────────────────┤
│ 3. 網路層 (Network) │ 網際網路層 (Internet) │
│ └─ IP │ (IP, ICMP, ARP) │
├─────────────────────────────┼─────────────────────────────┤
│ 2. 資料連結層 (Data Link) │ 網路存取層 │
│ 1. 實體層 (Physical) │ (Network Access) │
└─────────────────────────────┴─────────────────────────────┘
📍 位置:OSI Layer 7(應用層)/ TCP/IP Layer 4(應用層)
🔌 Port:22(SSH)
🚛 傳輸協定:TCP(透過 SSH 通道)為什麼 SFTP 用 TCP?
| 原因 | 說明 |
|---|---|
| 檔案完整性 📄 | 檔案傳輸不能有任何遺失或錯誤 |
| 順序保證 🔢 | 檔案內容必須按順序接收 |
| 可靠傳輸 ✅ | TCP 提供錯誤檢測和重傳機制 |
| SSH 要求 🔐 | SSH 協定建立在 TCP 之上 |
| 流量控制 🎚️ | 大檔案傳輸需要 TCP 的流量控制 |
💡 重點:SFTP 是透過 SSH 通道傳輸,所有資料都經過加密,確保安全性
🎯 什麼是 SFTP?
💡 比喻:加密的快遞服務
FTP = 明信片(任何人都能看到內容)
SFTP = 密封信件(只有收件人能解開)
FTP:帳號、密碼、檔案內容都是明文
SFTP:全程加密,無法被竊聽SFTP(SSH File Transfer Protocol) 是一種透過 SSH 協定提供安全檔案傳輸的網路協定。與 FTP 完全不同,SFTP 提供加密的檔案存取、傳輸和管理功能。
為什麼需要 SFTP?
FTP 的安全問題:
# 使用 Wireshark 監聽 FTP
tcpdump -i eth0 port 21 -A
# 可以看到:
USER alice ← 使用者名稱明文
PASS secret123 ← 密碼明文!
STOR confidential.pdf ← 檔案名稱明文
(檔案內容也是明文) ← 可以被竊聽SFTP 的解決方案:
# 使用 Wireshark 監聽 SFTP
tcpdump -i eth0 port 22 -A
# 只能看到:
&*#@!%^...亂碼... ← 全部加密
無法知道:
- 使用者是誰
- 密碼是什麼
- 傳輸了什麼檔案🆚 SFTP vs FTP vs FTPS
| 特性 | FTP | FTPS | SFTP |
|---|---|---|---|
| 全名 | File Transfer Protocol | FTP over SSL/TLS | SSH File Transfer Protocol |
| Port | 21(控制)+ 20(數據) | 990 或 21 | 22(單一) |
| 連線數 | 2 個(控制+數據) | 2 個 | 1 個 |
| 加密 | ❌ 無 | ✅ SSL/TLS | ✅ SSH |
| 認證方式 | 密碼 | 密碼 + 憑證 | 密碼 + 公鑰 |
| 防火牆 | 複雜(雙連線) | 複雜 | 簡單(單連線) |
| 安全性 | ❌ 不安全 | ✅ 安全 | ✅ 安全 |
| 命令加密 | ❌ 否 | ✅ 是 | ✅ 是 |
| 標準化 | RFC 959 | RFC 4217 | RFC 4251-4254 |
重要:SFTP ≠ FTPS
SFTP (SSH File Transfer Protocol):
- 完全基於 SSH
- 與 FTP 毫無關係
- 設計理念完全不同
- 推薦使用 ✅
FTPS (FTP over SSL/TLS):
- FTP + SSL/TLS 加密
- 仍是雙連線架構
- 相容性較好但較複雜
- 較少使用 ⚠️🏗️ SFTP 架構
單一連線設計
客戶端 SSH Server (Port 22)
│ │
├──── SSH 連線建立 ───────────>│
│ 1. 金鑰交換 │
│ 2. 加密協商 │
│ 3. 使用者認證 │
│ │
├──── SSH 通道 (加密) ─────────>│
│ SFTP 命令 & 資料 │
│ • ls, get, put │
│ • 檔案內容 │
│ • 所有資料都加密 │
│ │
│<──── 回應 (加密) ─────────────┤優點:
- ✅ 單一 Port(22),防火牆設定簡單
- ✅ 所有資料加密(命令+內容)
- ✅ 支援多種認證(密碼+公鑰)
- ✅ 可復用 SSH 連線(效率高)
🔑 SFTP 認證方式
1️⃣ 密碼認證
import paramiko
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 使用密碼登入
ssh.connect(
hostname='sftp.example.com',
port=22,
username='alice',
password='secret123'
)
sftp = ssh.open_sftp()問題:
- ⚠️ 密碼可能被猜測
- ⚠️ 需要在程式碼中儲存密碼
2️⃣ 公鑰認證(推薦)
💡 比喻:門鎖和鑰匙
私鑰 = 鑰匙(自己保管,不可外洩)
公鑰 = 鎖(可以給別人,放在伺服器上)
登入時:
1. 伺服器用「公鑰」加密一段資料
2. 客戶端用「私鑰」解密
3. 解密成功 = 證明你有私鑰 = 允許登入生成金鑰對:
# 生成 RSA 金鑰(4096 bits)
ssh-keygen -t rsa -b 4096 -C "alice@example.com"
# 會產生:
# ~/.ssh/id_rsa ← 私鑰(絕對不可外洩)
# ~/.ssh/id_rsa.pub ← 公鑰(可以給伺服器)
# 或使用更安全的 Ed25519
ssh-keygen -t ed25519 -C "alice@example.com"
# ~/.ssh/id_ed25519
# ~/.ssh/id_ed25519.pub安裝公鑰到伺服器:
# 方法 1:使用 ssh-copy-id(推薦)
ssh-copy-id alice@sftp.example.com
# 方法 2:手動複製
cat ~/.ssh/id_rsa.pub | ssh alice@sftp.example.com "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
# 方法 3:直接編輯
# 登入伺服器後:
mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys
# 貼上公鑰內容
chmod 600 ~/.ssh/authorized_keysPython 使用公鑰:
import paramiko
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 載入私鑰
private_key = paramiko.RSAKey.from_private_key_file(
'/home/alice/.ssh/id_rsa',
password='金鑰密碼' # 如果金鑰有設定密碼
)
# 使用公鑰認證
ssh.connect(
hostname='sftp.example.com',
username='alice',
pkey=private_key
)
sftp = ssh.open_sftp()💻 Python 使用 SFTP
安裝套件
pip install paramiko基本連線
import paramiko
# 建立 SSH 客戶端
ssh = paramiko.SSHClient()
# 自動新增主機金鑰(生產環境應驗證)
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 連線
ssh.connect(
hostname='sftp.example.com',
port=22,
username='alice',
password='password'
)
# 開啟 SFTP 會話
sftp = ssh.open_sftp()
# 列出檔案
files = sftp.listdir('.')
for file in files:
print(file)
# 關閉連線
sftp.close()
ssh.close()下載檔案
import paramiko
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect('sftp.example.com', username='alice', password='password')
sftp = ssh.open_sftp()
# 簡單下載
sftp.get('/remote/path/file.txt', 'local_file.txt')
print("下載完成")
# 帶進度條
def progress_callback(transferred, total):
percent = (transferred / total) * 100
print(f"\r下載進度:{percent:.1f}% ({transferred}/{total} bytes)", end='')
sftp.get(
'/remote/path/large_file.zip',
'local_file.zip',
callback=progress_callback
)
print("\n下載完成")
sftp.close()
ssh.close()上傳檔案
sftp = ssh.open_sftp()
# 簡單上傳
sftp.put('local_file.txt', '/remote/path/file.txt')
# 帶進度條
def progress_callback(transferred, total):
percent = (transferred / total) * 100
bar_length = 50
filled = int(bar_length * transferred // total)
bar = '█' * filled + '-' * (bar_length - filled)
print(f"\r上傳進度:[{bar}] {percent:.1f}%", end='')
sftp.put(
'large_file.zip',
'/remote/path/file.zip',
callback=progress_callback
)
print("\n上傳完成")
sftp.close()目錄操作
sftp = ssh.open_sftp()
# 列出目錄(詳細資訊)
for attr in sftp.listdir_attr('.'):
# 檔案類型判斷
if stat.S_ISDIR(attr.st_mode):
file_type = "DIR"
else:
file_type = "FILE"
# 格式化大小
size_mb = attr.st_size / (1024 * 1024)
print(f"{file_type:4} {attr.filename:30} {size_mb:10.2f} MB")
# 切換目錄
sftp.chdir('/var/www/html')
# 取得當前目錄
current_dir = sftp.getcwd()
print(f"當前目錄:{current_dir}")
# 建立目錄
sftp.mkdir('new_folder')
sftp.mkdir('new_folder', mode=0o755) # 設定權限
# 刪除目錄(必須為空)
sftp.rmdir('old_folder')
# 刪除檔案
sftp.remove('file.txt')
# 重新命名/移動
sftp.rename('old.txt', 'new.txt')
# 檢查檔案/目錄是否存在
try:
sftp.stat('file.txt')
print("檔案存在")
except FileNotFoundError:
print("檔案不存在")
# 取得檔案資訊
file_stat = sftp.stat('document.pdf')
print(f"大小:{file_stat.st_size} bytes")
print(f"修改時間:{file_stat.st_mtime}")
sftp.close()斷點續傳
import paramiko
import os
def sftp_resume_download(sftp, remote_file, local_file):
"""SFTP 斷點續傳下載"""
# 取得遠端檔案大小
remote_size = sftp.stat(remote_file).st_size
# 檢查本地檔案
if os.path.exists(local_file):
local_size = os.path.getsize(local_file)
else:
local_size = 0
# 已下載完成
if local_size == remote_size:
print("檔案已下載完成")
return
print(f"已下載:{local_size:,} / {remote_size:,} bytes")
print(f"剩餘:{remote_size - local_size:,} bytes")
# 從中斷處繼續
with open(local_file, 'ab') as local_f:
with sftp.file(remote_file, 'rb') as remote_f:
# 跳到中斷位置
remote_f.seek(local_size)
# 分塊讀取
chunk_size = 1024 * 1024 # 1 MB
while True:
chunk = remote_f.read(chunk_size)
if not chunk:
break
local_f.write(chunk)
# 顯示進度
current = local_size + local_f.tell()
percent = (current / remote_size) * 100
print(f"\r進度:{percent:.1f}%", end='')
print("\n下載完成")
# 使用
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect('sftp.example.com', username='alice', password='password')
sftp = ssh.open_sftp()
# 第一次下載(可能中斷)
try:
sftp_resume_download(sftp, '/remote/large_file.zip', 'local.zip')
except KeyboardInterrupt:
print("\n下載中斷")
# 重新執行,自動續傳
sftp_resume_download(sftp, '/remote/large_file.zip', 'local.zip')
sftp.close()
ssh.close()批次上傳目錄
import paramiko
import os
import stat as stat_module
class SFTPDeployer:
def __init__(self, hostname, username, password=None, private_key_path=None):
self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 使用私鑰或密碼
if private_key_path:
private_key = paramiko.RSAKey.from_private_key_file(private_key_path)
self.ssh.connect(hostname, username=username, pkey=private_key)
else:
self.ssh.connect(hostname, username=username, password=password)
self.sftp = self.ssh.open_sftp()
def upload_directory(self, local_dir, remote_dir, exclude=None):
"""遞迴上傳整個目錄"""
if exclude is None:
exclude = ['.git', 'node_modules', '__pycache__', '.DS_Store']
# 確保遠端目錄存在
try:
self.sftp.mkdir(remote_dir)
except IOError:
pass # 目錄已存在
for item in os.listdir(local_dir):
# 跳過排除的項目
if item in exclude:
continue
local_path = os.path.join(local_dir, item)
remote_path = f"{remote_dir}/{item}".replace('\\', '/')
if os.path.isfile(local_path):
# 上傳檔案
print(f"上傳:{local_path} → {remote_path}")
self.sftp.put(local_path, remote_path)
elif os.path.isdir(local_path):
# 遞迴上傳子目錄
self.upload_directory(local_path, remote_path, exclude)
def download_directory(self, remote_dir, local_dir):
"""遞迴下載整個目錄"""
# 建立本地目錄
os.makedirs(local_dir, exist_ok=True)
for item in self.sftp.listdir_attr(remote_dir):
remote_path = f"{remote_dir}/{item.filename}".replace('\\', '/')
local_path = os.path.join(local_dir, item.filename)
if stat_module.S_ISDIR(item.st_mode):
# 遞迴下載子目錄
self.download_directory(remote_path, local_path)
else:
# 下載檔案
print(f"下載:{remote_path} → {local_path}")
self.sftp.get(remote_path, local_path)
def sync_directory(self, local_dir, remote_dir):
"""同步目錄(只上傳修改過的檔案)"""
for root, dirs, files in os.walk(local_dir):
# 計算相對路徑
rel_path = os.path.relpath(root, local_dir)
if rel_path == '.':
remote_root = remote_dir
else:
remote_root = f"{remote_dir}/{rel_path}".replace('\\', '/')
# 確保遠端目錄存在
try:
self.sftp.mkdir(remote_root)
except IOError:
pass
# 上傳檔案
for file in files:
local_file = os.path.join(root, file)
remote_file = f"{remote_root}/{file}".replace('\\', '/')
# 檢查是否需要上傳
upload_needed = False
try:
remote_stat = self.sftp.stat(remote_file)
local_mtime = os.path.getmtime(local_file)
# 比較修改時間
if local_mtime > remote_stat.st_mtime:
upload_needed = True
reason = "檔案已更新"
except FileNotFoundError:
upload_needed = True
reason = "新檔案"
if upload_needed:
print(f"上傳 ({reason}):{local_file} → {remote_file}")
self.sftp.put(local_file, remote_file)
def close(self):
self.sftp.close()
self.ssh.close()
# 使用範例
deployer = SFTPDeployer(
hostname='server.example.com',
username='deploy_user',
private_key_path='/home/alice/.ssh/id_rsa'
)
# 部署網站
deployer.upload_directory('./dist', '/var/www/html', exclude=['.git', 'node_modules'])
# 同步(只上傳變更的檔案)
deployer.sync_directory('./src', '/var/www/html/src')
deployer.close()🎓 常見面試題
Q1:SFTP 和 FTP 的核心差異是什麼?
答案:
核心差異:安全性與架構
| 特性 | FTP | SFTP |
|---|---|---|
| 基礎協定 | 獨立協定 | 基於 SSH |
| Port | 21 + 20(雙連線) | 22(單連線) |
| 加密 | ❌ 全部明文 | ✅ 全部加密 |
| 認證 | 只有密碼 | 密碼 + 公鑰 |
| 防火牆 | 複雜 | 簡單 |
安全性比較:
# FTP(可被竊聽)
# 使用 Wireshark 可以看到:
USER alice ← 明文
PASS secret123 ← 明文密碼!
STOR file.pdf ← 檔案名明文
(檔案內容) ← 內容也是明文
# SFTP(加密)
# 使用 Wireshark 只能看到:
&*#@!%^... ← 全部亂碼
無法知道任何資訊什麼時候用哪個?
✅ 使用 SFTP:
- 公網環境(預設選擇)
- 傳輸敏感資料
- 需要強認證
- 現代系統
⚠️ 使用 FTP:
- 內網環境(確保安全)
- 極度要求速度
- 舊系統相容性
- 公開下載站台Q2:如何設定 SFTP 公鑰認證?
答案:
完整流程:
# 1. 客戶端生成金鑰對
ssh-keygen -t ed25519 -C "alice@example.com"
# 會產生:
# ~/.ssh/id_ed25519 ← 私鑰(不可外洩!)
# ~/.ssh/id_ed25519.pub ← 公鑰(給伺服器)
# 2. 安裝公鑰到伺服器
ssh-copy-id alice@server.example.com
# 或手動:
cat ~/.ssh/id_ed25519.pub | ssh alice@server.example.com "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
# 3. 測試連線
sftp alice@server.example.com
# 不需要密碼,直接登入成功 ✅伺服器端設定:
# 編輯 SSH 設定
sudo nano /etc/ssh/sshd_config
# 啟用公鑰認證
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
# (可選)禁用密碼認證(更安全)
PasswordAuthentication no
# 重啟 SSH
sudo systemctl restart sshdPython 使用公鑰:
private_key = paramiko.RSAKey.from_private_key_file('~/.ssh/id_rsa')
ssh.connect(hostname='server.com', username='alice', pkey=private_key)為什麼公鑰更安全?
密碼認證:
❌ 可能被暴力破解
❌ 密碼可能外洩
❌ 需要在多處儲存密碼
公鑰認證:
✅ 私鑰不離開本機
✅ 無法暴力破解(數學上不可行)
✅ 可設定不同機器使用不同金鑰
✅ 可隨時撤銷(刪除 authorized_keys)Q3:如何優化 SFTP 傳輸速度?
答案:
1. 啟用壓縮:
ssh.connect(
hostname='sftp.example.com',
username='alice',
password='password',
compress=True # SSH 壓縮
)2. 調整緩衝區大小:
# 增大讀取緩衝(預設 32KB)
with sftp.file(remote_file, 'rb', bufsize=1024*1024) as f: # 1 MB
data = f.read()3. 平行傳輸多個檔案:
from concurrent.futures import ThreadPoolExecutor
import paramiko
def upload_file(filename):
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect('server.com', username='alice', password='password')
sftp = ssh.open_sftp()
sftp.put(filename, f'/remote/{filename}')
sftp.close()
ssh.close()
# 平行上傳
files = ['file1.zip', 'file2.zip', 'file3.zip']
with ThreadPoolExecutor(max_workers=3) as executor:
executor.map(upload_file, files)4. 使用更快的加密演算法:
# 編輯 SSH 設定,使用 ChaCha20
sudo nano /etc/ssh/sshd_config
Ciphers chacha20-poly1305@openssh.com,aes128-gcm@openssh.com5. 打包後傳輸:
import tarfile
# 打包
with tarfile.open('archive.tar.gz', 'w:gz') as tar:
tar.add('large_directory')
# 上傳單一檔案(比傳輸多個小檔案快)
sftp.put('archive.tar.gz', '/remote/archive.tar.gz')速度比較:
場景:上傳 1000 個小檔案(總共 100 MB)
方法 1:逐一上傳
時間:~5 分鐘 ⏱️
方法 2:打包後上傳
時間:~30 秒 ⚡
節省:90% 時間Q4:如何限制 SFTP 使用者只能存取特定目錄?
答案:
使用 chroot 限制使用者在特定目錄。
設定步驟:
# 1. 編輯 SSH 設定
sudo nano /etc/ssh/sshd_config
# 2. 在檔案最後加入
Match User sftpuser
ChrootDirectory /home/sftpuser
ForceCommand internal-sftp
AllowTcpForwarding no
X11Forwarding no
# 3. 設定目錄權限(重要!)
sudo chown root:root /home/sftpuser
sudo chmod 755 /home/sftpuser
# 4. 建立可寫入的子目錄
sudo mkdir /home/sftpuser/uploads
sudo chown sftpuser:sftpuser /home/sftpuser/uploads
sudo chmod 755 /home/sftpuser/uploads
# 5. 重啟 SSH
sudo systemctl restart sshd測試:
# 使用者登入後
sftp sftpuser@server.com
sftp> pwd
/ ← 使用者看到的「根目錄」
← 實際是 /home/sftpuser
sftp> cd /etc
Couldn't canonicalize: No such file ← 無法存取其他目錄
sftp> cd uploads
sftp> put file.txt ← 只能在 uploads 寫入群組批次設定:
# 建立 SFTP 群組
sudo groupadd sftpusers
# 批次設定
Match Group sftpusers
ChrootDirectory /var/sftp/%u # %u = 使用者名稱
ForceCommand internal-sftpQ5:SFTP 如何處理大檔案傳輸中斷?
答案:
SFTP 本身不支援自動續傳,需要手動實作。
實作方式:
import paramiko
import os
def sftp_resume_upload(sftp, local_file, remote_file):
"""SFTP 斷點續傳上傳"""
local_size = os.path.getsize(local_file)
# 檢查遠端檔案
try:
remote_size = sftp.stat(remote_file).st_size
except FileNotFoundError:
remote_size = 0
if remote_size == local_size:
print("檔案已上傳完成")
return
print(f"已上傳:{remote_size:,} / {local_size:,} bytes")
# 從中斷處繼續
with open(local_file, 'rb') as local_f:
# 跳過已上傳的部分
local_f.seek(remote_size)
# 開啟遠端檔案(追加模式)
with sftp.file(remote_file, 'ab') as remote_f:
chunk_size = 1024 * 1024 # 1 MB
while True:
chunk = local_f.read(chunk_size)
if not chunk:
break
remote_f.write(chunk)
# 顯示進度
current = remote_size + remote_f.tell()
percent = (current / local_size) * 100
print(f"\r進度:{percent:.1f}%", end='')
print("\n上傳完成")
# 使用
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect('server.com', username='alice', password='password')
sftp = ssh.open_sftp()
# 第一次上傳(可能中斷)
try:
sftp_resume_upload(sftp, 'large_file.zip', '/remote/file.zip')
except KeyboardInterrupt:
print("\n上傳中斷")
# 重新執行,自動續傳
sftp_resume_upload(sftp, 'large_file.zip', '/remote/file.zip')
sftp.close()
ssh.close()另一種方式:使用 rsync over SSH
# rsync 自動支援續傳
rsync -avz -P -e ssh large_file.zip alice@server.com:/remote/
# -a: 保留屬性
# -v: 顯示詳細資訊
# -z: 壓縮傳輸
# -P: 顯示進度 + 續傳📝 總結
SFTP 是現代檔案傳輸的標準選擇:
- 安全 🔐:全程 SSH 加密
- 簡單 🎯:單一 Port(22),防火牆友善
- 靈活 🔑:支援密碼 + 公鑰認證
- 可靠 ✅:基於 TCP,檔案完整性保證
記憶口訣:「一安二密三目錄」
- 一安:單一連線,安全加密
- 二密:雙認證(密碼 + 公鑰)
- 三目錄:完整目錄操作(ls, mkdir, rm)
最佳實踐:
✅ 使用公鑰認證(不用密碼)
✅ 限制使用者目錄(chroot)
✅ 記錄所有操作(日誌)
✅ 定期更新 SSH 版本
✅ 禁用 root 登入🔗 延伸閱讀
- 上一篇:07-1. FTP 協定
- 下一篇:07-3. SCP 協定
- RFC 4251(SSH Protocol Architecture):https://tools.ietf.org/html/rfc4251
- RFC 4253(SSH Transport Layer):https://tools.ietf.org/html/rfc4253
- Paramiko 官方文件:https://www.paramiko.org/