python Socket 基礎(chǔ)+ftp服務(wù)器、客戶端

socket 編程基礎(chǔ)

1.客戶端服務(wù)器架構(gòu)
2.osi 七層
3.socket 就是封裝底層的 TCP 和UDP、IP 等協(xié)議的庫,
  我們直接調(diào)用 socket 為我們封裝的方法就可以利用 TCP 和 UDP 進(jìn)行通信
4.套接字,也就是 socket 的中文名,ip address + port,
  TCP用主機的IP地址加上主機上的端口號作為TCP連接的端點,這種端點就叫做套接字(socket)或插口
5.工作流程,看代碼吧,TCP/IP 協(xié)議通信

socket TCP 的 recv 自己這端的緩沖區(qū)為空時,阻塞
而在socket UDP 的recvfrom 自己這端的緩沖區(qū)為空時,就會收一個空 

TCP socket 連接例子,服務(wù)器端

# 服務(wù)器端
import socket
socket1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)  // 基于網(wǎng)絡(luò),tcp/ip通信

socket1.bind(('127.0.0.1',8088)) #綁定到固定端口,等待客戶端來訪問
socket1.listen(5)  #最多有5個等待連接

conn,addr = socket1.accept() #沒有連接時,會在這句一直等待
print('conn = ',conn) 
print('addr = ',addr) #IP地址和端口 ('127.0.0.1', 1941)

data = conn.recv(1024)
print('data = ', data.decode('utf-8')) 

conn.send('你好啊'.encode('utf-8')) # 發(fā)送只能是二進(jìn)制編碼

conn.close()
socket1.close()

輸出結(jié)果:
conn =  <socket.socket fd=492, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8088), raddr=('127.0.0.1', 1941)>
addr =  ('127.0.0.1', 1941)
data =  天天向上

在客戶端只需要負(fù)責(zé)連接

# 客戶端
import socket

socket1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

socket1.connect(('127.0.0.1', 8088)) # 發(fā)起 TCP 三次握手連接

socket1.send('天天向上'.encode('utf-8'))
data = socket1.recv(1024)
print(data.decode('utf-8'))
socket1.close()

輸出結(jié)果:
你好啊

升級版服務(wù)器端,server 可以捕捉異常并繼續(xù)運行。

# server
import socket
ip_port = ('127.0.0.1', 8088)
backlog = 5
buffersize = 1024
socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket_server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1) # 減少地址復(fù)用的時間
socket_server.bind(ip_port)
socket_server.listen(backlog)
while True:
    print('wait to connect')
    conn,addr = socket_server.accept()
    print('conn = ', conn)
    print('addr = ', addr)
    try:
        while True:
            data = conn.recv(buffersize)
            print('>>>', data.decode('utf-8'))
            if data.decode('utf-8') == 'bye':
                print('prepare to close connect')
                break
            send = input('>>>').strip()
            conn.send(send.encode('utf-8'))
            if not send or send == 'bye':
                print('prepare to close connect')
                conn.send('bye'.encode('utf-8'))
                break
    except Exception as e:
        print(e)
        conn.close()
        print('close')

socket_server.close()

輸出結(jié)果:
wait to connect
conn =  <socket.socket fd=404, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8088), raddr=('127.0.0.1', 6802)>
addr =  ('127.0.0.1', 6802)
>>> 
>>>d
[WinError 10053] 你的主機中的軟件中止了一個已建立的連接。
close
wait to connect
conn =  <socket.socket fd=484, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8088), raddr=('127.0.0.1', 6803)>
addr =  ('127.0.0.1', 6803)
>>> 413

升級版客戶端

# client
import socket
ip_port = ('127.0.0.1', 8088)
buffersize = 1024
socket_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket_client.connect(ip_port)
while(True):
    send = input('>>>').strip()
    socket_client.send(send.encode('utf-8'))
    if not send or send == 'bye':
        print('prepare to close connect')
        socket_client.send('bye'.encode('utf-8'))
        break
    data = socket_client.recv(buffersize)
    print('>>>', data.decode('utf-8'))
    if data.decode('utf-8') == 'bye':
        print('prepare to close connect')
        break
socket_client.close()
print('close')

輸出結(jié)果:
>>>413
Traceback (most recent call last):
  File "C:/Users/libai/PycharmProjects/begin/test.py", line 14, in <module>
    data = socket_client.recv(buffersize)
KeyboardInterrupt #手動終止客戶端,服務(wù)端自動斷開連接

基于UDP的套接字

#udp_server
import socket
import time
ip_port = ('127.0.0.1', 8088)
buffersize = 1024
udp_server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
udp_server.bind(ip_port)
print('udp is working')
while True:
    data,addr = udp_server.recvfrom(buffersize)
    print('addr = ', addr)
    print("data = ",data.decode('utf-8'))
    udp_server.sendto(time.strftime('%Y-%m-%d %X').encode('utf-8'), addr)

