Linux 网络编程(四)——基于UDP的服务器端/客户端

文章目录

4 基于UDP的服务器端/客户端

4.1 UDP套接字的特点

4.2 UDP协议适用场景

4.3 基于UDP数的I/O函数

4.4 基于UDP的回声服务器端/客户端

4.5 UDP的数据传输特性

4.6 已连接UDP套接字与未连接UDP套接字(可略过)


4 基于UDP的服务器端/客户端

4.1 UDP套接字的特点

  1. 通过寄信来说明 UDP 的工作原理,它与 UDP 的特点完全相同。寄信前应先在信封上填好寄信人和收信人的地址,之后贴上邮票放进邮筒即可。当然,信件的特点使我们无法确认信件是否被收到。邮寄过程中也可能发生信件丢失的情况。也就是说,信件是一种不可靠的传输方式,UDP 也是一种不可靠的数据传输方式。
  2. 如果只考虑可靠性,TCP 的确比 UDP 好但 UDP 在结构上比 TCP 更简洁。UDP 不会发送类似ACK的应答消息,也不会像SEQ那样给数据包分配序号。因此,UDP的性能有时比TCP高出很多。编程中实现UDP也比TCP简单。另外,UDP的可靠性虽比不上TCP,但也不会像想象中那么频繁地发生数据损毁。因此,在更重视性能而非可靠性的情况下,UDP是一种很好的选择。
  3. TCP在不可靠的 IP 层进行流控制,而 UDP 就缺少这种流控制机制。TCP 与 UDP 的区别很大一部分来源于流控制,也就是说 TCP 的生命在于流控。

4.2 UDP协议适用场景

虽然大部分网络编程都基于TCP实现,但也有一些是基于UDP实现的。网络传输特性导致信息丢失频发,可若要传递压缩文件(发送1万个数据包时,只要丢失1个就会产生问题),则必须使用TCP,因为压缩文件只要丢失一部分就很难解压。但通过网络实时传输视频或音频时的情况有所不同。对于多媒体数据而言,丢失一部分也没有太大问题,这只会引起短暂的画面抖动,或出现细微的杂音。但因为需要提供实时服务,速度就成为非常重要的因素。但 UDP 并非每次都快于 TCP,TCP 比 UDP 慢的原因通常有以下两点:

  • 收发数据前后进行的连接设置及清除过程(三次握手、四次挥手)
  • 收发过程中为保证可靠性而添加的流控制(超时重传、应答机制、校验和、滑动窗口等)

4.3 基于UDP数的I/O函数

UDP 通讯也使用 socket,但是接收和发送的函数与 TCP 不一样。由于 UDP 不存在握手这一步骤,所以在绑定地址之后,服务端不需要 listen,客户端也不需要 connect,服务端同样不需要 accept。只要服务端绑定以后,就可以相互发消息了,由于没有握手过程,两端都不能确定对方是否收到消息,这也是UDP协议不如TCP协议可靠的地方。

创建好 TCP 套接字后,收发数据时无需在添加地址信息(send、recv)。因为 TCP 套接字将保持与对方套接字的连接。换言之,TCP 套接字直到目标地址信息。但 UDP 套接字不会保持连接状态,因此每次传输数据都要添加目标地址信息(sendto、recvfrom)。

(1)发送数据 sendto

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                const struct sockaddr *dest_addr, socklen_t addrlen);
/**
* 向指定地址发送缓冲区中的数据(一般用于UDP模式)
* sockfd:用于传输数据的UDP套接字文件描述符
* buf:保存待传输数据的缓冲区地址
* len:待传输数据长度,以字节为单位
* flags:传输控制标志,通常为0,表示不使用特殊行为
* dest_addr:可以填NULL,如果不是NULL,填对方的sockaddrin地址
* addrlen:传递给参数dest_addr的地址值结构体长度
* return:成功返回传输字节数,失败返回-1
*/

(2)接收数据 recvfrom

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                    struct sockaddr *src_addr, socklen_t *addrlen);
/**
* 将接收到的消息放入缓冲区buf中
* sockfd:用于传输数据的UDP套接字文件描述符
* buf:保存待传输数据的缓冲区地址
* len:待传输数据长度,以字节为单位
* flags:传输控制标志,通常为0,表示不使用特殊行为
* src_addr:可以填NULL,如果不是NULL,则填对方sockaddrin的地址
* addrlen:传递给参数src_addr的地址值结构体长度
* return:成功返回传输字节数,失败返回-1
*/

4.4 基于UDP的回声服务器端/客户端

(1)服务器端 uecho_server.c

#define handle_error(cmd,result)    \
    if(result < 0)                  \
    {                               \
        perror(cmd);                \
        return -1;                  \
    }                               \

