Winsock

**Windows Sockets API (WSA)**, 簡短記為 **Winsock**, 是 Windows 的 TCP/IP 網絡編程接口（API）。其函數名稱兼容於 Berkeley 套接字 API。實際上，Winsock 的實現庫 (winsock.dll) 使用的是長名字。

Winsock 是一種能使 Windows 程序通過任意網絡傳輸協議發送數據的 API。Winsock 中有幾個只支持 TCP/IP 協議的函數（例如 gethostbyaddr ()），但是在 Winsock 2 中新增了所有這些函數的通用版本，以允許開發者使用其它的傳輸協議。

## 歷史

MS-DOS 與早期版本的 Microsoft Windows 使用的網絡協議是 NetBIOS. 因此，各方提供了各自的 MS-DOS 上的 TCP/IP 實現。由於各種解決方案的 API 函數名並不統一，使得軟體開發者難以下決心轉到 TCP/IP 協議上。

1991 年 10 月，以 Martin Hall, Mark Towfiq, Geoff Arnold, Henry Sanders 為首在 CompuServe BBS 上討論形成了 Windows Sockets API 規範 (specification) 並且版權屬於這五人。

### 規範版本

* Version 1.0 (1992 年 6 月) 雖然不限於 TCP/IP, 但 TCP 與 UDP 是僅有被明確提到的網絡協議。
* Version 1.1 (1993 年 1 月) 很多小的修正與澄清。Winsock 以 Windows 消息的形式發送通知，使程序無需擔心並發性。
* Winsock 2 向後兼容 Winsock 1.1，還增加了協議獨立的名字解析，異步操作（基於事件通知與完成過程 (completion routine))， 多播，QoS. 明文支持了多協議，包括 IPX/SPX 與 DECnet. 增加了服務提供者接口 (SPI) 與分層協議實現.
  * Versions 2.0.x (1994 年 5 月) 草案沒有正式公開發布。
  * Version 2.1.0 (1996 年 1 月) 是 Winsock 2 首次公開的規範。定義了兩種接口：一是應用程式接口（API），這種接口將開發者和底層隔離開；二是服務提供接口（SPI），這種接口允許對 Winsock 協議棧的擴展。對多種傳輸協議的官方支持。Winsock 2 的規範中寫入了對 OSI、Novell IPX/SPX 和 Digital DECNet 的官方支持。也添加了對一些諸如服務質量（QoS）、廣播等技術創新的支持。重疊 I/O 機制下工作。
  * Version 2.2.0 (1996 年 5 月) 包含很多小的矯正與澄清，並且不再支持 16 位 Windows 應用程式.
  * Version 2.2.1 (1997 年 5 月) 與 Version 2.2.2 (1997 年 8 月) 增加小的新功能。

Windows 95 OSR2 以後版本的 Windows 作業系統均支持 Windows Sockets version 2.2。此外，Windows 95 with the Windows Socket 2 Update 也支持 WinSock 2.2。

Windows 95、Windows NT 3.51 及更早版本的 Windows 作業系統，最高支持 Windows Sockets version 1.1。

## 技術

WinSock 編程時，可選擇下述編程模型之一：

* 同步阻塞模型：直接使用 recv () 與 send () 收發數據，是最直觀與最基本的 IO 模型；
* select 模型，可以阻塞 / 非阻塞 / 超時返回等方式查詢 socket 的狀態；
* WSAAsyncSelect 函數把 socket 綁定到一個窗口句柄，socket 的若干事件綁定到一個窗口消息（如 WM\_SOCKET\_NOTIFY），來異步響應的模型；
* WSAEventSelect 函數把 socket 與其若干事件綁定到一個作業系統的同步 event 上，來異步響應的模型；配套的 API 函數有：WSACreateEvent、WSAResetEvent、WSAWaitForMultiEvents、WSAEnumNewWorkEvents
* Overlapped I/O 事件通知模型：使用 WSA\_FLAG\_OVERLAPPED 標誌通過 WSASocket 創建一個套接字；使用 WSASend、WSASendTo、WSARecv、WSARecvFrom、WSAIoctl、AcceptEx、TransmitFile 等函數帶一個 WSAOVERLAPPED 結構參數來發起重疊 IO 操作。WSACreateEvent 函數創建所需要的事件對象。WSAWaitForMultipleEvents 函數來等待該事件對象。再調用 WSAGetOverlappedResult 函數判斷重疊 IO 操作是否成功完成。
* Overlapped I/O 完成例程模型，使用完成例程來通知 socket 事件；使用 ReadFileEx 和 WriteFileEx，或 WSARecv/WSASend，指定完成例程。
* 完成埠模型：首先使用 CreateCompletionPort 創建一個完成埠對象，然後把任意數量的 socket 或 IO 句柄綁定到該完成埠對象（通過 CreateCompletionPort 函數）。然後創建一批工作者執行緒，每個執行緒通過 GetQueuedCompletionStatus 就把自己綁定到完成埠對象。IO 完成包在完成埠對象先進先出，掛在完成埠對象上的工作者執行緒按照後進先出調度執行以減少執行緒切換的代價。

