一、TCP与UDP
| 对比项 |
TCP(确保把信号送到) |
UDP(不管在不在都送出去) |
| 特点 |
面向连接、可靠传输、流量控制、拥塞控制、全双工 |
无连接、尽力交付、无重传、无拥塞控制、低开销 |
| 优点 |
可靠性(ACK确认、超时重传)、顺序性 |
传输效率高(8字节首部)、实时性强 |
| 缺点 |
首部开销大(20字节)、连接管理复杂、延迟高 |
数据可能丢失、乱序 |
| 应用场景 |
HTTP/HTTPS、文件传输(FTP)、邮件(SMTP/IMAP) |
视频流(RTP)、DNS查询、在线游戏、VoIP |
TCP首部包含:源端口号;序列号;校验和
目标端口未监听;网络拥塞会导致TCP链接失效
二、TCP通信
2.1 TCP网络编程流程
| 步骤 |
服务器流程 |
函数 |
| 1 |
创建流式套接字 |
socket() |
| 2 |
填充服务器的网络信息结构体 |
struct sockaddr_in |
| 3 |
将套接字与服务器的网络信息结构体绑定 |
bind() |
| 4 |
将套接字设置成被动监听状态 |
listen() |
| 5 |
阻塞等待客户端连接 |
accept() |
| 6 |
收发数据 |
recv() / send() |
| 7 |
关闭套接字 |
close() |
2.2 socket编程
2.2.1 socket函数
| 项目 |
内容 |
| 头文件 |
#include <sys/socket.h> |
| 函数原型 |
int socket(int domain, int type, int protocol); |
| 参数 - domain |
AF_INET(IPv4)、AF_INET6(IPv6)、AF_PACKET(原始套接字)、AF_UNIX(本地通信)、AF_LOCAL(本地通信) |
| 参数 - type |
SOCK_STREAM(TCP)、SOCK_DGRAM(UDP)、SOCK_RAW(原始套接字) |
| 参数 - protocol |
无附加协议填 0(自动选择默认协议) |
| 返回值 |
成功返回套接字(文件描述符,非负整数),失败返回 -1(并重置错误码) |
2.2.2 bind函数
| 项目 |
内容 |
| 头文件 |
#include <sys/socket.h> |
| 函数原型 |
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
| 参数 - sockfd |
文件描述符(由 socket 函数返回) |
| 参数 - addr |
地址结构体指针,指向要绑定给 sockfd 的协议地址(以 IPv4 的 struct sockaddr_in 为例) |
| 参数 - addrlen |
地址的长度 |
| 返回值 |
成功返回 0,失败返回 -1(并重置错误码) |
cs
复制代码
//addr 结构体
struct sockaddr_in {
sa_family_t sin_family;
in_port_t sin_port;
struct in_addr sin_addr;
};
//sin_addr 结构体
struct in_addr {
uint32_t s_addr;
};
定义结构体后要用memset函数清空结构体
| 项目 |
内容 |
| 头文件 |
<string.h> |
| 函数原型 |
void *memset(void *s, int c, size_t n); |
| 作用 |
将指针 s 指向的内存区域的前 n 个字节,全部设置为 c 的值 |
| 返回值 |
返回指针 s(即填充后的内存起始地址) |
也可以直接 struct sockaddr_in serverInfo = {0};
结构体的初始化
cs
复制代码
//定义配置服务器地址结构体
struct sockaddr_in serverInfo;
//清空结构体
memset(&serverInfo,0,sizeof(serverInfo));
//初始化IPV4协议族
serverInfo.sin_family = AF_INET;
//初始化端口号 (主机字节序->网络字节序)
serverInfo.sin_port = htons(8888);
//初始化IP
serverInfo.sin_addr.s_addr = inet_addr("192.168.23.100");
2.2.3 listen函数
| 项目 |
内容 |
| 头文件 |
#include <sys/socket.h> |
| 函数原型 |
int listen(int sockfd, int backlog); |
| 参数 - sockfd |
表示要监听的 socket 套接字 |
| 参数 - backlog |
表示半连接队列的长度(即 socket 可以排队的最大连接个数) |
| 返回值 |
成功返回 0,失败返回 -1(并重置错误码) |
把socket从主动变成被动监听状态
2.2.4 accept函数(阻塞函数)
| 项目 |
内容 |
| 头文件 |
#include <sys/socket.h> |
| 函数原型 |
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); |
| 参数 - sockfd |
表示处于监听状态的 socket |
| 参数 - addr |
用于保存客户端地址的结构体指针,如果不关心客户端的信息,可以传 NULL |
| 参数 - addrlen |
输入时为 addr 的缓冲区大小,输出时为实际地址长度,如果不关心客户端的信息,可以传 NULL |
| 返回值 |
成功返回新的 socket 文件描述符(由内核生成,代表着与返回客户端的 TCP 连接,专用于与客户端通信),失败返回 -1(并重置错误码) |
2.2.5 connect函数
| 项目 |
内容 |
| 头文件 |
#include <sys/socket.h> |
| 函数原型 |
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); |
| 参数 - sockfd |
表示客户端的 socket 套接字 |
| 参数 - addr |
表示目标服务器的地址结构体 |
| 参数 - addrlen |
地址结构体的长度 |
| 返回值 |
成功返回 0,失败返回 -1(并重置错误码) |
2.2.6 recv函数
| 项目 |
内容 |
| 头文件 |
#include <sys/socket.h> |
| 函数原型 |
ssize_t recv(int sockfd, void *buf, size_t len, int flags); |
| 参数 - sockfd |
表示客户端的 socket 套接字(connect_fd) |
| 参数 - buf |
用于存储接收的数据 |
| 参数 - len |
数据大小 |
| 参数 - flags |
控制选项(如 MSG_DONTWAIT 非阻塞,MSG_PEEK 窥视数据,0 阻塞) |
| 返回值 |
成功返回实际接收数据字节数,0表示客户端断开链接,失败返回 -1(并重置错误码) |
recv() 不是从 buf 里收数据 ,而是从内核的接收缓冲区里收数据
2.2.7 send函数
| 项目 |
内容 |
| 头文件 |
#include <sys/socket.h> |
| 函数原型 |
ssize_t send(int sockfd, const void *buf, size_t len, int flags); |
| 参数 - sockfd |
表示客户端的 socket 套接字 |
| 参数 - buf |
要发送的数据首地址 |
| 参数 - len |
数据大小 |
| 参数 - flags |
控制选项(如 MSG_DONTWAIT 非阻塞,MSG_PEEK 窥视数据,0 阻塞) |
| 返回值 |
成功返回实际发送数据字节数,失败返回 -1(并重置错误码) |
2.2.8 close函数
| 项目 |
内容 |
| 头文件 |
#include <unistd.h> |
| 函数原型 |
int close(int fd); |
| 参数 - fd |
表示要关闭的套接字 |
| 返回值 |
成功返回 0,失败返回 -1 |
2.3 系统内核处理
| 函数 |
内核处理 |
| socket() |
创建套接字结构体,分配文件描述符 |
| bind() |
把IP和端口号绑定到套接字 |
| listen() |
创建半连接队列和全连接队列,转为监听状态 |
| accept() |
从全连接队列取出已完成连接的客户端,生成新套接字 |
| connect() |
发起三次握手,发送SYN包 |
| recv() |
从内核接收缓冲区拷贝数据到用户空间 |
| send() |
从用户空间拷贝数据到内核发送缓冲区 |
| close() |
释放套接字资源,发起四次挥手 |
2.4 TCP网络编程流程
| 步骤 |
客户端操作 |
调用的函数 |
| 1 |
创建流式套接字 |
socket() |
| 2 |
填充服务器的网络信息结构体 |
struct sockaddr_in |
| 3 |
与服务器建立连接 |
connect() |
| 4 |
收发数据 |
send() / recv() |
| 5 |
关闭套接字 |
close() |
Telnet 是一种远程登录协议,用于通过网络连接到另一台计算机的终端。后接服务器IP
2.5 三次握手
确保双方都能正常收发数据,同步初始序号,避免历史连接请求造成混乱,为后面的可靠传输打好基础。
2.5.1 三次握手的状态变化
| 发送方 |
服务端状态变化 |
客户端状态变化 |
原因说明 |
| 客户端 |
--- |
CLOSED → SYN_SENT |
客户端主动发起连接,发出同步请求,等待服务端确认 |
| 服务端 |
LISTEN → SYN_RCVD |
SYN_SENT(不变) |
服务端收到 SYN,回复确认并携带自己的 SYN,表示愿意连接;自身进入半连接状态,等待客户端最终确认 |
| 客户端 |
SYN_RCVD → ESTABLISHED |
SYN_SENT → ESTABLISHED |
客户端收到 SYN+ACK,确认连接可用,双方进入数据传输状态 |
2.5.2 两次握手不可以原因
| 问题类型 |
两次握手的问题 |
三次握手如何解决 |
| 数据包丢失 |
服务端发的 SYN+ACK 丢失后,客户端不知道、服务端空等,造成半开连接 |
客户端收到 SYN+ACK 后必须回复 ACK,服务端收到 ACK 才认为连接建立; 若 ACK 丢失会重发或超时关闭,避免空等 |
| SYN 攻击风险 |
攻击者发一个 SYN,服务端就立即分配资源建立连接,极易耗尽内存 |
服务端发 SYN+ACK 后不分配应用资源,收到客户端的 ACK 确认后才真正建立连接,有效抵抗伪造 SYN 攻击 |
2.6 四次挥手
2.6.1 四次握手的状态变化
| 第几次挥手 |
发送方 |
状态变化 |
原因 |
| 第一次 |
客户端 |
ESTABLISHED → FIN_WAIT_1 |
客户端主动关闭,表示"我没数据要发了",进入等待服务端确认的状态 |
| 第二次 |
服务端 |
ESTABLISHED → CLOSE_WAIT |
服务端收到 FIN,回复确认,表示"我知道你要关了";但服务端可能还有数据要发,所以先进入半关闭状态 |
| --- |
客户端 |
FIN_WAIT_1 → FIN_WAIT_2 |
收到服务端的 ACK 确认,进入 FIN_WAIT_2,等待服务端发 FIN |
| 第三次 |
服务端 |
CLOSE_WAIT → LAST_ACK |
服务端数据发完了,主动发送 FIN,表示"我也没数据要发了",等待客户端最后一次确认 |
| 第四次 |
客户端 |
FIN_WAIT_2 → TIME_WAIT |
收到服务端的 FIN,回复确认,但不确定 ACK 是否丢失,所以等待 2MSL(保证服务端能收到 ACK) |
| --- |
服务端 |
LAST_ACK → CLOSED |
收到客户端的 ACK,连接彻底关闭 |
| --- |
客户端 |
TIME_WAIT → CLOSED |
2MSL 时间到,没有收到重传的 FIN,确认服务端已收到 ACK,连接关闭 |
因为TCP的全双工特性,所以会有四次握手
2.6.2 四次挥手不可以合并成三次
服务端收到客户端的 FIN 后,可能还有数据没发完 ,所以先回复 ACK 表示"我知道你要关了",等数据发完再发 FIN。第二次和第三次挥手不能合并,因为中间可能有数据传输。
2.7 两种过程使用时间
| 过程 |
使用时机 |
| 三次握手 |
通信开始前,客户端和服务端建立连接时使用 |
| 四次挥手 |
通信结束后,客户端或服务端释放连接时使用 |
三、UDP通信流程
3.1 相关函数
3.1.1 recvfrom函数
| 项目 |
内容 |
| 头文件 |
#include <sys/socket.h> #include <sys/types.h> |
| 函数原型 |
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); |
| 参数 - sockfd |
表示客户端的 socket 套接字 |
| 参数 - buf |
要接收的数据首地址 |
| 参数 - len |
可接受数据的最大长度 |
| 参数 - flags |
控制选项(如 MSG_DONTWAIT 非阻塞,MSG_PEEK 窥视数据,0 阻塞) |
| 参数 - src_addr |
源地址,获取发送方的信息 |
| 参数 - addrlen |
地址长度 |
| 返回值 |
成功返回接收字节数,失败返回 -1(并重置错误码) |
3.1.2 sendto函数
| 项目 |
内容 |
| 头文件 |
#include <sys/socket.h> #include <sys/types.h> |
| 函数原型 |
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); |
| 参数 - sockfd |
表示客户端的 socket 套接字 |
| 参数 - buf |
要发送的数据首地址 |
| 参数 - len |
数据大小 |
| 参数 - flags |
控制选项(如 MSG_DONTWAIT 非阻塞,MSG_PEEK 窥视数据,0 阻塞) |
| 参数 - dest_addr |
目的地址,数据将要发向哪一个 IP 地址的主机 |
| 参数 - addrlen |
地址长度 |
| 返回值 |
成功返回发送字节数,失败返回 -1(并重置错误码) |
3.1.3 sendto函数
| 项目 |
内容 |
| 头文件 |
#include <sys/socket.h> #include <sys/types.h> |
| 函数原型 |
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen); |
| 参数 - sockfd |
表示客户端的 socket 套接字 |
| 参数 - buf |
要发送的数据首地址 |
| 参数 - len |
数据大小 |
| 参数 - flags |
控制选项(如 MSG_DONTWAIT 非阻塞,MSG_PEEK 窥视数据,0 阻塞) |
| 参数 - dest_addr |
目的地址,数据将要发向哪一个 IP 地址的主机 |
| 参数 - addrlen |
地址长度 |
| 返回值 |
成功返回发送字节数,失败返回 -1(并重置错误码) |
3.2 单播
单播:一对一通信,数据包从单一源地址发送到单一目标地址
| 层级 |
职责 |
| 应用层 |
调用 sendto() 发送数据到指定目标 IP 和端口 调用 recvfrom() 接收来自特定源的数据 |
| 传输层 |
封装 UDP 头部:源端口、目标端口、长度、校验和 不建立连接,直接发送数据报 |
| 网络层 |
封装 IP 头部:源 IP、目标 IP(单播地址,如 192.168.1.100) 根据目标 IP 查找路由表,选择下一跳 |
| 链路层 |
根据目标 IP 的 MAC 地址(通过 ARP 解析)封装以太网帧 通过物理网络设备(如网卡)发送到目标主机 |
3.3 组播
组播:一对多通信,数据包发送到一个组播组,组内所有成员均可接收
| 层级 |
职责 |
| 应用层 |
发送端:调用发送数据到组播地址(如 239.255.0.1) 接收端:调用加入组播组(IP_ADD_MEMBERSHIP) |
| 传输层 |
封装 UDP 头部:目标端口为组播端口(如 12345) 组播成员无需提前建立连接 |
| 网络层 |
封装 IP 头部:目标 IP 为组播地址(D 类地址,224.0.0.0~239.255.255.255) 接收端通过 IGMP 报文通知路由器加入/离开组播组 路由器维护组播组成员列表,仅向存在成员的子网转发数据 |
| 数据链路层 |
组播 MAC 地址映射:将 IP 组播地址转换为以太网组播 MAC(如 01:00:5E:XX:XX:XX) 交换机/路由器:根据组播 MAC 地址复制数据包到多个端口 |
3.4 setsockopt函数
| 项目 |
内容 |
| 头文件 |
#include <sys/socket.h> |
| 函数原型 |
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); |
| 参数 - sockfd |
目标套接字描述符,表示需要配置的套接字 |
| 参数 - level |
选项层级(如 SOL_SOCKET、IPPROTO_IP) |
| 参数 - optname |
选项名称(如 SO_REUSEADDR、IP_ADD_MEMBERSHIP) |
| 参数 - optval |
指向选项值的指针,类型和长度需与 optname 匹配 |
| 参数 - optlen |
optval 指向的数据长度 |
| 返回值 |
成功返回 0,失败返回非 0 数据 |
3.5 广播
广播:一对所有通信,数据包发送到同一网络内的所有主机
| 层级 |
职责 |
| 应用层 |
发送端:调用 setsockopt() 启用广播选项(SO_BROADCAST) 调用 sendto() 发送数据到广播地址(如 255.255.255.255) |
| 传输层 |
封装 UDP 头部:目标端口为广播端口(如 9999) 接收端无需加入组,但需监听指定端口 |
| 网络层 |
封装 IP 头部:目标 IP 为广播地址(受限广播 255.255.255.255 或定向广播 192.168.1.255) 受限广播仅在本局域网内传播,路由器默认不转发 定向广播可跨子网(需路由器支持,通常被禁用) |
| 数据链路层 |
广播 MAC 地址:FF:FF:FF:FF:FF:FF 交换机将广播包泛洪到所有端口(除源端口) |