WebDriver Wire協議是通用的,也就是說不管是FirefoxDriver還是ChromeDriver,啟動之后都會在某一個端口啟動基於這套協議的Web Service。例如FirefoxDriver初始化成功之后,默認會從http://localhost:7055開始,而ChromeDriver則大概是http://localhost:46350之類的。接下來,我們調用WebDriver的任何API,都需要借助一個ComandExecutor發送一個命令,實際上是一個HTTP request給監聽端口上的Web Service。在我們的HTTP request的body中,會以WebDriver Wire協議規定的JSON格式的字符串來告訴Selenium我們希望瀏覽器接下來做什么事情。
可以更通俗的理解:由於客戶端腳本(java, python, ruby)不能直接與瀏覽器通信,這時候可以把WebService當做一個翻譯器,它可以把客戶端代碼翻譯成瀏覽器可以識別的代碼(比如js).客戶端(也就是測試腳本)創建1個session,在該session中通過http請求向WebService發送restful的請求,WebService翻譯成瀏覽器懂得腳本傳給瀏覽器,瀏覽器把執行的結果返回給WebService,WebService把返回的結果做了一些封裝(一般都是json格式),然后返回給client,根據返回值就能判斷對瀏覽器的操作是不是執行成功
摘自官網對於chrome driver的描述:
The ChromeDriver consists of three separate pieces. There is the browser itself ("chrome"), the language bindings provided by the Selenium project ("the driver") and an executable downloaded from the Chromium project which acts as a bridge between "chrome" and the "driver". This executable is called "chromedriver", but we'll try and refer to it as the "server" in this page to reduce confusion.
大概意思就是我們下載的chrome可執行文件(.exe)是為作為瀏覽器與client(language binding)橋梁的作用,也更印證了對於Web Service(driver)的理解。
舉個實際的例子:
下圖表示了各種WebDriver的工作原理
從上圖中我們可以看出,不同瀏覽器的WebDriver子類,都需要依賴特定的瀏覽器原生組件,例如運行Firefox就需要一個add-on名字叫webdriver.xpi。而IE的話就需要用到一個dll文件來轉化Web Service的命令為瀏覽器native的調用。另外,圖中還標明了WebDriver Wire協議是一套基於RESTful的web service
關於WebDriver Wire協議的細節,比如希望了解這套Web Service能夠做哪些事情,可以閱讀Selenium官方的協議文檔, 在Selenium的源碼中,我們可以找到一個HttpCommandExecutor這個類,里面維護了一個Map<String, CommandInfo>,它負責將一個個代表命令的簡單字符串key,轉化為相應的URL,因為REST的理念是將所有的操作視作一個個狀態,每一個狀態對應一個URI。所以當我們以特定的URL發送HTTP request給這個RESTful web service之后,它就能解析出需要執行的操作。截取一段源碼如下:
1 .put(NEW_SESSION, post("/session")) 2 .put(QUIT, delete("/session/:sessionId")) 3 .put(GET_CURRENT_WINDOW_HANDLE, get("/session/:sessionId/window_handle")) 4 .put(GET_WINDOW_HANDLES, get("/session/:sessionId/window_handles")) 5 .put(GET, post("/session/:sessionId/url")) 6 7 // The Alert API is still experimental and should not be used. 8 .put(GET_ALERT, get("/session/:sessionId/alert")) 9 .put(DISMISS_ALERT, post("/session/:sessionId/dismiss_alert")) 10 .put(ACCEPT_ALERT, post("/session/:sessionId/accept_alert")) 11 .put(GET_ALERT_TEXT, get("/session/:sessionId/alert_text")) 12 .put(SET_ALERT_VALUE, post("/session/:sessionId/alert_text"))
可以看到實際發送的URL都是相對路徑,后綴多以/session/:sessionId開頭,這也意味着WebDriver每次啟動瀏覽器都會分配一個獨立的sessionId,多線程並行的時候彼此之間不會有沖突和干擾。例如我們最常用的一個WebDriver的API,getWebElement在這里就會轉化為/session/:sessionId/element這個URL,然后在發出的HTTP request body內再附上具體的參數比如by ID還是CSS還是Xpath,各自的值又是什么。收到並執行了這個操作之后,也會回復一個HTTP response。內容也是JSON,會返回找到的WebElement的各種細節,比如text、CSS selector、tag name、class name等等。以下是解析我們說的HTTP response的代碼片段:
1 try { 2 response = new JsonToBeanConverter().convert(Response.class, responseAsText); 3 } catch (ClassCastException e) { 4 if (responseAsText != null && "".equals(responseAsText)) { 5 // The remote server has died, but has already set some headers. 6 // Normally this occurs when the final window of the firefox driver 7 // is closed on OS X. Return null, as the return value _should_ be 8 // being ignored. This is not an elegant solution. 9 return null; 10 } 11 throw new WebDriverException("Cannot convert text to response: " + responseAsText, e); 12 } //...
PS:如果想更深入的了解WebDriver的架構,可以參考該文章http://www.aosabook.org/en/selenium.html。