Windows Sockets API 規範包含兩種接口：

* API, 用於應用程式開發者。
* 服務提供者接口 (SPI), 用於增加新的協議模塊到作業系統。實際上，現在 Windows 上實現 TCP/IP 以外的網絡協議並沒有什麼用途.

Windows Sockets 的代碼與設計是基於 Berkeley sockets，此外還提供了額外的功能使得 API 遵從 Windows 編程模式（如重疊 I/O）. API 函數名字都以前綴 WSA 開始，例如 WSASend().

為了便於從 Unix 向 Windows 移植網絡程序的原始碼，Winsock 提供了很多便利。例如，Unix 應用程式能使用 errno 碼記錄網絡錯誤與 C 運行時錯誤。Windows Sockets 引入了專門的函數 WSAGetLastError() 以獲取錯誤信息。但很多 TCP/IP 應用程式使用了 Unix 的特性，如偽終端與 fork 系統調用。這使得原始碼移植非常困難。

### 回復 Ack 的延時

當 Microsoft TCP 棧接收到一個數據包時，會啟動一個 200 毫秒的計時器。當 ACK 確認數據包發出之後，計時器會復位。接收到下一個數據包時，會再次啟動 200 毫秒的計時器。這稱為 TCP 確認延遲機制 (TCP Delayed acknowledge)，作用是接收到數據後延遲 ACK 的發送，使得 TCP 協議棧有機會合併多個 ACK 以提高性能。Microsoft TCP 棧使用了下面的策略來決定在接收到數據包後什麼時候發送 ACK 確認數據包：

1. 如果在 200 毫秒的計時器超時之前，接收到下一個數據包，則立即發送 ACK 確認數據包。
2. 如果當前恰好有數據包需要發給 ACK 確認信息的接收端，則把 ACK 確認信息附帶在數據包上立即發送。
3. 當計時器超時，ACK 確認信息立即發送。

### 發送小尺寸數據時默認採用 Nagle 算法拼接為大數據包

為了避免小數據包擁塞網絡，Microsoft TCP 棧默認啟用了納格算法，這個算法能夠將應用程式多次調用 Send 發送的小尺寸的數據拼接起來，一塊封包發送。Nagle 算法規定的特殊情況：

1. 如果 Microsoft TCP 棧拼接起來的數據超過了 MSS 值，這個數據會立即發送，而不等待前一個數據包的 ACK 確認信息。乙太網 MTU (Maximum Transmission Unit) 是 1460 位元組。
2. 如果以前發送的數據都收到了回復的 Ack，那麼當前的數據會立即發送，而沒有延遲。
3. 如果等待超時（例如 200ms）仍然沒有拼成一個 MSS 的數據包，當前的數據會被發送。
4. 如果設置了 TCP\_NODELAY 選項，就會禁用 Nagle 算法，應用程式調用 Send 發送的數據包會立即被投遞到網絡，而沒有延遲。

### Winsock 的內核發送緩衝區與 send 函數的實現

為了在應用層優化性能，Winsock 把應用程式調用 Send 發送的數據從應用程式的緩衝區複製到 Winsock 內核緩衝區。Microsoft TCP 棧利用類似 Nagle 算法的方法，決定什麼時候才實際地把數據投遞到網絡。內核緩衝區的默認大小是 8K，使用 SO\_SNDBUF 選項，可以改變 Winsock 內核緩衝區的大小。如果有必要的話，Winsock 能緩衝大於 SO\_SNDBUF 緩衝區大小的數據。在絕大多數情況下，應用程式完成 Send 調用僅僅表明數據 
被複製到了 Winsock 內核緩衝區，並不能說明數據就實際地被投遞到了網絡上。例外情況：通過設置 SO\_SNDBUT 為 0 禁用了 Winsock 內核緩衝區。

Winsock 使用下面的規則來向應用程式表明一個 Send 調用的完成：