輸出結(jié)果:
udp is working
addr =  ('127.0.0.1', 50122)
data =  123
addr =  ('127.0.0.1', 50122)
data =  88
# udp_client
import socket
ip_port = ('127.0.0.1', 8088)
buffersize = 1024
udp_client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)

while True:
    data = input('>>>').strip()
    udp_client.sendto(data.encode('utf-8'),ip_port)
    ntp_time, addr = udp_client.recvfrom(buffersize)
    print('ntp返回的時間:',ntp_time.decode('utf-8'))
    print(addr)

輸出結(jié)果:
>>>123
ntp返回的時間: 2018-04-21 11:04:03
('127.0.0.1', 8088)
>>>88
ntp返回的時間: 2018-04-21 11:04:08
('127.0.0.1', 8088)
>>>

遠(yuǎn)程執(zhí)行系統(tǒng)命令,調(diào)用 subprocess 模塊,十分有意思

# server_tcp
import socket
import subprocess
ip_port = ('127.0.0.1', 8088)
buffersize = 1024
backlog = 5
socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket_server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
socket_server.bind(ip_port)
socket_server.listen(backlog)

while True:
    print('watiing for connect')
    conn, addr = socket_server.accept()
    print('conn = ', conn)
    print('addr = ', addr)
    while True:
        data = conn.recv(buffersize)
        print('輸入>>>', data.decode('utf-8'))
        if not data or data.decode('utf-8')=='bye':
            break
        response = subprocess.Popen(data.decode('utf-8'),shell=True,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE,
                         stdin=subprocess.PIPE)
        error = response.stderr.read()
        if error:
            cmd_res = error
        else:
            cmd_res = response.stdout.read()
        conn.send(cmd_res)
    conn.close()

輸出結(jié)果:
watiing for connect
conn =  <socket.socket fd=456, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8088), raddr=('127.0.0.1', 27419)>
addr =  ('127.0.0.1', 27419)
輸入>>> dir
輸入>>> 156
輸入>>> bye
watiing for connect
# client_tcp
import socket
ip_port = ('127.0.0.1', 8088)
buffersize = 1024

socket_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket_client.connect(ip_port)
while True:
    data = input('>>>')
    socket_client.send(data.encode('utf-8'))
    if not data: continue
    if data == 'bye':
        break
    response = socket_client.recv(buffersize)
    print(response.decode('gbk'))
print('connect close')

輸出結(jié)果:

2018/04/21  15:25    <DIR>          .
2018/04/21  15:25    <DIR>          ..
2018/04/21  15:28    <DIR>          .idea
2018/04/20  17:23    <DIR>          a
2018/04/18  16:03                25 a.txt
2018/04/21  15:25             1,028 begin
2018/04/21  15:00                19 process.py
2018/04/21  15:23               392 test.py
               4 個文件          1,464 字節(jié)
               4 個目錄 68,050,141,184 可用字節(jié)

>>>156
'156' 不是內(nèi)部或外部命令,也不是可運行的程序
或批處理文件。

>>>bye
connect close

TCP 的粘包現(xiàn)象,UDP不會出現(xiàn)粘包現(xiàn)象

socket TCP 會出現(xiàn)粘包現(xiàn)象,也就是recv命令收取的buffersize沒有全部收完,就會等到下次再繼續(xù)收
例如:先運行 ipconfig 命令,沒有收完緩沖區(qū)使得打印信息不全,再運行別的命令,
    下次的命令會打印運行ipconfig命令剩余的信息。
解決粘包 第一個版本
# server
import socket
import subprocess
ip_port = ('127.0.0.1', 8088)
buffersize = 1024
backlog = 5
socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket_server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
socket_server.bind(ip_port)
socket_server.listen(backlog)

while True:
    print('watiing for connect')
    conn, addr = socket_server.accept()
    print('conn = ', conn)
    print('addr = ', addr)
    while True:
        data = conn.recv(buffersize)
        print('輸入>>>', data.decode('utf-8'))
        if not data or data.decode('utf-8')=='bye':
            break
        response = subprocess.Popen(data.decode('utf-8'),shell=True,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE,
                         stdin=subprocess.PIPE)
        error = response.stderr.read()
        if error:
            cmd_res = error
        else:
            cmd_res = response.stdout.read()
        if not cmd_res:
            cmd_res = 'execution succeed'.encode('gbk')
        length = len(cmd_res)
        conn.send(str(length).encode('gbk'))
        ready = conn.recv(buffersize)
        if ready.decode('gbk') == 'ready':
            conn.send(cmd_res)
    conn.close()
#client
import socket
ip_port = ('127.0.0.1', 8088)
buffersize = 1024

