zoukankan      html  css  js  c++  java
  • linux网络编程之socket编程(十三)

    今天继续学习socket编程,从今天起开始学习UDP,具体内容如下:

    ①、无连接

    UDP协议它内部并没有维护端到端的一些连接状态,这跟TCP是不同的,TCP是基于连接的,而在连接的时候是需要进行三次握手,而UDP是不需要的。

    ②、基于消息的数据传输服务

    对于TCP而言,它是基于流的数据传输服务,而在编程时,会遇到一个粘包问题,是需要我们进行处理的,而对于UDP来说不存在粘包问题,因为它是基于消息的数据传输服务,我们可以认为,这些数据包之间是有边界的,而TCP数据包之间是无边界的。

    ③、不可靠

    这里面的不可靠主要表现在数据报可能会丢失,还可能会重复,还可能会乱序,以及缺乏流量控制,

    ④、一般情况下UDP更加高效。

     这里提到了“一般情况”~

     首先先看一下它的流程示意图:

    从图中可以看出,相对TCP而言,要简单不少~

    下面就用编码的方式来认识一下UDP,在正式编码前,先看一下整个程序都需要用到哪些函数:

    下面正式一步步来实现它:

    服务端echosrv.c:

    首先第一个步骤是创建套接字:

    第二个步骤:初使化地址,并绑定套接口:

    相比TCP,UDP当绑定之后,并不需要监听,而可以直接接收客户端发来的消息了,所以接下来这一步是回射服务器:

    接下来,来利用recvfrom、sendto两个函数来实现回射服务器的内容,首先来看一下recvfrom的函数原形:

    如果成功接收消息后,接着得将消息用sendto发回给客户端,来看下它的函数原型:

    具体代码如下:

    好了,接着编写客户端:echocli.c:

    所以,接下来开始编写回射客户端的代码:

    接下来接收从服务端回显过来的数据:

    从以上代码的编写过程中,可以很直观的感受到UDP代码要比TCP代码简洁得多,下面编译运行一下:

    可见一切运行正常,另外,请问下,客户端并没有与服务器端建立连接,也就是调用connect,那客户端是什么时候与服务端绑定的呢?

    是在第一次sendto的时候就会绑定一个地址,也就是这句话:

    对于sock而言,它有两个地址:

    本地地址(也就是上图中说到的本地地址):可以通过getsockname来获取。

    远程地址:可以通过getpeername来获取。

    当第一次绑定成功之后, 之后就不会再次绑定了,关于UDP简单的实现就到这,贴上完整代码:

    echosrv.c:

    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    
    #define ERR_EXIT(m) 
            do 
            { 
                    perror(m); 
                    exit(EXIT_FAILURE); 
            } while(0)
    
    
    void echo_srv(int sock)
    {
        char recvbuf[1024] = {0};
        struct sockaddr_in peeraddr;
        socklen_t peerlen;
        int n;
        while (1)
        {
            peerlen = sizeof(peeraddr);
            memset(recvbuf, 0, sizeof(recvbuf));
            n = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, (struct sockaddr*)&peeraddr, &peerlen);
            if (n == -1)
            {
                if (errno == EINTR)
                    continue;
                
                ERR_EXIT("recvfrom");
            }
            else if (n > 0)
            {
                fputs(recvbuf, stdout);
                sendto(sock, recvbuf, n, 0, (struct sockaddr*)&peeraddr, peerlen);
            }
        }
    
        close(sock);
    }
    
    int main(void)
    {
        int sock;
        if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
            ERR_EXIT("socket");
    
        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(5188);
        servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
    
        if (bind(sock, (struct sockaddr*)&servaddr, sizeof(servaddr)) < 0)
            ERR_EXIT("bind");
    
        echo_srv(sock);//回射服务器 
    
        return 0;
    }

    echocli.c:

    #include <unistd.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #include <stdlib.h>
    #include <stdio.h>
    #include <errno.h>
    #include <string.h>
    
    #define ERR_EXIT(m) 
            do 
            { 
                    perror(m); 
                    exit(EXIT_FAILURE); 
            } while(0)
    
    void echo_cli(int sock)
    {
        struct sockaddr_in servaddr;
        memset(&servaddr, 0, sizeof(servaddr));
        servaddr.sin_family = AF_INET;
        servaddr.sin_port = htons(5188);
        servaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
    
        int ret;
        char sendbuf[1024] = {0};
        char recvbuf[1024] = {0};
        while (fgets(sendbuf, sizeof(sendbuf), stdin) != NULL)
        {        
            sendto(sock, sendbuf, strlen(sendbuf), 0, (struct sockaddr*)&servaddr, sizeof(servaddr));
            ret = recvfrom(sock, recvbuf, sizeof(recvbuf), 0, NULL, NULL);
            if (ret == -1)
            {
                if (errno == EINTR)
                    continue;
                ERR_EXIT("recvfrom");
            }
    
            fputs(recvbuf, stdout);
            memset(sendbuf, 0, sizeof(sendbuf));
            memset(recvbuf, 0, sizeof(recvbuf));
        }
    
        close(sock); 
    }
    
    int main(void)
    {
        int sock;
        if ((sock = socket(PF_INET, SOCK_DGRAM, 0)) < 0)
            ERR_EXIT("socket");
    
        echo_cli(sock);
    
        return 0;
    }

    下面来看一下它的一些注意点。

    ①、UDP报文可能会丢失、重复

    针对数据可能会丢失,发送端就要启动一个定时器,在超时时间到的时候要重传,要有一个超时的处理机制,对于接收方也是一样的,如果对等方发送的数据丢失了,接收方也会一直阻塞,也应该要有这种超时的机制;针对发送的数据可能会重复,所以应用层应该要维护数据报之间的序号,也就是第二条提到的。

    ②、UDP报文可能会乱序

    需要维护数据报之间的序号。

    ③、UDP缺乏流量控制

    UDP有对应自己的一个缓冲区,当缓冲区满的时候,如果再往里面发送数据,并不是将数据给丢失掉,而是将数据覆盖掉原来的缓冲区,并没有像TCP那样的滑动窗口协议,达到流量控制的目的,其实可以模拟TCP滑动窗口协议来实现流量控制的目的。

    ④、UDP协议数据报文截断

    如果接收到数据报大于我们接收的缓冲区,那么数据报文就会被截断,那些数据就已经被丢弃了,这边可以用一个例子来演示下,为了简单起见,客户端与服务端写在同一个文件中:

    编译运行:

    由于UDP是基于报式套接口,而不是基于流的,也就是说UDP只会接收对应大小的数据,其余的数据会从缓冲区中清除,由此可见,它不会产生粘包问题。

    ⑤、recvfrom返回0,不代表连接关闭,因为udp是无连接的。

    当我们发送数据时,不发送任何一个字节的数据,返回值就是0。

    ⑥、ICMP异步错误

    下面用一个实验场景来说明下,就是我们不启动服务端,而只是启动客户端,然后发送数据,这时会有什么反应呢?

    从结果来看,客户端阻塞了,并没有捕捉到对等方没有启动的信息,那这现象跟“ICMP异步错误”有什么关系呢?

    结合代码来分析:

    所以这时就称之为异步的ICMP错误,按正常的情况下是需要在recvfrom才会被通知到,而在服务端没有开启时,不应该sendto成功,但是由于sendto只是完成了一个数据的拷贝,所以错误延迟到recvfrom的时候才能够被通知,而这时recvfrom其实也不能够被通知的,因为TCP规定这种ICMP错误是不能够返回给未连接的套接字的,所以说也得不到通知,recvfrom会一直阻塞,如果说能够收到错误通知,那肯定会退出了,因为代码已经做了错误判断,如下:

    那如何解决此问题呢?采用下面这个方法:UDP connect。

    ⑦、UDP connect

    其实UDP也是能调用connect的,在客户端加入如下代码,看是否解决了上面的问题,能够收到对等方未启动的错误呢?

    下面来看下结果:

    可见,在服务端没有开启的情况下,客户端这次收到了ICMP异步错误通知,通知是在recvfrom中返回的,因为此时的sock是已连接的套接字了,这就是UDP connect的一个作用,那UDP connect是否真的意味着建立了跟TCP一样的连接呢?肯定不是这样的,UDP在调connect的时候,并不会调TCP的三次握手操作,并没有跟对方传递数据,它仅仅只是维护了一个信息,这个sock跟对方维护了一种状态,通过这个套接字能够发送数据给对等方,而且只能够发送给对等方,实际上也就是该sock中的远程地址得到了绑定,那么这种sock就不能够发送给其它地址了,另外一点,一旦连接成功之后,客户端的sendto代码可以进行下面的改装:

    编译运行:

    可见效果一样,正常收发,另外,当sock是已连接套接口时,sendto也可以改用send函数来进行发送,改装如下:

    这时就不演示了,效果是一样的,同样可以正常收发,可以UDP的connect的TCP的connect意义是不一样的。

    ⑧、UDP外出接口的确定

     

    好了,关于UDP的初入认识就先学到这,之后会再次慢慢研究,愉快的周末又要结束了,好好睡一觉迎接全新的一周,goodbye~~~

  • 相关阅读:
    POJ 1659 Frogs' Neighborhood
    zoj 2913 Bus Pass(BFS)
    ZOJ 1008 Gnome Tetravex(DFS)
    POJ 1562 Oil Deposits (DFS)
    zoj 2165 Red and Black (DFs)poj 1979
    hdu 3954 Level up
    sgu 249 Matrix
    hdu 4417 Super Mario
    SPOJ (BNUOJ) LCM Sum
    hdu 2665 Kth number 划分树
  • 原文地址:https://www.cnblogs.com/webor2006/p/4096380.html
Copyright © 2011-2022 走看看