1. 程式人生 > >ZeroMQ 中文指南 第二章 ZeroMQ進階【轉載】

ZeroMQ 中文指南 第二章 ZeroMQ進階【轉載】

作者資訊如下。
ZMQ 指南

作者: Pieter Hintjens [email protected], CEO iMatix Corporation.

翻譯: 張吉 [email protected], 安居客集團 好租網工程師

NOTE: 此翻譯涵蓋2011年10月份的ZMQ穩定版本,即2.1.0 stable release。但讀者仍然可以通過此文了解ZMQ的一些基本概念和哲學。

第二章 ZeroMQ進階

第一章我們簡單試用了ZMQ的若干通訊模式:請求-應答模式、釋出-訂閱模式、管道模式。這一章我們將學習更多在實際開發中會使用到的東西:

本章涉及的內容有:

  • 建立和使用ZMQ套接字
  • 使用套接字傳送和接收訊息
  • 使用ZMQ提供的非同步I/O套接字構建你的應用程式
  • 在單一執行緒中使用多個套接字
  • 恰當地處理致命和非致命錯誤
  • 處理諸如Ctrl-C的中斷訊號
  • 正確地關閉ZMQ應用程式
  • 檢查ZMQ應用程式的記憶體洩露
  • 傳送和接收多幀訊息
  • 在網路中轉發訊息
  • 建立簡單的訊息佇列代理
  • 使用ZMQ編寫多執行緒應用程式
  • 使用ZMQ線上程間傳遞訊號
  • 使用ZMQ協調網路中的節點
  • 使用標識建立持久化套接字
  • 在釋出-訂閱模式中建立和使用訊息信封
  • 如何讓持久化的訂閱者能夠從崩潰中恢復
  • 使用閾值(HWM)防止記憶體溢位

零的哲學

ØMQ一詞中的Ø讓我們糾結了很久。一方面,這個特殊字元會降低ZMQ在谷歌和推特中的收錄量;另一方面,這會惹惱某些丹麥語種的民族,他們會嚷道Ø並不是一個奇怪的0。

一開始ZMQ代表零中介軟體、零延遲,同時,它又有了新的含義:零管理、零成本、零浪費。總的來說,零表示最小、最簡,這是貫穿於該專案的哲理。我們致力於減少複雜程度,提高易用性。

套接字API

說實話,ZMQ有些偷樑換柱的嫌疑。不過我們並不會為此道歉,因為這種概念上的切換絕對不會有壞處。ZMQ提供了一套類似於BSD套接字的API,但將很多訊息處理機制的細節隱藏了起來,你會逐漸適應這種變化,並樂於用它進行程式設計。

套接字事實上是用於網路程式設計的標準介面,ZMQ之所那麼吸引人眼球,原因之一就是它是建立在標準套接字API之上。因此,ZMQ的套接字操作非常容易理解,其生命週期主要包含四個部分:

  • 建立和銷燬套接字:zmq_socket(), zmq_close()
  • 配置和讀取套接字選項:zmq_setsockopt(), zmq_getsockopt()
  • 為套接字建立連線:zmq_bind(), zmq_connect()
  • 傳送和接收訊息:zmq_send(), zmq_recv()

如以下C程式碼:

void *mousetrap;

//  Create socket for catching mice
mousetrap = zmq_socket (context, ZMQ_PULL);

//  Configure the socket
int64_t jawsize = 10000;
zmq_setsockopt (mousetrap, ZMQ_HWM, &jawsize, sizeof jawsize);

//  Plug socket into mouse hole
zmq_connect (mousetrap, "tcp://192.168.55.221:5001");

//  Wait for juicy mouse to arrive
zmq_msg_t mouse;
zmq_msg_init (&mouse);
zmq_recv (mousetrap, &mouse, 0);
//  Destroy the mouse
zmq_msg_close (&mouse);

//  Destroy the socket
zmq_close (mousetrap);

請注意,套接字永遠是空指標型別的,而訊息則是一個數據結構(我們下文會講述)。所以,在C語言中你通過變數傳遞套接字,而用引用傳遞訊息。記住一點,在ZMQ中所有的套接字都是由ZMQ管理的,只有訊息是由程式設計師管理的。

建立、銷燬、以及配置套接字的工作和處理一個物件差不多,但請記住ZMQ是非同步的,伸縮性很強,因此在將其應用到網路結構中時,可能會需要多一些時間來理解。

使用套接字構建拓撲結構

在連線兩個節點時,其中一個需要使用zmq_bind(),另一個則使用zmq_connect()。通常來講,使用zmq_bind()連線的節點稱之為服務端,它有著一個較為固定的網路地址;使用zmq_connect()連線的節點稱為客戶端,其地址不固定。我們會有這樣的說法:繫結套接字至端點;連線套接字至端點。端點指的是某個廣為周知網路地址。

ZMQ連線和傳統的TCP連線是有區別的,主要有:

  • 使用多種協議,inproc(程序內)、ipc(程序間)、tcp、pgm(廣播)、epgm;
  • 當客戶端使用zmq_connect()時連線就已經建立了,並不要求該端點已有某個服務使用zmq_bind()進行了繫結;
  • 連線是非同步的,並由一組訊息佇列做緩衝;
  • 連線會表現出某種訊息模式,這是由建立連線的套接字型別決定的;
  • 一個套接字可以有多個輸入和輸出連線;
  • ZMQ沒有提供類似zmq_accept()的函式,因為當套接字繫結至端點時它就自動開始接受連線了;
  • 應用程式無法直接和這些連線打交道,因為它們是被封裝在ZMQ底層的。

在很多架構中都使用了類似於C/S的架構。服務端元件式比較穩定的,而客戶端元件則較為動態,來去自如。所以說,服務端地址對客戶端而言往往是可見的,反之則不然。這樣一來,架構中應該將哪些元件作為服務端(使用zmq_bind()),哪些作為客戶端(使用zmq_connect()),就很明顯了。同時,這需要和你使用的套接字型別相聯絡起來,我們下文會詳細講述。

讓我們試想一下,如果先打開了客戶端,後開啟服務端,會發生什麼?傳統網路連線中,我們開啟客戶端時一定會收到系統的報錯資訊,但ZMQ讓我們能夠自由地啟動架構中的元件。當客戶端使用zmq_connect()連線至某個端點時,它就已經能夠使用該套接字傳送訊息了。如果這時,服務端啟動起來了,並使用zmq_bind()繫結至該端點,ZMQ將自動開始轉發訊息。

服務端節點可以僅使用一個套接字就能繫結至多個端點。也就是說,它能夠使用不同的協議來建立連線:

zmq_bind (socket, "tcp://*:5555");
zmq_bind (socket, "tcp://*:9999");
zmq_bind (socket, "ipc://myserver.ipc");

