Winsock

Windows Sockets API (WSA), 簡短記為Winsock, 是Windows的TCP/IP網路程式設計介面。其函式名稱兼容於Berkeley套接字API。實際上，Winsock的實現庫(winsock.dll)使用的是長名字。Winsock是一種能使Windows程式透過任意網路傳輸協議傳送資料的API。Winsock中有幾個只支援TCP/IP協議的函式，但是在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)並且版權屬於這五人。規範版本.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程式設計時，可選擇下述程式設計模型之一：Windows Sockets API規範包含兩種介面：Windows Sockets的程式碼與設計是基於Berkeley sockets，此外還提供了額外的功能使得API遵從Windows程式設計模式. API函式名字都以字首WSA開始, 例如WSASend().為了便於從Unix向Windows移植網路程式的原始碼，Winsock提供了很多便利. 例如，Unix應用程式能使用記錄網路錯誤與C執行時錯誤。Windows Sockets引入了專門的函式WSAGetLastError()以獲取錯誤資訊. 但很多TCP/IP應用程式使用了Unix的特性, 如與。這使得原始碼移植非常困難。回覆Ack的延時.當Microsoft TCP棧接收到一個數據包時，會啟動一個200毫秒的計時器。當ACK確認資料包發出之後，計時器會復位。接收到下一個資料包時，會再次啟動200毫秒的計時器。這稱為TCP確認延遲機制(TCP Delayed acknowledge)，作用是接收到資料後延遲ACK的傳送，使得TCP協議棧有機會合並多個ACK以提高效能。Microsoft TCP棧使用了下面的策略來決定在接收到資料包後什麼時候傳送ACK確認資料包：傳送小尺寸資料時預設採用Nagle演算法拼接為大資料包.為了避免小資料包擁塞網路，Microsoft TCP棧預設啟用了納格演算法，這個演算法能夠將應用程式多次呼叫Send傳送的小尺寸的資料拼接起來，一塊封包傳送。Nagle演算法規定的特殊情況：Winsock的核心傳送緩衝區與send函式的實現.為了在應用層最佳化效能，Winsock把應用程式呼叫Send傳送的資料從應用程式的緩衝區複製到Winsock核心緩衝區。Microsoft TCP棧利用類似Nagle演算法的方法，決定什麼時候才實際地把資料投遞到網路。核心緩衝區的預設大小是8K，使用SO_SNDBUF選項，可以改變Winsock核心緩衝區的大小。如果有必要的話，Winsock能緩衝大於SO_SNDBUF緩衝區大小的資料。在絕大多數情況下，應用程式完成Send呼叫僅僅表明資料被複制到了Winsock核心緩衝區，並不能說明資料就實際地被投遞到了網路上。例外情況：透過設定SO_SNDBUT為0禁用了Winsock核心緩衝區。Winsock使用下面的規則來嚮應用程式表明一個Send呼叫的完成：實現.Microsoft實現.Winsock1.1 API，需要宣告#include 並連結wsock32.lib，使用wsock32.dll。Winsock2 API，需要宣告#include ，連結ws2_32.lib，在Winsock2_32.dll中實現。 其下是兩種服務提供者介面：可透過ws2spi.h中的兩對API函式來安裝/解除安裝服務提供者介面：WSCInstallProvider、WSCDeinstallProvider、WSCInstallNameSpace、WSCUnInstallNameSpace。SPOrder.dll中提供了重排序服務提供者：WSCWriteNameSpaceOrder、WSCWriteProviderOrder。大部分Winsock2 API函式被對映到SPI函式，由當前安裝的服務提供者實現其功能。一條簡單規則是根據提供者鏈順序從WSA*函式名對映為WSP*函式名。下述函式不在SPI中實現：WSP字首的函式是傳輸服務提供者函式。例如WSPStartup函式用於初始化分層服務提供者。WPU字首的函式是被服務提供者呼叫的ws2_32.dll中的函式，WSC字首(WinSock Configuration)的函式名是LSP安裝程式呼叫的ws2_32.dll中的函式，如：WSCInstallProvider， WSCWriteProviderOrder。NSP字首的函式名用於名稱空間提供者。service provider interface (SPI)是各種功能的具體實施者。允許插入第三方廠商寫的 service providers 而無需改變Winsock 2 的API與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 &amp; Whiteside, DEC, Distinct, FTP Software, Frontier, IBM, Microdyne, , Novell, Sun Microsystems, Trumpet Software International.例子.列出所有服務提供者.char *ExpandServiceFlags(DWORD serviceFlags)/* A little utility function to make sense of all those bit flags *//* The following code leaks. Yeah, I know.. Go find Buffer 0v3rfl0w$ :-) */char *serviceFlagsText = (char *)malloc(2048);memset(serviceFlagsText, '\0', 2048);char *strip_comma;/* Hey - it's only for printing and demo purposes.. */if (serviceFlags &amp; XP1_CONNECTIONLESS)strcat(serviceFlagsText, "Connectionless, ");if (serviceFlags &amp; XP1_GUARANTEED_ORDER)strcat(serviceFlagsText, "Guaranteed Order, ");if (serviceFlags &amp; XP1_GUARANTEED_DELIVERY)strcat(serviceFlagsText, "Message Oriented, ");if (serviceFlags &amp; XP1_CONNECT_DATA)strcat(serviceFlagsText, "Connect Data, ");if (serviceFlags &amp; XP1_DISCONNECT_DATA)strcat(serviceFlagsText, "Disconnect Data, ");if (serviceFlags &amp; XP1_SUPPORT_BROADCAST)strcat(serviceFlagsText, "Broadcast Supported, ");if (serviceFlags &amp; XP1_EXPEDITED_DATA)strcat(serviceFlagsText, "Urgent Data, ");if (serviceFlags &amp; XP1_QOS_SUPPORTED)strcat(serviceFlagsText, "QoS supported, ");/** While we're quick and dirty, let's get as dirty as possible..strip_comma = strrchr(serviceFlagsText, ',');if (strip_comma)*strip_comma = '\0';return (serviceFlagsText);void PrintProtocolInfo(LPWSAPROTOCOL_INFOW prot)wprintf(L"Protocol Name: %s\n", prot-&gt;szProtocol); /* #%^@$! UNICODE...*/printf("\tServiceFlags1: %d (%s)\n",prot-&gt;dwServiceFlags1,ExpandServiceFlags(prot-&gt;dwServiceFlags1));printf("\tProvider Flags: %d\n", prot-&gt;dwProviderFlags);printf("\tNetwork Byte Order: %s\n",(prot-&gt;iNetworkByteOrder == BIGENDIAN) ? "Big Endian" : "Little Endian");printf("\tVersion: %d\n", prot-&gt;iVersion);printf("\tAddress Family: %d\n", prot-&gt;iAddressFamily);printf("\tSocket Type: ");switch (prot-&gt;iSocketType)case SOCK_STREAM:printf("STREAM\n");break;case SOCK_DGRAM:printf("DGRAM\n");break;case SOCK_RAW:printf("RAW\n");break;default:printf(" Some other type\n");printf("\tProtocol: ");switch (prot-&gt;iProtocol)case IPPROTO_TCP:printf("TCP/IP\n");break;case IPPROTO_UDP:printf("UDP/IP\n");break;default:printf("some other protocol\n");int _cdecl main(int argc, char** argv)LPWSAPROTOCOL_INFOW bufProtocolInfo = NULL;DWORD dwSize = 0;INT dwError;INT iNumProt;* Enum Protocols - First, obtain size requiredprintf("Sample program to enumerate Protocols\n");WSCEnumProtocols(NULL, // lpiProtocolsbufProtocolInfo, // lpProtocolBuffer&amp;dwSize, // lpdwBufferLength&amp;dwError); // lpErrnobufProtocolInfo = (LPWSAPROTOCOL_INFOW)malloc(dwSize);if (!bufProtocolInfo) {fprintf(stderr, "SHOOT! Can't MALLOC!!\n");exit(1);/* Now, Enum */iNumProt = WSCEnumProtocols(NULL, // lpiProtocolsbufProtocolInfo, // lpProtocolBuffer&amp;dwSize, // lpdwBufferLength&amp;dwError);if (SOCKET_ERROR == iNumProt)fprintf(stderr, "Darn! Can't Enum!!\n");exit(1);printf("%d Protocols detected:\n", iNumProt);for (int i = 0;i &lt; iNumProt;i++)PrintProtocolInfo(&amp;bufProtocolInfo[i]);printf("-------\n");printf("Done");return(0);阻塞式程式.int nPort =65000;//指定通訊埠WSADATA wsaData;WSAStartup( MAKEWORD( 2, 2 ), &amp;wsaData );// 建立監聽套接字，繫結本地埠，開始監聽SOCKET sListen = socket( AF_INET,SOCK_STREAM, 0 );SOCKADDR_IN addr;addr.sin_family = AF_INET;addr.sin_port = htons( nPort );addr.sin_addr.S_un.S_addr = INADDR_ANY;bind( sListen, (sockaddr *)&amp;addr, sizeof( addr ) );listen( sListen, 5 );SOCKADDR_IN saRemote;int nRemoteLen = sizeof( saRemote );SOCKET sRemote = accept( sListen, (sockaddr *)&amp;saRemote, &amp;nRemoteLen );優雅關閉.TCP連線的關閉過程有兩種：shutdown(clntSock, SD_SEND); //資料傳送完畢，斷開輸出流，向客戶端傳送FIN包recv(clntSock, buffer, BUF_SIZE, 0); //阻塞，等待客戶端接收完畢後closesocket，這將導致本方的recv從阻塞狀態返回fclose(fp); //釋放資源closesocket(clntSock);closesocket(servSock); //也要關閉用於listen/accept的socketWSACleanup();呼叫 close()/closesocket() 函式意味著完全斷開連線，即不能傳送資料也不能接收資料，這種“生硬”的方式有時候會顯得不太“優雅”。使用 shutdown() 函式和WSASendDisconnect函式可以優雅關閉連線，其函式原型為int shutdown(SOCKET s, int howto); //Windowshowto 在 Windows 下有以下取值：確切地說，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報文後，行為如下：收到RST報文的情況下，再做任何read write都是毫無意義。呼叫呼叫close()，根據引數設定不同，會出現如下兩種情況：當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總是會立即返回的。