Loading AI tools
来自维基百科,自由的百科全书
Windows Sockets API (WSA), 簡短記為Winsock, 是Windows的TCP/IP網絡編程接口(API)。其函數名稱兼容於Berkeley套接字API。實際上,Winsock的實現庫(winsock.dll)使用的是長名字。
此條目需要補充更多來源。 (2016年9月9日) |
開發者 | 微軟 |
---|---|
首次發布 | 1992年6月 |
作業系統 | Microsoft Windows |
類型 | API |
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)並且版權屬於這五人。
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編程模式(如重疊I/O). API函數名字都以前綴WSA開始, 例如WSASend().
為了便於從Unix向Windows移植網絡程序的原始碼,Winsock提供了很多便利. 例如,Unix應用程式能使用errno碼記錄網絡錯誤與C運行時錯誤。Windows Sockets引入了專門的函數WSAGetLastError()以獲取錯誤信息. 但很多TCP/IP應用程式使用了Unix的特性, 如偽終端與fork系統調用。這使得原始碼移植非常困難。
當Microsoft TCP棧接收到一個數據包時,會啟動一個200毫秒的計時器。當ACK確認數據包發出之後,計時器會復位。接收到下一個數據包時,會再次啟動200毫秒的計時器。這稱為TCP確認延遲機制(TCP Delayed acknowledge),作用是接收到數據後延遲ACK的發送,使得TCP協議棧有機會合併多個ACK以提高性能。Microsoft TCP棧使用了下面的策略來決定在接收到數據包後什麼時候發送ACK確認數據包:
為了避免小數據包擁塞網絡,Microsoft TCP棧默認啟用了納格算法,這個算法能夠將應用程式多次調用Send發送的小尺寸的數據拼接起來,一塊封包發送。Nagle算法規定的特殊情況:
為了在應用層優化性能,Winsock把應用程式調用Send發送的數據從應用程式的緩衝區複製到Winsock內核緩衝區。Microsoft TCP棧利用類似Nagle算法的方法,決定什麼時候才實際地把數據投遞到網絡。內核緩衝區的默認大小是8K,使用SO_SNDBUF選項,可以改變Winsock內核緩衝區的大小。如果有必要的話,Winsock能緩衝大於SO_SNDBUF緩衝區大小的數據。在絕大多數情況下,應用程式完成Send調用僅僅表明數據 被複製到了Winsock內核緩衝區,並不能說明數據就實際地被投遞到了網絡上。例外情況:通過設置SO_SNDBUT為0禁用了Winsock內核緩衝區。
Winsock使用下面的規則來向應用程式表明一個Send調用的完成:
Winsock1.1 API,需要聲明#include <winsock.h>並連結wsock32.lib,使用wsock32.dll。
Winsock2 API,需要聲明#include <winsock2.h>,連結ws2_32.lib,在Winsock2_32.dll中實現。 其下是兩種服務提供者接口(Service Provider Interface):
可通過ws2spi.h中的兩對API函數來安裝/卸載服務提供者接口:WSCInstallProvider、WSCDeinstallProvider、WSCInstallNameSpace、WSCUnInstallNameSpace。
SPOrder.dll中提供了重排序服務提供者:WSCWriteNameSpaceOrder、WSCWriteProviderOrder。
大部分Winsock2 API函數被映射到SPI函數,由當前安裝的服務提供者實現其功能。一條簡單規則是根據提供者鏈順序從WSA*函數名映射為WSP*函數名(Winsock Service Provider, 用於傳輸服務提供者函數)。下述函數不在SPI中實現:
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[1].
/**
* Winsock 2 API Protocol Enumerator -
*/
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#define WINSOCK_API_LINKAGE
#include <winsock2.h>
#include <ws2spi.h>
#include <wtypes.h>
#include <assert.h>
#include <winnt.h>
#include <stdlib.h>
#include <stdio.h>
#pragma comment(lib,"Ws2_32.lib")
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 & XP1_CONNECTIONLESS)
{
strcat(serviceFlagsText, "Connectionless, ");
}
if (serviceFlags & XP1_GUARANTEED_ORDER)
{
strcat(serviceFlagsText, "Guaranteed Order, ");
}
if (serviceFlags & XP1_GUARANTEED_DELIVERY)
{
strcat(serviceFlagsText, "Message Oriented, ");
}
if (serviceFlags & XP1_CONNECT_DATA)
{
strcat(serviceFlagsText, "Connect Data, ");
}
if (serviceFlags & XP1_DISCONNECT_DATA)
{
strcat(serviceFlagsText, "Disconnect Data, ");
}
if (serviceFlags & XP1_SUPPORT_BROADCAST)
{
strcat(serviceFlagsText, "Broadcast Supported, ");
}
if (serviceFlags & XP1_EXPEDITED_DATA)
{
strcat(serviceFlagsText, "Urgent Data, ");
}
if (serviceFlags & 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->szProtocol); /* #%^@$! UNICODE...*/
printf("\tServiceFlags1: %d (%s)\n",
prot->dwServiceFlags1,
ExpandServiceFlags(prot->dwServiceFlags1));
printf("\tProvider Flags: %d\n", prot->dwProviderFlags);
printf("\tNetwork Byte Order: %s\n",
(prot->iNetworkByteOrder == BIGENDIAN) ? "Big Endian" : "Little Endian");
printf("\tVersion: %d\n", prot->iVersion);
printf("\tAddress Family: %d\n", prot->iAddressFamily);
printf("\tSocket Type: ");
switch (prot->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->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 required
*/
printf("Sample program to enumerate Protocols\n");
WSCEnumProtocols(NULL, // lpiProtocols
bufProtocolInfo, // lpProtocolBuffer
&dwSize, // lpdwBufferLength
&dwError); // lpErrno
bufProtocolInfo = (LPWSAPROTOCOL_INFOW)malloc(dwSize);
if (!bufProtocolInfo) {
fprintf(stderr, "SHOOT! Can't MALLOC!!\n");
exit(1);
}
/* Now, Enum */
iNumProt = WSCEnumProtocols(
NULL, // lpiProtocols
bufProtocolInfo, // lpProtocolBuffer
&dwSize, // lpdwBufferLength
&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 < iNumProt;
i++)
{
PrintProtocolInfo(&bufProtocolInfo[i]);
printf("-------\n");
}
printf("Done");
return(0);
}
int nPort =65000;//指定通信端口
WSADATA wsaData;
WSAStartup( MAKEWORD( 2, 2 ), &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 *)&addr, sizeof( addr ) );
listen( sListen, 5 );
SOCKADDR_IN saRemote;
int nRemoteLen = sizeof( saRemote );
SOCKET sRemote = accept( sListen, (sockaddr *)&saRemote, &nRemoteLen );
TCP連接的關閉過程有兩種:
shutdown(clntSock, SD_SEND); //数据发送完毕,断开输出流,向客户端发送FIN包
recv(clntSock, buffer, BUF_SIZE, 0); //阻塞,等待客户端接收完毕后closesocket,这将导致本方的recv从阻塞状态返回
fclose(fp); //释放资源
closesocket(clntSock);
closesocket(servSock); //也要关闭用于listen/accept的socket
WSACleanup();
調用 close()/closesocket() 函數意味著完全斷開連接,即不能發送數據也不能接收數據,這種「生硬」的方式有時候會顯得不太「優雅」。使用 shutdown() 函數和WSASendDisconnect函數可以優雅關閉連接,其函數原型為
int shutdown(SOCKET s, int howto); //Windows
howto 在 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總是會立即返回的。