一、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(并重置错误码) |
//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};
结构体的初始化
//定义配置服务器地址结构体 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 交换机将广播包泛洪到所有端口(除源端口) |