⓪編著 :蕭沖

總的來說,Socket 可分為二種模式 : blocking mode, non-blocking mode。

要使socket成為non-blocking mode只要設定ioctlsocket為non-blocking mode就可以,然而,若配合使用了blocking functions,會使得讀/取不到資料時產生WSAEWOULDBLOCK的error。因此若要成功而完整的使用non-blocking mode,就必需配合適當的socket IO models。
Socket I/O 有六種模型:blocking, select, WSAAsyncSelect, WSAEventSelect, overlapped, and completion port。

針對不同的模型,我以一個虛擬的例子來說明: 假設有128條水管可以出水,但每一條出水的時間不同,且有快有慢,若我們的任務是在每一條水管必需各裝3桶水,那麼我們可以達成的方法有:  (人數相當於thread的數量,水管數相當於socket數,水桶相當於傳送或接收的Buffer)

1/ blocking : 請128人,每人帶一個水桶,站在每一條水管前等候,直到每個人都裝完3桶水
2/ WSAAsyncSelect,自行定義一個使用者msg,然後結合至windows msg loop,由winproc中的switch case 來處理。這如同-- 請1個人帶一個水桶站在128條水管前,拼命的裝先出水的水管。
3/ WSAEventSelect,請2個人各自帶一個水桶,站在128條水管前,一人管64條(最多),然後拼命的裝先出水的水管。
4/ overlapped with event,使用WSA_FLAG_OVERLAPPED旗標建立overlapped socket,然後使用overlapped socket functions ( APCs)。請2個人各自帶任意個水桶,在128條水管前,一人管64條(最多),將任意數的水桶放於水管前等被裝水,先被裝滿水的水桶則先被通知提領,這二個人再進行提取。
5/ overlapped with completion routine,使用WSA_FLAG_OVERLAPPED旗標建立overlapped socket,然後使用overlapped socket functions ( APCs) with completion routine。請128個人帶任意個水桶,在每一條水管前,將3個水桶放於水管前等被裝水,先被裝滿水的水桶則請1人馬上來提取處理。
6/ completion port(overlapped),建立一個completion port來queue completed notification packets. 當APCs完成IO後,系統會自動的發一個complete paket到指定的completion port上,此時我們之前所建立好的IO worker thread(等候中,in alartable waiting state,監控我們指定的completion port),便會自動的活化並開始取走queue裡的packet,並依之前指定的completion port handle(此worker thread的參數),從GetQueuedCompletionStatus( )得到一些提領訊息。請128個人帶任意個水桶,在每一條水管前,將3個水桶放於水管前等被裝水,先被裝滿水的水桶則被(1至少數人, NumberOfConcurrentThreads,依cpu的數量來決定比較好)自動的放在空地某一排等被提走。同時間,有1至數個工人一直在監控這個空地,若發現有裝滿的水桶等候被提,則開始努力的提取。


When your application calls WSAAsyncSelect / WSAEventSelect Model on a socket, the socket mode is automatically changed from blocking to the non-blocking mode。

All functions that allow overlapped operation (WSASend, WSARecv, WSASendTo, WSARecvFrom, WSAIoctl) 需要socket本身也是建立為overlapped mode才有overlapped的作用:WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, WSA_FLAG_OVERLAPPED);
若非overlapped socket使用這些function,則這些function就等同一般的blocking functions。

If both lpOverlapped and lpCompletionRoutine are NULL, the socket in this function will be treated as a nonoverlapped socket.
創作者介紹
創作者 aftcast 的頭像
aftcast

蕭沖的書房

aftcast 發表在 痞客邦 留言(2) 人氣()


留言列表 (2)