當然,你不能多次繫結至同一端點,這樣是會報錯的。

每當有客戶端節點使用zmq_connect()連線至上述某個端點時,服務端就會自動建立連線。ZMQ沒有對連線數量進行限制。此外,客戶端節點也可以使用一個套接字同時建立多個連線。

大多數情況下,哪個節點充當服務端,哪個作為客戶端,是網路架構層面的內容,而非訊息流問題。不過也有一些特殊情況(如失去連線後的訊息重發),同一種套接字使用繫結和連線是會有一些不同的行為的。

所以說,當我們在設計架構時,應該遵循“服務端是穩定的,客戶端是靈活的“原則,這樣就不太會出錯。

套接字是有型別的,套接字型別定義了套接字的行為,它在傳送和接收訊息時的規則等。你可以將不同種類的套接字進行連線,如PUB-SUB組合,這種組合稱之為釋出-訂閱模式,其他組合也會有相應的模式名稱,我們會在下文詳述。

正是因為套接字可以使用不同的方式進行連線,才構成了ZMQ最基本的訊息佇列系統。我們還可以在此基礎之上建立更為複雜的裝置、路由機制等,下文會詳述。總的來說,ZMQ為你提供了一套元件,供你在網路架構中拼裝和使用。

使用套接字傳遞資料

傳送和接收訊息使用的是zmq_send()和zmq_recv()這兩個函式。雖然函式名稱看起來很直白,但由於ZMQ的I/O模式和傳統的TCP協議有很大不同,因此還是需要花點時間去理解的。

1

讓我們看一看TCP套接字和ZMQ套接字之間在傳輸資料方面的區別:

  • ZMQ套接字傳輸的是訊息,而不是位元組(TCP)或幀(UDP)。訊息指的是一段指定長度的二進位制資料塊,我們下文會講到訊息,這種設計是為了效能優化而考慮的,所以可能會比較難以理解。
  • ZMQ套接字在後臺進行I/O操作,也就是說無論是接收還是傳送訊息,它都會先傳送到一個本地的緩衝佇列,這個記憶體佇列的大小是可以配置的。
  • ZMQ套接字可以和多個套接字進行連線(如果套接字型別允許的話)。TCP協議只能進行點對點的連線,而ZMQ則可以進行一對多(類似於無線廣播)、多對多(類似於郵局)、多對一(類似於信箱),當然也包括一對一的情況。
  • ZMQ套接字可以傳送訊息給多個端點(扇出模型),或從多個端點中接收訊息(扇入模型)

2

所以,向套接字寫入一個訊息時可能會將訊息傳送給很多節點,相應的,套接字又會從所有已建立的連線中接收訊息。zmq_recv()方法使用了公平佇列的演算法來決定接收哪個連線的訊息。

呼叫zmq_send()方法時其實並沒有真正將訊息傳送給套接字連線。訊息會在一個記憶體佇列中儲存下來,並由後臺的I/O執行緒非同步地進行傳送。如果不出意外情況,這一行為是非阻塞的。所以說,即便zmq_send()有返回值,並不能代表訊息已經發送。當你在用zmq_msg_init_data()初始化訊息後,你不能重用或是釋放這條訊息,否則ZMQ的I/O執行緒會認為它在傳輸垃圾資料。這對初學者來講是一個常犯的錯誤,下文我們會講述如何正確地處理訊息。

單播傳輸

ZMQ提供了一組單播傳輸協議(inporc, ipc, tcp),和兩個廣播協議(epgm, pgm)。廣播協議是比較高階的協議,我們會在以後講述。如果你不能回答我扇出比例會影響一對多的單播傳輸時,就先不要去學習廣播協議了吧。

一般而言我們會使用tcp作為傳輸協議,這種TCP連線是可以離線運作的,它靈活、便攜、且足夠快速。為什麼稱之為離線,是因為ZMQ中的TCP連線不需要該端點已經有某個服務進行了繫結,客戶端和服務端可以隨時進行連線和繫結,這對應用程式而言都是透明的。

程序間協議,即ipc,和tcp的行為差不多,但已從網路傳輸中抽象出來,不需要指定IP地址或者域名。這種協議很多時候會很方便,本指南中的很多示例都會使用這種協議。ZMQ中的ipc協議同樣可以是離線的,但有一個缺點——無法在Windows作業系統上運作,這一點也許會在未來的ZMQ版本中修復。我們一般會在端點名稱的末尾附上.ipc的副檔名,在UNIX系統上,使用ipc協議還需要注意許可權問題。你還需要保證所有的程式都能夠找到這個ipc端點。

程序內協議,即inproc,可以在同一個程序的不同執行緒之間進行訊息傳輸,它比ipc或tcp要快得多。這種協議有一個要求,必須先繫結到端點,才能建立連線,也許未來也會修復。通常的做法是先啟動服務端執行緒,繫結至端點,後啟動客戶端執行緒,連線至端點。

ZMQ不只是資料傳輸

經常有新人會問,如何使用ZMQ建立一項服務?我能使用ZMQ建立一個HTTP伺服器嗎?

他們期望得到的回答是,我們用普通的套接字來傳輸HTTP請求和應答,那用ZMQ套接字也能夠完成這個任務,且能執行得更快、更好。

只可惜答案並不是這樣的。ZMQ不只是一個數據傳輸的工具,而是在現有通訊協議之上建立起來的新架構。它的資料幀和現有的協議並不相容,如下面是一個HTTP請求和ZMQ請求的對比,同樣使用的是TCP/IPC協議:

3

HTTP請求使用CR-LF(換行符)作為資訊幀的間隔,而ZMQ則使用指定長度來定義幀:

4

所以說,你的確是可以用ZMQ來寫一個類似於HTTP協議的東西,但是這並不是HTTP。

不過,如果有人問我如何更好地使用ZMQ建立一個新的服務,我會給出一個不錯的答案,那就是:你可以自行設計一種通訊協議,用ZMQ進行連線,使用不同的語言提供服務和擴充套件,可以在本地,亦可通過遠端傳輸。賽德•肖的Mongrel2網路服務的架構就是一個很好的示例。

I/O執行緒

我們提過ZMQ是通過後臺的I/O執行緒進行訊息傳輸的。一個I/O執行緒已經足以處理多個套接字的資料傳輸要求,當然,那些極端的應用程式除外。這也就是我們在建立上下文時傳入的1所代表的意思:

void *context = zmq_init (1);

ZMQ應用程式和傳統應用程式的區別之一就是你不需要為每個套接字都建立一個連線。單個ZMQ套接字可以處理所有的傳送和接收任務。如,當你需要向一千個訂閱者釋出訊息時,使用一個套接字就可以了;當你需要向二十個服務程序分發任務時,使用一個套接字就可以了;當你需要從一千個網頁應用程式中獲取資料時,也是使用一個套接字就可以了。

