首页 / 资讯中心 / 文章详情

TCP与UDP

TCP与UDP ★ FEATURED ARTICLE
TCP与UDP的区别项目TCPUDP套接字类型SOCK_STREAMSOCK_DGRAM连接面向连接三次握手无连接函数listen、accept、connect、send、recv无 listen/acceptsendto、recvfrom 为主边界字节流有粘包面向报文无粘包一次 sendto 对应一次 recvfrom可靠性可靠传输ACK、重传、流量拥塞控制不可靠不保证到达、不保证顺序、不重传一、TCP服务端通信流程socket () 创建流式套接字bind () 绑定本机 IP 和端口listen () 设置监听设置连接等待队列accept () 阻塞等待客户端连接返回通信套接字recv/send 使用通信套接字收发数据close 关闭套接字断开连接。1.socket()创建套接字int fd socket(AF_INET, SOCK_STREAM, 0);参数AF_INETIPv4 协议SOCK_STREAM流式套接字 → TCP0自动匹配 TCP 协议作用向内核申请一个套接字文件描述符相当于拿到一根网线的端点。返回值成功返回 fd0失败 - 12.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_ANY0.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);阻塞函数没有客户端来连接的时候程序卡在这里不动。成功后返回一个全新的文件描述符 connfdfd监听套接字继续监听下一个客户端connfd专门和连接的客户端通信的套接字收发数据全用它listen 的 fd 是用于监听的“接待前台”accept 返回的 connfd 是单独通信客户端的窗口。5. 数据收发send()/recv()recv(connfd, buf, sizeof(buf), 0); // 读客户端发来的数据 send(connfd, buf, strlen(buf), 0); // 发给客户端读写都是用connfd不是 listen 的 fdTCP 是字节流存在粘包问题多次发送的数据粘在一起应用层自己处理分包。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触发握手阻塞等待握手完成才返回默认阻塞 socketaccept完全不参与握手只去全连接队列拿已经建好的连接返回通信文件描述符如果全连接队列为空accept 就阻塞等待。握手整个报文流程都是操作系统内核 TCP 模块做的不是这两个函数里面写 send 发包。第 1 次握手客户端 → 服务端SYN seq客户端调用 connect()内核挑选一个本机空闲的源端口向外发送 SYN 报文SYN1同步标志请求建立连接携带客户端初始序列号 seq x客户端开启 SYN 重传定时器 SYN 丢失超时自动重传客户端状态SYN_SENT同步已发送第 2 次握手服务端 → 客户端SYN seq、ACK ack服务端收到 SYN报文把这条连接放入半连接队列服务端状态切换为 SYN_RCVD同步收到服务端内核自动回复 SYNACK我收到你的连接请求我同意建立连接我的起始序列号是 y。SYN服务端自己的初始序列号 seqyACK 确认号ack x 1我收到了你 seqx 的 SYN 报文开启半连接超时定时器超时收不到 ACK内核删除半连接队列里这条记录第 3 次握手客户端 → 服务端ACK客户端收到 SYNACK清除刚才的 SYN 重传定时器客户端状态更新为 ESTABLISHED已连接回复 ACK 报文ack y 1seq 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 次挥手客户端 → 服务端FIN1sequ客户端调用close()不再向外发送数据发送 FIN 报文。客户端状态变化ESTABLISHED → FIN_WAIT_1我已经发出关闭方向的请求了正在等服务器对这个 FIN 的 ACK 确认包第 2 次挥手服务端 → 客户端ACK1seqvacku1服务端收到 FIN回复 ACK 确认收到关闭请求。服务端状态ESTABLISHED → CLOSE_WAIT客户端收到这个 ACKFIN_WAIT_1 → FIN_WAIT_2服务器知道客户端不发数据客户端等服务器这边发 FIN(服务器告诉客户端同样不发数据)半关闭状态客户端不能再发数据但是服务端仍然可以继续向客户端发送剩下的数据这个ACK 只能确认收到关闭请求服务端还有数据要发不能立刻发 FIN所以 ACK 和 FIN 不能合并。第 3 次挥手服务端 → 客户端FIN1ACK1seqwacku1等服务端把剩余数据全部发送完毕调用close()发送 FIN 报文表示服务端这边也没有数据要发送了。服务端状态CLOSE_WAIT → LAST_ACK客户端收到这个 FINFIN_WAIT_2 → TIME_WAIT第 4 次挥手客户端 → 服务端ACK1sequ1ackw1客户端回复 ACK 确认收到服务端的 FIN。服务端收到这个 ACK直接进入 CLOSED连接消失客户端进入 TIME_WAIT 状态必须等待 2MSL 之后才变成 CLOSED二、UDPUDP 服务器流程1.socket()创建数据报套接字SOCK_DGRAM 数据报代表 UDPint fd socket(AF_INET, SOCK_DGRAM, 0);2.bind()绑定固定 IP 端口服务器必须 bind客户端一般不用固定端口让客户端知道往哪个端口发数据3.recvfrom()阻塞等待接收客户端发来的数据调用 recvfrom 阻塞接收客户端报文同时获取客户端地址数据 发送方的地址结构客户端 IP、端口4.sendto()向刚才拿到的客户端地址发回复数据5. 循环 recvfrom /sendto6.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 加入组支持跨网段IGMP01:00:5e 开头组播 MAC1.单播一对一通信数据包从一个源地址发给唯一的一个目标地址是普通 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 是广播 MACFF:FF:FF:FF:FF:FF交换机收到这个帧会把包泛洪转发给本网段所有端口特点同一子网内所有主机网卡都会收到这个帧交给内核判断端口是否监听开销大容易产生广播风暴局域网谨慎使用组播多播一对多发给一个组播组只有加入这个组的成员主机才能接收组播 IP 是 D 类 IP224.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要配置的套接字 fdUDP 套接字level选项层级SOL_SOCKET套接字层选项比如SO_BROADCAST开启套接字的广播发送权限发广播必须开这个、SO_REUSEADDR端口复用服务端快速重启解决 TIME_WAIT 占用端口问题IPPROTO_IPIP 层选项比如IP_ADD_MEMBERSHIP让本机加入指定组播组接收组播数据optname选项名字要开启 / 设置的功能optval存放参数值的指针比如开启广播就传 onon1 打开0 关闭optlenoptval 这个变量的字节长度 返回值成功返回 0失败返回 - 1
阅读完成 · 觉得有帮助?
咨询建站