發表留言
  • hi
  • 細節

    blocking IO or non-blocking IO 對 UDP 來說是有點和 TCP 不一樣 , 因為 UDP 的傳送單元是資料包 , 它是不可分割

    的資料單元 , 所以下面是 udp 對於阻攔與非阻攔差異:


    阻攔:

    send[to] : 傳送的資料包大於 socket buffer 時 , 會等 socket buffer 完全可以接收此資料包才返回 ,

    差別在於 udp 所送出的封包是資料包 , 而 tcp 所送出封包是資料流.

    recv[from]: 接收的資料包若大於本身所指定長度 , 多出部分將丟棄 , 小於本身指定長度 , 將回傳所接收資料包的大小.

    當無資料接收時 , 處於等待狀態直到 socket buffer 有資料包進來.

    非阻攔:

    send[to] : 傳送的資料包大於 socket buffer 時 , 立即返回 , 因為資料包無法一次傳送 .

    recv[from]: 當無資料接收時立即返回 , 若有資料包可以接收 , 資料包若大於本身所指定長度 ,

    多出部分將丟棄 , 小於本身指定長度 , 將回傳所接收資料包的大小.


    UDP 在阻攔與非阻攔中 , recv[from] 運作依模式不同有蠻大差異, 但是 send[to] 而言 , 阻攔與非阻攔

    沒啥差異 , 因為 udp 是無連接狀態 , 在 socket buffer 中的資料包不會駐留許久 , 會立即送出去 , 這個時間

    很短暫 , 對於阻攔或非阻攔 , socket buffer 幾乎隨時都是空的 , send[to] 的呼叫幾乎都是立刻返回的.


    udp 呼叫 connect 時 , 使用 send 則是依照 connect的位置所提供 ; sendto 則是由 to 參數提供位置 , 也就是

    會把 connect 的位置忽略 , 就算ip.port 都是 *.* 傳入 sendto 還是送出 ip=0.0.0.0 & port=0 的值. MSDN sendto:

    Even if the connectionless socket has been previously connected to a specific address, the to parameter overrides the destination address for that particular datagram only. => 理由見下面的 sendto 實作說明!!
    ---------------------------------------------------------------------------------------------------------------------
    <網路應用程式設計介面 Socket 與 XTI> 第8章(強烈建議)

    UDP socket 類型:

    本地socket 外地socket 說明

    localIP,lport foreignIP,fport 限制在一個對象:
    socket(),bind(*,lport),connect(foreignIP,fport). or
    socket(),bind(localIP,lport),connect(foreignIP,fport).

    localIP,lport *.* 限制在抵達某一個本地界面的資料包:
    socket(),bind(localIP,lport).

    *.lport *.* 接收所有送到lport的資料包:
    socket(),bind(*,lport). -> 本範例類型的ps註解.


    本範例是tcp,而udp 和tcp 運作不同(server):

    一個socket() 的函式呼叫會建立起一個SOCKET,而一個socket 就有一組buffer(接收+送出) 來接收資料,此buffer位於kernel 管理.

    所以tcp 用來listen()的函式會傳回已連線的SOCKET,而傳回的SOCKET 就已經含有一組buffer,至於udp 則是利用原本SOCKET作

    接收和傳送,所以只含有一組buffer,故tcp 的server 有每個連線SOCKET,也就表示有每個連線獨立的一組buffer,所以資料接收

    和送出都不會有互相干擾現象,而udp server 只有一個本地socket,所以只有一組buffer和外界溝通,所以接收的資料有分外界

    送來優先順序.


    限定來源對象 UDP 和 TCP 差異 ?

    UDP 開啟的 server socket 本身就可以用來和外界溝通, 所以可以順便在呼叫 bind "之前" 先 connect 一個來源對象, 且因為 udp

    無連接導向不建立連線, 所以資料是直接送過來, 故可以使用 connect 限定資料送過來的對象是否要接收還是丟棄, 因為是無連接,

    所以不管是否封包有無遺失 重複 延遲等等, 只管是否要不要接收而已; 但是TCP 是連接導向, 會建立起來線, 所以封包遺失 重複

    延遲等等 都有在控制管理, 不能直接限制來源將封包丟棄, 否則對方會一直重送, 所以只能在 listen 聆聽時再加以拒絕連線達到

    限制來源對象(做法 1:firewall, 2:利用 winsock2 的 WSAAccept API), 況且 TCP server socket 本身只能用來作 listen 聆聽使用,

    client端 ip/port 沒bind, 只純粹作 listen 之用, 而 accept 所回傳 socket 才含有 client 的 ip/port.


    client: TCP 在呼叫 connect 時, 就會檢查之前有無 bind, UDP 則是在第一次使用 sendto 會檢查有無 bind.
    server: TCP 在呼叫 listen 時, 就會檢查之前有無 bind, UDP 則是在第一次使用 recv/recvfrom 會檢查有無 bind.

    <網路應用程式設計介面 Socket 與 XTI> P.8-10
    -----------------------------------------------------------------------------------------------------------------------
    tcp 與 udp 的 ip bind 差異: <網路應用程式設計介面 Socket 與 XTI> P.4-10

    tcp 與 udp 無論是 server 或是 client 的 ip , 都可以是 wildcard , 若綁定特定ip ,則會限制資料收送.

    接收: 目的 ip 需與綁定特定 ip 一致.

    送出: 來源 ip 是綁定特定 ip.

    反之 , 若綁定是wildcard ,則送出資料來源 ip 由kernel 決定 ,但是 tcp 與 udp 有部分差異,差異如下:

    tcp 送出時,其bind ip 為wildcard:

    server: 由所接收 client 的 syn 封包(連線請求)中的目的 ip 決定來 bind server 的 ip .

    client: connect 的 ip 經由 route 時 ,會送出哪個介面 , 就由哪個介面離開的 ip 來 bind client 的 ip.

    因為tcp 是屬於連接導向,其來源 ip 在 server 與 client 之中要維持不變就需要 bind , server 是 bind syn封包的目的 ip ,

    client 是 connect 時才 bind ip , 這就是由 kernel 所決定的過程.

    udp 送出時,其bind ip 為wildcard:

    server/client: "每次" 送出資料目的 ip 經由 route 時 ,會送出哪個介面 , 就由哪個介面離開的 ip 來填入來源ip(BSD 體系).

    就是因為 udp server/client 會由離開介面的 ip 所決定 , 才會有<網路應用程式設計介面 Socket 與 XTI> P.8-11 的問題出現.



    bind的使用與否:

    當沒有使用 bind , 其視為 *.* , 再依照上面所述的 wildcard 狀態決定其介面.

    server 端的 ip 想成為 wildcard 可以呼叫 bind => *.port (也可以bind *.* ,port 就由listen()呼叫時決定) , 至於 server

    端可不可以不呼叫 bind 而成為 *.* , 在每個 os 方面限制有點不同(註1) ;

    而 client 端 , 每個 os 做法一致 , 呼不呼叫 bind 無所謂 , 不呼叫 bind => *.* , 呼叫 bind ( sin_port=0/sin_addr.s_addr=0 )

    時也可以 => *.* ; 這種情況發生在每個 os 幾乎一樣對於 tcp/udp client 的 bind 使用與否.



    connect 的使用與否:

    若是 connect, 則不呼叫也視為 *.*, 例如 server tcp 的 listen socket, 所以才可以接受來自各地方的連線請求(不同地方 ip/port);

    反之, client 不呼叫也是 *.*, 所以才可以利用 sendto 送出任意端點或是 recv/recvfrom 接收任意端點. 目的端點的 *.* 型式只發生

    在 udp 的 server/client 中的 connect 未呼叫與 tcp 的 server 的 listen socket(也是沒呼叫 connect也不需要).


    至於呼叫 connect(*.*) 可否像 bind(*.*) 一樣成為 *.* 狀態!? 答案是不行的, 無此用法~~ 因為呼叫 connect(*.*) 後表示 send

    必定能使用, 但是卻只能使用 sendto, 所以當然不行; 而 bind(*.*) 後依然能使用 send, 原因在於還有 os 可以自己決定 ip.port.


    在 udp 時, 當使用 connect 虛連接成功時, 是否 sendto 的 to 參數可以覆蓋 socket 虛連接的設定!?

    這個情況比較特別, 一般來說虛連接成功後, 呼叫 sendto 時內部也會呼叫 connect, 因此 sendto 裡的 connect 暫時連接會發現 socket

    其實已經有目的地 ip.port(虛連接), 此時有 2 種情況產生: 1.to參數覆蓋 socket 中的 ip.port , 呼叫後再恢復之前 socket 狀態

    2.直接回傳 EISCONN 錯誤, 也就是 Transport endpoint is already connected, 表示說目的端已經連接. win98SE and NT4.0 和其它某些

    unix(solaris bsd) 上得到 EISCONN 屬於情況2; 但是在 windows XP/2000 和 linux 上 卻是可以屬於情況1 , 會有這些差異與

    connect/sendto 的標準描述有關, 因此影響到 connect 與 sendto 的實作:



    The connect() page says:

    "For SOCK_DGRAM sockets, the peer address identifies where all datagrams are sent on subsequent send() functions,
    and limits the remote sender for subsequent recv() functions."

    Since this only refers to send() and not to sendto(), and since

    the sendto() page clearly states:

    "If the socket is connectionless-mode, the message shall be sent to the address specified by dest_addr."

    this would seem to imply that an address pre-specified using connect() is ignored by sendto().

    Similar arguments apply to sendmsg().

    以上可知呼叫 connect 後仍可以呼叫 sendto.


    However, section 2.10.6 Socket Types states the following for the SOCK_DGRAM socket type:

    "An application may also pre-specify a peer address, in which case calls to output functions shall send to the
    pre-specified peer."

    Since this says "output functions" it implies that send(), sendto() and sendmsg() are all treated the same.

    由此可知呼叫 connect 後呼叫 sendto 時 to & tolen 參數必須為 NULL 和 0.


    Action:
    Here are two suggested ways to fix the problem, depending on whether
    the intention is for the pre-specified address to be used, or for
    the address given to sendto() and sendmsg() to override it.

    所以不是使用 to & tolen 的 connect 的虛連接 ip.port 就是傳入的 to & tolen 參數必須為 NULL 和 0.


    個人比較傾向後者的解釋 , udp 呼叫 connect 後不能使用 sendto. 因為語意上來說 , connect 虛連接後所接收的資料只限於虛連接

    的 ip.port; 當然所送的資料也應該傾向於虛連接的 ip.port, 而不是還能藉由 sendto 的 to 參數送往非虛連接的目的地.

    見 <TCP_IP Illustrated_Volume2(中譯)> p.610 也有提到 , 敘述的是後者做法.


    網路上對於 sendto 的使用觀點:

    If you have called connect() to connect the socket to a specific remote address then you cannot supply a destination address to sendto(). Either use send() or set 'to' and 'tolen' to zero in the sendto() call.

    Just because Linux lets it through doesn't mean it is right. In a contest beteween Linux and BSD for the correct semantics of the socket calls, I know which one I would believe. :-)


    UDP調用CONNECT

    在未連接UDP套接口上給兩個數據報調用函數sendto導致內核執行下列六步:


    1.連接套接口;
    2.輸出第一個數據報
    3.斷開套接口連接;
    4.連接套接口,
    5.輸出第二個數據報;
    6.斷開套接口連接

    已連接套接口發送兩個數據報的導致內核執行如下步驟;


    1.連接套接口;
    2.輸出第一個數據報;
    3.輸出第二個數據報。


    因此未呼叫 connect 的 udp 將花費很多時間~~
    見 <TCP_IP Illustrated_Volume2(中譯)>p.610



    註1:windows 不允許沒呼叫 bind 就 listen ip.port(*.*) 的 socket.
    有先收的動作時(windows): server => ip.port(*.*) , 一定是 bind sin_port=0 & sin_addr.s_addr=0.
    有先收的動作時(unix-like): server => ip.port(*.*) , 可能是 bind sin_port=0 & sin_addr.s_addr=0 或 沒有bind 呼叫.
    注意! 當沒 bind 時成為 *.* , 對於 tcp 來說是正常沒問題 ; 但是 udp 會成為 unbound udp socket , 此種形式只限內部使用,
    就算第一次 recv/recvfrom 也不會決定local port , 永遠都是 *.* 型式.

    unix/linux 允許的 tcp 不 bind 而成為 *.* 型式:
    int main()
    {
    int listensock,client;
    .....
    if(listen( listensock,1)==-1) //此時 listen socket -> *.隨機port
    {
    printf("listen()\n");
    return 0;
    }

    if((client=accept(listensock,...)
    {
    printf("accept\n");
    return 0;
    }
    .....
    }

    ----------------------------------------------------------------------------------------------------------------------------
    tcp/udp 在 server/client 端使用 bind 情況:


    server | tcp | udp
    --------+----------------------------+---------------------------------------------
    port(*)| 呼叫listen()時決定本地port | 第一次呼叫recv()/recvfrom()時決定本地port
    --------+----------------------------+---------------------------------------------
    ip (*) | *(註2) | * (接收本地任何網路介面所收到的封包)


    註2: listen socket 不限syn封包從哪個網路介面送進來 , 呼叫 accept()時回傳socket 中 , 決定回傳 socket 的
    local ip 由 syn 封包進入 listen socket 的哪個網路介面而定.



    有先送的動作時(unix/linux/windows): client => ip.port(*.*) , 可能是 bind sin_port=0 & sin_addr.s_addr=0 或 沒有bind 呼叫.

    client | tcp | udp
    --------+-----------------------------+---------------------------------------------
    port(*) | 呼叫connect()時決定本地port | 第一次呼叫send()/sendto()時決定本地port
    --------+-----------------------------+---------------------------------------------
    ip (*) | 呼叫connect()時決定本地ip | * (BSD體系中,每次經路由表中的網路介面決定本地ip)




  • EE
  • 這水桶比喻還真的完全的看不懂阿~~~