這一特性可能會顛覆網路應用程式的編寫步驟,傳統應用程式每個程序或執行緒會有一個遠端連線,它又只能處理一個套接字。ZMQ讓你打破這種結構,使用一個執行緒來完成所有工作,更易於擴充套件。

核心訊息模式

ZMQ的套接字API中提供了多種訊息模式。如果你熟悉企業級訊息應用,那這些模式會看起來很熟悉。不過對於新手來說,ZMQ的套接字還是會讓人大吃一驚的。

讓我們回顧一下ZMQ會為你做些什麼:它會將訊息快速高效地傳送給其他節點,這裡的節點可以是執行緒、程序、或是其他計算機;ZMQ為應用程式提供了一套簡單的套接字API,不用考慮實際使用的協議型別(程序內、程序間、TPC、或廣播);當節點調動時,ZMQ會自動進行連線或重連;無論是傳送訊息還是接收訊息,ZMQ都會先將訊息放入佇列中,並保證程序不會因為記憶體溢位而崩潰,適時地將訊息寫入磁碟;ZMQ會處理套接字異常;所有的I/O操作都在後臺進行;ZMQ不會產生死鎖。

但是,以上種種的前提是使用者能夠正確地使用訊息模式,這種模式往往也體現出了ZMQ的智慧。訊息模式將我們從實踐中獲取的經驗進行抽象和重組,用於解決之後遇到的所有問題。ZMQ的訊息模式目前是編譯在類庫中的,不過未來的ZMQ版本可能會允許使用者自行制定訊息模式。

ZMQ的訊息模式是指不同型別套接字的組合。換句話說,要理解ZMQ的訊息模式,你需要理解ZMQ的套接字型別,它們是如何一起工作的。這一部分是需要死記硬背的。

ZMQ的核心訊息模式有:

  • 請求-應答模式 將一組服務端和一組客戶端相連,用於遠端過程呼叫或任務分發。

  • 釋出-訂閱模式 將一組釋出者和一組訂閱者相連,用於資料分發。

  • 管道模式 使用扇入或扇出的形式組裝多個節點,可以產生多個步驟或迴圈,用於構建並行處理架構。

我們在第一章中已經講述了這些模式,不過還有一種模式是為那些仍然認為ZMQ是類似TCP那樣點對點連線的人們準備的:

  • 排他對接模式 將兩個套接字一對一地連線起來,這種模式應用場景很少,我們會在本章最末尾看到一個示例。

zmq_socket()函式的說明頁中有對所有訊息模式的說明,比較清楚,因此值得研讀幾次。我們會介紹每種訊息模式的內容和應用場景。

以下是合法的套接字連線-繫結對(一端繫結、一端連線即可):

  • PUB - SUB
  • REQ - REP
  • REQ - ROUTER
  • DEALER - REP
  • DEALER - ROUTER
  • DEALER - DEALER
  • ROUTER - ROUTER
  • PUSH - PULL
  • PAIR - PAIR

其他的組合模式會產生不可預知的結果,在將來的ZMQ版本中可能會直接返回錯誤。你也可以通過程式碼去了解這些套接字型別的行為。

上層訊息模式

上文中的四種核心訊息模式是內建在ZMQ中的,他們是API的一部分,在ZMQ的C++核心類庫中實現,能夠保證正確地執行。如果有朝一日Linux核心將ZMQ採納了進來,那這些核心模式也肯定會包含其中。

在這些訊息模式之上,我們會建立更為上層的訊息模式。這種模式可以用任何語言編寫,他們不屬於核心型別的一部分,不隨ZMQ發行,只在你自己的應用程式中出現,或者在ZMQ社群中維護。

本指南的目的之一就是為你提供一些上層的訊息模式,有簡單的(如何正確處理訊息),也有複雜的(可靠的釋出-訂閱模式)。

訊息的使用方法

ZMQ的傳輸單位是訊息,即一個二進位制塊。你可以使用任意的序列化工具,如谷歌的Protocal Buffers、XDR、JSON等,將內容轉化成ZMQ訊息。不過這種轉化工具最好是便捷和快速的,這個請自己衡量。

在記憶體中,ZMQ訊息由zmq_msg_t結構表示(每種語言有特定的表示)。在C語言中使用ZMQ訊息時需要注意以下幾點:

  • 你需要建立和傳遞zmq_msg_t物件,而不是一組資料塊;
  • 讀取訊息時,先用zmq_msg_init()初始化一個空訊息,再將其傳遞給zmq_recv()函式;
  • 寫入訊息時,先用zmq_msg_init_size()來建立訊息(同時也已初始化了一塊記憶體區域),然後用memcpy()函式將資訊拷貝到該物件中,最後傳給zmq_send()函式;
  • 釋放訊息(並不是銷燬)時,使用zmq_msg_close()函式,它會將對訊息物件的引用刪除,最終由ZMQ將訊息銷燬;
  • 獲取訊息內容時需使用zmq_msg_data()函式;若想知道訊息的長度,可以使用zmq_msg_size()函式;
  • 至於zmq_msg_move()、zmq_msg_copy()、zmq_msg_init_data()函式,在充分理解手冊中的說明之前,建議不好貿然使用。

以下是一段處理訊息的典型程式碼,如果之前的程式碼你有看的話,那應該會感到熟悉。這段程式碼其實是從zhelpers.h檔案中抽出的:

//  從套接字中獲取ZMQ字串,並轉換為C語言字串
static char *
s_recv (void *socket) {
    zmq_msg_t message;
    zmq_msg_init (&message);
    zmq_recv (socket, &message, 0);
    int size = zmq_msg_size (&message);
    char *string = malloc (size + 1);
    memcpy (string, zmq_msg_data (&message), size);
    zmq_msg_close (&message);
    string [size] = 0;
    return (string);
}

//  將C語言字串轉換為ZMQ字串,併發送給套接字
static int
s_send (void *socket, char *string) {
    int rc;
    zmq_msg_t message;
    zmq_msg_init_size (&message, strlen (string));
    memcpy (zmq_msg_data (&message), string, strlen (string));
    rc = zmq_send (socket, &message, 0);
    assert (!rc);
    zmq_msg_close (&message);
    return (rc);
}

你可以對以上程式碼進行擴充套件,讓其支援傳送和接受任一長度的資料。

需要注意的是,當你將一個訊息物件傳遞給zmq_send()函式後,該物件的長度就會被清零,因此你無法傳送同一個訊息物件兩次,也無法獲得已傳送訊息的內容。

如果你想傳送同一個訊息物件兩次,就需要在傳送第一次前新建一個物件,使用zmq_msg_copy()函式進行拷貝。這個函式不會拷貝訊息內容,只是拷貝引用。然後你就可以再次傳送這個訊息了(或者任意多次,只要進行了足夠的拷貝)。當訊息最後一個引用被釋放時,訊息物件就會被銷燬。