socket_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket_client.connect(ip_port)
try:
    while True:
        data = input('>>>')
        socket_client.send(data.encode('utf-8'))
        if not data: continue
        if data == 'bye':
            break
        length = socket_client.recv(buffersize)
        length = int(length.decode('gbk'))
        socket_client.send('ready'.encode("gbk"))
        response = b''
        while len(response) < length:
            response += socket_client.recv(buffersize)
        print(response.decode('gbk'))
except Exception as e:
    print(e)
    print('connect close')
    socket_client.close()
解決粘包 第二個版本
# server
import socket
import subprocess
ip_port = ('127.0.0.1', 8088)
buffersize = 1024
backlog = 5
socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket_server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
socket_server.bind(ip_port)
socket_server.listen(backlog)

while True:
    print('watiing for connect')
    conn, addr = socket_server.accept()
    print('conn = ', conn)
    print('addr = ', addr)
    while True:
        data = conn.recv(buffersize)
        print('輸入>>>', data.decode('utf-8'))
        if not data or data.decode('utf-8')=='bye':
            break
        response = subprocess.Popen(data.decode('utf-8'),shell=True,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE,
                         stdin=subprocess.PIPE)
        error = response.stderr.read()
        if error:
            #response.stderr.read() 返回的值為系統(tǒng)編碼的bytes類型,需要解碼
            cmd_res = error.decode('gbk')
        else:
            cmd_res = response.stdout.read().decode('gbk')
        if not cmd_res:
            cmd_res = 'execution succeed'
        length = len(cmd_res)
        send_data = str(length)+'!o!'+cmd_res
        # 字符長度,'!o!'符號作為分隔符,再加上返回的數(shù)據(jù)一起發(fā)送
        conn.send(send_data.encode('gbk'))
    conn.close()

輸出結(jié)果:
watiing for connect
conn =  <socket.socket fd=456, family=AddressFamily.AF_INET, type=SocketKind.SOCK_STREAM, proto=0, laddr=('127.0.0.1', 8088), raddr=('127.0.0.1', 28581)>
addr =  ('127.0.0.1', 28581)
輸入>>> ipconfig
輸入>>> bye
watiing for connect
import socket
ip_port = ('127.0.0.1', 8088)
buffersize = 1024

socket_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket_client.connect(ip_port)
try:
    while True:
        data = input('>>>')
        socket_client.send(data.encode('utf-8'))
        if not data: continue
        if data == 'bye':
            break
        send_data = socket_client.recv(buffersize)
        # 不知道發(fā)送的字符長度,從收取的第一個data中截取出來,沒讀完再繼續(xù)讀取
        send_data = send_data.decode('gbk')
        key_line = send_data.find('!o!')# 找到分隔符,返回的是第一個!的位置
        length = int(send_data[0:key_line]) # 切割出返回字符長度
        response = send_data[key_line+3:].encode('gbk') # 切割出返回字符,再次編碼以便和未發(fā)送完的字符串拼接
        while len(response) < length:
            response += socket_client.recv(buffersize)
        print(response.decode('gbk'))
        print('返回%s個字節(jié)' %length)
except Exception as e:
    print(e)
    print('connect close')
    socket_client.close()

輸出結(jié)果:#只復(fù)制了一部分
以太網(wǎng)適配器 VMware Network Adapter VMnet8:
   連接特定的 DNS 后綴 . . . . . . . :
   本地鏈接 IPv6 地址. . . . . . . . : fe80::8dae:48ae:e19c:207d%9
   IPv4 地址 . . . . . . . . . . . . : 192.168.1.1
   子網(wǎng)掩碼  . . . . . . . . . . . . : 255.255.255.0
   默認(rèn)網(wǎng)關(guān). . . . . . . . . . . . . :
無線局域網(wǎng)適配器 本地連接* 12:
   連接特定的 DNS 后綴 . . . . . . . :
   本地鏈接 IPv6 地址. . . . . . . . : fe80::7ca0:36c4:83dd:d3ac%22
   IPv4 地址 . . . . . . . . . . . . : 192.168.23.1
   子網(wǎng)掩碼  . . . . . . . . . . . . : 255.255.255.0
   IPv4 地址 . . . . . . . . . . . . : 192.168.137.1
   子網(wǎng)掩碼  . . . . . . . . . . . . : 255.255.255.0
   默認(rèn)網(wǎng)關(guān). . . . . . . . . . . . . :

返回1631個字節(jié)
>>>bye
解決粘包 第三版
#server
import socket
import subprocess
import struct
ip_port = ('127.0.0.1', 8088)
buffersize = 1024
backlog = 5
socket_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket_server.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
socket_server.bind(ip_port)
socket_server.listen(backlog)

