TCP

一、TCP特点

  1. 面向连接的协议,在传输数据前需要建立一条可靠连接

  2. 流式协议,TCP将数据看作字节流。传输过程中数据是可以被分割为多个数据包在接收端重新组装的。

  3. TCP通过校验和\序列号和确认应答来确保数据的可靠传输,出错回重新传输。

  4. 服务器被动连接,客户端主动连接。

二、TCP编程

server端

1.创建套接字socket()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

功能:
创建一个套接字,返回一个文件描述符
参数:
domain:通信域,协议族
AF_UNIX 本地通信
AF_INET ipv4网络协议
AF_INET6 ipv6网络协议
AF_PACKET 底层接口
type:套接字的类型
SOCK_STREAM 流式套接字(tcp)
SOCK_DGRAM 数据报套接字(udp)
SOCK_RAW 原始套接字(用于链路层)
protocol:附加协议,如果不需要,则设置为0

返回值:
成功:文件描述符
失败:‐1

2.绑定套接字bind()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <sys/types.h>

#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:
将套接字与网络信息结构体绑定
参数:
sockfd:文件描述符,socket的返回值
addr:网络信息结构体
通用结构体(一般不用)
struct sockaddr
网络信息结构体 sockaddr_in
#include <netinet/in.h>
struct sockaddr_in
addrlen:
addr的长度
返回值:
成功:0
失败:-1

3.监听套接字listen()

1
2
3
4
5
6
7
8
9
10
11
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int listen(int sockfd, int backlog);
功能:
将套接字设置为被动监听状态,这样做之后就可以接收到连接请求
参数:
sockfd:文件描述符,socket函数返回值
backlog:允许通信连接的主机个数,一般设置为510
返回值:
成功:0
失败:-1

4.接收连接accept()

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
功能:
阻塞等待客户端的连接请求
参数:
sockfd:文件描述符,socket函数的返回值
addr:接收到的客户端的信息结构体(自动填充,定义变量即可)
addrlen:addr的长度
返回值:
成功:新的文件描述符(只要有客户端连接,就会产生新的文件描述符,这个新的文件描述符专门与指定的客户端进行通信的)
失败:-1

5.通信recv()/send()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <sys/types.h>

#include <sys/socket.h>
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
功能:
接收数据
参数:
sockfd:文件描述符
客户端:socket函数的返回值
服务器:accept函数的返回值
buf:保存接收到的数据
len:buf的长度
flags:标志位
0 阻塞
MSG_DONTWAIT 非阻塞
返回值:
成功:接收的字节数
失败:-1
如果发送端关闭文件描述符或者关闭进程,则recv函数会返回0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <sys/types.h>
#include <sys/socket.h>
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

功能:
发送数据
参数:
sockfd:文件描述符

客户端:
socket函数的返回值
服务器:
accept函数的返回值

buf:发送的数据
len:buf的长度
flags:标志位
0 阻塞
MSG_DONTWAIT 非阻塞

返回值:
成功:发送的字节数
失败:-1

6.关闭套接字()

client端

1.socket()

2.connect()

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
功能:
给服务器发送客户端的连接请求
参数:
sockfd:文件描述符,socket函数的返回值
addr:要连接的服务器的网络信息结构体(需要自己设置)
addrlen:add的长度
返回值:
成功:0
失败:-1

3.send()/recv()

4.close()

三、OSI模型

应用层 HTTP、SMTP、SNMP、FTP、Telnet、SIP、SSH、NFS、RTSP、XMPP、Whois、ENRP、等等
表示层 XDR、ASN.1、SMB、AFP、NCP、等等
会话层 ASAP、SSH、RPC、NetBIOS、ASP、Winsock、BSD Sockets、等等
传输层 TCP、UDP、TLS、RTP、SCTP、SPX、ATP、IL、等等
网络层 IP、ICMP、IGMP、IPX、BGP、OSPF、RIP、IGRP、EIGRP、ARP、RARP、X.25、等等
数据链路层 以太网、令牌环、HDLC、帧中继、ISDN、ATM、IEEE 802.11、FDDI、PPP、等等
物理层 例如铜缆、网线、光缆、无线电等等

UDP

DTLS

基础部分

DTLS (Datagram Transport Layer Security) 基于 UDP 场景下数据包可能丢失或重新排序的现实情况下,为 UDP 定制和改进的 TLS 协议。

DTLS 的本质就是:试图在一个不可靠的传输通道(UDP)上,建立一个安全的加密连接(TLS)。

在 WebRTC 中使用 DTLS 的地方包括两部分:协商和管理 SRTP 密钥和为 DataChannel 提供加密通道。