ZMQ支援多幀訊息,即在一條訊息中儲存多個訊息幀。這在實際應用中被廣泛使用,我們會在第三章進行講解。

關於訊息,還有一些需要注意的地方:

  • ZMQ的訊息是作為一個整體來收發的,你不會只收到訊息的一部分;
  • ZMQ不會立即傳送訊息,而是有一定的延遲;
  • 你可以傳送0位元組長度的訊息,作為一種訊號;
  • 訊息必須能夠在記憶體中儲存,如果你想傳送檔案或超長的訊息,就需要將他們切割成小塊,在獨立的訊息中進行傳送;
  • 必須使用zmq_msg_close()函式來關閉訊息,但在一些會在變數超出作用域時自動釋放訊息物件的語言中除外。

再重複一句,不要貿然使用zmq_msg_init_data()函式。它是用於零拷貝,而且可能會造成麻煩。關於ZMQ還有太多東西需要你去學習,因此現在暫時不用去考慮如何削減幾微秒的開銷。

處理多個套接字

在之前的示例中,主程式的迴圈體內會做以下幾件事:

  1. 等待套接字的訊息;
  2. 處理訊息;
  3. 返回第一步。

如果我們想要讀取多個套接字中的訊息呢?最簡單的方法是將套接字連線到多個端點上,讓ZMQ使用公平佇列的機制來接受訊息。如果不同端點上的套接字型別是一致的,那可以使用這種方法。但是,如果一個套接字的型別是PULL,另一個是PUB怎麼辦?如果現在開始混用套接字型別,那將來就沒有可靠性可言了。

正確的方法應該是使用zmq_poll()函式。更好的方法是將zmq_poll()包裝成一個框架,編寫一個事件驅動的反應器,但這個就比較複雜了,我們這裡暫不討論。

我們先不使用zmq_poll(),而用NOBLOCK(非阻塞)的方式來實現從多個套接字讀取訊息的功能。下面將氣象資訊服務和並行處理這兩個示例結合起來:

msreader: Multiple socket reader in C

//
//  從多個套接字中獲取訊息
//  本示例簡單地再迴圈中使用recv函式
//
#include "zhelpers.h"

int main (void) 
{
    //  準備上下文和套接字
    void *context = zmq_init (1);

    //  連線至任務分發器
    void *receiver = zmq_socket (context, ZMQ_PULL);
    zmq_connect (receiver, "tcp://localhost:5557");

    //  連線至天氣服務
    void *subscriber = zmq_socket (context, ZMQ_SUB);
    zmq_connect (subscriber, "tcp://localhost:5556");
    zmq_setsockopt (subscriber, ZMQ_SUBSCRIBE, "10001 ", 6);

    //  處理從兩個套接字中接收到的訊息
    //  這裡我們會優先處理從任務分發器接收到的訊息
    while (1) {
        //  處理等待中的任務
        int rc;
        for (rc = 0; !rc; ) {
            zmq_msg_t task;
            zmq_msg_init (&task);
            if ((rc = zmq_recv (receiver, &task, ZMQ_NOBLOCK)) == 0) {
                //  處理任務
            }
            zmq_msg_close (&task);
        }
        //  處理等待中的氣象更新
        for (rc = 0; !rc; ) {
            zmq_msg_t update;
            zmq_msg_init (&update);
            if ((rc = zmq_recv (subscriber, &update, ZMQ_NOBLOCK)) == 0) {
                //  處理氣象更新
            }
            zmq_msg_close (&update);
        }
        // 沒有訊息,等待1毫秒
        s_sleep (1);
    }
    //  程式不會執行到這裡,但還是做正確的退出清理工作
    zmq_close (receiver);
    zmq_close (subscriber);
    zmq_term (context);
    return 0;
}

這種方式的缺點之一是,在收到第一條訊息之前會有1毫秒的延遲,這在高壓力的程式中還是會構成問題的。此外,你還需要翻閱諸如nanosleep()的函式,不會造成迴圈次數的激增。

示例中將任務分發器的優先順序提升了,你可以做一個改進,輪流處理訊息,正如ZMQ內部做的公平佇列機制一樣。

下面,讓我們看看如何用zmq_poll()來實現同樣的功能:

mspoller: Multiple socket poller in C

//
//  從多個套接字中接收訊息
//  本例使用zmq_poll()函式
//
#include "zhelpers.h"

int main (void) 
{
    void *context = zmq_init (1);

    //  連線任務分發器
    void *receiver = zmq_socket (context, ZMQ_PULL);
    zmq_connect (receiver, "tcp://localhost:5557");

    //  連線氣象更新服務
    void *subscriber = zmq_socket (context, ZMQ_SUB);
    zmq_connect (subscriber, "tcp://localhost:5556");
    zmq_setsockopt (subscriber, ZMQ_SUBSCRIBE, "10001 ", 6);

    //  初始化輪詢物件
    zmq_pollitem_t items [] = {
        { receiver, 0, ZMQ_POLLIN, 0 },
        { subscriber, 0, ZMQ_POLLIN, 0 }
    };
    //  處理來自兩個套接字的訊息
    while (1) {
        zmq_msg_t message;
        zmq_poll (items, 2, -1);
        if (items [0].revents & ZMQ_POLLIN) {
            zmq_msg_init (&message);
            zmq_recv (receiver, &message, 0);
            //  處理任務
            zmq_msg_close (&message);
        }
        if (items [1].revents & ZMQ_POLLIN) {
            zmq_msg_init (&message);
            zmq_recv (subscriber, &message, 0);
            //  處理氣象更新
            zmq_msg_close (&message);
        }
    }
    //  程式不會執行到這兒
    zmq_close (receiver);
    zmq_close (subscriber);
    zmq_term (context);
    return 0;
}

處理錯誤和ETERM訊號

ZMQ的錯誤處理機制提倡的是快速崩潰。我們認為,一個程序對於自身內部的錯誤來說要越脆弱越好,而對外部的攻擊和錯誤要足夠健壯。舉個例子,活細胞會因檢測到自身問題而瓦解,但對外界的攻擊卻能極力抵抗。在ZMQ程式設計中,斷言用得是非常多的,如同細胞膜一樣。如果我們無法確定一個錯誤是來自於內部還是外部,那這就是一個設計缺陷了,需要修復。

在C語言中,斷言失敗會讓程式立即中止。其他語言中可以使用異常來做到。

當ZMQ檢測到來自外部的問題時,它會返回一個錯誤給呼叫程式。如果ZMQ不能從錯誤中恢復,那它是不會安靜地將訊息丟棄的。某些情況下,ZMQ也會去斷言外部錯誤,這些可以被歸結為BUG。

