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~~~

  • 相关阅读:
    毕业面试试题汇总
    js获取系统日期
    非常酷的3D翻转相册展示特效
    CSS 替换元素和非替换元素 行内非替换元素
    怎样在linux下编写C程序并编译执行
    库和框架的区别
    转载:em(倍)与px的区别
    RPMForge介绍及安装
    linux下安装jdk和配置环境变量
    PCI PCI-X PCI-E介绍
  • 原文地址:https://www.cnblogs.com/webor2006/p/4096380.html
Copyright © 2011-2022 走看看