- CS144lab1-7 自顶向下 实现了HTTP(GET) , TCP , IP(util) ,ARP(NetworkInterface) , Ethernet(util)(NetworkInterface use) , 最后在用户态组成了一个可靠数据传输的socket
- FullStackSocket概述
- 2 thread (main thread , tcp thread)
- 3 fd(user socket , 内置的_thread_data(与协议栈交互) , tap fd)
- eventloop注册事件
- FullStackSocket概述
- 看完FullStackSocket之后 重新体会到socket是站在用户层和传输层之间的这句话了
- FullStackSocket通过一个socketpair 将user看到的Socket和实际与协议栈交互的Socket(_thread_data)分开. main thread 使用 _socket , tcp_thread使用_thread_data
- user只需要站在user层拿着_socket就好 等待数据到来 , 交付数据即可。背后协议栈的处理以及网卡数据的读写以及_thread_data交付数据都不需要管,都由tcp_thread的eventloop负责监听和处理了。user甚至可以认为 他就是直接通过这个_socket去和外界交互的.
FullStackSocket
- 镇楼
outbound buffer和inbound buffer就是俩bytestream
3Fd
- FullStackSocket中总共有3个Fd
- _socket fd : user see . (main thread)
- user认为就是这个fd和internet交互,实际上是在和_thread_data进行交互 , 最终和internet交互的是网卡TAP fd
- _thread_data fd : 和_socket交互 (TCP thread)
- TAP fd : 和外界交互,接收发送frame (TCP thread)
- _socket fd : user see . (main thread)
pipe(socketpair)
_socket和_thread_data是socketpair. 通过pipe通信
- tcp_thread对_thread_data进行操作
- main thread 对_socket进行操作.
- 避免了对同一socket复杂的线程通信操作.
- 感觉这就是所谓的”通过通信共享内存,而不是通过共享内存通信.”;
- 通过pipe , tcp_thread和main_thread“共享”(通信)了user要发送和下层读取到的data
main thread对_socket的操作
- main thread对_socket进行write , 将data送入pipe之后即可返回
- main thread对_socket进行read(block) , 从pipe中读到数据后即可返回
tcp thread对_thread_data的操作
- TCP thread负责监听_thread_data读事件,并读出_thread_data数据并送入TCP/IP协议栈处理
- TCP thread负责监听_thread_data写事件,并将data从_thread_data写入,送给pipe的另一端_socket
TAP Fd
- 除此之外,tcp thread还需要对TAP Fd进行操作
- 监听TAP Fd写事件,并处理
- 监听TAP Fd读事件,并处理
- TAP device接收上层构造好的链路层帧(link-layer frames)并直接发送出去
2 thread
- FullStackSocket中总共有2个thread
- main thread (user)
- main thread对_socket进行write , 将data送入pipe之后即可返回
- main thread对_socket进行read(block) , 从pipe中读到数据后即可返回
- TCP thread
- 一言以蔽之:
- 负责 和上层main thread的_socket交互.
- TCP/IP协议栈处理数据
- 以及读写网卡
- 有个很重要的组件 : eventloop. 下文介绍.
- 一言以蔽之:
- main thread (user)
eventloop (tcp thread)
eventloop
好像到处都有eventloop哈,libsponge有,muduo有,redis有。是个reactor就有。
大概来说如下
1
2
3
4
5while(condition)
{
poll();
handleEvents();
}- 不过只能说类似,因为我们没有listenfd.只有connfd
- 不过只能说类似,因为我们没有listenfd.只有connfd
eventloop监听事件如下
- EventA : adapter的读事件. 即TAP Fd 接收到frame
- Handler : network interface 读取frame并交付给上层协议栈
- 注册条件 : 只有local tcp存活时 才监听并处理该事件
- EventB : _thread_data的读事件. 即_thread_data接收到了_socket写入pipe的数据
- Handler : tcp sender 读取_thread_data的数据并发送
- 注册条件 : 在local tcp存活 && 写不关闭 && local tcp outbound_buffer仍有空闲空间时 可监听并处理此事件
- EventC : _thread_data的写事件.
- Handler : _thread_data从inbound buffer中读出数据,写入pipe
- 注册条件 : tcp inbound buffer有数据可读出写入_thread_data
- EventD : TAP Fd可写.
- Handler : datagram adapter读取TCPsender发送的segment。层层封装成frame 写给TAP Fd
- 注册条件 : tcp outbound buffer有数据可读出送入协议栈
- EventA : adapter的读事件. 即TAP Fd 接收到frame
关于写事件的注册
背景: 如何正确的在epoll/poll LT模式下正确的执行写操作.
- 易知poll为水平触发 , 在LT模式下如果注册了某fd的写事件到epoll上,那么只要fd背后的pipe亦或者kernel buffer没被填满,就会触发该事件,基本上一定会陷入死循环(busy wait)(loop刚刚回到poll就再次触发事件)。
如何正确执行写操作 -> 如何正确的在poll上注册写事件,使得写事件可以在有数据写的时候发生,没有的时候就不发生?而不会频繁的触发写事件陷入busy wait ?
- 解决方案很简单如下:
- 设置一个 注册写事件的先决条件(code中的interest).
- 如先决条件为有数据可写,那么如下
- 也即 只有在有数据时才注册写事件到poll
- 并且 在不满足该条件时,就立刻将该写事件从poll上拿下来.
- 如
1
2
3Event C interest
// interest : 如果tcp的outbound buffer有数据要可发 才注册 ; 不符合该条件时立刻移除
[&] { return not _tcp->segments_out().empty(); });1
2
3
4
5
6Event D interest
[&] {
// tcp的inbound buffer不空(有数据可交付给上层app) || tcp的inbound buffer读到eof亦或者出错error (那就可以返回给上层eof或是error)
return (not _tcp->inbound_stream().buffer_empty()) or
((_tcp->inbound_stream().eof() or _tcp->inbound_stream().error()) and not _inbound_shutdown);
});
- 解决方案很简单如下:
之前的一些问题
使用
1
2
3
4
5
6
7
8
9
10
11
12
13// des addr : ip and port
Address addr(host,"http"); // getaddrinfo : host -> ip ; http -> port
// connect
FullStackSocket tcp_socket;
tcp_socket.connect(addr);
// send req
string request("GET " + path + " HTTP/1.1\r\n" + "Host: " + host + "\r\n" + "Connection: close\r\n" + "\r\n");
tcp_socket.write(request);
while(!tcp_socket.eof())
{
cout<<tcp_socket.read();
}
tcp_socket.wait_until_closed();wait_until_closed()
- 此时peer已经停止写(receiver 接收fin)
- 于是我们 _socket 关闭读写, 致使 _thread_data关闭读 , 通过之前注册的回调 , 致使tcpconnection关闭写 , 于是local tcp sender发送fin给peer. 完成四次挥手. clean shutdown
1
2
3
4
5
6
7
8
9
10template <typename AdaptT>
void TCPSpongeSocket<AdaptT>::wait_until_closed() {
// 关闭user看到的_socketfd的读写
shutdown(SHUT_RDWR);
if (_tcp_thread.joinable()) {
cerr << "DEBUG: Waiting for clean shutdown... "; //
_tcp_thread.join(); // 等待tcp thread结束
cerr << "done.\n";
}
}
port
- port是传输层的,可是我们没在lab4的tcpconnection中填充,他在什么时候填充的 ?
- tcpConnection 填充了 seq num , ack num , window , data , ack , rst , syn , fin. urg和psh没用到
- tcpsegment自己计算了dataoffset , checksum
- 在wrap_tcp_in_ip中填充的
本地IP和本地Port在什么时候告知Socket?: connect时随机生成port ; peer的IP和Port呢?: user传入. 见connect.
port在什么时候起到作用 ? 怎么用于区分segment是不是给我们的socket的?
- tcp协议的端口只是逻辑上的端口,并没有实际物理意义,没有对应设备 只是个逻辑上的 用于区分socket罢了. 其范围0到65535是受报文中给port的空间限制的 .
- 在网络技术中,端口(Port)大致有两种意思:一是物理意义上的端口,比如,ADSL Modem、集线器、交换机、路由器用于连接其他网络设备的接口,如RJ-45端口、SC端口等等;二是逻辑意义上的端口,一般是指TCP/IP协议中的端口,端口号的范围从,比如用于浏览网页服务的80端口,用于FTP服务的21端口等等。
关于IP分片
- 可以看到 util以及libsonge中并没有IP分片的代码. 为什么 ?
- 因为我们只实现了TCP/IP协议栈. 并没有实现UDP/IP .
- UDP才会在IP分片,TCP不会在IP分片. 原因如下
- TCP会进行分段,临界为称为(Max Segment Size). MSS
- 放入TCP Segment的最大应用层报文段大小.
- MSS的大小一般都是参考了MTU. 如MTU = 1500bytes , 则 MSS = 1500 - IP Header - TCP Header = 1460 Bytes
- IP层分片的临界是(Max Transimssion Unit)MTU.
- MTU是由数据链路层决定的. 如Ethernet II的MTU就是1500 bytes
- MTU = IP Header + TCP Header + MSS
- 易知一个IP datagram里面封装一个 TCP Segment. 又由于 MSS < MTU , 故TCP分段避免了IP分片.
- TCP MSS目的 : 为了让IP层少分包或是不分包 ,因为IP分片丢失会导致TCP重传所有分片组成的那个Segment.
- 因为 IP 层本身没有超时重传机制,它由传输层的 TCP 来负责超时和重传。
- 当某一个 IP 分片丢失后,接收方的 IP 层就无法组装成一个完整的 TCP 报文(头部 + 数据),也就无法将数据报文送到 TCP 层,所以接收方不会响应 ACK 给发送方,因为发送方迟迟收不到 ACK 确认报文,所以会触发超时重传,就会重发「整个 TCP 报文(头部 + 数据)」(该TCPSegment大于MTU)。
- 因此,IP 层进行分片,如果丢失某个分片,则重传效率很低
listening fd
- 与kernel实现不同. Kernel里面,对于server,会有一个listening socket(与peer进行三次握手建立连接)。然后为到来的建立连接请求创建connection socket. 去和client的connfd进行通信.
- libsponge的实现没有listening socket,仅仅就是一个connection socket。这个connection去和client的connfd进行三次握手、通信以及四次挥手.
PS : eventloop相关code
- tcp_spong_socket.cc
收获
- 通过CS144 lab0-7
- 掌握了如何实现基本的TCP协议(TCPSender TCPReceiver) , 更加理解了TCP协议是如何应对 对于两军问题导致的双方难以达成clean shutdown的情况 , 并对TCP/IP协议栈(TCP IP ARP Ethernet)以及路由(Router)有了更深刻的理解.
- 学会了c++14的一些新特性,如optional , string_view
- 学习FullStackSocket的设计 : 事件交给worker thread的eventloop来做 , user 在另一个thread等待 , 二者通过pipe传输.