到目前為止,我們很少看到C語言的示例中有對錯誤進行處理。現實中的程式碼應該對每一次的ZMQ函式呼叫作錯誤處理。如果你不是使用C語言進行程式設計,可能那種語言的ZMQ類庫已經做了錯誤處理。但在C語言中,你需要自己動手。以下是一些常規的錯誤處理手段,從POSIX規範開始:

  • 建立物件的方法如果失敗了會返回NULL;
  • 其他方法執行成功時會返回0,失敗時會返回其他值(一般是-1);
  • 錯誤程式碼可以從變數errno中獲得,或者呼叫zmq_errno()函式;
  • 錯誤訊息可以呼叫zmq_strerror()函式獲得。

有兩種情況不應該被認為是錯誤:

  • 當執行緒使用NOBLOCK方式呼叫zmq_recv()時,若沒有接收到訊息,該方法會返回-1,並設定errno為EAGAIN;
  • 當執行緒呼叫zmq_term()時,若其他執行緒正在進行阻塞式的處理,該函式會中止所有的處理,關閉套接字,並使得那些阻塞方法的返回值為-1,errno設定為ETERM。

遵循以上規則,你就可以在ZMQ程式中使用斷言了:

void *context = zmq_init (1);
assert (context);
void *socket = zmq_socket (context, ZMQ_REP);
assert (socket);
int rc;
rc = zmq_bind (socket, "tcp://*:5555");
assert (rc == 0);

第一版的程式中我將函式呼叫直接放在了assert()函式裡面,這樣做會有問題,因為一些優化程式會直接將程式中的assert()函式去除。

讓我們看看如何正確地關閉一個程序,我們用管道模式舉例。當我們在後臺開啟了一組worker時,我們需要在任務執行完畢後關閉它們。我們可以向這些worker傳送自殺的訊息,這項工作由結果收集器來完成會比較恰當。

如何將結果收集器和worker相連呢?PUSH-PULL套接字是單向的。ZMQ的原則是:如果需要解決一個新的問題,就該使用新的套接字。這裡我們使用釋出-訂閱模式來發送自殺的訊息:

  • 結果收集器建立PUB套接字,並連線至一個新的端點;
  • worker將SUB套接字連線至這個端點;
  • 當結果收集器檢測到任務執行完畢時,會通過PUB套接字傳送自殺訊號;
  • worker收到自殺訊號後便會中止。

這一過程不會新增太多的程式碼:

    void *control = zmq_socket (context, ZMQ_PUB);
    zmq_bind (control, "tcp://*:5559");
    ...
    //  Send kill signal to workers
    zmq_msg_init_data (&message, "KILL", 5);
    zmq_send (control, &message, 0);
    zmq_msg_close (&message);

5

下面是worker程序的程式碼,它會開啟三個套接字:用於接收任務的PULL、用於傳送結果的PUSH、以及用於接收自殺訊號的SUB,使用zmq_poll()進行輪詢:

taskwork2: Parallel task worker with kill signaling in C

//
//  管道模式 - worker 設計2
//  添加發布-訂閱訊息流,用以接收自殺訊息
//
#include "zhelpers.h"

int main (void) 
{
    void *context = zmq_init (1);

    //  用於接收訊息的套接字
    void *receiver = zmq_socket (context, ZMQ_PULL);
    zmq_connect (receiver, "tcp://localhost:5557");

    //  使用者傳送訊息的套接字
    void *sender = zmq_socket (context, ZMQ_PUSH);
    zmq_connect (sender, "tcp://localhost:5558");

    //  使用者接收控制訊息的套接字
    void *controller = zmq_socket (context, ZMQ_SUB);
    zmq_connect (controller, "tcp://localhost:5559");
    zmq_setsockopt (controller, ZMQ_SUBSCRIBE, "", 0);

    //  處理接收到的任務或控制訊息
    zmq_pollitem_t items [] = {
        { receiver, 0, ZMQ_POLLIN, 0 },
        { controller, 0, ZMQ_POLLIN, 0 }
    };
    //  處理訊息
    while (1) {
        zmq_msg_t message;
        zmq_poll (items, 2, -1);
        if (items [0].revents & ZMQ_POLLIN) {
            zmq_msg_init (&message);
            zmq_recv (receiver, &message, 0);

            //  工作
            s_sleep (atoi ((char *) zmq_msg_data (&message)));

            //  傳送結果
            zmq_msg_init (&message);
            zmq_send (sender, &message, 0);

            //  簡單的任務進圖指示
            printf (".");
            fflush (stdout);

            zmq_msg_close (&message);
        }
        //  任何控制命令都表示自殺
        if (items [1].revents & ZMQ_POLLIN)
            break;                      //  退出迴圈
    }
    //  結束程式
    zmq_close (receiver);
    zmq_close (sender);
    zmq_close (controller);
    zmq_term (context);
    return 0;
}

下面是修改後的結果收集器程式碼,在收集完結果後向所有worker傳送自殺訊息:

tasksink2: Parallel task sink with kill signaling in C

//
//  管道模式 - 結構收集器 設計2
//  添加發布-訂閱訊息流,用以向worker傳送自殺訊號
//
#include "zhelpers.h"

int main (void) 
{
    void *context = zmq_init (1);

    //  用於接收訊息的套接字
    void *receiver = zmq_socket (context, ZMQ_PULL);
    zmq_bind (receiver, "tcp://*:5558");

    //  用以傳送控制資訊的套接字
    void *controller = zmq_socket (context, ZMQ_PUB);
    zmq_bind (controller, "tcp://*:5559");

    //  等待任務開始
    char *string = s_recv (receiver);
    free (string);

    //  開始計時
    int64_t start_time = s_clock ();

    //  確認100個任務處理完畢
    int task_nbr;
    for (task_nbr = 0; task_nbr < 100; task_nbr++) {
        char *string = s_recv (receiver);
        free (string);
        if ((task_nbr / 10) * 10 == task_nbr)
            printf (":");
        else
            printf (".");
        fflush (stdout);
    }
    printf ("總執行時間: %d msec\n", 
        (int) (s_clock () - start_time));

    //  傳送自殺訊息給worker
    s_send (controller, "KILL");

    //  結束
    sleep (1);              //  等待發送完畢

    zmq_close (receiver);
    zmq_close (controller);
    zmq_term (context);
    return 0;
}

處理中斷訊號

現實環境中,當應用程式收到Ctrl-C或其他諸如ETERM的訊號時需要能夠正確地清理和退出。預設情況下,這一訊號會殺掉程序,意味著尚未傳送的訊息就此丟失,檔案不能被正確地關閉等。

在C語言中我們是這樣處理訊息的:

interrupt: Handling Ctrl-C cleanly in C