while True:
    print('waiting for connect')
    conn, addr = socket_server.accept()
    print('conn = ', conn)
    print('addr = ', addr)
    while True:
        data = conn.recv(buffersize)
        print('輸入>>>', data.decode('utf-8'))
        if not data or data.decode('utf-8')=='bye':
            break
        response = subprocess.Popen(data.decode('utf-8'),shell=True,
                         stdout=subprocess.PIPE,
                         stderr=subprocess.PIPE,
                         stdin=subprocess.PIPE)
        error = response.stderr.read()
        if error:
            #response.stderr.read() 返回的值為系統(tǒng)編碼的bytes類型,需要解碼
            cmd_res = error.decode('gbk')
        else:
            cmd_res = response.stdout.read().decode('gbk')
        if not cmd_res:
            cmd_res = 'execution succeed'
        length = len(cmd_res)
        length_pack = struct.pack('i', length) #struct.pack 編碼出來的length_pack 為四個字節(jié)
        conn.send(length_pack)
        conn.send(cmd_res.encode('gbk'))
    conn.close()
#client
import socket
import struct

ip_port = ('127.0.0.1', 8088)
buffersize = 1024

socket_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
socket_client.connect(ip_port)
try:
    while True:
        data = input('>>>')
        socket_client.send(data.encode('utf-8'))
        if not data: continue
        if data == 'bye':
            break
        length_pack = socket_client.recv(4) #取出四字節(jié)的編碼字符數(shù)據(jù)長度
        length = int(struct.unpack('i',length_pack)[0])
        data = b''
        while len(data) < length:
            data += socket_client.recv(buffersize)
        print(data.decode('gbk'))
        print('返回%s個字節(jié)' %length)
except Exception as e:
    print(e)
    print('connect close')
    socket_client.close()

下面是一個ftp服務(wù)器和客戶端

有著上傳下載功能,添加了一些常用的命令,還有斷點續(xù)傳的功能,但存在bug,當(dāng)練練手了,這個東西搞了好幾天

python ftp.png

輸入啟動命令:python C:\PycharmProjects\begin\ftp_client\ftp_client.py -P 8889 -S 127.0.0.1

#ftp_client
# python C:\PycharmProjects\begin\ftp_client\ftp_client.py -P 8889 -S 127.0.0.1
import optparse # 解析獲得的命令行參數(shù)
import socket
import json #傳輸
import os
import sys
import hashlib #對上傳下載文件的 md5 校驗
# 用數(shù)字代表一些特定字符,可以減少發(fā)送的字符,不過我只用到了幾個
STATUS_CODE  = {
    250 : "Invalid cmd format, e.g: {'action':'get','filename':'test.py','size':344}",
    251 : "Invalid cmd ",
    252 : "Invalid auth data",
    253 : "Wrong username or password",
    254 : "Passed authentication",
    255 : "Filename doesn't provided",
    256 : "File doesn't exist on server",
    257 : "ready to send file",
    258 : "md5 verification",

    800 : "the file exist,but not enough ,is continue? ",
    801 : "the file exist !",
    802 : " ready to receive datas",

    900 : "md5 valdate success"

}