int main(int argc, char const *argv[])
{
    struct sockaddr_in serv_addr,clnt_addr;
    char *buf = malloc(sizeof(char) * 1024);
    memset(&serv_addr,0,sizeof(serv_addr));
    memset(&clnt_addr,0,sizeof(clnt_addr));
    if(argc != 2) {
        printf("Usage:%s <port>\n",argv[0]);
        exit(1);
    }
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(atoi(argv[1]));
    inet_pton(AF_INET,"0.0.0.0",&serv_addr.sin_addr);
    //为了创建UDP套接字,向socket函数第二个参数传递SOCK_DGRAM
    int serv_sock = socket(AF_INET,SOCK_DGRAM,0);
    handle_error("socket",serv_sock);
    int temp=bind(serv_sock,(struct sockaddr*)&serv_addr,sizeof(serv_addr));
    handle_error("bind",temp);

    socklen_t client_len = sizeof(clnt_addr);
    do {
        memset(buf,0,sizeof(buf));
        //UDP协议接收到客户端发送过来的数据后,系统会记录客户端的IP地址和端口号到结构体中
        int str_len=recvfrom(serv_sock,buf,1024,0,(struct sockaddr*)&clnt_addr,&client_len);
        printf("接收到客户端%s %d 信息:%s",inet_ntoa(clnt_addr.sin_addr),
                                ntohs(clnt_addr.sin_port),buf);
        sendto(serv_sock,buf,str_len,0,(struct sockaddr*)&clnt_addr,sizeof(clnt_addr));
    } while(1);
    close(serv_sock);
    free(buf);
    return 0;
}

注:为了创建UDP套接字,向socket函数第二个参数传递 SOCK_DGRAM

(2)客户端 uecho_client.c

#define handle_error(cmd,result)    \
    if(result < 0)                  \
    {                               \
        perror(cmd);                \
        return -1;                  \
    }                               \

int main(int argc, char const *argv[])
{
    struct sockaddr_in serv_addr,clnt_addr;
    char* buf = malloc(sizeof(char)*1024);
    memset(&serv_addr,0,sizeof(serv_addr));
    memset(&clnt_addr,0,sizeof(clnt_addr));
    if(argc != 3) {
        printf("Usage:%s <IP> <port>\n",argv[0]);
        exit(1);
    }
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(atoi(argv[2]));
    inet_pton(AF_INET,argv[1],&serv_addr.sin_addr);

    int clnt_sock = socket(AF_INET,SOCK_DGRAM,0);
    handle_error("socket",clnt_sock);

    socklen_t serv_len = sizeof(serv_addr);
    do
    {
        memset(buf,0,sizeof(buf));
        fputs("Input message(Q to quit):",stdout);
        fgets(buf,1024,stdin);
        if(!strcmp(buf,"q\n") || !strcmp(buf,"Q\n"))
            break;
        sendto(clnt_sock,buf,strlen(buf),0,(struct sockaddr *)&serv_addr,sizeof(serv_addr));
        recvfrom(clnt_sock,buf,1024,0,(struct sockaddr *)&serv_addr,&serv_len);
        printf("Message from server:%s",buf);
    } while (1);
    close(clnt_sock);
    free(buf);
    return 0;
}

(3)测试结果

(4)UDP客户端套接字的地址分配

如果仔细观察 UDP 客户端就会发现缺少了把IP和端口分配给套接字的过程。TCP 客户端调用 connect 函数自动完成此过程,而 UDP 中连能承担相同功能的函数调用语句都没有。究竟在什么时候分配IP和端口号呢?

在 UDP 程序中,在首次调用 sendto 函数时给相应套接字自动分配 IP 和端口号(IP 用主机IP,端口号用未选用的任意端口号),此时分配的地址一直保留到程序结束为止,该地址可以用来和其他 UDP 套接字进行数据交换。因此,UDP客户端通常无需额外的地址分配。

4.5 UDP的数据传输特性

上一章节说过TCP数据传输不存在数据边界,这表示数据传输过程中调用 I/O 函数的次数不具有任何意义。相反,UDP 是具有数据边界的协议,因此,输入函数的调用次数应和输出函数调用次数完全一致,这样才能保证接收全部已经发送的数据。例如,调用 3 次输出函数发送的数据必须通过调用 3 次输入函数才能接收完。下面通过简单示例进行验证

(1)bound_host1.c

