TCP与UDP

发布时间:2026/11/3 19:17:43
TCP与UDP

TCP与UDP的区别

项目TCPUDP
套接字类型SOCK_STREAMSOCK_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()创建套接字

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

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

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()开启监听

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

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

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

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

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

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

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 连接

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

int fd = socket(AF_INET, SOCK_DGRAM, 0);

2.bind()绑定固定 IP + 端口

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

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

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

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

5. 循环 recvfrom /sendto

6.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 函数

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