1. 程式人生 > >linux 下 rpc python 例項之使用XML-RPC進行遠端檔案共享

linux 下 rpc python 例項之使用XML-RPC進行遠端檔案共享

這是個不錯的練習,使用python開發P2P程式,或許通過這個我們可以自己搞出來一個P2P下載工具,類似於迅雷。說到迅雷,關於其原理不知道大家是否瞭解,如果你不瞭解,我想看完這篇文章,你一定會了解的。啥,你已經瞭解了?那就過來指點一番。

以前在java中也接觸過類似的概念。一個是RMI( Remote Method Invocation)的概念,另外一個就是XML-RPC的概念。

那麼什麼是XML-RPC呢?它和P2P有什麼關係?下面談談我的個人理解。

XML-RPC是一個遠端過程呼叫(remote procedure call,RPC)的分散式計算協議,通過XML將呼叫函式封裝,並使用HTTP協議作為傳送機制[摘自維基百科]。所以這個XML-RPC可以幫助我們完成遠端呼叫的工作,即呼叫相鄰電腦中的方法,當然前提是在相鄰電腦中已經有我們編寫的供遠端呼叫的程式在執行(不管是在前臺還是後臺,就像迅雷一樣,總是悄悄執行)。

這裡還要提出來一個概念:Node,即節點。每一個電腦被為一個節點,這個只是針對每個電腦只執行一個我們通過XML-RPC編寫的程式,如果電腦中同時運行了多個程式,其實每一個程式都是一個節點。有了節點這樣的一個概念之後,我想大家可以想象的出來了,不同節點之間相連,形成各種複雜的網狀結構。

這時每個節點可以和其他多個節點進行相連,但是我們沒必要讓一個節點通其他所有的節點都相連,連結太多會很亂,就像人際關係一樣。那什麼時候連哪些節點呢?這時就要說到P2P了,所謂P2P即指peer to peer,也就是點到點。說是點到點,你可千萬別認為只是從一點到一點,因為他可能是從多點到一點,或者一點到多點。而沒有固定的從哪個點到哪個點,所有的點都可以相連。

因此在下載東西方面,這樣的協議就比傳統的只是從某一點下載資料要快很多,資源也會多很多。

其運作流程是這樣的,我打一個比方:比如小A在迅雷裡下載B片,迅雷上面可以沒有這個資源,但是他可以幫你從節點中找,看誰又這個資源,剛好小C電腦裡有,並且在迅雷共享目錄下,然後迅雷就會把小C電腦中把資源通過自己的節點傳回到小A的電腦上,當然更可能的情況是直接讓小A和小C相連。

大家在使用迅雷下載東西的時候肯定注意過裡面有一項資源:x/xx這樣的東西,我覺得,前面的那個x的意思表示當為你提供資源的節點數量,後面的那個xx表示,所有擁有該資源的節點數目,這些節點可能並不在線。

理解了基本的概念之後,再來看python中如何來實現。

可以先做一個小小的嘗試: 首先進入命令列,輸入python,然後輸入一下程式碼:

from simpleXMLRPCServerr import SimpleXMLRPCServerr
s = SimpleXMLRPCServer(("",4242))
def twice(x):
return x*x

s.register_function(twice) #向伺服器新增功能
s.serve_forever() #啟動伺服器

然後在啟動一個命令列,進入pyhon。 輸入:

from xmlrpclib import ServerProxy s = ServerProxy('http://localhost:4242') s.twice(2) #通過ServerProxy呼叫遠端的方法,

然後你就會看到通過遠端方法的計算完成。

是不是很輕鬆,這個還是比較簡陋,不過足以讓你理解python的遠端呼叫,再來看看完整的吧。

先上程式碼,然後再詳解。 首先是Server.py:

#coding=utf-8

from xmlrpclib import ServerProxy,Fault
from os.path import join, abspath,isfile
from SimpleXMLRPCServer import SimpleXMLRPCServer
from urlparse import urlparse
import sys

