1. 程式人生 > >python網路程式設計--socketserver 和 ftp功能簡單說明

python網路程式設計--socketserver 和 ftp功能簡單說明

1. socketserver

我們之前寫的tcp協議的socket是不是一次只能和一個客戶端通訊,如果用socketserver可以實現和多個客戶端通訊。它是在socket的基礎上進行了一層封裝,也就是說底層還是呼叫的socket,在py2.7裡面叫做SocketServer也就是大寫了兩個S,在py3裡面就小寫了。後面我們要寫的FTP作業,需要用它來實現併發,也就是同時可以和多個客戶端進行通訊,多個人可以同時進行上傳下載等。

我們舉一個使用socketserver的例子

import socketserver                              #1、引入模組
class MyServer(socketserver.BaseRequestHandler): #2、自己寫一個類,類名自己隨便定義,然後繼承socketserver這個模組裡面的BaseRequestHandler這個類

    def handle(self):                            #3、寫一個handle方法,必須叫這個名字
        #self.request                            #6、self.request 相當於一個conn

        self.request.recv(1024)                  #7、收訊息
        msg = '這是傳送的訊息'
        self.request.send(bytes(msg,encoding='utf-8')) #8、發訊息

        self.request.close()                     #9、關閉連線

        # 拿到了我們對每個客戶端的管道,那麼我們自己在這個方法裡面的就寫我們接收訊息傳送訊息的邏輯就可以了
        pass
if __name__ == '__mian__':
    #thread 執行緒,現在只需要簡單理解執行緒,彆著急,後面很快就會講到啦,看下面的圖
    server = socketserver.ThreadingTCPServer(('127.0.0.1',8090),MyServer)#4、使用socketserver的ThreadingTCPServer這個類,將IP和埠的元祖傳進去,還需要將上面咱們自己定義的類傳進去,得到一個物件,相當於我們通過它進行了bind、listen
    server.serve_forever()                       #5、使用我們上面這個類的物件來執行serve_forever()方法,他的作用就是說,我的服務一直開啟著,就像京東一樣,不能關閉網站,對吧,並且serve_forever()幫我們進行了accept


#注意:
#有socketserver 那麼有socketclient的嗎?
#當然不會有,我要作為客戶去訪問京東的時候,京東幫我也客戶端了嗎,客戶端是不是在我們自己的電腦啊,並且socketserver對客戶端沒有太高的要求,只需要自己寫一些socket就行了。
 

來看下完整的客戶端與服務端程式碼

服務端程式碼:

import socketserver

class Myserver(socketserver.BaseRequestHandler):

    def handle(self):
        while 1:
            from_client_msg = self.request.recv(1024)  # self.request = conn
            print(from_client_msg.decode('utf-8'))
            msg = input('服務端說:')
            self.request.send(msg.encode('utf-8'))


if __name__ == '__main__':

    ip_port = ('127.0.0.1',8001)
    
    # 設定allow_reuse_address允許伺服器重用地址
    socketserver.TCPServer.allow_reuse_address = True

    #server = socketserver.TCPServer((HOST, PORT),Myserver)  
    server = socketserver.ThreadingTCPServer(ip_port,Myserver)
    
     # 讓server永遠執行下去,除非強制停止程式
    server.serve_forever() 

客戶端:

import socket
client = socket.socket()
client.connect(('127.0.0.1',8001))

while 1:
    msg = input('客戶端說>>>')
    client.send(msg.encode('utf-8'))
    from_server_msg = client.recv(1024)
    print(from_server_msg.decode('utf-8'))

  

2.驗證客戶端的連結合法性(加密)

  首先,我們來探討一下,什麼叫驗證合法性, 舉個例子:有一天,我開了一個socket服務端,只想讓咱們這個班的同學使用,但是有一天,隔壁班的同學過來問了一下我開的這個服務端的ip和埠,然後他是不是就可以去連線我了啊,那怎麼辦,我是不是不想讓他連線我啊,我需要驗證一下你的身份,這就是驗證連線的合法性,再舉個例子,就像我們上面說的你的windows系統是不是連線微軟的時間伺服器來獲取時間的啊,你的mac能到人家微軟去獲取時間嗎,你願意,人家微軟還不願意呢,對吧,那這時候,你每次連線我來獲取時間的時候,我是不是就要驗證你的身份啊,也就是你要帶著你的系統資訊,我要判斷你是不是我微軟的windows,對吧,如果是mac,我是不是不讓你連啊,這就是連接合法性。如果驗證你的連線是合法的,那麼如果我還要對你的身份進行驗證的需求,也就是要驗證使用者名稱和密碼,那麼我們還需要進行身份認證。連線認證>>身份認證>>ok你可以玩了。