1. 如果 socket 仍然在 SO\_SNDBUF 限額內，Winsock 複製應用程式要發送的數據到內核緩衝區，完成 Send 調用。
2. 如果 Socket 超過了 SO\_SNDBUF 限額並且先前只有一個被緩衝的發送數據在內核緩衝區，Winsock 複製要發送的數據到內核緩衝區，完成 Send 調用。
3. 如果 Socket 超過了 SO\_SNDBUF 限額並且內核緩衝區有不只一個被緩衝的發送數據，Winsock 複製要發送的數據到內核緩衝區，然後投遞數據到網絡，直到 Socket 降到 SO\_SNDBUF 限額內或者只剩餘一個要發送的數據，才完成 Send 調用。

## 實現

### Microsoft 實現

* Microsoft 未提供 Winsock 1.0 實現.
* Version 1.1 作為插件包 (稱為 Wolverine) 用於 Windows for Workgroups (代號 **Snowball**). 它是 Windows 95 與 Windows NT 3.5 版以後的組成部分。
* Version 2.1 是 Windows 95 的插件包。它是 Windows 98, Windows NT 4.0 及其以後版本的組成部分。 Microsoft 不提供 Windows 3.x 或 Windows NT 3.x 的 Winsock 2 實現。

Winsock1.1 API，需要聲明 #include <winsock.h> 並連結 wsock32.lib，使用 wsock32.dll。

Winsock2 API，需要聲明 #include <winsock2.h>，連結 ws2\_32.lib，在 Winsock2\_32.dll 中實現。 其下是兩種服務提供者接口（Service Provider Interface）：

* Transport SPI: 提供建立連接、數據傳輸、QoS、錯誤處理等功能。rsvpsp.dll 實現了 RSVP QoS；mswsock.dll 實現了 Winsock core。
* NameSpace SPI：名字解析，如 getXXXbyYYY 等函數。支持 TCP/IP、NT DS、NLA 等命名空間。

可通過 ws2spi.h 中的兩對 API 函數來安裝 / 卸載服務提供者接口：WSCInstallProvider、WSCDeinstallProvider、WSCInstallNameSpace、WSCUnInstallNameSpace。

SPOrder.dll 中提供了重排序服務提供者：WSCWriteNameSpaceOrder、WSCWriteProviderOrder。

大部分 Winsock2 API 函數被映射到 SPI 函數，由當前安裝的服務提供者實現其功能。一條簡單規則是根據提供者鏈順序從 WSA\* 函數名映射為 WSP\* 函數名（Winsock Service Provider, 用於傳輸服務提供者函數）。下述函數不在 SPI 中實現：

* 事件處理函數與等待函數，直接映射到 Windows API:
  * WSACreateEvent,
  * WSACloseEvent,
  * WSASetEvent,
  * WSAResetEvent
  * WSAWaitForMultipleEvents
* 名字服務函數:
  * 特定 IP 名字轉換函數與名字解析函數：Winsock 1.1 的 GetXXXByYYY 以及 WSAAsyncGetXXXbyYYY, WSACancelAsyncRequest, gethostname 等。
  * 支持（support）函數： ntohs, ntohl, htonl, htons
  * IP 轉換函數：inet\_XtoY, inet\_addr, ...
* 錯誤處理函數：WSAGetLastError, WSASetLastError
* 其他函數
  * WSAEnumProtocols
  * WSAIsBlocking,
  * WSASetBlockingHook,
  * WSAUnhookBlockingHook

WSP 前綴（Winsock Service Provider）的函數是傳輸服務提供者函數。例如 WSPStartup 函數用於初始化分層服務提供者。

WPU 前綴（Winsock Provider Upcall）的函數是被服務提供者調用的 ws2\_32.dll 中的函數，

WSC 前綴 (WinSock Configuration) 的函數名是 LSP 安裝程序調用的 ws2\_32.dll 中的函數，如：WSCInstallProvider， WSCWriteProviderOrder。

NSP 前綴（NameSpace Provider）的函數名用於命名空間提供者。

 
service provider interface (SPI) 是各種功能的具體實施者。允許插入第三方廠商寫的 service providers 而無需改變 Winsock 2 的 API 與 DLL（ws2\_32.dll）；從而應用程式開發人員寫的基於 Winsock 的代碼也無需改變 。

Winsock 2 SPI 有兩種類型的 service providers —— transport 和 namespace。Transport providers（通常稱之為協議棧）提供建立連接、傳輸數據、進行流控制和差錯控制等功能。Namespace providers 提供網絡協議的尋址屬性、協議無關的名字解析。

