不落辰

知不可乎骤得,托遗响于悲风

0%

计算机网络-CS144-Socket及Lab总结

  • 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之后 重新体会到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)

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. 下文介绍.

eventloop (tcp thread)

eventloop

  • 好像到处都有eventloop哈,libsponge有,muduo有,redis有。是个reactor就有。

  • 大概来说如下

    1
    2
    3
    4
    5
    while(condition)
    {
    poll();
    handleEvents();
    }
    • 不过只能说类似,因为我们没有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有数据可读出送入协议栈

关于写事件的注册

  • 背景: 如何正确的在epoll/poll LT模式下正确的执行写操作.

    • 易知poll为水平触发 , 在LT模式下如果注册了某fd的写事件到epoll上,那么只要fd背后的pipe亦或者kernel buffer没被填满,就会触发该事件,基本上一定会陷入死循环(busy wait)(loop刚刚回到poll就再次触发事件)。
  • 如何正确执行写操作 -> 如何正确的在poll上注册写事件,使得写事件可以在有数据写的时候发生,没有的时候就不发生?而不会频繁的触发写事件陷入busy wait ?

    • 解决方案很简单如下:
      • 设置一个 注册写事件的先决条件(code中的interest).
      • 如先决条件为有数据可写,那么如下
      • 也即 只有在有数据时才注册写事件到poll
      • 并且 在不满足该条件时,就立刻将该写事件从poll上拿下来.
    • 1
      2
      3
      Event C interest
      // interest : 如果tcp的outbound buffer有数据要可发 才注册 ; 不符合该条件时立刻移除
      [&] { return not _tcp->segments_out().empty(); });
      1
      2
      3
      4
      5
      6
      Event 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
      10
      template <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传输.