//
//  Shows how to handle Ctrl-C
//
#include <zmq.h>
#include <stdio.h>
#include <signal.h>

//  ---------------------------------------------------------------------
//  訊息處理
//
//  程式開始執行時呼叫s_catch_signals()函式;
//  在迴圈中判斷s_interrupted是否為1,是則跳出迴圈;
//  很適用於zmq_poll()。

static int s_interrupted = 0;
static void s_signal_handler (int signal_value)
{
    s_interrupted = 1;
}

static void s_catch_signals (void)
{
    struct sigaction action;
    action.sa_handler = s_signal_handler;
    action.sa_flags = 0;
    sigemptyset (&action.sa_mask);
    sigaction (SIGINT, &action, NULL);
    sigaction (SIGTERM, &action, NULL);
}

int main (void)
{
    void *context = zmq_init (1);
    void *socket = zmq_socket (context, ZMQ_REP);
    zmq_bind (socket, "tcp://*:5555");

    s_catch_signals ();
    while (1) {
        //  阻塞式的讀取會在收到訊號時停止
        zmq_msg_t message;
        zmq_msg_init (&message);
        zmq_recv (socket, &message, 0);

        if (s_interrupted) {
            printf ("W: 收到中斷訊息,程式中止...\n");
            break;
        }
    }
    zmq_close (socket);
    zmq_term (context);
    return 0;
}

這段程式使用s_catch_signals()函式來捕捉像Ctrl-C(SIGINT)和SIGTERM這樣的訊號。收到任一訊號後,該函式會將全域性變數s_interrupted設定為1。你的程式並不會自動停止,需要顯式地做一些清理和退出工作。

  • 在程式開始時呼叫s_catch_signals()函式,用來進行訊號捕捉的設定;
  • 如果程式在zmq_recv()、zmq_poll()、zmq_send()等函式中阻塞,當有訊號傳來時,這些函式會返回EINTR;
  • 像s_recv()這樣的函式會將這種中斷包裝為NULL返回;
  • 所以,你的應用程式可以檢查是否有EINTR錯誤碼、或是NULL的返回、或者s_interrupted變數是否為1。

如果以下程式碼就十分典型:

s_catch_signals ();
client = zmq_socket (...);
while (!s_interrupted) {
    char *message = s_recv (client);
    if (!message)
        break;          //  按下了Ctrl-C
}
zmq_close (client);

如果你在設定s_catch_signals()之後沒有進行相應的處理,那麼你的程式將對Ctrl-C和ETERM免疫。

檢測記憶體洩露

任何長時間執行的程式都應該妥善的管理記憶體,否則最終會發生記憶體溢位,導致程式崩潰。如果你所使用的程式設計序言會自動幫你完成記憶體管理,那就要恭喜你了。但若你使用類似C/C++之類的語言時,就需要自己動手進行記憶體管理了。下面會介紹一個名為valgrind的工具,可以用它來報告記憶體洩露的問題。

  • 在Ubuntu或Debian作業系統上安裝valgrind:sudo apt-get install valgrind

  • 預設情況下,ZMQ會讓valgrind不停地報錯,想要遮蔽警告的話可以在編譯ZMQ時使用ZMQ_MAKE_VALGRIND_HAPPY巨集選項:

$ cd zeromq2
$ export CPPFLAGS=-DZMQ_MAKE_VALGRIND_HAPPY
$ ./configure
$ make clean; make
$ sudo make install
  • 應用程式應該正確地處理Ctrl-C,特別是對於長時間執行的程式(如佇列裝置),如果不這麼做,valgrind會報告所有已分配的記憶體發生了錯誤。

  • 使用-DDEBUG選項編譯程式,這樣可以讓valgrind告訴你具體是哪段程式碼發生了記憶體溢位。

  • 最後,使用如下方法執行valgrind:

valgrind --tool=memcheck --leak-check=full someprog

解決完所有的問題後,你會看到以下資訊:

==30536== ERROR SUMMARY: 0 errors from 0 contexts...

多幀訊息

ZMQ訊息可以包含多個幀,這在實際應用中非常常見,特別是那些有關“信封”的應用,我們下文會談到。我們這一節要講的是如何正確地收發多幀訊息。

多幀訊息的每一幀都是一個zmq_msg結構,也就是說,當你在收發含有五個幀的訊息時,你需要處理五個zmq_msg結構。你可以將這些幀放入一個數據結構中,或者直接一個個地處理它們。

下面的程式碼演示如何傳送多幀訊息:

zmq_send (socket, &message, ZMQ_SNDMORE);
...
zmq_send (socket, &message, ZMQ_SNDMORE);
...
zmq_send (socket, &message, 0);

然後我們看看如何接收並處理這些訊息,這段程式碼對單幀訊息和多幀訊息都適用:

while (1) {
    zmq_msg_t message;
    zmq_msg_init (&message);
    zmq_recv (socket, &message, 0);
    // 處理一幀訊息
    zmq_msg_close (&message);
    int64_t more;
    size_t more_size = sizeof (more);
    zmq_getsockopt (socket, ZMQ_RCVMORE, &more, &more_size);
    if (!more)
        break; // 已到達最後一幀
}

關於多幀訊息,你需要了解的還有:

  • 在傳送多幀訊息時,只有當最後一幀提交發送了,整個訊息才會被髮送;
  • 如果使用了zmq_poll()函式,當收到了訊息的第一幀時,其它幀其實也已經收到了;
  • 多幀訊息是整體傳輸的,不會只收到一部分;
  • 多幀訊息的每一幀都是一個zmq_msg結構;
  • 無論你是否檢查套接字的ZMQ_RCVMORE選項,你都會收到所有的訊息;
  • 傳送時,ZMQ會將開始的訊息幀快取在記憶體中,直到收到最後一幀才會傳送;
  • 我們無法在傳送了一部分訊息後取消傳送,只能關閉該套接字。

中介軟體和裝置

當網路元件的數量較少時,所有節點都知道其它節點的存在。但隨著節點數量的增加,這種結構的成本也會上升。因此,我們需要將這些元件拆分成更小的模組,使用一箇中間件來連線它們。

這種結構在現實世界中是非常常見的,我們的社會和經濟體系中充滿了中介軟體的機制,用以降低複雜度,壓縮構建大型網路的成本。中介軟體也會被稱為批發商、分包商、管理者等等。

ZMQ網路也是一樣,如果規模不斷增長,就一定會需要中介軟體。ZMQ中,我們稱其為“裝置”。在構建ZMQ軟體的初期,我們會畫出幾個節點,然後將它們連線起來,不使用中介軟體:

6

隨後,我們對這個結構不斷地進行擴充,將裝置放到特定的位置,進一步增加節點數量:

7