#輸入啟動命令:python C:\PycharmProjects\begin\ftp_client\ftp_client.py -P 8889 -S 127.0.0.1
#定義一個類,實例化在最下面
class ClientHandler:
    def __init__(self): # 解析參數(shù)
        self.op = optparse.OptionParser()
        self.op.add_option('-S', '--server', dest='server')
        self.op.add_option('-P', '--port', dest='port')
        self.op.add_option('-U', '--username', dest='username')
        self.op.add_option('-p', '--password', dest='password')
        self.options, self.args = self.op.parse_args()
        # print(self.options)
        # print(self.args)
        self.verify_args() # 檢測輸入是否合法
        self.make_connect() # 建立連接
        self.mainPath = os.path.dirname(os.path.abspath(__file__)) #獲得文件的執(zhí)行路徑,上傳下載時需要用到
    # 檢測輸入的是否合法,我這里只檢測了端口值
    def verify_args(self):
            if int(self.options.port) >0 and int(self.options.port)<65535:
                return True
            else:
                exit('port is error')
    # 建立連接,傳入元組形式的 ip 地址和端口值,(要先啟動服務(wù)器)
    def make_connect(self):
        self.s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self.s.connect((self.options.server, int(self.options.port)))
        print('connect successful')

    def get_response(self):
        data= self.s.recv(1024)
        data = json.loads(data.decode('utf-8'))
        return data
    # 通信開始,用的也是反射
    def interactive(self):
        if self.authenticate():
            print('pass')
            while True:
                cmd_info = input('[%s]' %self.current_dir).strip()
                if len(cmd_info) == 0:continue # 如果輸入為空,重新輸入一遍,TCP協(xié)議不能發(fā)送空
                cmd_list = cmd_info.split()
                if hasattr(self, cmd_list[0]):
                    func = getattr(self, cmd_list[0])
                    func(*cmd_list)
                else:
                    print('please input again')
    def ls(self, *cmd_list):
        data = {
            'action' : 'ls'
        }
        self.s.sendall(json.dumps(data).encode('utf-8')) #把 data 轉(zhuǎn)成json字符串發(fā)送給服務(wù)器
        msg = self.s.recv(1024).decode('utf-8')
        if msg != 'empty': # 用 empty 代替發(fā)送為空
            print(msg)

    def cd(self, *cmd_list):
        cmd_list = list(cmd_list)
        if len(cmd_list) == 1: #如果命令中只有一個 cd 時,返回最上級目錄,也就是 home 下的家目錄
            cmd_list.append('top')
        if len(cmd_list) != 2: # cmd_list 的長度不等于2,就是輸入出錯了
            print('please input again')
            return
        data = {
            'action' : 'cd',
            'dirname' : cmd_list[1]
        }
        self.s.sendall(json.dumps(data).encode('utf-8'))
        msg = self.s.recv(1024).decode('utf-8')
        if msg != 'no':
            self.current_dir = msg[msg.find('libai', 10):] #
            #libai 是我定義的賬號名稱,在home 下會有一個libai的文件作為家目錄
            # print(self.current_dir)
        else:
            print('no this dir')

    def put(self, *cmd_list):
        print('put')
        # put 12.jpg image 命令形式
        s = hashlib.md5() #實例一個 md5,檢查文件是否傳輸無誤
        action,local_path,target_path = cmd_list
        local_path = os.path.join(self.mainPath, local_path)
        #把 self.mainPath 和 image 合成一個完整目錄

        file_name = os.path.basename(local_path) #這句好像是多余的,額
        file_size = os.stat(local_path).st_size

        data = {
            'action' : 'put',
            'file_name' : file_name,
            'file_size' : file_size,
            'target_path' : target_path
        }
        self.s.send(json.dumps(data).encode('utf-8'))

        ###############################################
        has_send = 0
        is_exit = self.s.recv(1024).decode('utf-8')

        if is_exit == '800':
            #文件不完整
            choice = input('the file is exist, but no enough, is continue? [Y/N  ]').strip()
            if choice.upper() == 'Y':#是否續(xù)傳
                self.s.sendall('Y'.encode('utf-8'))
                has_send = int(self.s.recv(1024).decode('utf-8')) #獲得已經(jīng)傳輸?shù)奈募笮?            else:
                self.s.sendall('N'.encode('utf-8'))
        elif is_exit == '801':
            #文件存在,就直接返回了
            print('the file is exist')
            return
        #如果文件已經(jīng)傳輸了一部分,需要續(xù)傳,重新計算md5 的值,
        print('send filename: %s, filesize: %s' %(file_name, file_size))
        f = open(local_path, 'r+b')
        has_send_b = has_send
        if has_send > 0:
            while True:
                if has_send - 1024 > 0:
                    s.update(f.read(1024)) # 更新md5
                    has_send = has_send - 1024
                else:
                    s.update(f.read(has_send))
                    break
        f.seek(has_send_b) #這句好像也是多余的額,
        print('has_send_b = ', has_send_b)
        while has_send_b < file_size:
            data = f.read(1024)
            s.update(data) # 更新md5
            self.s.sendall(data)
            has_send_b += len(data)
            self.show_progress(has_send_b, file_size) #這是一個進(jìn)度條函數(shù)
        f.close()

        self.s.sendall(str(s.hexdigest()).encode('utf-8')) #收發(fā)client和server的md5
        server_md5 = self.s.recv(1024).decode('utf-8')
        if server_md5 == s.hexdigest():
            print('\nMd5 checksum succeeded, Uploaded successfully')
        else:
            print('\nMd5 check failed ,upload failed')

        #time.sleep(20)
    # 下載和上傳也是差不多了,我不寫了
    def downloads(self, *cmd_list):
        # downloads 12.jpg
        s = hashlib.md5()
        if len(cmd_list) != 2:
            print('please input again')
            return
        data = {
            'action' : 'downloads',
            'file_name' : cmd_list[1],
        }

        self.s.sendall(json.dumps(data).encode('utf-8'))
        msg = self.s.recv(1024).decode('utf-8')
        print(msg)
        path = os.path.dirname(os.path.abspath(__file__))
        path = os.path.join(path, cmd_list[1]).replace('\\', '/')
        has_send = 0

        if msg == 'exist this file':
            if os.path.exists(path):
                print('you want to continue transmission?')
                choice = input('[Y/N]').upper()
                if choice == 'Y':
                    has_send = os.stat(path).st_size
                    f = open(path, 'r+b') #keep going
                else:
                    f = open(path, 'r+b') #downloading
            else:
                f = open(path, 'wb')
        else:
            print(msg)#bu cun zai wen jian
            return
        file_size = int(self.s.recv(1024).decode('utf-8'))
        self.s.sendall(str(has_send).encode('utf-8'))
        print('downloading')
        has_send_b = has_send
        f.seek(0)
        if has_send > 0:
            while True:
                if has_send - 1024 > 0:
                    mess = f.read(1024)
                    s.update(mess)
                    has_send = has_send - 1024
                else:
                    s.update(f.read(has_send))
                    break
        f.seek(has_send_b)
        print('has_send = ', has_send)
        while has_send_b < file_size:
            message = self.s.recv(1024)
            f.write(message)
            s.update(message)
            has_send_b += len(message)
            self.show_progress(has_send_b, file_size)
        f.close()
        self.s.sendall(str(s.hexdigest()).encode('utf-8'))
        server_md5 = self.s.recv(1024).decode('utf-8')
        if server_md5 == s.hexdigest():
            print('\nMd5 checksum succeeded, Uploaded successfully')
        else:
            print('\nMd5 check failed ,upload failed')


        # print('downloads data = ', data)
        # self.s.sendall(json.dumps(data).encode('utf-8'))
        # msg = self.s.recv(1024)
        # print(msg)
    #創(chuàng)建文件夾
    def mkdir(self, *cmd_list):
        if len(cmd_list) != 2:
            print('please input again')
            return
        data = {
            'action' : 'mkdir',
            'dirname' : cmd_list[1]
        }
        self.s.sendall(json.dumps(data).encode('utf-8'))
        msg = self.s.recv(1024).decode('utf-8')
        print(msg)
    #刪除文件夾或文件
    def rm(self, *cmd_list):
        if len(cmd_list) != 2:
            print('please input again')
            return
        data = {
            'action' : 'rm',
            'dirname' : cmd_list[1]
        }
        self.s.sendall(json.dumps(data).encode('utf-8'))
        msg = self.s.recv(1024).decode('utf-8')
        print(msg)

    # 這個是進(jìn)度條函數(shù)
    def show_progress(self, has_send, file_size):
        procentage = int(float(has_send)/file_size*100)
        sys.stdout.write('%s%% %s\r' %(procentage, '*'*procentage))

    #這個是檢測你是否輸入了用戶名和密碼,如果沒有,會提示你輸入的
    def authenticate(self):
        if self.options.username == None or self.options.password == None:
            username = input('username : ')
            password = input('password : ')
            return self.get_auth_result(username, password)
        return self.get_auth_result(self,self.options.username,self.options.password)

    #校驗?zāi)愕挠脩裘兔艽a對不對
    def get_auth_result(self, username, password):
        data = {
            'action' : 'auth',
            'username' : username,
            'password' : password
        }
        self.s.send(json.dumps(data).encode('utf-8'))
        response = self.get_response()

        print(response)
        print('status_code : ', response['status_code'])
        if response['status_code'] == 254:
            self.username = username
            self.current_dir = username
            print(STATUS_CODE[254])
            return True
        else:
            print(STATUS_CODE[response['status_code']])

