Unix网络编程之UDP常见缺陷与实例

UDP与TCP相比,各有优缺点,下来来列举一下UDP的缺点:

1.UDP是一种不可靠的协议(缺乏流量控制)

实例代码:

//server.c

#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>

int main()
{
    int sockfd;
    struct sockaddr_in servaddr,clientaddr;
    char recv[1024];
    socklen_t len;
    int count = 0;   

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
   
    memset(&servaddr, '\0', sizeof(servaddr));
    memset(recv, '\0', 1024);
    servaddr.sin_family = AF_INET;
    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    servaddr.sin_port = htons( 5000 );

    bind(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));

    for( ; ; )
    {
        recvfrom(sockfd, recv, 1024, 0, (struct sockaddr *)&clientaddr, &len);
        memset(recv, '\0', 1024);
    count++;
    printf("count = %d\n",count);
    }

    close(sockfd);
}

//client.c

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main()
{
    int sockfd;
    struct sockaddr_in servaddr;
    char buff[1024];
    int count = 0;

    memset(&servaddr, '\0', sizeof(servaddr));
    memset(buff, '\0', 1024);

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons( 5000 );
    servaddr.sin_addr.s_addr = inet_addr("192.168.59.129");  
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
   
    for(count; count < 4000; count++)
    {
         sendto(sockfd, buff, 1024, 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
    }

    close(sockfd);
    return 0;
}

客户端快速给服务器发送4000个数据报,然后在服务器端统计接收到的数据报的个数,发现接收到的数据报数量小于4000,这是由于UDP并不是一种可靠的协议,一旦发生丢包等情况,没有重发等机制来应对这种情况。上面程序中出现的情况主要是由于服务器端的接收区缓冲区溢出导致的,一旦缓冲区溢出,后续的数据报就被丢弃。

针对上面这种情况,我们有两种方式来处理:

1.通过socket选项来修改接收缓冲区的大小,以容纳更多的数据报(通过setsockopt函数来设置)

2.发送端每次发送一个数据报以后,暂停一段时间(即不要连续发送大量的数据报,可以使用usleep,sleep等函数来挂起进程)

但是上面两种方式只能用来缓解这种丢包的情况,而不能消除,这是UDP本身的机制决定的。


2.UDP是一种无连接协议

由于UDP是无连接的,因此即使服务器端没有程序在运行,客户端仍然可以运行客户端程序:

//client.c

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main()
{
    int sockfd;
    struct sockaddr_in servaddr;
    char recv[1024];
    char send[1024];
    int len;

    memset(&servaddr, '\0', sizeof(servaddr));
    memset(recv, '\0', 1024);
    memset(send, '\0', 1024);

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons( 5000 );
    servaddr.sin_addr.s_addr = inet_addr("192.168.59.129");   
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    
    sendto(sockfd, send, 1024, 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
    printf("stop at sendto\n");
    recvfrom(sockfd, recv, 1024, 0, (struct sockaddr *)&servaddr, &len);
    printf("stop at recvfrom\n");

    close(sockfd);
    return 0;
}

我们打开tcpdump工具来查看数据报的发送情况,然后运行客户端程序,可以看到客户端程序正常运行,但是阻塞在了recvfrom函数,并且通过tcpdump看到了如下的情况:

也就是说,数据在传输的过程中其实发生了错误(上例中是端口不可达错误,这是一个ICMP报文),但是客户端程序并不知道有错误发生,仍然阻塞在recvfrom等待数据返回,这样就不利于我们快速定位错误。

那么有没有什么办法来得到错误信息呢?
—使用已连接的UDP套接口能够得到错误信息。

Unix网络编程这本书上面说对于已连接的UDP套接口,不能给输出操作指定IP地址和端口号,即有如下两种方式:
1.使用write函数代替sendto函数
2.仍然使用sendto函数,但是不能指定IP地址与端口,即sendto函数的第五个参数必须为空指针(个人觉得这种说法太绝对)

但实际上经过试验,sendto中的第五个参数仍然可以指定IP地址与端口,只不过需要与connect函数的第一个参数保持一致(即由connect中的参数为准)。

//client.c

#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>

int main()
{
    int sockfd;
    struct sockaddr_in servaddr;
    char recv[1024];
    char send[1024];
    int len;

    memset(&servaddr, '\0', sizeof(servaddr));
    memset(recv, '\0', 1024);
    memset(send, '\0', 1024);

    servaddr.sin_family = AF_INET;
    servaddr.sin_port = htons( 5000 );
    servaddr.sin_addr.s_addr = inet_addr("192.168.59.129");   
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
   
    connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr));
    sendto(sockfd, send, 1024, 0, (struct sockaddr *)&servaddr, sizeof(servaddr));
    printf("stop at sendto\n");
    recvfrom(sockfd, recv, 1024, 0, (struct sockaddr *)&servaddr, &len);
    printf("stop at recvfrom\n");
    printf("the imformation of error is %s\n", recv);

    close(sockfd);
    return 0;
}
该例与上一个例子相比多出了两条语句,但是在Ubuntu下运行发现,这个程序并没有阻塞在recvfrom函数上,因为通过connect建立连接,应用程序已经能够得到错误了,但是最后打印recv并没有打印出任何错误信息,这个与内核是否把ICMP消息反送给UDP套接口有关。我在Ubuntu下进行试验发现并没有返回这个ICMP消息。

注意:
如果是在TCP程序中调用connect函数,那么服务器端未运行程序,connect会直接返回错误(因为三次握手在此时建立),但是在UDP中则不会返回错误。


几个要注意的问题:
1.一般来说,在客户端我们并不指定ip和端口号,在这种情况下,端口号有内核临时分配( 分配端口在第一次调用sendto函数时即确定,随后不能改变)。但是如果客户端有多个IP地址,那么客户端发送的每个UDP数据报的源IP地址可能就会不同。
2.如果客户端通过bind函数绑定了一个ip地址,但是内核决定使用另外一个数据链路将数据报发出,那么IP数据报将包含一个不同于外出链路IP地址的源IP地址。
3.在实际应用中,如果存在这样源IP地址变化的IP数据报,要如何进行识别他们属于同一个客户端?

你可能感兴趣的:(unix网络编程)