UDP编程

UDP协议特点

UDP:用户数据报协议,是不可靠的无连接的协议。在数据发送前,因为不需要进行连接,所以可以进行高效率的数据传输。UDP没有客户端和服务器区别,都可以作为发送端和接收端。


UDP传输


UDP的优缺点

UDP不要求保持一个连接。

UDP没有因接收方认可收到数据包而带来的开销。

UDP要求的网络带宽比TCP更小。


什么是粘包拆包?

假设客户端向服务端连续发送了两个数据包,用 packet1 和 packet2 来表示,那么服务端收到的数据可以分为三种:

第一种:服务端正常接收到这两个数据包 package1 和 package2,即没有发生拆包和粘包。
第二种:接收端只接收到一个包,由于TCP不会出现丢包的现象。所以这一个数据包中包含了发送端发送的两个数据包的信息,这种现象即为粘包。这种情况由于接收端不知道这两个数据包的界限,所以对于接收端来说很难处理。
第三种:这种情况有两种表现形式,接收端收到了两个数据包,但是这两个数据包要么是不完整的,要么就是多出来一块,这种情况即发生了拆包和粘包。(package1不完整,package2多了package1一部分;package1多了package2的一部分,package2不完整)


为什么会发生TCP粘包拆包?

TCP是面向流的的传输协议,发送端可以一次发送不定长度的数据,而接收端也可以一次提取不定长度的数据。即这种传输方式是无保护消息边界的。发送方和接收方对数据的处理都有可能引发粘包现象。

(1)发送方:
TCP为了提高传输效率,会在收集到足够多数据后才一起发送,同时一条数据太长,TCP还会将数据进行拆分发生。这将会导致三种情况发生:
多条数据被组合成一条数据发送。
长数据被拆分成片段分别发送。
长数据被拆分的片段和短数据一起发送。

(2)接收方:
接收方收到的数据会保存在缓存中,如果应用层提取数据不够快就会导致缓存中多条数据粘在一起。


TCP粘包处理方式

也不是所有时候都要去处理粘包,比如如果类似于文件传输这样发送的数据无结构,那么接收方正常接受存储就行,不必考虑分包问题。只有在TCP长链接且发送不同结构数据时(数据毫不相干,或者必须分开解读),那就要处理粘包问题了。

发送方的处理方式
可以关闭Nagle算法,通过TCP_NODELAY选项来关闭。缺点是TCP传输效率降低。
Nagle算法主要做两件事:1)只有上一个分组得到确认,才会发送下一个分组;2)收集多个小分组,在一个确认到来时一起发送。正是Nagle算法造成了发送方有可能造成粘包现象。

接收方的处理方式
TCP中接收方无法处理!

应用层的处理方式
长度编码:发送端给每个数据包添加包首部,首部中应该至少包含数据包的长度,这样接收端在接收到数据后,通过读取包首部的长度字段,便知道每一个数据包的实际长度了。

特殊字符分隔符协议:可以在数据包之间设置边界,如添加特殊符号,这样,接收端通过这个边界就可以将不同的数据包拆分开。

定长协议:发送端将每个数据包封装为固定长度(不够的可以通过补0填充),这样接收端每次从接收缓冲区中读取固定长度的数据就自然而然的把每个数据包拆分开来。


为什么UDP不会发生拆包和粘包?

TCP协议是面向流的协议,UDP是面向消息的协议。

粘包拆包问题在数据链路层、网络层以及传输层都有可能发生。日常的网络应用开发大都在传输层进行,由于UDP有消息保护边界,不会发生粘包拆包问题,因此粘包拆包问题只发生在TCP协议中。UDP每一段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据。

UDP具有保护消息边界,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样对于接收端来说就容易进行区分处理了。传输协议把数据当作一条独立的消息在网上传输,接收端只能接收独立的消息。接收端一次只能接收发送端发出的一个数据包,如果一次接受数据的大小小于发送端一次发送的数据大小,就会丢失一部分数据,即使丢失,接受端也不会分两次去接收。


服务器端/接收端

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>

int main()
{
    int ret;                                 // 返回值
    int sockfd;                              // 监听套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字(ipv4 UDP)
    if (sockfd == -1)
    {
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in my_addr;                                       // 本机的地址信息
    my_addr.sin_family = AF_INET;                                     // 协议
    my_addr.sin_port = htons(8888);                                   // 端口
    my_addr.sin_addr.s_addr = inet_addr("0");                         // 本机ip,可以直接填"0"
    ret = bind(sockfd, (struct sockaddr *)&my_addr, sizeof(my_addr)); // 绑定本机ip和端口
    if (ret == -1)
    {
        perror("bind");
        exit(-1);
    }

    struct sockaddr_in cli_addr; // 记录对方的地址信息
    int len = sizeof(cli_addr);
    char buf[256];
    while (1)
    {
        // 清空数组
        memset(buf, 0, sizeof(buf));
        // 接收数据报
        ret = recvfrom(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&cli_addr, &len);
        // 打印发送端信息
        printf("ip:%s, prot:%d\n", inet_ntoa(cli_addr.sin_addr), ntohs(cli_addr.sin_port));
        // 打印接收信息
        printf("recv:%d bytes, buf:%s\n", ret, buf);

        // 添加自己的业务逻辑
        // 解析发送端请求,根据请求给响应数据

        time_t t;                    // 定义time相关函数传参变量
        struct tm *p;                // 因为localtime函数返回的是一个结构体的地址,我们这里就需要定义一个结构体指针来接受这个返回的地址
        memset(buf, 0, sizeof(buf)); // 清空数组
        time(&t);
        p = localtime(&t);
        sprintf(buf, "%d-%d-%d %d:%d:%d\n", p->tm_year + 1900, p->tm_mon + 1, p->tm_mday, p->tm_hour, p->tm_min, p->tm_sec);

        // 发送数据报(当前时间)
        sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&cli_addr, len);
    }

    close(sockfd);
    return 0;
}

客户端/发送端

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <arpa/inet.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <time.h>

int main()
{
    int ret;
    int sockfd;                              // 监听套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字(ipv4 UDP)
    if (sockfd == -1)
    {
        perror("socket");
        exit(-1);
    }

    struct sockaddr_in ser_addr;     // 服务器地址信息
    ser_addr.sin_family = AF_INET;   // 协议
    ser_addr.sin_port = htons(8888); // 端口
    ser_addr.sin_addr.s_addr = inet_addr("192.168.2.81");

    char buf[256];
    while (1)
    {
        printf("请输入要发送的数据:");
        fgets(buf, sizeof(buf), stdin);
        buf[strlen(buf) - 1] = 0;
        if (*buf == 'Q')
        {
            printf("quit\n");
            break;
        }
        // 发送数据
        ret = sendto(sockfd, buf, sizeof(buf), 0, (struct sockaddr *)&ser_addr, sizeof(ser_addr));
    }

    close(sockfd);

    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值