TCP与UDP
TCP与UDP的区别
| 项目 | TCP | UDP |
|---|---|---|
| 套接字类型 | SOCK_STREAM | SOCK_DGRAM |
| 连接 | 面向连接,三次握手 | 无连接 |
| 函数 | listen、accept、connect、send、recv | 无 listen/accept;sendto、recvfrom 为主 |
| 边界 | 字节流,有粘包 | 面向报文,无粘包,一次 sendto 对应一次 recvfrom |
| 可靠性 | 可靠传输,ACK、重传、流量拥塞控制 | 不可靠,不保证到达、不保证顺序、不重传 |
一、TCP
服务端通信流程:
- socket () 创建流式套接字;
- bind () 绑定本机 IP 和端口;
- listen () 设置监听,设置连接等待队列;
- accept () 阻塞等待客户端连接,返回通信套接字;
- recv/send 使用通信套接字收发数据;
- 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()关闭套接字
- 先
close(connfd):断开和当前客户端的 TCP 连接,触发四次挥手 - 服务程序退出时
close(fd):关闭监听套接字,释放端口
客户端通信流程:
- socket () 创建流式套接字;
- connect () 指定服务器地址端口,发起连接(三次握手);
- send/recv 和服务器通信;
- 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。
- 服务端收到这个 ACK:直接进入 CLOSED,连接消失
- 客户端进入 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 客户端流程
socket()创建 UDP 套接字sendto()直接发给服务器 IP + 端口,不需要提前连接recvfrom()接收服务器回复- 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 唯一支持的模式。
- 应用层:
sendto()发数据;recvfrom()接收,拿到发送方地址 - 传输层:封装 UDP 头部(源端口、目的端口、校验和);无连接
- 网络层:封装 IP 头部,目标 IP 是普通单播 IP(如 192.168.1.100);查路由,选下一跳
- 数据链路层:通过 ARP 解析目标 IP 对应的单播 MAC 地址,封装以太网帧,发给指定主机
特点: 只发给指定一台主机;路由器可以跨网段转发单播包。
2.广播
一对所以,数据包发给同一个局域网内全部主机
广播只能在同一个局域网(子网)传播,默认路由器不转发广播包(防止全网风暴)
- 应用层
- 发送端:必须先用
setsockopt开启SO_BROADCAST选项,否则 sendto 发广播地址会报错! - 然后
sendto(),目标 IP 填广播地址:- 有限广播:
255.255.255.255(只能本子网,路由器绝不转发) - 定向广播:比如
192.168.1.255(部分路由器支持跨子网,一般禁用)
- 有限广播:
- 接收端:不需要额外加入什么组,只要绑定对应端口,监听这个端口就能收到广播包
- 发送端:必须先用
- 传输层:封装 UDP 头部,目标端口是广播监听端口
- 网络层:IP 头部目标 IP = 广播地址
- 数据链路层:目的 MAC 是广播 MAC:
FF:FF:FF:FF:FF:FF交换机收到这个帧,会把包泛洪转发给本网段所有端口
特点:
- 同一子网内所有主机网卡都会收到这个帧,交给内核判断端口是否监听
- 开销大,容易产生广播风暴,局域网谨慎使用
组播(多播)
一对多,发给一个组播组,只有加入这个组的成员主机才能接收
组播 IP 是 D 类 IP:
224.0.0.0 ~ 239.255.255.255
- 应用层
- 发送端:直接 sendto 往组播 IP发数据,发送端不需要加入组!
- 接收端:必须调用
setsockopt设置IP_ADD_MEMBERSHIP,加入组播组,才可以收到这个组的数据
- 传输层:封装 UDP 头部,目标端口为组播监听端口,不需要提前建立连接
- 网络层:目标 IP 是 D 类组播地址;依靠 IGMP 协议,告诉路由器:本网段有主机加入了这个组播组;路由器维护组播成员列表,只向存在组成员的子网转发组播包
- 数据链路层:组播 IP 映射成以太网组播 MAC(固定前缀
01:00:5e开头);交换机复制数据包,只转发给加入该组的端口
和广播最大区别: 广播 = 子网里所有主机都收到包,只是应用层不监听就丢弃; 组播 =只有主动加入这个组的主机,才会接收这个组的数据包,路由器 / 交换机按需分发,不会无脑泛洪,效率更高。
setsockopt 函数
int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen);逐个参数解释:
sockfd:要配置的套接字 fd(UDP 套接字)level:选项层级SOL_SOCKET:套接字层选项,比如SO_BROADCAST(开启套接字的广播发送权限,发广播必须开这个)、SO_REUSEADDR(端口复用,服务端快速重启,解决 TIME_WAIT 占用端口问题)IPPROTO_IP:IP 层选项,比如IP_ADD_MEMBERSHIP(让本机加入指定组播组,接收组播数据)
optname:选项名字(要开启 / 设置的功能)optval:存放参数值的指针,比如开启广播就传 &on(on=1 打开,0 关闭)optlen:optval 这个变量的字节长度 返回值:成功返回 0,失败返回 - 1