ch = ClientHandler() # 實例化的同時,連接上服務(wù)器
ch.interactive() # 通信開始

ftp_server 我分為幾個模塊來寫,這是一個啟動文件,整個 ftp_server 的入口 ,啟動命令: C:/PycharmProjects/begin/ftp_server/bin/ftp_server.py srart,當(dāng)然每個人的文件存放路徑都不相同了

# ftp_server 啟動

import sys,os
base_path = os.path.dirname(os.path.dirname(__file__))# 獲得本文件的上上級目錄,也就是ftp_server目錄
sys.path.append(base_path) # 將 base_path 添加到臨時的環(huán)境變量
print(base_path)
from core import main  # 在環(huán)境變量中就可以找到 core 文件夾了
main.test() # 用文件名加類名運行

setting 配置文件

#配置文件

import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))

IP = '127.0.0.1'
PORT = 8889

ACCOUNT_PATH = os.path.join(BASE_DIR, 'conf', 'accounts.cfg')

accounts.cfg 賬號密碼文件

[DEFAULT]

[libai]
password = 123456
quotation = 100

[root]
password = root
quotation = 100

main.py 文件

import optparse #解析出start
import socketserver

# 在啟動文件中已經(jīng)加入了環(huán)境變量,不需要再添加了
from conf import setting
from core import server

# 啟動 ftp_server 的命令:python C:\PycharmProjects\begin\ftp_server\bin\ftp_server.py start
# 運行 test 會首先運行 test 的構(gòu)造方法 __init__,解析命令行參數(shù),
# 用反射,可以十分方便的增添函數(shù),
class test:
    def __init__(self):
        self.op = optparse.OptionParser()
        options, args = self.op.parse_args()
        print(options)
        print(args)
        if hasattr(self, args[0]): # 傳入的參數(shù)是 start ,用 hasattr 檢測類是否有 start 方法,有返回True
            func = getattr(self,args[0]) # 存在 start 方法,用 getattr 獲取,加括號運行
            func()
    def start(self):
        print('waiting for connect') # 連接
        # 用 socketserver 內(nèi)置的類實例化 s ,并傳入自定義的 server 文件中的 ServerHandler 類
        s = socketserver.ThreadingTCPServer((setting.IP, setting.PORT), server.ServerHandler)
        # 啟動 實例化的 s 對象
        s.serve_forever()

