背景知识

OSI 七层模型 (Open Systems Interconnection model)


  • 物理层(Physical)
  • 真实世界的物理连接.
    规范 以太网线缆(Ethernet cable) 的电压, 连接器件的针脚, Wi-Fi 的无线电频率, 光纤里的光.

  • 数据链路层(Data link)
  • 处理两个节点之间直连通信的协议.
    定义节点间直连消息的开头和结尾(framing, 帧化), 检错和纠错, 流量控制.

  • 网络层(Network layer)
  • 为从属于不同网络的节点之间传输数据序列(packet, 包) 提供方法.
    此时, 包从一个节点传输到另一个节点( 节点间不存在直连物理连接), 需要穿过若干中继节点, 为此过程提供路由.
    IP(Internet Protocol, 网际协议) 于此层定义.

  • 传输层(Transport layer)
  • 提供端到端(between hosts, 主机间), 参差数据(variable length, 变长), 可靠传输的方法.
    处理诸如 分割数据, 重组数据, 保证数据有序到达 之类的问题.
    TCP(Transimission Control Protocol, 传输控制协议), UDP(User Datagram Protocol, 用户数据报协议) 于此层定义.

  • 会话层(Session layer)
  • 提供方法, 建立/ 检查点(checkpoint)/ 挂起(suspend)/ 恢复/ 终止 对话(dialogs).

  • 表示层(Presentation layer)
  • 涉及定义应用(application) 的数据结构和表示.
    关注数据编码, 序列化, 加密.

  • 应用层(Application layer)
  • 带用户界面(user interface) 的应用存在于此层, 如 web 浏览器, email 客户端.
    这些应用利用底层提供的各种服务.

注意


经常用不同的名字分别指代不同层的数据块.
2 层数据单元被称为帧(frame), 3 层数据单元被称为包(packet).
对于 4 层, TCP 连接的数据单元被称为分片(segment, 段), UDP 消息的数据单元被称为数据报(datagram).
包(packet) 经常被用作所有数据单元的通称.
分片(segment) 和 数据报(datagram) 则是特指.

TCP/IP 四层模型


  • 网络接入层 (Network access layer)
  • 关注 物理连接 和 数据装帧(data framing), 如 发送 以太网包(Ethernet packet) 和 无线网包(Wi-Fi packet).

  • 网络互联层 (Internet layer)
  • 关注 多个网络互联的环境下, 包的 寻址(addressing) 和 路由(routing).
    IP 地址于此层定义.

  • 端到端层 (Host-to-Host layer)
  • 提供两种协议, TCP 和 UDP.
    关注 数据顺序(data order), 数据分片(data segmentation), 网络拥塞(network congestion), 和 纠错(error correction).

  • 进程/应用 层(Process/Application layer)
  • 实现了诸如 HTTP, SMTP, FTP 之类的协议.

对比 OSI 和 TCP/IP


对比 OST 模型 和 TCP/IP 模型

TCP/IP 模型 和 OSI 模型的层与层之间并不是严格对应的.
他们实现的功能都相同, 只是划分方式不同.
OSI 模型更有助于理解 组网(networking) 都关注些什么, TCP/IP 模型更反映真实世界中的实现.

数据封装(Data encapsulation)


做这些抽象的意义(advantage, 优势) 在于, 在编写应用时, 只需考虑高层协议.
举例来说, web 浏览器只需实现与网站(website) 相关的那些协议, 如 HTTP, HTML, CSS 之类.
无需困扰怎么实现 TCP/IP, 更无需了解以太网包或 Wi-Fi 包是如何编码的.
这些工作只需依赖底层已有的实现即可, 由操作系统提供.

通过网络进行通信时, 数据先自上而下经过发送者每一层处理, 再自下而上经过接收者每一层处理.
服务器想要向接收者发送一段文本(text), 这段文本想要被接收者正确的显示出来(be rendered, 被渲染), 必须被编码进 HTML 结构.
HTML 也不能直接传送, 必须添加相应的 HTTP 回应头部(HTTP response header), 作为 HTTP 回应的一部分才能被传送.
HTTP 作为 TCP 会话的一部分被传送, 这一操作由操作系统 TCP/IP 协议栈完成.
TCP 包被封装成 IP 包( 路由), IP 包被封装成以太网包, 然后经过线路传送.