1
2
3
4
5
6
7
8
9
+-----------------------+
| WebRTC 应用 | <-- 视频流、音频流、DataChannel 数据
+-----------------------+
| DTLS | <-- 【关键层】负责加密,并处理丢包/乱序带来的解密问题
+-----------------------+
| UDP | <-- 负责快速传输,不管丢不丢包
+-----------------------+
| IP | <-- 网络层
+-----------------------+

UDP(User Datagram Protocol(用户数据报协议)):网络传输层的一个协议。

  • 无连接 (Connectionless): 发送数据前不需要建立连接(没有“握手”过程)。
  • 不可靠 (Unreliable): 它不保证数据包一定能送到。如果半路丢了,UDP 协议层本身不会重发(除非应用层自己写逻辑去补救)。
  • 无序 (Unordered): 先发的数据包不一定先到。
  • 低延迟 (Low Latency): 因为它省去了建立连接、确认收到、重传丢失包等复杂的步骤,所以它的速度非常快,开销很小。

为什么 WebRTC 要用 UDP?
在实时音视频通话(WebRTC 的核心场景)中,实时性完整性更重要。

DTLS

  1. 继承了 TLS 的安全性: 依然提供加密、身份认证和完整性校验。
  2. 适应了 UDP 的不可靠:
    • 它在 TLS 协议中显式增加了序列号(Sequence Number),自己来处理重排序问题。
    • 它增加了重传机制,如果在握手阶段包丢了,DTLS 会自己负责重传,而不是依赖底层协议。
      DTLS 的握手过程比 TLS 多了一个 HelloVerifyRequest(为了防止 DoS 攻击)

DTLS 的握手流程

TLS 握手 (标准 TCP 场景)

在 TCP 已经建立连接(三次握手完成)之后,TLS 握手开始。它的核心目标是:交换随机数 -> 协商算法 -> 生成密钥 -> 验证密钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    Client                                                 Server
+-------+ +-------+
| | | |
1. | ClientHello (随机数1, 支持的加密套件) --------> | |
| | | |
| | <-------- ServerHello (随机数2, 选定的套件) |
| | <-------- Certificate (服务器证书/身份证) | 2.
| | <-------- ServerKeyExchange (可选, DH参数) |
| | <-------- ServerHelloDone (我说完了) |
| | | |
3. | ClientKeyExchange (预主密钥/公钥) --------> | |
| [ChangeCipherSpec] (以后开始加密传输) --------> | |
| Finished (用密钥把前面所有消息哈希一下) --------> | |
| | | |
| | <-------- [ChangeCipherSpec] (我也加密了) |
| | <-------- Finished (我也哈希验证一下) | 4.
| | | |
+-------+ (握手完成,开始传输数据) +-------+

关键步骤详解:

1、 Client Hello: 客户端打招呼。

  • “我有这些加密算法(Cipher Suites),这是我的随机数 $Random_C$。”

2、Server Hello & Certificate: 服务端回应。

  • “我选这个算法,这是我的随机数 $Random_S$,这是我的证书(公钥在里面)。”

3、Key Exchange (密钥交换): 这是最关键的一步。客户端生成一个预主密钥 (Pre-Master Secret),用服务器的公钥加密发过去。

  • 此时,双方都有了三个要素:$Random_C$, $Random_S$, Pre-Master Secret。
  • 双方利用这三个要素,通过同样的算法算出最终的 Master Secret (会话密钥)

4、ChangeCipherSpec: 信号,表示“从这条消息以后,我发的都是密文”。

5、Finished: 双方互相发一条加密的消息(包含之前所有握手消息的 Hash)。如果对方能解密并校验 Hash 成功,说明双方密钥一致,握手成功。

DTLS 握手流程 (UDP 场景)

DTLS 基于 TLS,但 UDP 是不可靠的(没连接、易丢包、易被伪造)。

为了解决这些问题,DTLS 在流程中增加了两个核心机制:

  1. Cookie 验证 (防 DoS 攻击): UDP 很容易伪造源 IP。为了防止攻击者伪造大量 IP 向服务器发送 ClientHello 消耗服务器资源,DTLS 要求客户端先“验证身份”。
  2. 重传与消息分片 (Flight 机制): 把几条消息打包成一个Flight。如果这个组里缺了包,就超时重传。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    Client                                               Server
