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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

webor2006

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值