最核心的文件,server.py

import socketserver
import json #作為傳輸
import configparser #解析存儲的賬號密碼
from conf import setting
import os
import shutil #刪除文件夾的庫
import hashlib

STATUS_CODE  = {
    250 : "Invalid cmd format, e.g: {'action':'get','filename':'test.py','size':344}",
    251 : "Invalid cmd ",
    252 : "Invalid auth data",
    253 : "Wrong username or password",
    254 : "Passed authentication",
    255 : "Filename doesn't provided",
    256 : "File doesn't exist on server",
    257 : "ready to send file",
    258 : "md5 verification",

    800 : "the file exist,but not enough ,is continue? ",
    801 : "the file exist !",
    802 : " ready to receive datas",

    900 : "md5 valdate success"

}
#定義一個繼承 socketserver.BaseRequestHandler 的類,實例化時自動運行 handle 方法
class ServerHandler(socketserver.BaseRequestHandler):
    def handle(self):
        print(self.request) # self.request 就是類同與 socket 定義服務(wù)器時返回的實例對象
        print(self.client_address) #
        while True:
            data = self.request.recv(1024)
            data = json.loads(data.decode('utf-8'))
            if data.get('action'):
                if hasattr(self, data.get('action')):
                    func = getattr(self, data.get('action'))
                    func(**data)
                else:
                    print('invaild')
            else:
                print('invaild')

    def send_response(self, status_code):
        response = {'status_code': status_code}
        self.request.sendall(json.dumps(response).encode('utf-8'))

    def auth(self, **data):
        username = data['username']
        password = data['password']
        username = self.authenticate(username,password)
        if username:
            self.send_response(254) #正常返回
        else:
            self.send_response(253) #用戶名或密碼錯誤返回

    def authenticate(self, username, password): #驗證用戶名了密碼,我放在了conf/accounts.cfg
        cfg = configparser.ConfigParser()
        cfg.read(setting.ACCOUNT_PATH)
        if username in cfg.sections():
            if password == cfg[username]['Password']:
                print('login successful')
                self.username= username
                self.mainPath = os.path.join(setting.BASE_DIR,'home',self.username).replace('\\','/')
                return username

    def put(self, **data):
        s = hashlib.md5()
        print('put')
        print('data : ', data)
        print(type(data))
        file_name = data['file_name']
        file_size = data['file_size']
        target_path = data['target_path']
        print('file_name = ', file_name)
        print('file_size = ', file_size)
        print('target_path = ', target_path)

        abs_path = os.path.join(self.mainPath,target_path,file_name)
        print('self.mainPath = ', self.mainPath)
        print('target_path = ', target_path)
        print('file_name = ', file_name)
        print(abs_path)

        ################################################
        has_received = 0
    #斷點續(xù)傳十分重要的就是調(diào)整文件指針的位置
        if os.path.exists(abs_path):
            file_has_size = os.stat(abs_path).st_size
            if file_has_size < file_size:
                #斷點續(xù)傳
                self.request.sendall('800'.encode('utf-8'))
                choice = self.request.recv(1024).decode('utf-8')
                if choice == "Y":
                    self.request.sendall(str(file_has_size).encode('utf-8'))
                    has_received = file_has_size
                    f = open(abs_path, 'r+b') #
                else:
                    f = open(abs_path, 'r+b')
            else:
                # 文件存在
                self.request.sendall('801'.encode('utf-8'))
                return
        else:
            self.request.sendall('802'.encode('utf-8'))
            f = open(abs_path, 'wb')
        print('putting : %s' %file_name)
        has_received_b = has_received
        if has_received > 0:
            f.seek(0)
            while True:
                if has_received - 1024 > 0:
                    s.update(f.read(1024))
                    has_received = has_received - 1024
                else:
                    s.update(f.read(has_received))
                    break
        f.seek(has_received_b)
        print('has_received_b = ',has_received_b)
        while has_received_b < file_size:
            msg = self.request.recv(1024)
            f.write(msg)
            s.update(msg)
            has_received_b += len(msg)
        f.close()
        client_md5 = self.request.recv(1024).decode('utf-8')
        self.request.sendall(str(s.hexdigest()).encode('utf-8'))
        if client_md5 == s.hexdigest():
            print('Md5 checksum succeeded, Uploaded successfully')
        else:
            print('Md5 check failed ,upload failed')

    def ls(self, **data): #接收到傳來的數(shù)據(jù) data
        print('ls')
        file_list = os.listdir(self.mainPath) #列出在當(dāng)前目錄下的所有文件
        file_str = '\n'.join(file_list) #獲得的為列表,轉(zhuǎn)成字符串
        if not len(file_list):
            file_str = 'empty'
        self.request.sendall(file_str.encode('utf-8')) #將結(jié)果發(fā)送回去

    def cd(self, **data):
        top_directory = 'C:PycharmProjects/begin/ftp_server/home/libai'#需要用的話要這里要改一下路徑啊
        print('cd')
        dirname = data.get('dirname')
        if dirname == 'top': #返回頂級目錄
            self.mainPath = top_directory
        elif dirname =='..': # 返回上一層目錄
            if self.mainPath != top_directory:
                self.mainPath = os.path.dirname(self.mainPath)
        else: #else 就是進(jìn)入某某目錄了
            back = self.mainPath
            self.mainPath = os.path.join(self.mainPath, dirname).replace('\\','/')
            if not os.path.isdir(self.mainPath):
                self.mainPath = back
                self.request.sendall('no'.encode('utf-8'))
                return
        self.request.sendall(self.mainPath.encode('utf-8'))

    def mkdir(self, **data):#創(chuàng)建文件夾,嵌套或者單個文件夾
        print('mkdir')
        dirname = data.get('dirname')
        path = os.path.join(self.mainPath, dirname).replace('\\','/')

        if not os.path.exists(path):
            if '/' in dirname:
                os.makedirs(path)
            else:
                os.mkdir(path)
            self.request.sendall('Created successfully'.encode('utf-8'))
        else:
            self.request.sendall('Directory already exists'.encode('utf-8'))

    def rm(self, **data): #刪除命令,嵌套或者單個文件
        print('rm')
        dirname = data.get('dirname')
        path = os.path.join(self.mainPath, dirname).replace('\\', '/')

        if not os.path.exists(path):
            print('wen jian bu cun zai')
            self.request.sendall('no'.encode('utf-8'))
        else:
            try:
                shutil.rmtree(path) #這個是嵌套刪除文件
            except:
                os.remove(path) #刪除單個文件
            print('delete')
            self.request.sendall('remove'.encode('utf-8'))

    #下載和上傳是一樣的了,
    def downloads(self, **data):
        # downloads 12.jpg
        print('downloads')
        s = hashlib.md5()
        print('data : ', data)

        file_name = data['file_name']
        abs_path = os.path.join(self.mainPath,file_name).replace('\\', '/')
        print('self.mainPath = ', self.mainPath)

        print('abs_path = ', abs_path)

        if os.path.exists(abs_path):
            print('have this abs_path')
            self.request.sendall('exist this file'.encode('utf-8'))
        else:
            self.request.sendall('no this file'.encode('utf-8'))
            return
        ###############################################
        file_size = os.stat(abs_path).st_size

        print('file_size = ', file_size)
        self.request.sendall(str(file_size).encode('utf-8'))
        has_send = int(self.request.recv(1024).decode('utf-8'))
        f = open(abs_path, 'rb')
        has_send_b = has_send
        if has_send > 0:
            while True:
                if has_send - 1024 > 0:
                    s.update(f.read(1024))
                    has_send = has_send - 1024
                else:
                    s.update(f.read(has_send))
                    break
        f.seek(has_send_b)
        print('has_send = ', has_send)
        while has_send_b < file_size:
            message = f.read(1024)
            s.update(message)
            self.request.sendall(message)
            has_send_b += len(message)
        f.close()
        client_md5 = self.request.recv(1024).decode('utf-8')
        self.request.sendall(str(s.hexdigest()).encode('utf-8'))
        if client_md5 == s.hexdigest():
            print('Md5 checksum succeeded, Uploaded successfully')
        else:
            print('Md5 check failed ,upload failed')

#我自我覺得功能還是不錯的,有些bug,修改了一下,發(fā)現(xiàn)bug還挺多,就算了,
#當(dāng)練練手了,

對了,我的家目錄設(shè)置在 ftp_server 下的home/libai 和root,上傳默認(rèn)在ftp_clent.py 目錄下尋找文件上傳,用 put 12.jpg image 這樣的形式,要不然報錯啊,會提示列表出界

下載啊,在你所在的家目錄下的所在的路徑下載,下載到ftp_client 所在的目錄
大概就是這樣了

總結(jié): 斷點續(xù)傳主要就是要調(diào)整文件指針?biāo)诘奈恢茫募羔樀奈恢每梢杂靡汛嬖诘奈募笮淼贸觯?br> 追加模式的文件指針是默認(rèn)在文件末尾的
好了,就記錄到這里了,
分享一下文件:鏈接:https://pan.baidu.com/s/1CCaIpw5D48Lf0cUFUud2zw 密碼:lz2r
(不知道啥時候會和諧掉呢)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容