SimpleXMLRPCServer.allow_reuse_address = 1

MAX_HISTORY_LENGTH = 6

UNHANDLED = 100
ACCESS_DENIED = 200

class UnhandledQuery(Fault):
    '''
    that's show can't handle the query exception
    '''
    def __init__(self,message="Couldn't handle the query"):
        Fault.__init__(self, UNHANDLED, message)

class AccessDenied(Fault):
    '''
    when user try to access the forbiden resources raise exception
    '''
    def __init__(self, message="Access denied"):
        Fault.__init__(self, ACCESS_DENIED, message)

def inside(dir,name):
    '''
    check the dir that user defined is contain the filename the user given
    '''
    dir = abspath(dir)
    name = abspath(name)
    return name.startswith(join(dir,''))
def getPort(url):
    '''
    get the port num from the url
    '''
    name = urlparse(url)[1]
    parts = name.split(':')
    return int(parts[-1])

class Node:

    def __init__(self, url, dirname, secret):
        self.url = url
        self.dirname = dirname
        self.secret = secret
        self.known = set()

    def query(self, query, history = []):
        try:
            return self._handle(query)
        except UnhandledQuery:
            history = history + [self.url]
            if len(history) > MAX_HISTORY_LENGTH: raise
            return self._broadcast(query,history)

    def hello(self,other):
        self.known.add(other)
        return 0
    def fetch(self, query, secret):

        if secret != self.secret: raise
        result = self.query(query)
        f = open(join(self.dirname, query),'w')
        f.write(result)
        f.close()
        return 0

    def _start(self):
        s = SimpleXMLRPCServer(("",getPort(self.url)),logRequests=False)
        s.register_instance(self)
        s.serve_forever()

    def _handle(self, query):
        dir = self.dirname
        name = join(dir, query)
        if not isfile(name):raise UnhandledQuery
        if not inside(dir,name):raise AccessDenied
        return open(name).read()

    def _broadcast(self, query, history):

        for other in self.known.copy():
            if other in history: continue
            try:
                s = ServerProxy(other)
                return s.query(query, history)
            except Fault, f:
                if f.faultCode == UNHANDLED:pass
                else: self.known.remove(other)
            except:
                self.known.remove(other)

        raise UnhandledQuery

def main():
    url, directory, secret = sys.argv[1:]
    n = Node(url,directory,secret)
    n._start()

if __name__ == '__main__': main()

首先來看上面的幾個常量設定: SimpleXMLRPCServer.allow_reuse_address表示,其所佔用的埠可以重用,即如果你強制關閉node server之後再次重啟,不會出現埠被佔用的情況。

MAX_HISTORY_LENGTH = 6 這個是設定最大的節點長度,因為不能讓讓節點無休止的搜尋下去。

UNHANDLED = 100 ACCESS_DENIED = 200 這倆就是返回碼。

然後再來看個node節點的具體流程。 這個段程式碼的流程這這樣的,首先,啟動供遠端呼叫的伺服器,呼叫的介面就是Node類。在Node類中有三個方法供遠端呼叫的,一個是hello,一個是fetch還有一個query。hello 這個方法就是新增鄰節點資訊到當前節點中。而fetch則是用來獲取資料的方法,query是節點之間用來互動的。

在fetch方法中,首先判斷密碼是否正確,然後通過呼叫自己的query方法查詢資料。我們來看query方法,這個方法中,先是呼叫私有方法_handle本地查詢,如果沒找到,那麼在通過_broadcast介面在所有已知節點中傳送廣播,這裡要注意histroy,每次廣播都會傳遞history這個引數,這個引數的作用有二:一是、防止往重複的節點中傳送廣播;二是、限制當前所有連結節點的長度。

理解了一個node server的基礎功能之後,再來看對server進行管理的控制類程式碼。

client.py:

#coding=utf-8

from xmlrpclib import ServerProxy, Fault
from cmd import Cmd
from random import choice
from string import lowercase
from server import Node,UNHANDLED  #引入前面的server
from threading import Thread
from time import sleep

