Windows 實現TCP/IP 協議也是建立在上一篇博客的OSI 基礎之上的。用戶態是由ws2_32.dll 和一些其它服務提供者的 dll 共同實現,其中ws2_32.dll 是一個框架,可以容納很多的服務提供者,這些服務提供者其實就是各種協議的實現者,如比較常見的有 TCP/IP 協議,IPX 協議。而 TCP/IP 協議的服務實現是由 msafd.dll 和 mswsock.dll 來完成。HKEY_LOCAL_MACHINESYSTEMCurrentControlSetServicesWinSock2,該注冊表下記錄了協議(服務)及其一些其它的信息。
就 TCP/IP 而言,我們普遍會使用 posix標準的 socket 接口來完成我們應用程序的功能,這樣要想完成跨平臺的代碼就會比較方便。
在上一篇文章中,我們知道,tcp/ip 協議的用戶態部分由msafd.dll 完成,它與內核部分的 afd.sys 交互來實現 socket 接口的系統調用。然后 afd.sys 完成 socket 的一些機制,并且和 tcpip.sys 驅動程序交互,總結一下如下。
1. Msafd.dll : socket 接口的用戶態部分,與afd.sys 通信。
2. Afd.sys : socket 接口的內核態部分,滿足 msafd.dll的調用,向下與 tcpip.sys 通信。
3. Tcpip.sys : tcp/ip 協議的主要實現部分,滿足afd.sys 的調用,向下與小端口網卡驅動通過 IRP通信。
4. socket 的概念是在 msafd.dll和 afd.sys 中才有的,它們兩個實現了 socket 的用戶態和內核態部分。它們的下層是傳輸層(TDI)層,TDI 層完成了 TCP, UDP, RawIp的機制,在 TDI 層中,只有地址對象,連接對象,控制通道的概念。TDI 的下層是網絡層(IP 層),在 IP 層中,只有 Packet 的概念,收到數據時,通過 IP 包中的標識,知道要提交給 TCP 或 UDP 等處理。TDI 層和 IP 層都由 tcpip.sys 來實現。
知道上面的概念后,就有了比較清晰的結構,當然驅動和設備的管理由 IO 管理器來管理,但tcpip 協議族卻沒有用常規設備棧的方式來處理數據包,afd.sys 與 tcpip.sys 以及 tcpip.sys 與 miniport 驅動之間都是由發送 IRP 來實現。這也使中間過濾層驅動的實現略微復雜,這里且不談。
那么我們把上面零散的概念串起來,看看從普通的 socket 接口到數據最終由網卡發出的整個過程。
Socket :
Ws2_32.dll 加載時會根據注冊表初始化服務提供者,服務者會告知自己支持的地址族,socket 類型,和協議類型。當我們調用socket(AF_INET, SOCK_DGRAM, IPPROT_UDP) 來創建一個 UDP 類型的套接字的時候,根據傳入的參數,會定位到 msafd.dll 這個服務提供者,并會調用相應的 socket 創建接口,它會打開設備 DeviceAfdEndPoint ,由于 afd.sys 創建了一個 DeviceAfd 設備,所以一個 IRP_MJ_CREATE 的 IRP 便會發送到 afd.sys 驅動的創建函數,它會創建一個FAD_FCB 結構體來表示這個套接字,并且記錄下 FileObject,并返回。
Bind :
要想接收數據包,我們會把 socket 綁定到本地的一個IP-Port 對,就是調用 Bind 接口,msafd.dll 會通過一個控制消息,次功能號為 IOCTL_AFD_BIND,此時afd.sys 會接著根據上面 FCB 記錄的設備名打開相應的 DeviceUDP 設備,并把輸入參數標識為是一個傳輸層的地址,那么 tcpip.sys 會創建接口就會創建一個地址對象來表示這次綁定,當然還會分配相應的端口信息。
Connect:
如果是 TCP,還需要連接到對方的socket,與 Bind 類似,它也會根據 FCB 記錄的設備名打開相應的設備,并把輸入參數標識為是一個連接對象,tcpip.sys 會創建一個連接對象來表示這次連接。
其實在 TDI 層,還有一種叫做控制通道,當其它驅動想得到 TDI 層的一些信息,如當前的 TCP或 UDP 連接有哪些,那么它會直接打開 DeviceTCP 等設備,由于沒有傳入參數,那么 tcpip.sys 則會創建一個控制通道。TDI 層這些對象的標識都會保存在與之對應的 FileObject->FsContext2 里,以便后來區分。
當前面準備工作做好后,我們就來看數據的接收和發送。
SendTo:
由 msafd.dll 發送一個 IOCTL_AFD_SEND_DATAGRAM到 afd.sys ,afd.sys 創建一個主功能號為 IRP_MJ_INTERNAL_DEVICE_CONTROL ,次功能號為 TDI_SEND_DATAGRAM的 IRP 到 tcpip.sys,tcpip.sys 調用相就的 UDPSendDatagram,組裝一個 UDP 包,最后通過 IpSendDatagram 到協議層,然后由相應的小端口驅動發送出去。
RecvFrom:
接收數據稍微復雜一點,接收數據都是由afd.sys 驅動發送一個次功能號為 TDI_RECEIVE_DATAGRAM (afd.sys 與 tcpip.sys 的傳輸層都是以 IRP_MJ_INTERNAL_DEVICE_CONTROL 為主功能號)的 IRP 到 TDI 層,而 TDI 層都是以接收請求的形式來掛在地址對象的接收請求(DATAGRAM_RECEIVE_REQUEST)隊列中,在地址對象創建的時候會創建這個隊列。那么什么時候這個請求會被滿足呢,這要從網卡接到數據說起。當網卡接收到數據時,協議驅動也會收到這個數據,一般情況下只有能處理這個協議的驅動才會去處理這個包,此時就會進行到 tcpip.sys 的協議部分,即 IP 協議,tcpip.sys 根據相應的標識,確定是 IP 包,因為 tcpip.sys 還完成了 ARP 包的處理,最后會上交到 Ipv4 的處理流程,它會調用ProcessFragment ->IpDispatchProtocol ,IpDispatchProtocl 會區分出是什么包,如果是 UDP 包,由會調用UDPReceive ,并進一步根據地址對象鏈表來找到匹配的地址對象,DGDeliverData 來交付數據,它會查看對址對象的接收請求隊列中是否有請求,如果沒有,則查看是否注冊了接收數據的處理過程,如果也沒有注冊,那么就會丟掉這個包,這就是 UDP 不可靠的一個原因。
那么有人就會有疑問,我們如果調用完 Bind 之后,還沒來得及調用 RecvFrom ,那么,接收到的包不就丟了么,其實,在調用 Bind 之后,就會馬上發送一個接收請求到隊列中,也就避免了這種情況的發生。這只是整個過程的導火索,在 Bind 里面它是通過調用TdiReceiveDatagram 來投遞一個接收請求的,它會創建一個TDI_RECEIVE_DATAGRAM 的 IRP,并為這個 IRP 設置一個完成例程PacketSocketRecvComplete, tcpip.sys 會響應這個 IRP,并在相應的地址對象的接收請求隊列中插入一個請求,并設置這個請求的完成函數為DGReceiveComplete,用戶完成函數為DispDataRequestComplete。 當通過 DGDeliverData 交付數據時,如果隊列中有請求,那么就去滿足這個請求,拷貝數據到與這個請求相應的緩沖中,當調用請求的完成函數 DGReceiveComplete,它會調用用戶完成函數 DispDataRequestComplete,DiapDataRequestComplete會完成這個 IRP,那么 IRP 的完成例程PacketSocketRecvComplete 就會得到調用了, 在 PacketSocketRecvComplete中(該函數在 afd.sys 中)要做的工作先暫停一下,回到 RecvFrom 的調用,在 RecvFrom 向下直到 afd.sys 層,它并不會直接發送 IRP 到 tcpip.sys 中去請求接收數據,如果 FCB->DatagramList 中沒有數據,它會把 msafd.dll 下發的這個 IRP 放到 FCB->PendingIrpList 中,并掛起,所以到 tcpip.sys 的請求都是由在 Bind 最后發送的那個導火索引起,回到 PacketSocketRecvComplete 中,它會從FCB->PendingIrpList 中摘掉一個 IRP 并插入一個數據包到 FCB->DatagramList 中,最后完成這個 IRP,那么 RecvFrom 下發的這個 IRP 就完成了。最后它又調用TdiReceiveDatagram 來投遞一個接收請求,然后周而復始。
一個 UDP Socket的大致過程就到此為止了。