SPI transport service providers 細分為兩類 —— base service providers 和 layered service providers。Base service providers 實現了傳輸協議的實際細節：建立連接、傳送數據、流控制和差錯控制。Layered service providers 僅實現了高層的自定義的通訊功能，而且依賴於已有的下層的 base provider 來與遠程端進行實際的數據交換。也就是說，LSP 是做什麼的，就是做一些附件的高端的可選的功能。如在 base TCP/IP 棧的頂端實現一個帶寬管理器。 而 base service providers 提供必需的基礎的功能。

Winsock 2 不允許 namespace providers 的 LSP。雖然可以使用 Winsock 2 SPI 來實現一個新的 namespace provider，但是不能改變或擴展已有 namespace provider 的命名、註冊和查詢行為。

layered transport service provider 一般由高層應用開發；而 base transport providers 和 namespace providers 一般由作業系統廠商和協議棧廠商開發。

編寫 service provider 的就是標準的 DLL，其導出函數只有運輸服務的 WSPStartup 或名字服務的 NSPStartup。WSPStartup 和 NSPStartup 通過輸出參數 lpProcTable 作為 LSP 的 dispatch table，提供了約 30 個可用的函數的地址，這些函數原型在 WS2spi.h 中聲明。WSPStartup 的另外一個參數 UpcallTable 為 LSP 提供了 ws2\_32.dll 中的 15 個函數的地址表，這些函數原型也在 WS2spi.h 中聲明。如果 ws2\_32.dll 提供了額外的函數，就需要在 WSPStartup 通過 GetProcAddress 獲取函數地址，目前僅有一個例子 WPUCompleteOverlappedResult。

LSP 可以形成一個鏈，通過調用下層 LSP 的 WSPStartup 函數，下層 LSP 由上層 LSP 裝入。最上層的 LSP 被 ws2\_32.dll 裝入。WSPStartup 函數參數 lpProtocolInfo 指向一個 WSAPROTOCOL\_INFOW 結構組成的鍊表。鍊表最底層是 base provider。 WSPStartup 與 WSPCleanup 使用引用計數來加載 / 清除。

### 其他實現

遵從 Winsock 規範的 TCP/IP 與 UDP/IP 協議棧有：3Com, Beame & Whiteside, DEC, Distinct, FTP Software, Frontier, IBM, Microdyne, NetManage, Novell, Sun Microsystems, Trumpet Software International.

## 例子

### 列出所有服務提供者

### 阻塞式程序

### 優雅關閉

TCP 連接的關閉過程有兩種：

* 優雅關閉（graceful close）如果發送緩存中還有數據未發出則其發出去，並且收到所有數據的 ACK 之後，發送 FIN 包，開始關閉過程
* 強制關閉（hard close 或 abortive close）如果緩存中還有數據，則這些數據都將被丟棄，然後發送 RST 包，直接重置 TCP 連接。

調用 close ()/closesocket () 函數意味著完全斷開連接，即不能發送數據也不能接收數據，這種「生硬」的方式有時候會顯得不太「優雅」。使用 shutdown () 函數和 WSASendDisconnect 函數可以優雅關閉連接，其函數原型為

```
int shutdown(SOCKET s, int howto);  //Windows  
```

howto 在 Windows 下有以下取值：

* SD\_RECEIVE：關閉接收操作，也就是斷開輸入流。
* SD\_SEND：關閉發送操作，也就是斷開輸出流。
* SD\_BOTH：同時關閉接收和發送操作。

確切地說，close () /closesocket () 用來關閉套接字，將套接字描述符（或句柄）從內存清除，之後再也不能使用該套接字。應用程式關閉套接字後，與該套接字相關的連接和緩存也失去了意義，TCP 協議會自動觸發關閉連接的操作。shutdown () 用來關閉連接，而不是套接字；套接字依然存在，直到調用 close () /closesocket () 將套接字從內存清除。調用 close ()/closesocket () 關閉套接字時，或調用 shutdown () 關閉輸出流時，都會向對方發送 FIN 包。FIN 包表示數據傳輸完畢，對端收到 FIN 包就知道不會再有數據傳送過來了。默認情況下，close ()/closesocket () 會立即向網絡中發送 FIN 包，不管輸出緩衝區中是否還有數據，而 shutdown () 會等輸出緩衝區中的數據傳輸完畢再發送 FIN 包。也就意味著，調用 close ()/closesocket () 將丟失輸出緩衝區中的數據，而調用 shutdown () 不會。