int main(int argc, char const *argv[])
{
    struct sockaddr_in my_addr,your_addr;
    char* message = malloc(sizeof(char)*1024);
    memset(&my_addr,0,sizeof(my_addr));
    memset(&your_addr,0,sizeof(your_addr));
    if(argc != 2) {
        printf("bufUsage:%s <port>\n",argv[0]);
        exit(1);
    }
    my_addr.sin_family = AF_INET;
    my_addr.sin_port = htons(atoi(argv[1]));
    inet_pton(AF_INET,"0.0.0.0",&my_addr.sin_addr);
    int serv_sock = socket(AF_INET,SOCK_DGRAM,0);
    handle_error("socket",serv_sock);
    int temp = bind(serv_sock,(struct sockaddr*)&my_addr,sizeof(my_addr));
    handle_error("bind",temp);

    socklen_t your_addr_len = sizeof(your_addr);
    for(int i = 0;i < 3;i++) {
        memset(message,0,sizeof(message));
        sleep(5);
        recvfrom(serv_sock,message,1024,0,
                    (struct sockaddr *)&your_addr,&your_addr_len);
        printf("接收到客户端%s %d 信息第%d条消息:%s\n",
                                    inet_ntoa(your_addr.sin_addr),
                                    ntohs(your_addr.sin_port),i+1,message);
    }
    close(serv_sock);
    free(message);
    return 0;
}

上述示例中需要特别留意的是 for 语句。循环中调用了 sleep 函数,使程序停顿时间等于传递来的时间(以秒为单位)。也就是说,循环中每隔5秒调用1次 recvfrom 函数。

(2)bound_host2.c

int main(int argc, char const *argv[])
{
    struct sockaddr_in your_addr;
    char msg1[] = "Hi";
    char msg2[] = "I'm another UDP host!";
    char msg3[] = "Nice to meet you";
    memset(&your_addr,0,sizeof(your_addr));
    if(argc != 3) {
        printf("Usage:%s <IP> <port>\n",argv[0]);
        exit(1);
    }
    your_addr.sin_family = AF_INET;
    your_addr.sin_port = htons(atoi(argv[2]));
    inet_pton(AF_INET,argv[1],&your_addr.sin_addr);

    int clnt_sock = socket(AF_INET,SOCK_DGRAM,0);
    handle_error("socket",clnt_sock);

    socklen_t serv_len = sizeof(your_addr);
    sendto(clnt_sock,msg1,sizeof(msg1),0,(struct sockaddr *)&your_addr,sizeof(your_addr));
    sendto(clnt_sock,msg2,sizeof(msg2),0,(struct sockaddr *)&your_addr,sizeof(your_addr));
    sendto(clnt_sock,msg3,sizeof(msg3),0,(struct sockaddr *)&your_addr,sizeof(your_addr));
    close(clnt_sock);
    return 0;
}

bound_host2.c 程序 3 次调用 sendto 函数传输数据,bound_host1.c 则调用 3 次 recvfrom 函数接收数据。recvfrom 函数调用间隔为 5 秒,因此,调用 recvfrom 函数之前就已经调用了 3 次 sendto函数。也就是说,此时数据已经传输到 bound_host1.c。如果是 TCP 程序,这时只需调用 1 次输入函数即可读人数据。UDP 则不同,在这种情况下也需要调用 3 次 recvfrom 函数接收。

(3)测试结果

4.6 已连接UDP套接字与未连接UDP套接字(可略过)

TCP 套接字中需注册待传输数据的目标IP和端口号,而在 UDP 中无需注册。因此通过 sendto 函数传输数据的过程大概可以分为以下 3 个阶段:

  1. 向 UDP 套接字注册目标 IP 和端口号
  2. 传输数据
  3. 删除 UDP 套接字中注册的目标地址信息

每次调用 sendto 函数时重复上述过程。每次都变更目标地址,因此可以重复利用同一 UDP 套接字向不同目标传递数据。这种未注册目标地址信息的套接字称为未连接套接字,反之,注册了目标地址的套接字称为连接套接字。显然,UDP 套接字默认属于未连接套接字。当一台主机向另一台主机传输很多信息时,上述的三个阶段中,第一个阶段和第三个阶段占整个通信过程中近三分之一的时间,缩短这部分的时间将会大大提高整体性能。

sock = socket(AF_INET, SOCK_DGRAM, 0);
memset(&adr, 0, sizeof(adr));
adr.sin_family = AF_INET;
adr.sin_addr.s_addr = inet_addr(argv[1]);
adr.sin_port = htons(atoi(argv[2]));
connect(sock, (struct sockaddr *)&adr, sizeof(adr));

上述代码看似与 TCP 套接字创建过程一致,但 socket 函数的第二个参数分是 SOCK_DGRAM 。也就是说,创建的的确是 UDP 套接字。当然 UDP 调用 connect 函数并不是意味着要与对方 UDP 套接字连接,这只是向 UDP 套接字注册目标IP和端口信息。之后就与 TCP 套接字一致,每次调用 sendto 函数时只需传递信息数据。不仅可以使用 sendto、recvfrom 函数,还可以使用 write、read 函数进行通信。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值