就用到兩個方法

1. os.urandom(n)

  其中os.urandom(n) 是一種bytes型別的隨機生成n個位元組字串的方法,而且每次生成的值都不相同。再加上md5等加密的處理,就能夠成內容不同長度相同的字串了。

 

官方解釋為:

os.urandom(n)函式在python官方文件中做出了這樣的解釋函式定位:

  Return a string of n random bytes suitable for cryptographic use. 意思就是,返回一個有n個byte那麼長的一個string,然後很適合用於加密。然後這個函式,在文件中,被歸結於os這個庫的Miscellaneous Functions,意思是不同種類的函式(也可以說是混種函式) 

  原因是: This function returns random bytes from an OS-specific randomness source. (函式返回的隨機位元組是根據不同的作業系統特定的隨機函式資源。即,這個函式是呼叫OS內部自帶的隨機函式的。有特異性)

2. hamc

Python自帶的hmac模組實現了標準的Hmac演算法,我們首先需要準備待計算的原始訊息message,隨機key,雜湊演算法,這裡採用MD5,使用hmac的程式碼如下:

import hmac
message = b'Hello world'
key = b'secret'
h = hmac.new(key,message,digestmod='MD5')
print(h.hexdigest())
比較兩個密文是否相同,可以用hmac.compare_digest(密文、密文),然會True或者False。 

可見使用hmac和普通hash演算法非常類似。hmac輸出的長度和原始雜湊演算法的長度一致。需要注意傳入的key和message都是bytes型別,str型別需要首先編碼為bytes

def hmac_md5(key, s):
    return hmac.new(key.encode('utf-8'), s.encode('utf-8'), 'MD5').hexdigest()

class User(object):
    def __init__(self, username, password):
        self.username = username
        self.key = ''.join([chr(random.randint(48, 122)) for i in range(20)])
        self.password = hmac_md5(self.key, password)

  

如果你想在分散式系統中實現一個簡單的客戶端連結認證功能,又不像SSL那麼複雜,那麼利用hmac+加鹽的方式來實現,看程式碼

sendall()與send()沒有什麼區別 ,可視為send()

server端

from socket import *
import hmac,os

secret_key=b'Jedan has a big key!'
def conn_auth(conn):
    '''
    認證客戶端連結
    :param conn:
    :return:
    '''
    print('開始驗證新連結的合法性')
    msg=os.urandom(32)#生成一個32位元組的隨機字串
    conn.sendall(msg)
    h=hmac.new(secret_key,msg)
    digest=h.digest()
    respone=conn.recv(len(digest))
    return hmac.compare_digest(respone,digest)

def data_handler(conn,bufsize=1024):
    if not conn_auth(conn):
        print('該連結不合法,關閉')
        conn.close()
        return
    print('連結合法,開始通訊')
    while True:
        data=conn.recv(bufsize)
        if not data:break
        conn.sendall(data.upper())

def server_handler(ip_port,bufsize,backlog=5):
    '''
    只處理連結
    :param ip_port:
    :return:
    '''
    tcp_socket_server=socket(AF_INET,SOCK_STREAM)
    tcp_socket_server.bind(ip_port)
    tcp_socket_server.listen(backlog)
    while True:
        conn,addr=tcp_socket_server.accept()
        print('新連線[%s:%s]' %(addr[0],addr[1]))
        data_handler(conn,bufsize)

if __name__ == '__main__':
    ip_port=('127.0.0.1',9999)
    bufsize=1024
    server_handler(ip_port,bufsize)

client端

from socket import *
import hmac,os

secret_key=b'Jedan has a big key!'
def conn_auth(conn):
    '''
    驗證客戶端到伺服器的連結
    :param conn:
    :return:
    '''
    msg=conn.recv(32)
    h=hmac.new(secret_key,msg)
    digest=h.digest()
    conn.sendall(digest)