shutdown () 並不實際關閉 socket，而是僅僅改變其可用性。shutdown 是一種優雅地單方向或者雙方向關閉 socket 的方法。 如果有多個進程共享一個 socket，shutdown 影響所有進程，而 close 只影響本進程。shutdown 本身並不影響底層，也即此前發出的異步 send/recv 不會返回。在所有已發送的包被對端確認後，本方會發送 FIN 包給 client，開始 TCP 四次揮手過程。 對端收到 FIN 報文後，並不知道 server 端以何種方式 shutdown，甚至不知道 server 端是 shutdown 還是 close。

若本方發送 FIN 報文後沒有收到對端的 FIN-ACK，會兩次重傳 FIN 報文，若一直收不到對端的 FIN-ACK，則會給對端發送 RST 信號，關閉 socket 並釋放資源。對端收到 FIN 信號後，再調用 read 函數會返回 0；因為接收了 FIN，表明以後再無數據可以接收。

對端收到 RST 報文後，行為如下：

* 阻塞模型下，內核無法主動通知應用層出錯，只有應用層主動調用 read () 或者 write () 這樣的 IO 系統調用時，內核才會利用出錯來通知應用層已經收到 RST 報文
* 非阻塞模型下，select 或者 epoll 會返回 sockfd 可讀，應用層對其進行讀取時，read () 會報 RST 錯誤。

收到 RST 報文的情況下，再做任何 read write 都是毫無意義。

調用調用 close ()，根據參數設置不同，會出現如下兩種情況：

* l\_onoff 為非 0，l\_linger 為 0： 向對端發送一個 RST 報文，丟棄本地緩衝區的未讀數據，關閉 socket 並釋放相關資源，此種方式為強制關閉。
* l\_onoff 為非 0，l\_linger 為非 0：向對端發送一個 FIN 報文，收到對端 FIN-ACK 後，進入了 FIN\_WAIT\_2 階段，參考 TCP 四次揮手過程，此種方式為優雅關閉。如果在 l\_linger 的時間內仍未完成四次揮手，則強制關閉。

當 l\_onoff 值設置為 0 時，closesocket 會立即返回，並關閉用戶 socket 句柄。如果此時緩衝區中有未發送數據，則系統會在後台將這些數據發送完畢後關閉 TCP 連接，是一個優雅關閉過程，但是這裡有一個副作用就是 socket 的底層資源會被保留直到 TCP 連接關閉，這個時間用戶應用程式是無法控制的。

當 l\_onoff 值設置為非 0 值，而 l\_linger 也設置為 0，那麼 closesocket 也會立即返回並關閉用戶 socket 句柄，但是如果此時緩衝區中有未發送數據，TCP 會發送 RST 包重置連接，所有未發數據都將丟失，這是一個強制關閉過程。

當 l\_onoff 值設置為非 0 值，而 l\_linger 也設置為非 0 值時，同時如果 socket 是阻塞式的，此時如果緩衝區中有未發送數據，如果 TCP 在 l\_linger 表明的時間內將所有數據發出，則發完後關閉 TCP 連接，這時是優雅關閉過程；如果如果 TCP 在 l\_linger 表明的時間內沒有將所有數據發出，則會丟棄所有未發數據然後 TCP 發送 RST 包重置連接，此時就是一個強制關閉過程了。

另外還有一個 socket 選項 SO\_DONTLINGER，它的參數值是一個 bool 類型的，如果設置為 true，則等價於 SO\_LINGER 中將 l\_onoff 設置為 0。

注意 SO\_LINGER 和 SO\_DONTLINGER 選項只影響 closesocket 的行為，而與 shutdown 函數無關，shutdown 總是會立即返回的。

## 參見

* Berkeley sockets
* 分層服務提供者 (Winsock LSP)

## 參考文獻

## 外部連結

* MSDN - Winsock2 Reference （頁面存檔備份，存於網際網路檔案館）
* MSDN - Winsock2 Home （頁面存檔備份，存於網際網路檔案館）
* Sockets FAQ （頁面存檔備份，存於網際網路檔案館） - Windows Sockets FAQ
* Client / Server Programming with TCP/IP Sockets - Winsock C++ Programming
* Porting Berkley Socket programs to Winsock （頁面存檔備份，存於網際網路檔案館）
* Windows Network Development blog （頁面存檔備份，存於網際網路檔案館） — Microsoft developer blog covering Winsock, WSK, WinINet, Http.sys, WinHttp, QoS and System.Net, with a focus on features being introduced in Windows Vista
* Brief History of Microsoft on the Web （頁面存檔備份，存於網際網路檔案館）
* Winsock error codes list with descriptions
* WinSock Development Information （頁面存檔備份，存於網際網路檔案館）
* Winsock Programmer's FAQ （頁面存檔備份，存於網際網路檔案館）