TCP与UDP

TCP与UDP的区别

项目 TCP UDP
套接字类型 SOCK_STREAM SOCK_DGRAM
连接 面向连接,三次握手 无连接
函数 listen、accept、connect、send、recv 无 listen/accept;sendto、recvfrom 为主
边界 字节流,有粘包 面向报文,无粘包,一次 sendto 对应一次 recvfrom
可靠性 可靠传输,ACK、重传、流量拥塞控制 不可靠,不保证到达、不保证顺序、不重传

一、TCP

服务端通信流程:

  1. socket () 创建流式套接字;
  2. bind () 绑定本机 IP 和端口;
  3. listen () 设置监听,设置连接等待队列;
  4. accept () 阻塞等待客户端连接,返回通信套接字;
  5. recv/send 使用通信套接字收发数据;
  6. close 关闭套接字,断开连接。

1. socket() 创建套接字

cpp 复制代码
int fd = socket(AF_INET, SOCK_STREAM, 0);
  • 参数:
    • AF_INET:IPv4 协议
    • SOCK_STREAM:流式套接字 → TCP
    • 0:自动匹配 TCP 协议
  • 作用:向内核申请一个套接字文件描述符 ,相当于拿到一根网线的端点。
  • 返回值:成功返回 fd(>0);失败 - 1

2. bind() 绑定本机 IP 地址和端口号

cpp 复制代码
struct sockaddr_in srv_addr;
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(8888);   // 端口:主机序转网络字节序!
srv_addr.sin_addr.s_addr = htonl(INADDR_ANY); // INADDR_ANY 绑定本机所有网卡IP

bind(fd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));

作用:告诉操作系统,这个 socket 要固定占用哪个 IP、哪个端口,客户端才知道去哪连接。

INADDR_ANY:0.0.0.0,表示本机所有网卡,不管客户端连本机哪个 IP 都能接收。

3. listen() 开启监听

cpp 复制代码
listen(fd, 5);
  • 第一个参数:刚才创建的监听 socket fd
  • 第二个参数:等待连接队列的最大长度(正在三次握手、排队等待 accept 的连接数量)
  • 作用:把 socket 从普通套接字,变成监听套接字,告诉内核:我这个端口准备好接 TCP 连接了。

listen 之后这个 fd 只负责 "等连接",不负责收发数据

4. accept() 阻塞等待客户端连接

cpp 复制代码
int connfd = accept(fd, (struct sockaddr *)&cli_addr, &len);
  • 阻塞函数:没有客户端来连接的时候,程序卡在这里不动。
  • 成功后:返回一个全新的文件描述符 connfd !
    • fd:监听套接字,继续监听下一个客户端
    • connfd:专门和连接的客户端通信的套接字,收发数据全用它

listen 的 fd 是用于监听的"接待前台";accept 返回的 connfd 是单独通信客户端的窗口。

5. 数据收发 send() / recv()

cpp 复制代码
recv(connfd, buf, sizeof(buf), 0);  // 读客户端发来的数据
send(connfd, buf, strlen(buf), 0);  // 发给客户端
  • 读写都是用 connfd,不是 listen 的 fd!
  • TCP 是字节流,存在粘包问题(多次发送的数据粘在一起),应用层自己处理分包。

6. close() 关闭套接字

  1. 先 close(connfd):断开和当前客户端的 TCP 连接,触发四次挥手
  2. 服务程序退出时 close(fd):关闭监听套接字,释放端口

客户端通信流程:

  1. socket () 创建流式套接字;
  2. connect () 指定服务器地址端口,发起连接(三次握手);
  3. send/recv 和服务器通信;
  4. close 关闭套接字。

1. socket() 创建套接字

和服务器一模一样,拿到一个 socket fd。

2. connect() 主动发起 TCP 连接

cpp 复制代码
struct sockaddr_in srv_addr;
srv_addr.sin_family = AF_INET;
srv_addr.sin_port = htons(8888);
srv_addr.sin_addr.s_addr = inet_addr("192.168.1.100"); // 服务器IP

connect(fd, (struct sockaddr *)&srv_addr, sizeof(srv_addr));

作用:向服务器发送连接请求,底层自动完成TCP 三次握手。