IPv4 和 IPv6


IPv4 使用 32 位地址, 总共有约 43 亿个地址(2 的 32 次方).
IPv6 使用 128 位地址, 总共有约 3.4 × (10 的 38 次方) 个地址.
今天, 大部分 桌面系统 和 智能手机系统 通过一种称为" 双栈配置(dual-stack)" 的方式, 同时支持 IPv4 和 IPv6.
部署网络程序仍然要支持 IPv4, 否则有些用户可能无法连接.
但更要支持 IPv6, 帮助世界从 IPv4 过渡到 IPv6.

IPv4 地址的例子:

  • 127.0.0.1
  • 回环地址(loopback), 和自己建立一个连接, 发往该地址的包会被操作系统限制在本地, 禁止进入网络.
    可以用于测试目的.

  • 10.0.0.0 到 10.255.255.255
  • 172.16.0.0 到 172.31.255.255
  • 192.168.0.0 到 192.168.255.255
  • 三类保留的私有地址, 家庭和组织使用这些地址组建局域网(Local Area Networks, LANs).
    局域网内的设备之间, 直接使用各自的局域网私有地址寻址.
    发往 互联网(Internet) 的流量, 需要经过路由器进行地址翻译(Network Address Translation, NAT).
    路由器会把这些流量的源 IP 地址保存到内存, 并把流量的源地址从私有 IP 地址改写成路由器自己的公开 IP 地址.
    当路由器收到从互联网返回的流量, 就查看内存, 把目的 IP 地址从自己的公开 IP 地址改回发送者的私有 IP 地址.

  • 10.0.0.0/8
  • 172.16.0.0/12
  • 192.168.0.0/16
  • CIDR(Classless Inter-Domain Routing) 记法, 地址范围的简写.
    10.0.0.0/8, 表明 32 位地址中, 前 8 位固定, 后 24 位可以是任意值(每一位取 0 或 1), 这些地址都从属于 10.0.0.0/8 地址块.
    因此, 10.0.0.0/8 涵盖从 10.0.0.0 至 10.255.255.255 的地址范围.

IPv6 地址的例子:

  • 0000:0000:0000:0000:0000:0000:0000:0001
  • 回环地址, 可以简写成 ::1.

  • 2001:0db8:0000:0000:0000:ff00:0042:8329
  • 可以简写成 2001:db8::ff00:42:8329.

  • fe80:0000:0000:0000:75f4:ac69:5fa7:67f9
  • 可以简写成 fe80::75f4:ac69:5fa7:67f9.

  • fe80:0000:0000:0000:75f4:0000:5fa7:67f9
  • 可以简写成 fe80::75f4:0:5fa7:67f9 或 fe80:0:0:0:75f4::5fa7:67f9.

  • ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff

本地链接地址(link-local): 在指定 IP 地址之前, 有些自动配置函数(auto-configuration) 就可能会用到 IP 地址, 供这些函数使用.
IPv4 使用 169.254.0.0/8 地址块, IPv6 使用 fe80::/10 地址块.

IPv6 不仅仅是扩展了 IPv4 的地址范围.
IPv6 地址有新属性, 如 域(scope), 生存期(lifetime), 而且, IPv6 的网络接口经常会绑定多个 IPv6 地址.
IPv6 的使用和管理都不同于 IPv4.

多播, 广播, 任意播(Multicast, broadcast, anycast)


