先明确一个问题,如果定义了一个数据结构,大小是,比方说 32 个字节,然后 udp 客户端连续向服务端发了两个包。现在假设这两个包都已经到达了服务器,那么服务端调用 recvfrom 来接收数据,并且缓冲区开得远大于 64,例如,开了 1024 个字节,那么,服务端的 recvfrom 函数是会一次收到两个数据包呢,还是只能收到一个。 答案是只能收到一个。 来看代码: struct.h
#ifndef struct_h #define struct_h typedef struct _udp_msg { int add1; int add2; int sum; char str1[16]; char str2[16]; char cat[32]; } udp_msg; #endif /* struct_h */
服务器的代码:
#include#include #include #include #include #include #include #include "struct.h" #define max_line 1024 #define serv_port 8080 int udp_serv(); int main() { return udp_serv(); } int udp_serv() { int sockfd = socket(af_inet, sock_dgram, 0); if (sockfd == -1) { perror("socket"); return -1; } struct sockaddr_in serv_addr; memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = af_inet; serv_addr.sin_addr.s_addr = htonl(inaddr_any); serv_addr.sin_port = htons(serv_port); if (bind(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) { perror("bind"); return -1; } while (1) { printf("sleeping\n"); sleep(10); printf("akwaked\n"); char buf[bufsiz]; struct sockaddr_in cli_addr; memset(&cli_addr, 0, sizeof(cli_addr)); socklen_t cli_addr_len = sizeof(cli_addr); int recvn = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr*)&cli_addr, &cli_addr_len); printf("recv %d bytes\n", recvn); udp_msg msg; memset(&msg, 0, sizeof(msg)); printf("udp_msg size is %d\n", sizeof(msg)); memcpy(&msg, buf, sizeof(msg)); msg.sum = msg.add1 msg.add2; strcpy(msg.cat, msg.str1); strcat(msg.cat, msg.str2); printf("msg.add1 is: %d\n", msg.add1); printf("msg.add2 is: %d\n", msg.add2); printf("msg.sum is: %d\n", msg.sum); printf("msg.str1 is: %s\n", msg.str1); printf("msg.str2 is: %s\n", msg.str2); printf("msg.cat is: %s\n", msg.cat); sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&cli_addr, cli_addr_len); } return 0; }
客户端的:
#include#include #include #include #include #include #include #include #include "struct.h" #define max_line 1024 #define serv_port 8080 int udp_cli(const char* serv_ip); int main(int argc, char* argv[]) { if (argc < 2) { printf("usage: %s serv_ip\n", argv[0]); return 1; } return udp_cli(argv[1]); } int udp_cli(const char* serv_ip) { int sockfd = socket(af_inet, sock_dgram, 0); if (sockfd == -1) { perror("socket"); return -1; } struct sockaddr_in serv_addr; memset(&serv_addr, 0, sizeof(serv_addr)); serv_addr.sin_family = af_inet; serv_addr.sin_port = htons(serv_port); if (inet_pton(af_inet, serv_ip, &serv_addr.sin_addr) <= 0) { perror("inet_pton"); return -1; } while (1) { udp_msg msg; memset(&msg, 0, sizeof(msg)); msg.add1 = 1; msg.add2 = 2; scanf("%s%s", msg.str1, msg.str2); if (sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) { perror("write"); return -1; } else { printf("send success\n"); } if (sendto(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1) { perror("write2"); return -1; } else { printf("send2 success\n"); } socklen_t serv_addr_len = sizeof(serv_addr); if (recvfrom(sockfd, &msg, sizeof(msg), 0, (struct sockaddr*)&serv_addr, &serv_addr_len) == -1) { perror("read"); return -1; } printf("msg.add1 is: %d\n", msg.add1); printf("msg.add2 is: %d\n", msg.add2); printf("msg.sum is: %d\n", msg.sum); printf("msg.str1 is: %s\n", msg.str1); printf("msg.str2 is: %s\n", msg.str2); printf("msg.cat is: %s\n", msg.cat); break; } return 0; }
运行起来后,服务端的输出如下:
sleeping akwaked recv 76 bytes udp_msg size is 76 msg.add1 is: 1 msg.add2 is: 2 msg.sum is: 3 msg.str1 is: 123 msg.str2 is: 456 msg.cat is: 123456 sleeping akwaked recv 76 bytes udp_msg size is 76 msg.add1 is: 1 msg.add2 is: 2 msg.sum is: 3 msg.str1 is: 123 msg.str2 is: 456 msg.cat is: 123456 sleeping
客户端如下:
123 456 send success send2 success msg.add1 is: 1 msg.add2 is: 2 msg.sum is: 3 msg.str1 is: 123 msg.str2 is: 456 msg.cat is: 123456
这里涉及到一个边界的问题。 tcp 是流式的数据传输,消息没有边界,需要应用层自己去定义消息边界,而 udp 是数据报传输,所以协议保证了一次只能接收一个数据报。
2010-08-11 14:56 有关tcp和udp 粘包 消息保护边界在socket网络程序中,tcp和udp分别是面向连接和非面向连接的。因此tcp的socket编程,收发两端(客户端和服务器端)都要有一一成对的socket,因此,发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(nagle算法),将多次间隔较小且数据量小的数据,合并成一个大的数据块,然后进行封包。这样,接收端,就难于分辨出来了,必须提供科学的拆包机制。
对于udp,不会使用块的合并优化算法,这样,实际上目前认为,是由于udp支持的是一对多的模式,所以接收端的skbuff(套接字缓冲区)采用了链式结构来记录每一个到达的udp包,在每个udp包中就有了消息头(消息来源地址,端口等信息),这样,对于接收端来说,就容易进行区分处理了 保护消息边界和流
那么什么是保护消息边界和流呢? 保护消息边界,就是指传输协议把数据当作一条独立的消息在网上
传输,接收端只能接收独立的消息.也就是说存在保护消息边界,接收
端一次只能接收发送端发出的一个数据包.
而面向流则是指无保护消息保护边界的,如果发送端连续发送数据,
接收端有可能在一次接收动作中,会接收两个或者更多的数据包. 我们举个例子来说,例如,我们连续发送三个数据包,大小分别是2k,
4k , 8k,这三个数据包,都已经到达了接收端的网络堆栈中,如果使
用udp协议,不管我们使用多大的接收缓冲区去接收数据,我们必须有
三次接收动作,才能够把所有的数据包接收完.而使用tcp协议,我们
只要把接收的缓冲区大小设置在14k以上,我们就能够一次把所有的
数据包接收下来.只需要有一次接收动作. 这就是因为udp协议的保护消息边界使得每一个消息都是独立的.而
流传输,却把数据当作一串数据流,他不认为数据是一个一个的消息. 所以有很多人在使用tcp协议通讯的时候,并不清楚tcp是基于流的
传输,当连续发送数据的时候,他们时常会认识tcp会丢包.其实不然,
因为当他们使用的缓冲区足够大时,他们有可能会一次接收到两个甚
至更多的数据包,而很多人往往会忽视这一点,只解析检查了第一个
数据包,而已经接收的其他数据包却被忽略了.所以大家如果要作这
类的网络编程的时候,必须要注意这一点. 结论:
根据以上所说,可以这样理解,tcp为了保证可靠传输,尽量减少额外
开销(每次发包都要验证),因此采用了流式传输,面向流的传输,
相对于面向消息的传输,可以减少发送包的数量。从而减少了额外开
销。但是,对于数据传输频繁的程序来讲,使用tcp可能会容易粘包。
当然,对接收端的程序来讲,如果机器负荷很重,也会在接收缓冲里
粘包。这样,就需要接收端额外拆包,增加了工作量。因此,这个特
别适合的是数据要求可靠传输,但是不需要太频繁传输的场合(
两次操作间隔100ms,具体是由tcp等待发送间隔决定的,取决于内核
中的socket的写法) 而udp,由于面向的是消息传输,它把所有接收到的消息都挂接到缓冲
区的接受队列中,因此,它对于数据的提取分离就更加方便,但是,
它没有粘包机制,因此,当发送数据量较小的时候,就会发生数据包
有效载荷较小的情况,也会增加多次发送的系统发送开销(系统调用,
写硬件等)和接收开销。因此,应该最好设置一个比较合适的数据包
的包长,来进行udp数据的发送。(udp最大载荷为1472,因此最好能
每次传输接近这个数的数据量,这特别适合于视频,音频等大块数据
的发送,同时,通过减少握手来保证流媒体的实时性)