ZMQ裝置沒有具體的設計規則,但一般會有一組“前端”端點和一組“後端”端點。裝置是無狀態的,因此可以被廣泛地部署在網路中。你可以在程序中啟動一個執行緒來執行裝置,或者直接在一個程序中執行裝置。ZMQ內部也提供了基本的裝置實現可供使用。

ZMQ裝置可以用作路由和定址、提供服務、佇列排程、以及其他你所能想到的事情。不同的訊息模式需要用到不同型別的裝置來構建網路。如,請求-應答模式中可以使用佇列裝置、抽象服務;釋出-訂閱模式中則可使用流裝置、主題裝置等。

ZMQ裝置比起其他中介軟體的優勢在於,你可以將它放在網路中任何一個地方,完成任何你想要的事情。

釋出-訂閱代理服務

我們經常會需要將釋出-訂閱模式擴充到不同型別的網路中。比如說,有一組訂閱者是在外網上的,我們想用廣播的方式釋出訊息給內網的訂閱者,而用TCP協議傳送給外網訂閱者。

我們要做的就是寫一個簡單的代理服務裝置,在釋出者和外網訂閱者之間搭起橋樑。這個裝置有兩個端點,一端連線內網上的釋出者,另一端連線到外網上。它會從釋出者處接收訂閱的訊息,並轉發給外網上的訂閱者們。

wuproxy: Weather update proxy in C

//
//  氣象資訊代理服務裝置
//
#include "zhelpers.h"

int main (void)
{
    void *context = zmq_init (1);

    //  訂閱氣象資訊
    void *frontend = zmq_socket (context, ZMQ_SUB);
    zmq_connect (frontend, "tcp://192.168.55.210:5556");

    //  轉發氣象資訊
    void *backend = zmq_socket (context, ZMQ_PUB);
    zmq_bind (backend, "tcp://10.1.1.0:8100");

    //  訂閱所有訊息
    zmq_setsockopt (frontend, ZMQ_SUBSCRIBE, "", 0);

    //  轉發訊息
    while (1) {
        while (1) {
            zmq_msg_t message;
            int64_t more;

            //  處理所有的訊息幀
            zmq_msg_init (&message);
            zmq_recv (frontend, &message, 0);
            size_t more_size = sizeof (more);
            zmq_getsockopt (frontend, ZMQ_RCVMORE, &more, &more_size);
            zmq_send (backend, &message, more? ZMQ_SNDMORE: 0);
            zmq_msg_close (&message);
            if (!more)
                break;      //  到達最後一幀
        }
    }
    //  程式不會執行到這裡,但依然要正確地退出
    zmq_close (frontend);
    zmq_close (backend);
    zmq_term (context);
    return 0;
}

我們稱這個裝置為代理,因為它既是訂閱者,又是釋出者。這就意味著,新增該裝置時不需要更改其他程式的程式碼,只需讓外網訂閱者知道新的網路地址即可。

8

可以注意到,這段程式能夠正確處理多幀訊息,會將它完整的轉發給訂閱者。如果我們在傳送時不指定ZMQ_SNDMORE選項,那麼下游節點收到的訊息就可能是破損的。編寫裝置時應該要保證能夠正確地處理多幀訊息,否則會造成訊息的丟失。

請求-應答代理

下面讓我們在請求-應答模式中編寫一個小型的訊息佇列代理裝置。

在Hello World客戶/服務模型中,一個客戶端和一個服務端進行通訊。但在真實環境中,我們會需要讓多個客戶端和多個服務端進行通訊。關鍵問題在於,服務端應該是無狀態的,所有的狀態都應該包含在一次請求中,或者存放其它介質中,如資料庫。

我們有兩種方式來連線多個客戶端和多個服務端。第一種是讓客戶端直接和多個服務端進行連線。客戶端套接字可以連線至多個服務端套接字,它所傳送的請求會通過負載均衡的方式分發給服務端。比如說,有一個客戶端連線了三個服務端,A、B、C,客戶端產生了R1、R2、R3、R4四個請求,那麼,R1和R4會由服務A處理,R2由B處理,R3由C處理:

9

這種設計的好處在於可以方便地新增客戶端,但若要新增服務端,那就得修改每個客戶端的配置。如果你有100個客戶端,需要新增三個服務端,那麼這些客戶端都需要重新進行配置,讓其知道新服務端的存在。

這種方式肯定不是我們想要的。一個網路結構中如果有太多固化的模組就越不容易擴充套件。因此,我們需要有一個模組位於客戶端和服務端之間,將所有的知識都匯聚到這個網路拓撲結構中。理想狀態下,我們可以任意地增減客戶端或是服務端,不需要更改任何元件的配置。

下面就讓我們編寫這樣一個元件。這個代理會繫結到兩個端點,前端端點供客戶端連線,後端端點供服務端連線。它會使用zmq_poll()來輪詢這兩個套接字,接收訊息並進行轉發。裝置中不會有佇列的存在,因為ZMQ已經自動在套接字中完成了。

在使用REQ和REP套接字時,其請求-應答的會話是嚴格同步。客戶端傳送請求,服務端接收請求併發送應答,由客戶端接收。如果客戶端或服務端中的一個發生問題(如連續兩次傳送請求),程式就會報錯。

但是,我們的代理裝置必須要是非阻塞式的,雖然可以使用zmq_poll()同時處理兩個套接字,但這裡顯然不能使用REP和REQ套接字。

幸運的是,我們有DEALER和ROUTER套接字可以勝任這項工作,進行非阻塞的訊息收發。DEALER過去被稱為XREQ,ROUTER被稱為XREP,但新的程式碼中應儘量使用DEALER/ROUTER這種名稱。在第三章中你會看到如何用DEALER和ROUTER套接字構建不同型別的請求-應答模式。

下面就讓我們看看DEALER和ROUTER套接字是怎樣在裝置中工作的。

下方的簡圖描述了一個請求-應答模式,REQ和ROUTER通訊,DEALER再和REP通訊。ROUTER和DEALER之間我們則需要進行訊息轉發:

10

請求-應答代理會將兩個套接字分別繫結到前端和後端,供客戶端和服務端套接字連線。在使用該裝置之前,還需要對客戶端和服務端的程式碼進行調整。

* rrclient: Request-reply client in C *

//
//  Hello world 客戶端
//  連線REQ套接字至 tcp://localhost:5559 端點
//  傳送Hello給服務端,等待World應答
//
#include "zhelpers.h"

int main (void) 
{
    void *context = zmq_init (1);

    //  用於和服務端通訊的套接字
    void *requester = zmq_socket (context, ZMQ_REQ);
    zmq_connect (requester, "tcp://localhost:5559");

    int request_nbr;
    for (request_nbr = 0; request_nbr != 10; request_nbr++) {
        s_send (requester, "Hello");
        char *string = s_recv (requester);
        printf ("收到應答 %d [%s]\n", request_nbr, string);
        free (string);
    }
    zmq_close (requester);
    zmq_term (context);
    return 0;
}

