发布时间:2025-12-09 21:33:15 浏览次数:4
参照上篇一起阅读更佳:网络协议之TCP/IP协议包
为什么TCP有粘包,UDP没有粘包?
TCP是面向流连接,数据的“粘包”问题:客户端发送的多个数据包使用了优化算法(Nagle算法),将多次间隔较小、数据量小的数据包,合并成一个大的数据包发送(把发送端的缓冲区填满一次性发送);接收端底层会把TCP段整理排序交给缓冲区,这样接收端应用程序从缓冲区取数据就只能得到整体数据而不知道怎么拆分。也称数据的无边界性,read()/recv()函数不知道数据包的开始或结束标志(实际上也没有任何开始或结束标志),只把它们当做连续的数据流来处理。
UDP协议是面向数据包协议,不会使用合并优化算法。接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的UDP包,在每个UDP包中就有了消息头(消息来源地址,端口等信息) ,这样对于接收端来说,就容易进行区分处理了。
所以每次发送UDP数据包都是一条消息,它有明显的边界保护,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据(不像TCP协议在recv/read可以指定大小,读取几次把缓冲区读完或者一次读完几次消息)。这样接收端很容易区分处理,传输协议把数据当作一条独立的消息在网上传输,接收端只能接收独立的消息。也就是说,发送端send了几次,接收端必须recv几次。接收端一次只能接收发送端发出的一个数据包,如果一次接受数据的大小小于发送端一次发送的数据大小,就会丢失一部分数据,即使丢失,接受端也不会分两次去接收。
// client.c#include<stdio.h>#include<string.h>#include<stdlib.h>#include<sys/types.h>#include<sys/socket.h>#include<netinet/in.h>#include<arpa/inet.h>#include <unistd.h>#define LENGTH 24int main(){int sock=socket(AF_INET,SOCK_DGRAM,0);if(sock<0){perror("socket");return 2;}struct sockaddr_in server;server.sin_family=AF_INET;server.sin_port=htons(8888);server.sin_addr.s_addr=inet_addr("101.132.100.196");char buf[LENGTH];int i =0;for(;i < LENGTH;i++){buf[i] = 'a';}struct sockaddr_in peer;int cnt = 0 ;while(1){socklen_t len=sizeof(peer);//printf("Please Enter# ");//fflush(stdout);//ssize_t s=read(0,buf,sizeof(buf)-1);//if(s>0)//{// buf[s-1]=0;int ret = sendto(sock,buf,strlen(buf),0,(struct sockaddr*)&server,sizeof(server));printf(" ========> ret:%d\n",ret);//}if(cnt == 0){break;}cnt++;}close(sock);return 0;} // server.c#include <sys/socket.h>#include <sys/types.h>#include <errno.h>#include <string.h>#include <netinet/in.h>#include <arpa/inet.h>#include <stdio.h>#include <unistd.h>#define LENGTH 12int main(){int fd = socket(AF_INET, SOCK_DGRAM, 0);if( fd < 0 ) {printf("socket error: %s\n", strerror(errno));return 1;}struct sockaddr_in srvaddr;socklen_t len = sizeof(srvaddr);bzero(&srvaddr, len);srvaddr.sin_family = AF_INET;srvaddr.sin_port = htons(8888); // need open 8888 of portsrvaddr.sin_addr.s_addr = INADDR_ANY; int ret = bind(fd, (struct sockaddr *)&srvaddr, sizeof(srvaddr));if( ret < 0 ) {printf("bind error: %s\n", strerror(errno));return 1;}char buf[LENGTH];ssize_t size;printf("ready to recv\n");while(1){bzero(buf, LENGTH);struct sockaddr_in addr;socklen_t slen = sizeof(addr);size = recvfrom(fd, buf, LENGTH, 0, (struct sockaddr *)&addr, &slen);if( size < 0 ){printf("recvfrom error: %s\n", strerror(errno));return 1;}printf("size:%d, len:%d\n",size,strlen(buf));printf("%s \n", buf);printf("recvfrom ip: %s, port: %d\n", inet_ntoa(addr.sin_addr), ntohs(addr.sin_port));}close(fd);return 0;}可以看到,client发送24+1个字节数据,但server第一次recvfrom只read 12个字节数据,再次recvfrom便阻塞。
如果如果网络接口的MTU是1500字节,Client发送一个8000字节大小的UDP包,那么Server端阻塞模式下接包,在不丢包的情况下,recvfrom(9000)是收到1500,还是8000。如果某个IP分片丢失了,recvfrom(9000),又返回什么呢?
所以,使用udp一定要控制好每个包的大小,太大的话,会导致ip层分片,降低效率。
注意tcp长度超过mss也会分片,但是在tcp层分片,效率比ip层高。
UDP数据包大小由IP层的头部16位总长度影响,所以UDP数据包最大长度是
2^16 - 20(IP头部长度) - 8(UDP头部长度) = 65507所以,数据包大小超过65507就会在发送的时候直接报错。所以数据包超过65507字节,需要应用层自己分片处理发送。
将上述代码中LENGTH修改如下:
//client.c#define LENGTH 65507 // server.c#define LENGTH 65535如上面所示,发送65507个字节数据可以成功发送。但是若发送字节为65508则client程序在sendto时直接返回-1报错。
综上,即使UDP数据包理论值时65507+8字节,但是实际还是不要超过MTU。