客户端一般不需要 bind :操作系统自动 给客户端分配随机的源端口。

3. send() / recv() 和服务器双向收发数据

客户端直接用自己这个 fd 收发,不需要新 fd。

4. close(fd) 关闭套接字,断开连接,四次挥手

TCP 建立连接:三次握手

服务端已经执行完 socket() → bind() → listen(),内核创建半连接、全连接两个队列,准备接收连接。

  • connect:触发握手,阻塞等待握手完成才返回(默认阻塞 socket)
  • accept:**完全不参与握手,只去全连接队列拿已经建好的连接,**返回通信文件描述符;如果全连接队列为空,accept 就阻塞等待。

握手整个报文流程,都是操作系统内核 TCP 模块做的,不是这两个函数里面写 send 发包。

第 1 次握手:客户端 → 服务端,SYN + seq

客户端调用 connect(),内核挑选一个本机空闲的源端口向外发送 SYN 报文,SYN=1:同步标志,请求建立连接;携带客户端初始序列号 seq = x

客户端开启 SYN 重传定时器: SYN 丢失,超时自动重传

客户端状态:SYN_SENT(同步已发送)

第 2 次握手:服务端 → 客户端,SYN + seq、ACK +ack

服务端收到 SYN报文:把这条连接放入半连接队列,服务端状态切换为 SYN_RCVD(同步收到)

服务端内核自动回复 SYN+ACK:我收到你的连接请求,我同意建立连接,我的起始序列号是 y。

  • SYN:服务端自己的初始序列号 seq=y
  • ACK 确认号:ack = x + 1,我收到了你 seq=x 的 SYN 报文

开启半连接超时定时器(超时收不到 ACK,内核删除半连接队列里这条记录)

第 3 次握手:客户端 → 服务端,ACK

客户端收到 SYN+ACK:清除刚才的 SYN 重传定时器,客户端状态更新为 ESTABLISHED已连接

回复 ACK 报文,ack = y + 1,seq = x + 1:确认收到服务器的 SYN 同步报文,收到序列号 y 确认同意建立TCP 连接。

服务端收到 ACK 后,清除半连接超时定时器,连接从半连接队列移出,放入全连接队列,等待 accept 取出。

TCP 断开连接:四次挥手

TCP 是全双工通信 :两个传输方向是独立的。一个方向关闭,不代表另一个方向不能继续传数据。客户端主动发起关闭 (谁调用 close,谁先发 FIN)初始状态:客户端、服务端都处于 ESTABLISHED,正常收发数据。

客户端(主动关闭方)状态流转

  • ESTABLISHED → 发 FIN → FIN_WAIT_1(客户端发了 FIN,等待服务端对这个 FIN 的 ACK)
  • 收到服务端 ACK → FIN_WAIT_2(客户端收到 ACK,等待服务端发来 FIN)
  • 收到服务端 FIN → 回复 ACK → TIME_WAIT(等待 2MSL) → CLOSED(客户端收到对方 FIN、并且回完 ACK,等待一段时间收尾)

服务端(被动关闭方)

  • ESTABLISHED → 收到 FIN → 回复 ACK → CLOSE_WAIT(服务端知道客户端不发数据了,服务端此时还可以继续发自己剩下的数据)
  • 服务端数据发完、调用 close,发送 FIN → LAST_ACK(服务端发出 FIN,等待客户端最后的 ACK)
  • 收到客户端最后的 ACK → CLOSED

第 1 次挥手:客户端 → 服务端,FIN=1,seq=u

客户端调用 close() ,不再向外发送数据,发送 FIN 报文 。

客户端状态变化:ESTABLISHED → FIN_WAIT_1

我已经发出关闭方向的请求了,正在等服务器对这个 FIN 的 ACK 确认包

第 2 次挥手:服务端 → 客户端,ACK=1,seq=v,ack=u+1

服务端收到 FIN,回复 ACK 确认收到关闭请求 。

服务端状态:ESTABLISHED → CLOSE_WAIT

客户端收到这个 ACK:FIN_WAIT_1 → FIN_WAIT_2

服务器知道客户端不发数据;客户端等服务器这边发 FIN(服务器告诉客户端同样不发数据)