下面是服務程式碼:

rrserver: Request-reply service in C

//
//  Hello World 服務端
//  連線REP套接字至 tcp://*:5560 端點
//  接收Hello請求,返回World應答
//
#include "zhelpers.h"

int main (void) 
{
    void *context = zmq_init (1);

    //  用於何客戶端通訊的套接字
    void *responder = zmq_socket (context, ZMQ_REP);
    zmq_connect (responder, "tcp://localhost:5560");

    while (1) {
        //  等待下一個請求
        char *string = s_recv (responder);
        printf ("Received request: [%s]\n", string);
        free (string);

        //  做一些“工作”
        sleep (1);

        //  返回應答資訊
        s_send (responder, "World");
    }
    //  程式不會執行到這裡,不過還是做好清理工作
    zmq_close (responder);
    zmq_term (context);
    
            
           

相關推薦

ZeroMQ 中文指南 第二 ZeroMQ轉載

作者資訊如下。 ZMQ 指南 作者: Pieter Hintjens [email protected], CEO iMatix Corporation. 翻譯: 張吉 [email protected], 安居客集團 好租網工

Kotlin詳解:第二

一,高階函式 1,基本概念:將函式作為引數或返回一個函式,稱為高階函式,常用的高階函式如下。 ①,forEach函式,用於遍歷集合 fun main(args: Array<String>): Unit { val list : List<String

Python3入門與筆記

1、二、八、十六進位制轉十進位制:int('10', base=2)、int('10', base=8)、int('10', base=16); 2、八、十、十六進位制轉二進位制:bin(0o+xxx)、bin(xxx)、bin(0x+xxx); 3、二、十、十六進位制轉八進位制:oct(0b+xxx)、

ZeroMQ 中文指南 第三 高階請求-應答模式轉載

作者資訊如下。 ZMQ 指南 作者: Pieter Hintjens [email protected], CEO iMatix Corporation. 翻譯: 張吉 [email protected], 安居客集團 好租網工

操作系統_第二_程與線程

重要 輸出 中斷 原因 之前 存儲 活動 進程與線程 系統初始 2018-06-30 1.進程:對正在運行的程序的一個抽象 2.一個進程就是一個正在執行的程序的實例 3.快速的切換稱為:多道程序設計 4.一個進程是某種類型的一個活動,它有程序,輸入,輸出,以及狀態 5..四

Python資料分析基礎教程:NumPy學習指南 第二 常用函式

目錄 第二章 常用函式 1    檔案讀寫示例 建立對角矩陣: np.eye(2)  儲存為txt檔案:np.savetxt("eye.txt", i2) 2    CSV檔案讀取: loadtxt() 3  &nb

離散數學複習--第二:一邏輯

2.1 一階邏輯基本概念 如果沒有事先給出一個個體域,都以全總個體域為個體域。於是,引入一個新的謂詞 M (

第八 SpringMVC

檢視解析器 <?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/

2.2Android程式設計權威指南第二程式碼

activity_quiz.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

Chrome開發者工具不完全指南(二、篇)

function a () { b(); } function b() { c(); } function c() { //在該處斷點,檢視call stack } a->b->c. call stack 從上到下的順序就是 c

第二天H5(Css)

css介紹   1. css是什麼   CSS 指層疊樣式表 (Cascading Style Sheets)   樣式定義如何顯示 HTML 元素   樣式通常儲存在樣式表中   把樣式新增到 HTML 4.0 中,是為了解決內容與表現分離的問題   外部樣式表可以

HTTP 權威指南 第二 URL 與資源

機制 應用程序 之間 轉義 mailto amp 路徑 path 內容 前言 這一章節講述了關於 URL 的相關知識,主要包括下面的內容: URL 語法 URL 快捷方式 URL 編碼與字符規則 常見的 URL 方案 URL 的未來—&mdash

SSH之路Hibernate映射——一對一單向關聯映射(五)

技術 iyu 標識 tails for sso 3.0 sdn 例如 【SSH進階之路】Hibernate基本原理(一) ,小編介紹了Hibernate的基本原理以及它的核心,採用對象化的思維操作關系型數據庫。 【SSH進階之路】Hibernate搭建開發環境+簡單實例

SSH之路Struts + Spring + Hibernate 開端(一)

height 一段 ioc 效率 陽光大道 面向對象的思想 text ase 們的 Long Long ago。就聽說過SSH。起初還以為是一個東東,詳細內容更是不詳,總認為高端大氣上檔次,經過學習之後才發現,不不過高大上,更是低調奢華有內涵,經過一段時間的

SSH之路Hibernate基本映射(三)

tor res 主動 tran clas oid 支持包 lose 包括 【SSH進階之路】Hibernate基本原理(一) ,小編介紹了Hibernate的基本原理以及它的核心。採用對象化的思維操作關系型數據庫。 【SSH進階之路】Hibernate搭建開發環境+簡單

SSH之路Struts基本原理 + 實現簡單登錄(二)

target doctype 掌握 pack insert enter snippet file manage 上面博文,主要簡單的介紹了一下SSH的基本概念,比較宏觀。作為剛開始學習的人可以有一個總體上的認識,個人覺得對學習有非常好的輔助功能,它不不過

Python第一篇:Python簡介

代碼 簡潔 處理 ros 進一步 基礎 得到 運行速度 動態 Python簡介 1.Python的由來 Python是著名的“龜叔”Guido van Rossum在1989年聖誕節期間,為了打發無聊的聖誕節而編寫的一個編程語言。 2.C 和 Python、Java、C#等

Python第九篇裝飾器

turn spa none app light fun rap log python 什麽是裝飾器 裝飾器本身就是函數,並且為其他函數添加附加功能 裝飾器的原則:1.不修改被裝飾對象的源代碼 2.不修改被裝飾對象的調用方式裝飾器=高階函數+函數嵌套+閉包 # res=t

Python第十篇模塊(上)

path 變量 屬性 一個 第三方 sys pre 應用程序 bsp ·一、模塊 模塊就是一組功能的集合體,我們的程序可以導入模塊來復用模塊裏的功能。為了編寫可維護的代碼,我們把很多函數分組,分別放到不同的文件裏,這樣,每個文件包含的代碼就相對較少,很多編程語言都采用這種組

我的Android之旅解決Android Studio 運行gradle命令時報錯: 錯誤: 編碼GBK的不可映射字符

定義 編碼 string pretty 出現 mage watermark build issue 原文:【我的Android進階之旅】解決Android Studio 運行gradle命令時報錯: 錯誤: 編碼GBK的不可映射字符 1、問題描述 最近在負責公司基礎