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的大致过程就到此为止了。