半关闭状态:客户端不能再发数据;但是服务端仍然可以继续向客户端发送剩下的数据!

这个ACK 只能确认收到关闭请求,服务端还有数据要发,不能立刻发 FIN,所以 ACK 和 FIN 不能合并。

第 3 次挥手:服务端 → 客户端,FIN=1,ACK=1,seq=w,ack=u+1

等服务端把剩余数据全部发送完毕,调用 close(),发送 FIN 报文 ,表示服务端这边也没有数据要发送了。

服务端状态:CLOSE_WAIT → LAST_ACK

客户端收到这个 FIN:FIN_WAIT_2 → TIME_WAIT

第 4 次挥手:客户端 → 服务端,ACK=1,seq=u+1,ack=w+1

客户端回复 ACK 确认收到服务端的 FIN。

  1. 服务端收到这个 ACK:直接进入 CLOSED,连接消失
  2. 客户端进入 TIME_WAIT 状态,必须等待 2MSL 之后,才变成 CLOSED

二、UDP

UDP 服务器流程

  1. socket() 创建数据报套接字,SOCK_DGRAM = 数据报,代表 UDP
cpp 复制代码
int fd = socket(AF_INET, SOCK_DGRAM, 0);
  1. bind() 绑定固定 IP + 端口

服务器必须 bind,客户端一般不用:固定端口,让客户端知道往哪个端口发数据

  1. recvfrom() 阻塞等待接收客户端发来的数据

调用 recvfrom 阻塞接收客户端报文,同时获取客户端地址:数据 + 发送方的地址结构(客户端 IP、端口)

  1. sendto() 向刚才拿到的客户端地址发回复数据

  2. 循环 recvfrom /sendto

  3. close(fd) 关闭套接字

服务器可以源源不断接收不同客户端的数据,天然支持一对多通信(广播、组播)

UDP 客户端流程

  1. socket() 创建 UDP 套接字
  2. sendto() 直接发给服务器 IP + 端口,不需要提前连接
  3. recvfrom() 接收服务器回复
  4. close()

拓展:UDP 也可以调用 connect ()!

注意:UDP 的 connect 不会产生握手报文 ,只是在内核中记录对方的地址,之后就可以直接用 send /recv,不用每次都传地址;本质只是 "绑定对端地址",不是建立连接。

注意:

recvfrom 输出参数会拿到发送者地址(通信):

  • UDP 是无连接通信, 内核不会维护 socket 和对端的绑定关系,同一个 UDP 套接字可以接收来自多个不同客户端的数据。
  • recvfrom 通过输出参数拿到数据包的发送者地址,应用层拿到该地址后,才能使用 sendto 定向回复数据;( UDP 的 connect 只是固定对端地址,不握手、不建立连接)

UDP 支持广播、组播;UDP 不存在半连接队列、全连接队列。

UDP的三种通信模式

类型 通信模式 IP 范围 接收方要求 路由器转发 底层 MAC
单播 一对一 A/B/C 类普通 IP 不需要额外配置 可以跨网段转发 单播 MAC(唯一主机)
广播 一对全部 255.255.255.255 / 子网定向广播 绑定对应端口即可接收 默认不转发 FF:FF:FF:FF:FF:FF
组播 一对多 D 类:224.0.0.0~239.255.255.255 必须 IP_ADD_MEMBERSHIP 加入组 支持跨网段(IGMP) 01:00:5e 开头组播 MAC

1.单播

**一对一通信,数据包从一个源地址发给唯一的一个目标地址,**是普通 UDP 通信,也是 TCP 唯一支持的模式。

  1. 应用层 :sendto()发数据;recvfrom()接收,拿到发送方地址
  2. 传输层 :封装 UDP 头部(源端口、目的端口、校验和);无连接
  3. 网络层:封装 IP 头部,目标 IP 是普通单播 IP(如 192.168.1.100);查路由,选下一跳
  4. 数据链路层 :通过 ARP 解析目标 IP 对应的单播 MAC 地址,封装以太网帧,发给指定主机

特点: 只发给指定一台主机;路由器可以跨网段转发单播包。


2.广播

一对所以,数据包发给同一个局域网内全部主机

