zoukankan      html  css  js  c++  java
  • Linux 系统编程学习笔记

    TCP/IP协议栈与数据包封装

    TCP/IP协议栈分为4层:应用层Application、传输层Transport、网络层Network、链路层Link。

    两台PC通过TCP/IP协议通讯过程示意图:

    每一层协议协议作为数据,到下一层协议都要加一个数据首部(header),称为封装(Encapsulation)。

    不同协议层的数据包有不同称呼,在传输层叫段segment,在网络层叫数据包datagram,在链路层叫帧frame。
    如果通讯的两台PC不在同一网段,那么数据从一台PC到另一台PC传输过程中要经过一个或多个路由器。

    • 物理层
      工作在链路层之下,还有物理层,指的是电信号的传递方式,如以太网通用网线(双绞线)、光纤等都工作在物理层。物理层决定了信号的高低电平、最大传输速率、传输距离、抗干扰性等。
      集线器(Hub)工作在物理层,用于双绞线的连接和信号中继(放大衰减信号,使之传得更远)。

    • 链路层
      包含了以太网、令牌环网等标准,负责网卡设备的驱动、帧同步(什么信号代表一帧的开始)、冲突检测(检测到冲突自动重发)、数据差错校验等工作。
      交换机工作在链路层,可以在不同的链路层网络之间转发数据帧(如十兆以太网和百兆以太网之间,以太网和令牌环网之间),因为不同链路层的帧格式不同,交换机要将进来的数据包拆掉链路层首部重新封装后再转发。

    • 网络层
      网络层IP协议是构成Internet基础,Internet上的主机通过IP地址来标识,Internet上有大量路由器负责根据IP地址选择合适的路径转发数据包,数据包从源主机到目的主机往往需要经过多个路由器。Ip协议不保证传输的可靠性,数据包在传输过程中可能丢失,可靠性由上层协议或应用程序中提供支持。
      路由器工作网络层,同时兼有交换机的功能,可以在不同的链路层接口之间转发数据包,因此路由器需要将进来的数据包拆掉链路层和网络层首部并重新封装。

    • 传输层
      网络层负责点到点(point-to-point)的传输,这里“点”指的是主机或路由器。传输层负责端到端(end-to-end)的传输,这里“端”指源主机和目的主机。传输层可选择TCP或UDP协议。
      TCP是一种面向连接的、可靠的协议,像打电话,双方通话之前需要先建立连接,一边说话另一边保证听得到,而且是按说话的顺序。同样地,TCP协议保证数据收发的可靠性,丢失的数据包自动重发,上层应用程序收到的总是可靠的数据流,通讯关闭之后连接。
      UDP协议不是面向连接的,也不保证可靠性,像邮寄信件,写好的信件放到邮筒里,不保证信件在邮递过程中不丢失,也不保证信件是按顺序寄到目的地的。使用UDP协议的应用程序需要自行完成丢包重发、消息排序等工作。

    Multiplexing多路复用
    目的主机收到数据包后,如何经过各层协议栈,最后达到应用程序?

    注意:

    1. IP、ARP、RARP数据报都需要以太网驱动程序封装成帧,但从功能上划分,ARP和RARP属于链路层,IP属于网络层;
    2. ICMP、IGMP、TCP、UDP的数据都需要IP协议进行封装成数据报,但从功能上划分,ICMP、IGMP属于网络层,TCP和UDP属于传输层。

    一帧的解析过程示例

    1. 数据链路层收到一帧数据包frame -> IP,ARP or PARP
      以太网驱动程序首先根据(以太网帧格式)首部的“上层协议”类型字段来确定该数据帧的payload(有效载荷),从而判断是IP、ARP or RARP协议的数据报。然后交给相应的协议去处理。
    2. IP数据包 -> TCP,UDP,ICMP or IGMP
      如果是IP数据包,IP协议再根据IP首部中的“上层协议”类型,来确定数据帧的payload是TCP,UDP,ICMP or IGMP,然后交给相应的协议处理。
      如果是TCP段或UDP段,TCP或UDP协议再根据TCP首部的“端口号”字段来确定将应用层的数据交给哪个用户进程。

    IP地址与端口号
    IP地址是标识网络中不同主机的地址,端口号是同一台主机上标识不同进程的地址。IP地址和端口号合起来标识网络中唯一的进程。

    以太网RFC 894帧格式

    字段 描述
    目的地址,源地地址 指网卡的硬件地址(MAC地址),48bit,全球唯一,网卡出厂固话。可以用ifconfig命令查看。
    类型 有3种值:0800、0806、0835分别对应IP、ARP、RARP。
    数据 数据长度最小46byte,最大1500byte,ARP/RARP数据段不够46byte字节,要在后面用填充位填充。最大值1500称为以太网的最大传输单元(MTU),不同网络类型有不同MTU。如果要传输的数据包 > MTU,则需要对数据包进行分片(fragmentation)。查看命令ifconfig。
    注意:MTU指的是帧数据段(payload)的最大长度,不包括帧首部。
    CRC 帧末尾,表示CRC校验码。

    ARP数据报格式

    网络通讯时,数据包首先被网卡接收到,再去处理上层协议,如果接收到的数据包MAC地址(硬件地址)与本机不符,则直接丢弃。而源主机APP知道目的主机IP地址和端口号port,但是不知道目的主机的MAC地址。那么源主机要如何知道目的主机的MAC地址呢?
    这就需要用到ARP协议。

    ARP协议工作过程
    源主机发出ARP请求,询问“IP地址是192.168.0.1的主机硬件地址是多少”,并将这个请求广播到本地网段(以太网帧首部硬件地址=FF:FF:FF:FF::FF:FF表示广播),目的主机接收到广播的ARP请求,发现其中的IP地址与本机相符,则发送一个ARP应答数据包给源主机,将自己的硬件地址填在应答包中。
    每台主机都维护一个ARP缓存表,命令arp- a查看。缓存表表项有过期时间(一般20分钟),如果20分钟内没有再次使用某个表项,则该表项失效,下次还要发ARP请求来获得目的主机的硬件地址。

    为什么要有过期时间,而不是一直有效呢?
    因为一直有效的话, ARP缓存表表项由于新加入的主机,可能会异常庞大,导致效率下降,而之前的主机可能已经不会再接入网络或者已失效,需要及时更新。另外,虽然MAC地址是固定的,但是IP地址是可修改的,如果不及时更新ARP映射表,可能会产生异常行为。
    当然,如果过期时间过短,就需要经常更新ARP映射表,频繁地更新ARP映射表会让网络都被ARP维护占用,导致网络资源浪费。

    ARP数据报格式

    字段 描述
    以太网目的地址,以太网源地址 也称为MAC目的地址,MAC源地址。在以太网首部和ARP请求各出现一次,对于链路层为以太网的情况是多余的,但链路层是其他类型的网络可能是是必要的。MCA目的地址=FF:FF:FF:FF:FF:FF:FF,表示广播,请求本地网段所有主机MAC地址。
    帧类型 值为0806,表示为ARP帧
    硬件类型 1 - 以太网
    协议类型 0x0800 - IP地址
    硬件地址长度 MAC地址长度,通常为6
    协议地址长度 IP地址长度,对于IPv4,该值为4
    op 1 - 表示为ARP请求;
    2 - 表示为ARP应答;
    发送端以太网地址 发送者的MAC地址
    发送端IP地址 发送者的IP地址
    目的以太网地址 对于ARP请求,一般全0填充
    目的IP地址 目的主机的IP地址

    例子

    ARP请求帧
    以太网首部(6+6+2 = 14byte)
    0000: ff ff ff ff ff ff 00 05 5d 61 58 a8 08 06 
    # 以太网首部:目的主机采用广播地址ff:ff:ff:ff:ff:ff
    # 源主机MAC地址:00:05:5d:61:58:a8
    # 帧类型:0x0806表示上层协议为ARP
    
    ARP帧(28byte)
    0000: 00 01
    0010: 08 00 06 04 00 01 00 05 5d 61 58 a8 c0 a8 00 37
    0020: 00 00 00 00 00 00 c0 a8 00 02
    填充位(46-28=18byte)
    0020: 00 77 31 d2 50 10
    0030: fd 78 41 d3 00 00 00 00 00 00 00 00
    # ARP帧:硬件类型=0x0001表示以太网
    # (上层)帧类型:0x0800表示IP协议
    # 硬件地址长度:6,MAC地址长度
    # 协议地址长度:4,IP地址长度
    # op:0x0001表示请求目的主机的MAC地址
    # 源主机MAC地址:00:05:5d:61:58:a8
    # 源主机IP地址:c0 a8 00 37,即192.168.0.55
    # 目的主机MAC地址:全0表示待填写
    # 目的主机IP地址:c0 a8 00 02,即192.168.0.2
    # 填充位:内容未定义,与具体实现相关
    
    ARP应答帧
    以太网首部
    0000: 00 05 5d 61 58 a8 00 05 5d a1 b8 40 08 06
    # 目的主机MAC地址:00:05:5d:61:58:a8
    # 源主机MAC地址:00:05:5d:a1:b8:40
    # (上层)帧类型:0x0806表示ARP
    
    ARP帧
    0000: 00 01
    0010: 08 00 06 04 00 02 00 05 5d a1 b8 40 c0 a8 00 02
    0020: 00 05 5d 61 58 a8 c0 a8 00 37
    填充位
    0020: 00 77 31 d2 50 10
    0030: fd 78 41 d3 00 00 00 00 00 00 00 00
    # 硬件类型:0x0001表示以太网
    # 协议类型:0x0800表示IP协议
    # 硬件地址长度:6
    # IP地址长度:4
    # op:0x0002表示ARP应答
    # 源主机MAC地址:00:05:5d:a1:b8:40
    # 源IP地址:c0 a8 00 02,即192.168.0.2
    # 目的主机MAC地址:00:05:5d:a1:b8:a8
    # 目的IP地址:c0 a8 00 37,即192.168.0.55
    

    问题:如果源主机和目的主机不在同一网段,ARP请求的广播帧无法穿过路由器,源主机如何与目的主机通信?
    可以通过中间节点(路由器)转发报文。

    IP数据报格式

    IP数据报的首部长度和数据长度都是可变的,但总是4byte的整数倍。

    字段 描述
    4位版本 首部长度的单位。对于IPv4,值为4,表示首部长度的数值的单位为4byte。4bit所表示最小值5,也就是说首部最小4x5=20byte;4bit最大值15,首部长度最大4x15=60byte
    4位首部长度 首部长度的数量。 首部长度 = value x unit(byte)
    8位服务类型TOS 3bit用来指定数据报的优先级(废弃);
    4bit表示可选服务类型(最小延迟、最大吞吐量、最大可靠性、最小成本);
    1bit总是0
    16位总长度 整个数据报的字节数,包括IP首部和IP层payload
    16位标识 毎传一个IP数据报,16位标识+1。可用于分片和重新组装数据
    3位标志 用于分片
    13位片偏移 用于分片
    8位生存时间(TTL) TTL,Time to live:源主机为数据包设定一个生存时间,如64,毎过1个路由器把该值-1,如果减到0表示路由已经太长了,仍然找不到目的主机的网络,就丢弃该包。因此生存时间单位不是秒,而是跳(hop)
    8位协议 指示上层协议是TCP、UDP、ICMP or IGMP
    16位首部校验和 只校验IP首部。数据的校验由更高层协议负责
    32位源IP地址 源主机IP地址,对IPv4是32bit
    32位目的IP地址 目的主机IP地址,对IPv4是32bit
    选项(如果有)
    数据

    问题:前面讲了以太网帧中的最小数据长度为46字节,不足46字节的要用填充字节补上,那么如何界定这46字节里前多少个字节是IP、ARP或RARP数据报而后面是填充字节?

    1. 根据帧首部的上层协议类型,来确定链路层帧的数据段是IP、ARP还是RARP协议数据包;
    2. 如果是IP数据报,可根据16bit总长度,来确定IP数据报的长度=总长度,需要填充 (46 - 总长度) > 0? (46 - 总长度) : 0;
      如果是ARP或RARP,请求和应答部分长度是固定28byte,需要填充46 - 28 = 18byte.

    IP地址与路由

    IPv4的IP地址长度4byte,通常采用点分十进制表示法(dotted decimal representation),如0xc0a80002表示为192.168.0.2。
    Internet被各种路由器和网关设备分割成很多网段,为了标识这些不同的网段,需要把32bit的IP地址划分成网络号 + 主机号。网络号相同的主机位于同一网段,相互可以直接通信;网络号不同的主机之间通信,则需要通过路由器转发。

    5类IP地址(旧的划分方法):

    IP地址范围
    A类:0.0.0.0 ~ 127.255.255.255
    B类:128.0.0.0 ~ 191.255.255.255
    C类:192.0.0.0 ~ 223.255.255.255
    D类:224.0.0.0 ~ 239.255.255.255
    E类:240.0.0.0 ~ 247.255.255.255

    新的划分方法:CIDR,子网掩码sub-net mask来决定网络号net-id、主机号pc-id的划分,而与IP地址(IP addr)是A类、B类,还是C类无关。
    即net-id = IP addr & sub-net mask
    例子:

    IP地址 140.252.20.68 8C FC 14 44
    子网掩码 255.255.255.0 FF FF FF 00
    网络号 140.252.20.0 8C FC 14 00
    子网地址范围 140.252.20.0 ~ 140.252.20.255
    IP地址 140.252.20.68 8C FC 14 44
    子网掩码 255.255.255.240 FF FF FF F0
    网络号 140.252.20.64 8C FC 14 40
    子网地址范围 140.252.20.64 ~ 140.252.20.79

    另一种IP地址、子网掩码的简洁表示法:140.252.20.68/24,表示IP addr = 140.252.20.6,sub-net mask的高24位是1,即255.255.255.0。
    注意:子网掩码对应bit为1的部分必须是从高位到低位,而且连续。

    私有IP地址
    RFC 1918规定了用于组建局域网的私有IP地址,不会出现在公网(Internet):

    • 10.*/8
    • 172.16.到172.31./12
    • 192.168.*/16

    私有IP地址可以通过代理服务器或NAT(网络地址转换)等技术连接到公网。

    特殊IP地址
    除了私有IP地址,还有几种特殊IP地址。

    • 127.*,通常是127.0.0.1用作本地环回(loop back)测试,数据包不会发到网络上,而是通过环回设备再发回给上层协议和应用程序,主要用于测试;

    不能作为主机IP地址的特殊地址

    • 目的地址255.255.255.255,表示本地网络内部广播,路由器不能转发这样的广播数据包;
    • 主机号全0的地址,表示网络而不能用于表示某个主机,如192.168.10.0/24;
    • 目的地址主机号全为1,表示广播至某个网络的所有主机,例如目的地址192.168.10.255/24表示广播至192.168.10.0的网络;

    路由
    路由(名词) - 数据包从源地址到目的地址所经过的路径,由一系列路由节点组成。

    路由(动词) - 某个路由节点为数据报选择投递方向的选路过程。

    路由节点 - 一个具有路由能力的主机或路由器,维护一张路由表,通过查询路由表来决定哪个接口发送数据包。

    接口 - 路由节点与某个网络相连的网卡接口。

    路由表 - 由很多条目组成,每个条目都指明去往某个网络的数据包应该由哪个接口发送,其中最后一条是缺省路由条目。

    路由条目 - 路由表中的一行,每个条目主要由目的网络地址、子网掩码、下一跳地址、发送接口,这四部分组成。如果要发送的数据包的目的网络地址匹配路由表中的某一行,就按规定的接口发送到下一跳地址。

    缺省路由条目 - 路由表中的最后一行,主要由下一跳地址和发送接口两部分组成,当目的地址与路由表中其他行都不匹配时,就按缺省路由条目规定的接口发送到下一跳地址。

    ifconfig查看ip地址信息,route查看路由信息

    从上图可知,
    这台PC有2个网络接口:192.168.10.0/24网络和192.168.56.0/24;
    Destination是目的网络地址;
    Genmask是子网掩码,Gateway是下一跳地址;
    Iface是发送接口,Flags中的U标志表示此条目有效,G表示此条目的下一跳地址是某个路由器的地址,没有G标志的条目表示目的网络地址与本机接口直接相连的网络,不必经由路由器转发,因此下一跳地址记为*。

    如果要发送数据包的目的IP地址 = 192.168.56.3,跟第一行子网掩码做与运算得到192.168.56.0,与第一行目的网络地址不相等。再跟第二行子网掩码按位与,得到192.168.56.0,与目的网络地址相等,因此从eth1发送出去。
    如果要发送的数据包的目的地址,跟前3行路由都不匹配,就按缺省路由条目,从eth0发送出去,让下一跳的路由器来决定其下一跳地址。

    UDP段格式

    UDP的目的端口号
    一般的网络通信双方分别是客户端和服务器,客户端主动发起请求,服务器被动等待、接收和应答请求。
    客户端的IP地址和端口号标识了该主机上的客户端进程,服务器IP地址和端口号唯一标识了该主机上的服务进程。由于客户端是主动发起请求的一方,它必须知道服务器IP地址和服务进程的端口号,因此一些常见的网络协议都有默认的服务器端口,例如HTTP服务默认TCP协议的80端口,FTP默认TCP的21端口,TFTP默认UDP的69端口,这些端口通常称为知名端口号(well-known port),范围0~1023。
    服务器的很多服务都是使用well-know端口号,但客户端不必是well-know的,往往是每次运行客户端程序时由系统自动分配一个空闲端口号,用完就释放掉,称为ehemeral的端口号,范围1024~5000。


    为什么客户端的端口号不必是well-known的,可以用完就释放掉?
    因为客户端是通信的请求方,可以将自己的端口号在请求时就告诉服务器,不必是well-known的。再加上客户端可能会打开多个进程,使用相同服务,而且可能随时关闭进程,如果都绑定well-known端口号,收到数据时,就无法知道是哪个进程发送的,这样容易产生混乱。


    如何查询知名端口号?
    /etc/services列出了所有well-known的服务端口和对应的传输层协议,由IANA规定。有些服务可以用TCP,也可以用UDP,IANA规定这样的服务采用相同的TCP或UDP默认端口号。

    UDP协议不面向连接,也不保证传输的可靠性,体现在:

    • 发送端UDP只管把应用传来的数据封装成段交给IP协议层,就算完成任务。如果因为网络故障,导致该段无法发送到对方,UDP不会返回给应用层任何错误信息;
    • 接收端UDP只管把收到的数据根据端口号交给相应的应用程序,就算完成任务。如果发送端发来多个数据包,并且在网络上经过不同的路由,达到接收端时顺序已经错乱,UDP协议层也不保证按发送时的顺序交给应用层。
    • 通常接收端的UDP将收到的数据放在一个固定大小的缓冲区中等待应用程序来提取和处理,如果应用程序提取和处理的速度很慢,而发送端发送的速度很快,就会失去数据包,UDP不会报告这种错误。

    通常,使用UDP的应用程序实现较简单,发送一些对可靠性要求不高的消息,不发送大量的数据。
    如,基于UDP的TFTP协议,用于传送小文件;而基于TCP的FTP协议,适用于各种文件传输。

    TCP协议

    TCP段格式

    通讯时序

    TCP连接建立(三次握手)、断开(四次挥手)

    上图示例中,双方发送的段按时间顺序编号为1~10,各段的主要信息在箭头上标出,如段2 “SYN, 8000(0), ACK 1001, < mss 1024>”,表示该段中的SYN位置1,32位序号8000,该段不携带有效载荷(payload自字节数=0),ACK位置1,32位确认序号1001,带有一个mss选项值为1024。

    建立连接过程:

    1. client 发段1:SYN, 1000(0), <mss 1460>
      SYN表示连接请求;1000(0)表示序号1000,是临时地址,每发1byte数据,序号+1,(0)表示发送数据字节数为0;mss表示最大段尺寸,封装成帧超过最大帧长就需要在IP层分片;
      SYN和FIN位占用1bit,虽然没有数据,但下次发送应该用序号1001;

    2. server发段2:SYN, 8000(0), ACK 1001, <mss 1024>
      SYN表示连接请求;8000(0)表示序号8000,(0)表示发送数据字节数0;ACK 1001表示应答,确认收到了1000(含1000)及之前序号的所有段,请求下次发送用序号1001;<mss 1024>表示最大段尺寸;

    3. client发段3:ACK 8001
      对server连接请求的应答,确认序号8001(下次发送应该用序号8001);

    关键点:client和server都分别给对方发起了连接请求,也应答了对方的连接请求,server的连接请求和应答在一个段中发出,因此共有3段用于建立连接,称为“三次握手”。

    TCP连接与端口号异常处理:如果一方收到另一方发来的段,读出目的端口号,发现本机没有任何进程使用该端口,就会应答一个包含RST位的段给另一方。如,server没有使用8080端口,client用telnet连接,server收到client发来的SYN段就会应答一个RST段,client的telnet程序收到RST段后报告Connection refuesed错误:

    $ telnet 192.168.0.200 8080Trying 192.168.0.200...telnet: Unable to connect to remote host: Connection refused
    

    数据传输的过程:

    1. client发段4:1001(20), ACK 8001
      clien发送序号从1001开始的20个byte 数据;确认收到序号为8000的段(server连接请求),并请求发送序号8001开始的数据;

    2. server发段5:8001(10), ACK 1021
      server发送序号从8001开始的10个byte数据;确认收到client发送的序号1001~1020的数据,同时请求发送序号1021开始的数据;

    3. client发段6:ACK 8011
      client确认收到序号8001~8010的数据,同时请求发送序号8011开始的数据;

    关闭连接的过程:

    1. client发段7:FIN, 1021(0), ACK 8011
      FIN表示请求关闭连接;

    2. server发段8:ACK 1022
      server应答client关闭连接请求,收到1021(含)之前序号对应段;

    3. server发段9:FIN, 8011(0), ACK 1022
      server发出段也包含FIN,向client请求关闭连接;FIN占用1个序号,但数据个数为0;

    4. client发段10:ACK 8012
      client应答server的关闭连接请求;

    关闭连接通常需要4个段,因为服务器的应答和关闭连接请求,通常不在一个段中。

    流量控制

    如果发送端发送速度较快,接收端来处理速度慢,而且接收缓冲区大小固定,就会丢失数据。TCP如何解决这个问题?
    TCP通过“滑动窗口(Sliding Window)”机制来解决该问题。

    例,滑动窗口示例

    1. 三次握手 建立双向连接
      (1)sender发段1:SYN, 0(0), win 4096, <mss 1460>,请求建立到receiver的连接,声明最大段尺寸1460,初始序号0,窗口大小4K(4096),表明“我的接收缓冲区还有4Kbyte空闲,你发的数据不要超过4K”;
      (2)receiver发段2:SYN, 8000(0), ACK 1, win 6144, <mss 1024>,确认sender的连接请求,并请求建立到sender的连接,声明最大段尺寸1024,初始序号8000,窗口大小6K(6144)。
      (3)sender发段3:ACK 8001, win 4096,确认receiver连接请求,请求发送序号8001开始的段,表明窗口大小4K;

    2. sender发送数据,直到receiver缓冲区满
      sender发段4~9,每个段含1K数据,共6K达到receiver窗口大小,停止发送数据。

    3. receiver提取数据,告诉sender新窗口大小
      receiver提走2K数据,接收缓冲区有2K空闲,receiver发出段10,同时应答已收到6K数据,同时声明新窗口大小2K。

    4. receiver提取数据,告诉sender新窗口大小
      receiver又提走2K数据,接收缓冲区有4K空闲,receiver发出段11,同时应答已收到6K数据,同时声明新窗口大小4K。

    四次挥手:段13、14、17、18
    5. sender发送数据完毕后,停止发送,请求断开连接
    sender发送端12~13,每个段1K数据;段13还带有FIN,请求断开sender到reciver的连接。

    1. receiver响应断开连接请求,处理完接收数据后,请求断开到sender的连接
      receiver发送段14~16,确认已经收到sender的数据,更新了3次窗口大小;
      receiver发送段17,包含FIN,请求断开到receiver的连接请求;

    2. sender响应断开连接请求
      sender发送段18,响应断开到sender的连接请求,至此连接完全关闭;

    为什么UDP是面向消息的,TCP是面向字节流的?
    因为每个UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据;
    TCP应用程序一次提走多个字节的数据,可能1K,可能3K、6K等,而底层通讯中这些数据可能被拆分成很多数据包,但是一个数据包有多少字节对应用程序是不可见的,因此TCP是面向字节流的协议。

    参考

    《linux C编程一站式学习》

  • 相关阅读:
    人生转折点:弃文从理
    人生第一站:大三暑假实习僧
    监听器启动顺序和java常见注解
    java常识和好玩的注释
    182. Duplicate Emails (Easy)
    181. Employees Earning More Than Their Managers (Easy)
    180. Consecutive Numbers (Medium)
    178. Rank Scores (Medium)
    177. Nth Highest Salary (Medium)
    176. Second Highest Salary(Easy)
  • 原文地址:https://www.cnblogs.com/fortunely/p/14619969.html
Copyright © 2011-2022 走看看