def client_handler(ip_port,bufsize=1024):
    tcp_socket_client=socket(AF_INET,SOCK_STREAM)
    tcp_socket_client.connect(ip_port)

    conn_auth(tcp_socket_client)

    while True:
        data=input('>>: ').strip()
        if not data:continue
        if data == 'quit':break

        tcp_socket_client.sendall(data.encode('utf-8'))
        respone=tcp_socket_client.recv(bufsize)
        print(respone.decode('utf-8'))
    tcp_socket_client.close()

if __name__ == '__main__':
    ip_port=('127.0.0.1',9999)
    bufsize=1024
    client_handler(ip_port,bufsize)

 

3.實現ftp簡單上傳的功能

流程和思路:

    我們可以把報頭做成字典,字典裡包含將要傳送的真實資料的描述資訊(大小啊之類的),然後json序列化,然後用struck將序列化後的資料長度打包成4個位元組。
我們在網路上傳輸的所有資料 都叫做資料包,資料包裡的所有資料都叫做報文,報文裡面不止有你的資料,還有ip地址、mac地址、埠號等等,其實所有的報文都有報頭,這個報頭是協議規定的,看一下

傳送時:
1.先發報頭長度
2.再編碼報頭內容然後傳送,使用json,不要用eval,因為eval容易造成記憶體溢位
3.最後發真實內容

接收時:
1.先手報頭長度,用struct取出來
2.根據取出的長度收取報頭內容,然後解碼,反序列化
3.從反序列化的結果中取出待取資料的描述資訊,然後去取真實的資料內容

 

服務端server

import json
import socket
import struct

server = socket.socket()
server.bind(('127.0.0.1',8001))
server.listen()
conn,addr = server.accept()

#首先接收檔案的描述資訊的長度
struct_data_len = conn.recv(4)
data_len = struct.unpack('i',struct_data_len)[0]

# 通過檔案資訊的長度將檔案的描述資訊全部接收
print('data_len>>>',data_len)
file_info_bytes = conn.recv(data_len)
#將檔案描述資訊轉換為字典型別,以便操作
file_info_json = file_info_bytes.decode('utf-8')
file_info_dict = json.loads(file_info_json) #{'file_name': 'aaa.mp4', 'file_size': 24409470}

print(file_info_dict)

#統計每次接收的累計長度
recv_sum = 0

#根據檔案描述資訊,指定檔案路徑和檔名稱
file_path = 'D:\s18\jj' + '\\' + file_info_dict['file_name']

#接收檔案的真實資料
with open(file_path,'wb') as f:
    #迴圈接收,迴圈結束的依據是檔案描述資訊中檔案的大小,也是通過一個初始值為0的變數來統計
    while recv_sum < file_info_dict['file_size']:
        every_recv_data = conn.recv(1024)
        recv_sum += len(every_recv_data)
        f.write(every_recv_data)

客戶端client:

import os
import socket
import json
import struct
client = socket.socket()
client.connect(('127.0.0.1',8001))

#統計檔案大小
file_size = os.path.getsize(r'D:\python_workspace_s18\day029\aaa.mp4')


#統計檔案描述資訊,給服務端,服務端按照我的檔案描述資訊來儲存檔案,命名檔案等等,現在放到一個字典裡面了
file_info = {
    'file_name':'aaa.mp4',
    'file_size':file_size,
}

#由於字典無法直接轉換成bytes型別的資料,所以需要json來將字典轉換為json字串.在把字串轉換為位元組型別的資料進行傳送
#json.dumps是將字典轉換為json字串的方法
file_info_json = json.dumps(file_info)

#將字串轉換成bytes型別的資料
file_info_byte = file_info_json.encode('utf-8')

#為了防止黏包現象,將檔案描述資訊的長度打包後和檔案的描述資訊的資料一起傳送過去
data_len = len(file_info_byte)
data_len_struct = struct.pack('i',data_len)

#傳送檔案描述資訊
client.send(data_len_struct + file_info_byte)

#定義一個變數,=0,作為每次讀取檔案的長度的累計值
sum = 0
#開啟的aaa.mp4檔案,rb的形式,
with open('aaa.mp4','rb') as f:
    #迴圈讀取檔案內容
    while sum < file_size:
        #每次讀取的檔案內容,每次讀取1024B大小的資料
        every_read_data = f.read(1024)
        #將sum累加,統計長度
        sum += len(every_read_data)
        #將每次讀取的檔案的真實資料返送給服務端
        client.send(every_read_data)