+-------+ +-------+
| | | |
1. | ClientHello (不带 Cookie) --------> | |
| | | |
| | <---- HelloVerifyRequest (给你个Cookie) | 2.
| | | |
3. | ClientHello (带着 Cookie) --------> | |
| | | |
| | <-------- ServerHello |
| | <-------- Certificate | 4.
| | <-------- ServerKeyExchange |
| | <-------- ServerHelloDone |
| | | |
5. | ClientKeyExchange --------> | |
| [ChangeCipherSpec] --------> | |
| Finished --------> | |
| | | |
| | <-------- [ChangeCipherSpec] | 6.
| | <-------- Finished |
| | | |
+-------+ (DTLS 通道建立成功) +-------+

第一阶段:防 DoS 验证(Cookie Exchange)

  • Step 1: 客户端发送 ClientHello
  • Step 2 (关键区别): 服务端立即分配资源,而是计算一个无状态的 Cookie,通过 HelloVerifyRequest 发回给客户端。意思是:“如果你是真的客户端,就把这个 Cookie 放在下个包里带回来。”
    • 目的: 确认发包的 IP 是真实存在的,不是伪造的攻击源。
  • Step 3: 客户端收到 Cookie,再次发送 ClientHello(这次带上了 Cookie)。服务端验证通过后,才正式开始 TLS 握手。

第二阶段:正式握手(带重传机制)

  • 接下来的步骤(Step 4, 5, 6)和 TLS 几乎一样。
  • 区别在于“Flight(飞行组)”:
    • 比如 Step 4 中的四条消息(ServerHello 到 ServerHelloDone)被视为 Flight 4
    • UDP 可能会丢掉其中的 Certificate 包。
    • 如果客户端长时间没收到完整的 Flight 4,它不会回复。
    • 服务端超时后,会重传整个 Flight 4,而不是只重传丢失的那一个包。
字段 作用 解决 UDP 的问题
Epoch (纪元) 标记当前是否处于加密状态(握手前是0,握手后是1)。 处理加密/未加密状态切换的模糊性。
Sequence Number (序列号) 每个记录都有显式的序列号(TLS 是隐式的)。 解决乱序 (Reordering) 和 丢包 (Loss)。接收方根据序号重排。
Cookie 握手第一步的令牌。 解决 DoS 攻击。防止伪造源 IP 耗尽服务器内存。
Fragment (分片) 允许把一个大的 TLS 握手包拆成多个小的 UDP 包。 解决 MTU 限制。TLS 证书可能很大,超过 UDP 单包限制,必须拆分。

握手方式

基于证书 (Certificate-based)

  • 工作原理:
    • 双方交换证书(Certificate)。
    • 利用非对称加密算法(如 RSA 或 ECDSA)来验证身份并交换密钥。
  • 在 WebRTC 中的特殊性(重点):

    • WebRTC 通常不使用权威机构(CA)签发的证书。
    • WebRTC 使用的是自签名证书 (Self-signed Certificate)
    • 怎么验证真伪? 依靠信令通道(SDP)交换的 指纹 (Fingerprint)

      流程: 浏览器 A 在 SDP 里告诉浏览器 B:“我等会儿会给你发个证书,它的哈希值是 SHA-256: XX:YY:ZZ…”。等 DTLS 握手时,B 收到证书,算出哈希值一对比,对上了就是真的。

  • 优点: 灵活,不需要双方提前预设密码,适合 P2P 这种陌生人之间的连接。
  • 缺点: 计算量大(非对称加密消耗 CPU),握手包比较大(证书占流量,可能导致 UDP 分片)。

PSK (Pre-Shared Key)

  • 工作原理:
    • 不需要证书。
    • 双方提前在本地配置好同一个密钥(Key)和 ID(Identity)。
    • 握手时,直接告诉对方:“我是 ID: client1,我知道那个秘密”。
  • 典型套件长这样: TLS_PSK_WITH_AES_128_GCM_SHA256
  • 在 WebRTC 中的地位:
    • 标准浏览器(Chrome/Safari):通常不支持或不开放 PSK 模式给开发者。浏览器主要只跑基于证书的模式。
    • 嵌入式/服务器端 WebRTC (如 GStreamer, Janus, IoT 设备):为了省电或省 CPU,可能会魔改使用 PSK。
  • 优点:
    • 极快: 少了繁重的证书验证和公钥运算。
    • 极小: 握手包非常小,非常适合带宽受限的弱网环境。
  • 缺点: 扩展性差。需要给每个设备预埋密钥。

区别手段:Server Hello 中的 Cipher Suite

在抓包看到 Server Hello 数据包选中的 Cipher Suite

  1. 看到 ECDHE-RSA / ECDHE-ECDSA 字样:
    • 例如:TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
    • 结论: 基于证书(WebRTC 默认就是这种)。
  2. 看到 PSK 字样:
    • 例如:TLS_PSK_WITH_AES_128_CBC_SHA
    • 结论: PSK 握手。