包从一个发送者路由到单一的接收者, 使用 单播寻址(unicast addressing) 方式.
广播寻址(broadcast addressing) 允许一个发送者同时寻址所有的接收者, 可以用于想要向子网的每个接收者都发送包的情况.
如果把广播形容为 一对所有(one-to-all), 多播就是 一对多(one-to-many).
多播(multicast, 组播) 包含对组的管理, 消息通过寻址, 传送给一个组的多个成员.
发送一个消息, 又不关心具体的接收者是谁, 这时, 包寻址可以采用 任意播(anycast) 的方式.
假设存在多台服务器提供相同的功能, 如果只是简单的希望有服务器处理我们的请求, 而具体哪台则无所谓, 此时就可以选择任意播寻址.

IPv4 和底层网络支持本地广播.
IPv4 对 多播 提供部分支持, IPv6 对多播的支持相比 IPv4 更多一些.
IPv6 不考虑广播, 如果必要, 其多播基本上可以模拟广播.
在 IP 层, 采取多播的方式发送, 相比起将相同的消息通过单播的方式多次发送, 前者更节约带宽.
尽管如此, 应用层才是经常使用多播的那一层.
当一个应用想要向多个接收者传递相同的消息, 采用的是每次只向一个接收者发送, 发送多次的方式, 并没有利用 IP 层多播的优势.

端口号(Port number)


通过 IP 地址, 可以将 包 路由到一个特定的系统.
通过端口号, 可以将 包 路由到系统的一个应用.
系统可能正运行着 web 浏览器, email 客户端, 视频会议客户端等.
现在, 收到一个 TCP 分片, 系统查看其目的端口号, 这个端口号决定应该由哪个应用去处理该分片.
端口号保存为 16 位的无符号整数, 意味着其范围是 0 到 65535.

端口号连接类型协议
20, 21TCPFile Transfer Protocol(FTP)
22TCPSecure Shell(SSH)
23TCPTelnet
25TCPSimple Mail Transfer Protocol(SMTP)
53UDPDomain Name System(DNS)
80TCPHypertext Transfer Protocol(HTTP)
110TCPPost Office Protocol, Version 3(POP3)
143TCPInternet Message Access Protocol(IMAP)
194TCPInternet Relay Chat(IRC)
443TCPHTTP over TLS/SSL(HTTPS)
993TCPIMAP over TLS/SSL(IMAPS)
995TCPPOP3 over TLS/SSL(POP3S)

套接字(Socket)


套接字是系统之间的通信链路的一个端点, 是 应用 通过网络收发数据的一种抽象.
一个开放的套接字可以由以下 5 元(5-tuple) 唯一的定义:
本地 IP 地址, 本地端口, 远端 IP 地址, 远端端口, 协议 (TCP 或 UDP).

使用 socket APIs 进行网络编程, 会自动处理各种底层细节.

列出主机的 IP 地址(Linux)


  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. #include <sys/socket.h>
  4. #include <netdb.h>
  5. #include <ifaddrs.h>
  6. int main(int argc, char ** argv) {
  7. struct ifaddrs * addresses;
  8. if (getifaddrs(&addresses) == -1) {
  9. fprintf(stderr, "getifaddrs() failed\n");
  10. return EXIT_FAILURE;
  11. }
  12. struct ifaddrs * p;
  13. int family;
  14. char ip_addr[100];
  15. for (p = addresses; p != NULL; p = p->ifa_next) {
  16. family = p->ifa_addr->sa_family;
  17. if (family == AF_INET) {
  18. getnameinfo(
  19. p->ifa_addr, sizeof(struct sockaddr_in),
  20. ip_addr, sizeof(ip_addr), 0, 0, NI_NUMERICHOST
  21. );
  22. printf("%s\t%s\t%s\n", p->ifa_name, "IPv4", ip_addr);
  23. continue;
  24. }
  25. if (family == AF_INET6) {
  26. getnameinfo(
  27. p->ifa_addr, sizeof(struct sockaddr_in6),
  28. ip_addr, sizeof(ip_addr), 0, 0, NI_NUMERICHOST
  29. );
  30. printf("%s\t%s\t%s\n", p->ifa_name, "IPv6", ip_addr);
  31. }
  32. }
  33. freeifaddrs(addresses);
  34. return EXIT_SUCCESS;
  35. }

《C 语言网络编程实践》 (《Hands-On Network Programming With C》)