import sys

HEAD_START = 0.1
SECRET_LENGTH = 100

def randomString(length):
    chars = []
    letters = lowercase[:26]
    while length > 0:
        length -= 1
        chars.append(choice(letters))
    return ''.join(chars)

class Client(Cmd):
    prompt = '> '

    def __init__(self, url, dirname, urlfile):

        Cmd.__init__(self)
        self.secret = randomString(SECRET_LENGTH)
        n = Node(url, dirname, self.secret)
        t = Thread(target = n._start)
        t.setDaemon(1)
        t.start()

        sleep(HEAD_START)
        self.server = ServerProxy(url)
        for line in open(urlfile):
            line = line.strip()
            self.server.hello(line)

    def do_fetch(self, arg):
        try:
            self.server.fetch(arg,self.secret)
        except Fault,f:
            if f.faultCode != UNHANDLED: raise
            print "Couldn't find the file",arg

    def do_exit(self,arg):
        print
        sys.exit()

    do_EOR = do_exit

def main():
    urlfile, directory, url = sys.argv[1:]
    client = Client(url, directory, urlfile)
    client.cmdloop()

if __name__ == '__main__':main()

來分析一下這段程式碼,前面的引數就不看了,很好理解,一開始有一個隨機生成密碼的函式,做什麼用的呢?主要是用來防止別人非法呼叫該控制所控制的node server的。這密碼 我們也不用記,因為我們有client的合法使用權。呵呵。

這段程式碼的總體作用就是為你提供一個可視的命令列的介面,通過繼承cmd這個類,來解析你輸入的命令,比如程式執行之後,出現命令提示符,你輸入fetch,那麼它會呼叫到do_fetch這個方法中來,並把引數傳遞進來。

do_fetch這個方法的所用就是呼叫node server中的fetch方法,獲取資源。

另外的一個do_exit很好理解,就是接受exit命令退出程式。

在程式初始化的時候,還有一點需要注意,就是它會讀取你urlfile引數傳遞的檔案中的資料,這個裡面放的是節點的url地址。讀取之後程式會把這些地址加到相鄰節點中,供以後訪問。不過這個程式還有些不完善的地方就是在程式執行時,如果你修改了url配置的檔案,他不會讀取你新新增的節點url。不過這個修改很簡單,把獲取url的程式碼放到do_fetch中就行了。

在執行程式之前還有一些工作要做。 首先需要建立兩個資料夾,A和C,C資料夾裡面建立一個檔案,B.txt,在A和C所在資料夾中建立urlsA.txt和urlsC.txt檔案。裡面在urlsA.txt中寫入:http://localhost:4243,然後開啟兩個命令列,

第一個輸入:python client.py urlsA.txt Ahttp://localhost:4242回車,是不是出來提示符了。輸入fetch B.txt回車,看到提示Couldn't find the file B.txt。

然後在第二個命令列中輸入python client.py urlsC.txt Chttp://localhost:4243回車。同樣輸入fetch B.txt回車,是不是沒反應。說明該檔案存在。接在在第一個命令列中再次輸入fetch B.txt看,是否還是提示沒找到檔案,如果你對程式碼根據我上面的建議進行了修改的話,就不會出現錯誤了,如果沒有修改,此時你需要把輸入exit退出程式,再次重啟,然後在fetch B.txt,然後到A資料夾下檢視一下,看是不是把B.txt下載到你的資料夾中了。

PS:上面的程式只能傳輸文字檔案,大檔案或者其他格式的檔案無法傳輸,剛才研究了一下,使用xmlrpclib這個庫中的Binary函式即可,具體使用訪問為: 先引入xmlrpclib,import xmlrpclib 在server類的的_handle方法中最後返回的那句程式碼return open(name).read() 修改為 return xmlrpclib.Binary(open(name,'rb').read()) 再把fetch方法中的f.write(result)修改為f.write(result.data) 另外這句話前面的那個寫檔案的方式要改為wb。