1. 程式人生 > >客戶端connect()函式阻塞問題解決

客戶端connect()函式阻塞問題解決

轉自:http://zhucuicui.96986.blog.163.com/blog/static/5833370220136219016445/

建立socket後預設connect()函式為阻塞連線狀態,在大多數實現中,connect的超時時間在75s至幾分鐘之間,想要縮短超時時間,可解決問題的兩種方法:方法一、將socket控制代碼設定為非阻塞狀態,方法二、採用訊號處理函式設定阻塞超時控制。

在一個TCP套介面被設定為非阻塞之後呼叫connect,connect會立即返回EINPROGRESS錯誤,表示連線操作正在進行中,但是仍未完成;同時TCP的三路握手操作繼續進行;在這之後,我們可以呼叫select來檢查這個連結是否建立成功;非阻塞connect有三種用途:
1.我們可以在三路握手的同時做一些其它的處理.connect操作要花一個往返時間完成,而且可以是在任何地方,從幾個毫秒的區域網到幾百毫秒或幾秒的廣域網.在這段時間內我們可能有一些其他的處理想要執行;
2.可以用這種技術同時建立多個連線.在Web瀏覽器中很普遍;
3.由於我們使用select來等待連線的完成,因此我們可以給select設定一個時間限制,從而縮短connect的超時時間.在大多數實現中,connect的超時時間在75秒到幾分鐘之間.有時候應用程式想要一個更短的超時時間,使用非阻塞connect就是一種方法;
非阻塞connect聽起來雖然簡單,但是仍然有一些細節問題要處理:
1.即使套介面是非阻塞的,如果連線的伺服器在同一臺主機上,那麼在呼叫connect建立連線時,連線通常會立即建立成功.我們必須處理這種情況;
2.源自Berkeley的實現(和Posix.1g)有兩條與select和非阻塞IO相關的規則:
A:當連線建立成功時,套介面描述符變成可寫;
B:當連接出錯時,套介面描述符變成既可讀又可寫;
注意

:當一個套接口出錯時,它會被select呼叫標記為既可讀又可寫;

非阻塞connect有這麼多好處,但是處理非阻塞connect時會遇到很多可移植性問題;

處理非阻塞connect的步驟:
第一步:建立socket,返回套介面描述符;
第二步:呼叫fcntl把套介面描述符設定成非阻塞;
第三步:呼叫connect開始建立連線;
第四步:判斷連線是否成功建立;
A:如果connect返回0,表示連線簡稱成功(伺服器可客戶端在同一臺機器上時就有可能發生這種情況);
B:呼叫select來等待連線建立成功完成;
如果select返回0,則表示建立連線超時;我們返回超時錯誤給使用者,同時關閉連線,以防止三路握手操作繼續進行下去;
如果select返回大於0的值,則需要檢查套介面描述符是否可讀或可寫;如果套介面描述符可讀或可寫,則我們可以通過呼叫getsockopt來得到套介面上待處理的錯誤(SO_ERROR),如果連線建立成功,這個錯誤值將是0,如果建立連線時遇到錯誤,則這個值是連線錯誤所對應的errno值(比如:ECONNREFUSED,ETIMEDOUT等).
"讀取套介面上的錯誤"是遇到的第一個可移植性問題;如果出現問題,getsockopt源自Berkeley的實現是返回0,等待處理的錯誤在變數errno中返回;但是Solaris會讓getsockopt返回-1,errno置為待處理的錯誤;我們對這兩種情況都要處理;

這樣,在處理非阻塞connect時,在不同的套介面實現的平臺中存在的移植性問題,首先,有可能在呼叫select之前,連線就已經建立成功,而且對方的資料已經到來.在這種情況下,連線成功時套介面將既可讀又可寫.這和連線失敗時是一樣的.這個時候我們還得通過getsockopt來讀取錯誤值;這是第二個可移植性問題;
移植性問題總結:
1.對於出錯的套介面描述符,getsockopt的返回值源自Berkeley的實現是返回0,待處理的錯誤值儲存在errno中;而源自Solaris的實現是返回0,待處理的錯誤儲存在errno中;(套介面描述符出錯時呼叫getsockopt的返回值不可移植)
2.有可能在呼叫select之前,連線就已經建立成功,而且對方的資料已經到來,在這種情況下,套介面描述符是既可讀又可寫;這與套介面描述符出錯時是一樣的;(怎樣判斷連線是否建立成功的條件不可移植)

這樣的話,在我們判斷連線是否建立成功的條件不唯一時,我們可以有以下的方法來解決這個問題:
1.呼叫getpeername代替getsockopt.如果呼叫getpeername失敗,getpeername返回ENOTCONN,表示連線建立失敗,我們必須以SO_ERROR呼叫getsockopt得到套介面描述符上的待處理錯誤;
2.呼叫read,讀取長度為0位元組的資料.如果read呼叫失敗,則表示連線建立失敗,而且read返回的errno指明瞭連線失敗的原因.如果連線建立成功,read應該返回0;
3.再呼叫一次connect.它應該失敗,如果錯誤errno是EISCONN,就表示套介面已經建立,而且第一次連線是成功的;否則,連線就是失敗的;

被中斷的connect:
如果在一個阻塞式套介面上呼叫connect,在TCP的三路握手操作完成之前被中斷了,比如說,被捕獲的訊號中斷,將會發生什麼呢?假定connect不會自動重啟,它將返回EINTR.那麼,這個時候,我們就不能再呼叫connect等待連線建立完成了,如果再次呼叫connect來等待連線建立完成的話,connect將會返回錯誤值EADDRINUSE.在這種情況下,應該做的是呼叫select,就像在非阻塞式connect中所做的一樣.然後,select在連線建立成功(使套介面描述符可寫)或連線建立失敗(使套介面描述符既可讀又可寫)時返回;

方法二、定義訊號處理函式:

  1. sigset(SIGALRM, u_alarm_handler);
  2. alarm(2);
  3. code = connect(socket_fd, (struct sockaddr*)&socket_st, sizeof(struct sockaddr_in));
  4. alarm(0);
  5. sigrelse(SIGALRM);

首先定義一箇中斷訊號處理函式u_alarm_handler,用於超時後的報警處理,然後定義一個2秒的定時器,執行connect,當系統connect成功,則系統正常執行下去;如果connect不成功阻塞在這裡,則超過定義的2秒後,系統會產生一個訊號,觸發執行u_alarm_handler函式, 當執行完u_alarm_handler後,程式將繼續從connect的下面一行執行下去。
其中,處理函式可以如下定義,也可以加入更多的錯誤處理。

  1. void u_alarm_handler()
  2. {
  3. }