CS144番外
由于之前赶着做CS144,一直没有把Minnow的源代码好好看看,现在有时间了,准备将代码逻辑缕缕,看看是如何将包从发送方发出,并一步步到达接收方。
本人仓库代码
首先从TCP发送方接收方的耦合开始。
checkpoint 0
webget.cc 这个文件引用了 socket.hh头文件,查看一下。
首先定义了一个Socket类,通常不直接使用该类,而是使用其子类TCPSocket或者UDPSocket。注意Socket类的基类是 FileDescripter。
Socket 中定义了一些常用的方法(如构造函数),并用protected修饰,只能由子类或者该类的友元类和友元函数访问,不能被外界代码访问。
- 默认构造函数
1 | // default constructor for socket of (subclassed) domain and type |
其中 domain描述了沟通的域, 即协议家族,常见的有 AF_UNIX
(用于本地通信)和 AF_INET
(用于IPV4通信)。
type描述了沟通语法。例如:SOCK_STREAM 支持有序,可靠,双工,连接的字节流,可能支持外带数据的传送机制。
SOCK_DGRAM 支持 数据报( 无连接,固定最大长度的不可靠报文)
protocol描述了描述了套接字使用的特别的协议。多数情况,协议家族只支持一种协议,这样就将protocol置0,少数情况下,就需要描述使用的协议类型。
- 使用文件描述符构造
与上面类似
不过使用确定的文件描述符来构造套接字。
然后就是其他一些成员函数,获取地址,监听端口等等。
checkpoint 4
还记得 我当时写这个Lab时,将webget的头文件添加了
\#include "tcp_minnow_socket.hh"
,让我们一起剖析一下这个头文件。
这里面定义了 CS144TCPSocket(继承自 TCPMinnowSocket) , 由于webget.cc 中使用的就是这个,说明它集成了我们所有的实现。
这个文件引用了 tcppeer。
TCPPeer类,模拟对等体,将TCPSender和TCPReciver结合起来,里面还出现了TCPMessage(包括将报文段和确认报文一起发送,记住TCP是双向的,这是捎带确认)。
声明了 TCPMinnowSocket类,继承自LocalStreamSocket类。
TCPMinnowSocket 是TCPPeer类的多线程封装,模拟Unix套接字。
构造函数使用了适配器TCPOverIPv4OverTunFdAdapter(使不兼容的对象能够相互合作)。
通过该适配器,读取和写入TUN设备,TUN设备用于内核网络协议栈(链路层)和用户空间之间传递数据,是虚拟的网络接口。使得链路层及以下变得透明。
TUN设备
TCPOverIPv4OverTunFdAdapter有一个成员变量 TunFD ,这是TUN设备的文件描述符,这样我们就可以在用户态将IP数据报度写入内核。
再看看 CS144TCPSocket 的 connect 函数:
1 | void connect( const Address& address ) |
首先定义了TCPConfig,用于 配置TCP的参数。通过FdAdapterConfig
配置原地址和目标地址信息。然后使用父类
TCPOverIPv4MinnowSocket(TCPMinnowSocket
在TCPMinnowSocket的connect的函数中将发送者的transmit函数设置为向适配器写入的函数,这样当发送方发送时,实际上是写入到TUN设备中。
那么接收方是如何从TUN设备读取数据呢?
找到了部分代码
1 | if ( auto seg = _datagram_adapter.read() ) { |
这里通过从TUN设备中获取数据,再将其交给TCPPeer。
至此我们弄明白,CS144TCPSocket通过使用TCPPeer(简单理解为我们写的代码的集合),构造TCP报文段,通过适配器转化为IP报文并写入TUN设备(也是通过文件描述符)中,接下来的所有事就交由给内核处理。 反之则从TUN设备中读取IP报文段,转化为 TCP报文段,然后通过TCPPeer转化为字节流,交付给上层进程。
TCPMinnowSocket中的两个线程
TCPMinnowSocket 和 正常的TCPSocket的区别:
- 只能接收单个连接
- listen_and_accept 是 listen和accept的结合
- 如果在TCP连接时被销毁,会直接发送RST终止,但可以通过wait_until_close 来避免,从而四次挥手。
一个前台线程,使得和TCPMinnowSocket
交互就像和TCPSocket交互一样:
连接,侦听和从可靠字节流(TCP)中读取。
另一个是后台(TCPPeer)线程,负责处理内核为TCPSocket所执行的后台工作: 读取和解析网络上的数据报,过滤无关报文段等。
后台线程 监听事件通过eventloop完成,eventloop在TCP初始化中加入了三种事件的规则:
- 接收到的数据报,经过适配器的解析,交给TCPPeer, 然后TCPPeer将其分解为TCPSenderMessage和TCPReciverMessage ,TCPSenderMessage交给 TCPReciver 处理(Reassembler, 等等), TCPReciverMessage交给 TCPSender处理。
- 从本地程序收到的外出比特, TCPMinnowSocket 的 描述符和_thread_data (用来进程间传送数据) 是一对LocalStreamsocket。外部程序向CS144TCPSocket 写入,就会传送(本质上还是TCP)到 _thread_data,从而后台线程就可以读取。
- 接收到的被Reassembler重组的字节流,同样需要写入thread_data然后被应用程序读取。
我们实现的TCP,仅支持监听和接收一条线路,不支持多路复用和排队。
TUN设备会接收哪些IP数据报?
查看tun.sh
1 | TUN_IP_PREFIX=169.254 |
checkpoint 7

服务器和客户端,通过中继服务器连接。
如果没有中继服务器,那么服务器和客户端在同一个NAT网络下,能否进行交互呢?
答案应该是不能,因为一个是在192.168.0.1网段下的,另一个是在172.16.0.1 网段下的,而路由器不能识别这些ip源地址。
所以在这部分中,使用UDP将以太网帧封装,才能发送出去。
配置路由:
1 | if ( is_client ) { |
和上面图片一样。
配置客户端或者服务器
1 | /* set up the client */ |
一个地址为其ip地址,另一个是下一跳地址。
然后设置网络的各种事件:
- 主机到路由器
- 路由器到主机
- 路由器到网络
- 网络到路由器 ,然后转到网络接口,再由路由器网络接口转入主机网络接口,再由适配器读入,然后TCPMinnowSocket读入,和之前一样。
新定义的TCPSocketEndToEnd连接了主机和路由(一对套接字),使用UDPSocket连接 连接远程终端(网络)。
1 | while ( true ) { |
这里为接口记录时间。
可以看出TCP实际传送是通过 TCP-in-IP-Ethernet-UDP 方法。
将其当做 UDP 数据报 进行传输。因为UDP是不可靠传输,如果传输过程中发生损坏,就可以通过我们实现的TCP 来进行纠错检测等等操作,来保证数据的正确传输。