传输层¶
- 面向连接的服务(connection-oriented service)就是按照电话系统建模的
- 面向无连接的服务(conncetionless service)则是按照邮政系统建模的
Socket¶
参考:
Socket 套接字,起初是由加州大学伯克利分校在 UNIX 的 BSD 分支提出的,所以也被叫做伯克利套接字,本身不是协议,只规定了 API,屏蔽了底层协议栈的差别,抽象出来的一层,可以理解为网络插槽。
- TCP 套接字通常叫做字节流套接字(SOCK_STREAM)
- UDP 套接字通常叫做数据报套接字(SOCK_DGRAM
Socket 本质上就是个代码库,包含 socket()
、connect()
、bind()
等方法,这就是纯裸的 TCP 实现
首先服务端初始化 socket,执行 bind 函数,将自己的服务能力绑定在一个众所周知的地址和端口上,紧接着,服务器端执行 listen 操作,将原先的 socket 转化为服务端的 socket,服务端最后阻塞在 accept 上等待客户端请求的到来。
然后客户端需要先初始化 socket,再执行 connect 向服务器端的地址和端口发起连接请求,客户端可以通过 DNS 服务解析出 URL 对应的地址和端口,即服务发现。
三次握手后客户端和服务器端建立连接,就进入了数据传输过程,可以进行 HTTP 请求传输数据等,然后需要和服务器端断开连接时,执行 close 函数。
传输方式¶
TCP 是一种基于字节流的协议,字节流是一种抽象,表示一系列连续的、没有明确边界的字节。
应用程序可以随时发送任意长度的数据,TCP 将其视为一个连续的字节流,并负责将这些数据分成适合传输的段。
数据传输单元¶
消息在进入每一层时都会多加一个报头,报头上面记录着消息从哪来,到哪去,以及消息多长等信息
- MTU, Maximum Transmission Unit 最大传输单元
- MSS, Maximum Segment Size 最大段长度
每一种链路层协议都规定了帧能传输的最大数据长度「MTU」,以太网的 MTU 一般默认为 1500 Bytes。
MTU = MSS + TCP首部(20B) + IP首部(20B) + 可选部分
如果来自传输层的报文超过 MTU,IP 层则需要分段传输给 MAC 层。所以需要设置一个合适的段长度「MSS」,过大会导致 IP 分片传输,过小则网络利用率太低。
传输标识¶
URI¶
Uniform Resource Identifier 统一资源标识符,是一种抽象的定义,只要能唯一标识资源的都可以叫 URI
- URN(Uniform Resource Name) 统一资源名称,通过名称定位标识资源,比如书籍的 isbn 号
- URL(Uniform Resource Locator) 统一资源定位符,通过地址定位标识资源,最常用的就是网址
URL 组成:scheme:subscheme://username:password@host:port/path?query#fragment
- scheme 协议,比如:https, ftp 等
- subscheme 子协议,用于区分数据库
比如:jdbc:mysql://localhost:3306/...
- host 域名 或 IP
格式:<四级域名.三级域名.二级域名.顶级域名>,比如
- port 服务端端口号
服务端端口通常是固定的,SMTP 默认 25,HTTP 默认 80,HTTPS 默认 443,Django 默认 8000
- resources 资源
- path 带层次的文件路径,
/虚拟目录/文件名
?query
查询字符串,传入参数#fragment
片段标识符,比如定位文档内具体位置
- path 带层次的文件路径,
资源就是网络上的一个实体,可以有不同的表现形式,称之为表现层。它可以是文本(TXT、XML、JSON、HTML)、图片(PNG、JPG)、音频、视频、服务等等
可以在请求头中使用 Accept
和 Content-Type
字段指定表现形式以及版本号等
端口号¶
每个连接使用不同的端口来区分不同的服务和客户,服务端的端口是固定的,客户端是系统内核临时分配的,每个客户端每个连接的临时端口号都是不同的,确保数据不会混淆。
IP 和 Port 组成一个四元组,也叫套接字对:(clientaddr:clientport, serveraddr: serverport)
IP 地址¶
分配而来
类似于办公地址,有定位功能
localhost
不是 IP,而是域名,用于指代当前 host 或 computer,通常被指向 IPv4 的 127.0.0.1
和 IPv6 的 ::1
127.0.0.1
凡是以 127 开头的地址,都属于回环地址(loop back address),即发送给此地址的请求会被自己接收,外部设备无法访问
127.0.0.1 只是回环地址中的一个,可用于测试网络是否正常:ping 127.0.0.1
如果遇到 DDos 攻击,可以将域名 A 记录解析到 127.0.0.1,让其攻击自己
0.0.0.0
并不是一个真实的的 IP 地址,无意义或无效的地址
通常用于不便使用具体地址的特殊情况,可表示本机中所有的 IPv4 地址
Mac 地址¶
每块网卡出厂时都有的唯一标识,类似于 身份证号,无定位功能
地址解析协议
- ARP:已知 IP 求 MAC
- RARP:已知 MAC 求 IP
DNS¶
Domain Name System 域名系统,是域名与 IP 地址相互映射的一个分布式数据库,因为 IP 地址不方便记忆,使用域名来映射它,比如 localhost
对应的是 127.0.0.1
域名地址有不同的记录类型,每种类型可以包含多条记录。
A, Address Record 将域名映射到一个IPv4地址,最常用
CNAME, Canonical Name 将域名指向另一个域名,而不是 IP,根域名不能设置为 CNAME
MX, Mail Exchange Record 指向处理邮件的服务器地址
浏览器通过 URL 解析出域名后
- 首先会查询本地 DNS 缓存是否有该域名的地址记录
先查看浏览器自身的 DNS 缓存
没有再查找操作系统的 DNS 缓存文件 /etc/hosts
本地缓存通常包含以下信息
域名
IP地址
TTL(Time to Live)有效期限
- 本地如果没有就会查询 DHCP 下发的 DNS 服务器
通常由 DHCP 服务器(路由器)自动分配,或者在拨号时从 PPP 服务器中自动获取,也可以用户手动指定
其实就是 DNS 服务器的缓存
- DNS 服务器进行递归查询:根域名服务器、顶级域名服务器、权威域名服务器
权威域名服务器根据访问者所处的不同地区(如华北、华南、东北等)、不同服务商(如电信、移动、联通等)等因素智能选择出某种记录类型对应的最合适的记录,返回给客户端
如果域名服务器挂了,或域名服务器的 IP 填错了,或请求被 GFW 拦截了,最终都会没有响应然后报错
基于 UDP 的 DNS 分级查询意味着每一级都有可能受到中间人攻击的威胁,比如域名劫持、域名污染/欺骗
可以使用安全的DNS协议来解决,比如 DoH(DNS over HTTPS)
交换¶
电路交换¶
电路交换主要用于语音传输,性能更高
所有的电话两两相连是不现实的,而是先通过低速线路连接到电话交换局的电话交换机,然后电话交换局通过高速线路连接到长途局,这些线路和交换机设备组成了一个个子网,最后形成电信网,由电话公司负责管理,而电话本身并不属于子网。
早期使用接线员作为人肉交换机,后来改为了自动交换机,但原理都一样,拨号,然后交换机自动选择一条线路,这条线路被称之为虚电路(VC, Virtual Circuit),虚线路建立后,对方电话铃响,开是通话,只要通话不断,则相应线路会被一直占用
报文交换¶
需要存储全部的报文才会转发,比如以前的电报
存储转发(Store and forward)建模自邮政系统,也类似于物流系统,即信件/货物/信息被发送到中间站,查找转发表继续转发到下一站
分组交换¶
也称数据包交换,或封包交换,不需要建立通道,健壮性更好
报文被分割为一个个分组,每个分组都是一个数据包,由首部和数据两部分组成
路由器存储转发时,不需要等待完整的报文,接收方收到所有数据后再排序组装
UDP¶
UDP 跟 IP一样,传输形式都是包,它只管转发数据,不需要建立连接,并不关心数据是否被送达,所以也不会关心数据被送达的顺序等等,可以一对多。
UDP 可以用在环境简单、需要多播,以及应用层自己控制传输的场景:
- DHCP广播获取IP
- VXLAN
- QUIC(Quick UDP Internet Connections,快速 UDP 互联网连接)(由Google 提出)
- 实时游戏
- 很多直播应用,都基于 UDP 实现了自己的视频传输协议,因为基于TCP的直播协议RTMP在延迟较高时卡顿比较严重
- IoT物联网通信协议 Thread(由Google 旗下的 Nest 建立 Thread Group推出)
TCP¶
TCP 在 IP 报文的基础上,增加了诸如重传、确认、有序传输、拥塞控制等能力,通信的双方是在一个确定的上下文中工作的
- 面向连接:TCP是面向连接的协议,在传输数据之前,需要通过三次握手建立连接,确保发送端和接收端都准备就绪。即只能一对一。
- 连续字节流:TCP将应用层的数据视为一个连续的字节流,没有明确的边界,分段传输。
- 双向通信:TCP连接支持全双工通信,允许双方同时发送和接收数据。
- 流量控制:TCP使用滑动窗口机制来控制发送方发送的数据量,确保不会超出接收方的处理能力。
- 拥塞控制:TCP通过算法(如慢启动、拥塞避免)动态调整发送速率,以避免网络拥塞。遇到阻塞数据丢失等会重传。
- 可靠传输
- 数据的完整性:TCP为每个数据段计算校验和,接收方通过校验和验证数据的完整性,若发现错误会请求重传。
- 数据顺序性:TCP使用序列号来标识每个字节的数据位置,接收方根据序列号将收到的数据重新排序,确保数据按顺序到达。
所谓的建立连接,只是逻辑上的概念,本质是指客户端和服务端各自维护一定的数据结构(一种状态机),来记录和维护这个连接的状态。
如果说前三层协议建立了一座桥,则四层的 TCP 只是在桥的两端增加了记录审查人员,它只能努力保障自己这一层传输的可靠性,至于下层的桥是否牢固可靠,跟 TCP 无关。
端口号大小:2Bytes * 8bit = 16bit
,所以取值范围为:1 ~ 65535
(2^16,0和65536不使用),使用IP+端口号则可以一对一连接。
UDP 头部只有源端口和目的端口,而 TCP 相对比较复杂
- 源/目标端口号,应用程序会监听端口号,通过端口号来区分收/发程序
- 序号,用于重发与控制顺序,保证可靠通信
- 确认序号,为了确认是否丢包
- 窗口大小,流量控制和阻塞控制
- 校验和,用于校验信息是否在传输中出现损失
- 状态标识位,连接维护
三次握手&四次挥手¶
报文
SYN 建立连接
FIN 关闭连接
ACK 响应
PSH 有DATA数据传输
RST 连接重置
建立连接时需要进行三次握手(Three-way Handshake)
- 确保客户端和服务器接收和发送能力都正常
- 协商初始序列号,用于后续的数据传输和确认
- 防止重复连接:确保不是旧的连接请求被误处理
为了防止冲突,起始序号并不能从 1 开始,起始序号要随着时间变化,每 4 微秒 +1,每个连接都要有不同的序号。
四次挥手是TCP连接关闭的过程,确保连接双方都能够正常结束数据传输。
Socket 编程¶
server.py
import socket
def start_server():
# 创建 socket 对象
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 绑定主机和端口
host = 'localhost'
port = 12345
server_socket.bind((host, port))
# 监听连接
server_socket.listen(5)
print(f"服务器已启动,监听地址:{host}:{port}")
while True:
# 等待客户端连接
print("等待客户端连接...")
client_socket, addr = server_socket.accept() # 三次握手在 accept() 调用过程中完成
print(f"连接地址: {addr}")
# 接收数据
data = client_socket.recv(1024).decode('utf-8')
print(f"收到数据: {data}")
# 发送响应
client_socket.send("Hello, Client".encode('utf-8'))
# 关闭连接
client_socket.close() # 四次挥手在 close() 调用过程中完成
print("连接关闭")
if __name__ == '__main__':
start_server()
client.py
import socket
def start_client():
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接服务器
host = 'localhost'
port = 12345
print("尝试连接服务器...")
client_socket.connect((host, port)) # 三次握手在 connect() 调用过程中完成
# 发送数据
message = 'Hello, Server'
client_socket.send(message.encode('utf-8'))
# 接收响应
response = client_socket.recv(1024).decode('utf-8')
print(f"服务器响应: {response}")
# 关闭连接
client_socket.close() # 四次挥手在 close() 调用过程中完成
print("连接关闭")
if __name__ == '__main__':
start_client()
- run
先运行 server 端,然后运行 client 端
客户端发送一个连接请求(SYN包)给服务器,使用的是服务器的IP地址和端口号(即 localhost: 12345),然后随机分配一个临时端口号(如 51191)一起发送给服务器,服务器接收到请求后,使用该临时端口号作为目的端口号发送响应数据包(SYN-ACK包),客户端接收到响应后,确认连接(发送ACK包),完成三次握手,建立TCP连接。
# server端打印
服务器已启动,监听地址:localhost:12345
等待客户端连接...
连接地址: ('127.0.0.1', 51191)
收到数据: Hello, Server
连接关闭
# client端打印
尝试连接服务器...
服务器响应: Hello, Client
连接关闭