广播只能在同一个局域网(子网) 传播,默认路由器不转发广播包(防止全网风暴)

  1. 应用层
    • 发送端:必须先用 setsockopt 开启 SO_BROADCAST 选项,否则 sendto 发广播地址会报错!
    • 然后 sendto(),目标 IP 填广播地址:
      • 有限广播:255.255.255.255(只能本子网,路由器绝不转发)
      • 定向广播:比如 192.168.1.255(部分路由器支持跨子网,一般禁用)
    • 接收端:不需要额外加入什么组,只要绑定对应端口,监听这个端口就能收到广播包
  2. 传输层:封装 UDP 头部,目标端口是广播监听端口
  3. 网络层:IP 头部目标 IP = 广播地址
  4. 数据链路层 :目的 MAC 是广播 MAC:FF:FF:FF:FF:FF:FF 交换机收到这个帧,会把包泛洪转发给本网段所有端口

特点:

  • 同一子网内所有主机网卡都会收到这个帧,交给内核判断端口是否监听
  • 开销大,容易产生广播风暴,局域网谨慎使用

组播(多播)

一对多,发给一个组播组,只有加入这个组的成员主机才能接收

组播 IP 是 D 类 IP:224.0.0.0 ~ 239.255.255.255

  1. 应用层
    • 发送端:直接 sendto 往组播 IP 发数据,发送端不需要加入组!
    • 接收端:必须调用 setsockopt 设置 IP_ADD_MEMBERSHIP,加入组播组,才可以收到这个组的数据
  2. 传输层:封装 UDP 头部,目标端口为组播监听端口,不需要提前建立连接
  3. 网络层 :目标 IP 是 D 类组播地址;依靠 IGMP 协议,告诉路由器:本网段有主机加入了这个组播组;路由器维护组播成员列表,只向存在组成员的子网转发组播包
  4. 数据链路层 :组播 IP 映射成以太网组播 MAC(固定前缀01:00:5e开头);交换机复制数据包,只转发给加入该组的端口

和广播最大区别: 广播 = 子网里所有主机都收到包 ,只是应用层不监听就丢弃; 组播 = 只有主动加入这个组的主机,才会接收这个组的数据包,路由器 / 交换机按需分发,不会无脑泛洪,效率更高。


setsockopt 函数

cpp 复制代码
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);

逐个参数解释:

  1. sockfd:要配置的套接字 fd(UDP 套接字)
  2. level:选项层级
    • SOL_SOCKET:套接字层选项,比如 SO_BROADCAST(开启套接字的广播发送权限,发广播必须开这个 )、SO_REUSEADDR(端口复用,服务端快速重启,解决 TIME_WAIT 占用端口问题)
    • IPPROTO_IP:IP 层选项,比如 IP_ADD_MEMBERSHIP(让本机加入指定组播组,接收组播数据)
  3. optname:选项名字(要开启 / 设置的功能)
  4. optval:存放参数值的指针,比如开启广播就传 &on(on=1 打开,0 关闭)
  5. optlen:optval 这个变量的字节长度 返回值:成功返回 0,失败返回 - 1
相关推荐
车载中间件通信3 小时前
[TC8测试]TCP_PROBING_WINDOWS_05
网络·网络协议·tcp/ip·tc8
曦夜日长3 小时前
主从 Reactor 高并发服务器实战 (三):服务器套接字Socket模块详解与实现
运维·服务器·网络
鹿鸣天涯4 小时前
2026年下半年网络规划设计师论文
网络·计算机网络·网络规划设计师
echome8884 小时前
Linux 定时任务:crontab 写法与常见失效原因
linux·运维·网络·定时任务·crontab
从琳开始9894 小时前
从Socket到TCP回显服务器:Java网络编程完整指南
java·服务器·网络·tcp/ip·java-ee
誰能久伴不乏4 小时前
用 UDP 做一条“可靠“的控制通道
c++·qt·网络协议·udp
跨境数据猎手5 小时前
HTTP 响应体编码与压缩:识别、解码
网络协议·http·ios
安易算力5 小时前
智算中心网络架构:RDMA/IB/以太网选型深度解析与优化实践
网络·架构·php
B1118521Y466 小时前
网络安全案例1:远程登陆实战
网络