第六章:基于UDP的服务器端/客户端

第六章:基于UDP的服务器端/客户端

6、1 理解UDP:

  • 数据交换过程可以分为通过TCP套接字完成的TCP方式和通过UDP套接字完成的UDP方式。

  • UDP套接字的特点:

    • UDP提供的是不可靠的数据传输服务。无法确认对方是否收到,也有可能发生数据丢失的情况。
    • UDP结构上比TCP更简洁。UDP不会发送ACK的应答信息,也不会像SEQ那样给数据分配序号。UDP的性能比TCP高出很多。在更重视性能而非可靠性的情况下,UDP是一种很好的选择。
    • 流控制是区分UDP和TCP的最重要标志。TCP的生命在于流控制。
    • TCP的速度无法超过UDP,在交换的数据量越大,TCP的传输速率就越接近UDP的传输速率。
  • UDP内部工作原理:
    在这里插入图片描述

    • IP的作用就是让离开主机B的UDP数据包准确的传递到主机A。把UDP包最终交给主机A的某一UDP套接字的过程则是由UDP完成的。UDP最重要的作用就是根据端口号将传到主机的数据包交付给最终的UDP套接字。
    • TCP比UDP慢的原因:
      • 收发数据前后进行的连接设置及清除过程。
      • 收发数据过程中为保证可靠性而添加的流控制。
    • 收发数据量小但需要频繁连接时,UDP比TCP更高效。建议深入学习TCP/IP协议的内部构造。

6、2 实现基于UDP的服务器端/客户端

  • UDP中的服务器端和客户端没有连接:

    • UDP无需经过连接过程,不必调用TCP连接过程中调用的listen函数和accept函数,UDP中只有创建套接字的过程和数据交换过程。
  • UDP服务器端和客户端均只需1个套接字:

    • TCP中,套接字之间是一一对应的,要向10个客户端提供服务,除了守门的服务器套接字,还需要10个服务器端套接字,UDP中,服务器端和客户端都只需要1个套接字。只需要1个UDP套接字就可以向任意主机传输数据。
      在这里插入图片描述

    • 图示展示了1个UDP套接字和2个不同主机交换数据的过程,只需要1个UDP套接字就能和多台主机通信。

  • 基于UDP的数据I/O函数:

    • 创建好TCP套接字后,传输数据无需添加地址信息,TCP套接字会保持与对方套接字的连接,TCP套接字知道目标地址的信息。

    • UDP套接字不会保持连接状态,每次传输数据都要添加目标地址信息。

    • 填写地址并传输数据时调用的UDP相关函数:

      • #include <sys/socket.h>
        // 成功返回传输的字节数,失败返回-1
        ssize_t sendto(int sock, void *buffer, size_t nbytes, int flags, struct sockaddr*to,socklen_t addrlen);
        
        • sock:用于传输数据的UDP套接字文件描述符
        • buffer:保存待传输数据的缓冲地址值
        • nbytes:待传输的数据长度,以字节为单位
        • flags:可选参数,没有则传递0
        • to:存有目标地址信息的sockaddr结构体变量的地址值
        • addrlen:传递给参数to的地址结构体变量长度。
        • 与TCP输出函数的区别在于:函数sendto()需要向他传递目标信息。
      • #include <sys/socket.h>
        // 成功返回接收的字节数,失败返回-1
        ssize_t recvfrom(int sock, void *buffer, size_t nbytes,int flags, struct sockaddr* from,socklen_t *addrlen);
        
        • sock:用于接收数据的UDP套接字文件描述符
        • buffer:保存接收数据的缓冲地址值
        • nbytes:可接收的最大字节数,无法超过参数buffer所指的缓冲大小
        • flags:可选项参数,没有传0
        • from:存有发送端地址信息的sockaddr结构体变量的地址值
        • addrlen:保存参数from的结构体变量长度的变量地址值
  • 基于UDP的回声服务器端/客户端:

    • UDP不同于TCP,不存在请求连接和受理过程,某种意义上无法明确区分服务器端和客户端。

    • 服务器端:提供服务

      • #include <stdio.h>
        #include <stdlib.h>
        #include <string.h>
        #include <unistd.h>
        #include <arpa/inet.h>
        #include <sys/socket.h>
        
        #define BUF_SIZE 30
        void error_handling(char* message);
        
        int main(int argc, char argv[]){
            int serv_sock;
            char message[BUF_SIZE];
            int str_len;
            socklen_t clnt_adr_sz;
            struct sockaddr_in serv_adr,clnt_adr;
        
            if(argc!= 2){
                printf("Usage : %s <port>\n",argv[0]);
                exit(1);
            }
            // 创建UDP套接字,参数传递SOCK_DGRAM
            serv_sock = socket(PF_INET,SOCK_DGRAM,0);
            if(serv_sock == -1)
                error_handling("UDP socker creation error");
        
            memset(&serv_adr,0,sizeof(serv_adr));
            serv_adr.sin_family = AF_INET;
            serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);
            serv_adr.sin_port = htons(atoi(argv[1]));
            // 分配IP地址和端口号
            if(bind(serv_sock,(struct sockaddr*)&serv_adr,sizeof(serv_adr)) == -1)
                error_handling("bind() error");
            
            while(1){
                clnt_adr_sz = sizeof(clnt_adr);
                // recvfrom函数调用获取数据传输端的地址,利用该地址将接收的数据逆向重传
                str_len = recvfrom(serv_sock,message,BUF_SIZE,0,(struct sockaddr*)&clnt_adr,clnt_adr_sz);
                sendto(serv_sock,message,str_len,0,(struct sockaddr*)&clnt_adr,clnt_adr_sz);
            }
            close(serv_sock);
            return 0;
        
        }
        
        void error_handling(char* message){
            fputs(message,stderr);
            fputc('\n',stderr);
            exit(1);
        }
        
    • 客户端:

      • #include <stdio.h>
        #include <stdlib.h>
        #include <string.h>
        #include <unistd.h>
        #include <arpa/inet.h>
        #include <sys/socket.h>
        
        #define BUF_SIZE 1024
        void error_handling(const char* message);
        
        int main(int argc, char *argv[]){
            int sock;
            char message[BUF_SIZE];
            int str_len;
            socklen_t adr_sz;
        
            struct sockaddr_in serv_adr,from_adr;
            if(argc != 3){
                printf("Usage : %s <IP> <port>\n",argv[0]);
                exit(1);
            }
        
            sock = socket(PF_INET, SOCK_STREAM,0);
            if(sock == -1)
                error_handling("socket() error");
        
            memset(&serv_adr, 0, sizeof(serv_adr));
            serv_adr.sin_family = AF_INET;
            serv_adr.sin_addr.s_addr = inet_addr(argv[1]);
            serv_adr.sin_port = htons(atoi(argv[2]));
        
            while(1){
                fputs("Iput message(Q to quit): ",stdout);
                fgets(message,BUF_SIZE,stdin);
                if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
                    break;
                
                // 向服务器传输数据
                sendto(sock,message,strlen(message),0,(struct sockaddr*)&from_adr,sizeof(serv_adr));
                adr_sz = sizeof(from_adr);
                // 接收数据
                str_len = recvfrom(sock,message,BUF_SIZE,0,(struct sockaddr*)&from_adr,&adr_sz);
                message[str_len] = 0;
                printf("Message from server : %s",message);
            }
            close(sock);
            return 0;
        }
        
        void error_handling(const char* message){
            fputs(message,stderr);
            fputc('\n',stderr);
            exit(1);
        }
        
  • UDP客户端套接字的地址分配:

    • TCP客户端通过调用connect函数自动完成IP和端口的分配。
    • 调用sento函数传输数据前应完成对套接字的地址分配工作,所以调用bind函数,bind函数在TCP程序中出现过,bind函数不区分TCP和UDP,在UDP程序中同样可以调用,
    • 如果在调用sendto函数时发现尚未分配地址信息,则在首次调用sendto函数时给相应套接字自动分配IP和端口。此时分配的地址一直保留到程序结束为止。也可以用来与其他UDP套接字进行数据交换,IP用主机IP,端口选尚未使用的任意端口号。
    • 调用sendto函数时自动分配IP和端口号,UDP客户端通常无需额外的地址分配过程。

6、3 UDP的数据传输特性和调用connect函数:

  • 验证UDP数据传输中存在数据边界。

  • 存在数据边界的UDP套接字:

    • TCP数据传输中不存在边界,表示:“数据传输过程中调用I/O函数的次数不具有意义。”

    • UDP是具有数据边界的协议,传输中调用I/O函数的次数很重要,输入函数的调用次数应和输出函数的调用次数完全一致,这样才能保证接收全部已发送数据。

    • bound_host1.c:调用3次recvfrom函数以接收数据,recvfrom函数调用间隔为5秒。

      • #include <stdio.h>
        #include <stdlib.h>
        #include <string.h>
        #include <unistd.h>
        #include <sys/socket.h>
        #include <arpa/inet.h>
        
        #define BUF_SIZE 30
        void error_handling(char* message);
        
        int main(int argc, char* argv[]){
        
            int sock;
            struct sockaddr_in my_adr, your_adr;
            socklen_t adr_sz;
            int str_len, i;
            char message[BUF_SIZE];
            sock = socket(PF_INET,SOCK_DGRAM,0);
            if(sock == -1)
                error_handling("socket creation error");
        
            memset(&my_adr, 9,sizeof(my_adr));
            my_adr.sin_family = AF_INET;
            my_adr.sin_addr.s_addr = htonl(INADDR_ANY);
            my_adr.sin_port = htons(atoi(argv[1]));
        
            if(bind(sock,(struct sockaddr*)&my_adr, sizeof(my_adr)) == -1)
                error_handling("bind() error");
        
            for(i = 0; i < 3; i++){
                // 程序停顿时间等于传递来的时间,for循环中每隔5秒调用1次recvfrom函数
                sleep(5);
                adr_sz = sizeof(your_adr);
                str_len = recvfrom(sock,message,BUF_SIZE,0,(struct sockaddr*)&your_adr,&adr_sz);
                printf("Message %d: %s \n",i + 1, message);
            }
            close(sock);
            return 0;
        }
        
        void error_handling(char* message){
            fputs(message,stderr);
            fputc('\n',stderr);
            exit(1);
        }
        
    • bound_host2.c:调用3次sendto函数,由于bound_host1.c中调用了sleep5秒,所以调用recvfrom函数前就已经调用了3次sendto函数,此时数据已经传输到了bound_host1.c,如果是TCP程序,只需要调用1次输入函数即可读入数据。UDP则需要调用3次recvfrom函数。

      • #include <stdio.h>
        #include <stdlib.h>
        #include <string.h>
        #include <unistd.h>
        #include <sys/socket.h>
        #include <arpa/inet.h>
        
        #define BUF_SIZE 30
        void error_handling(char* message);
        
        int main(int argc, char* argv[]){
            int sock;
            char msg1[] = "Hi!";
            char msg2[] = "I'm another UDP host!";
            char msg3[] = "Nice to meet you";
        
            struct sockaddr_in your_adr;
            socklen_t your_adr_sz;
        
            if(argc != 3){
                printf("Usage : %s <IP> <port>\n",argv[0]);
                exit(1);
            }
        
            sock = socket(PF_INET,SOCK_DGRAM,0);
            if(sock == -1)
                error_handling("socket() error");
            
            memset(&your_adr,0,sizeof(your_adr));
            your_adr.sin_family = AF_INET;
            your_adr.sin_addr.s_addr = inet_addr(argv[1]);
            your_adr.sin_port = htons(atoi(argv[2]));
        
            sendto(sock,msg1,sizeof(msg1),0,(struct sockaddr*)&your_adr,sizeof(your_adr));
        
            sendto(sock,msg2,sizeof(msg2),0,(struct sockaddr*)&your_adr,sizeof(your_adr));
        
            sendto(sock,msg3,sizeof(msg3),0,(struct sockaddr*)&your_adr,sizeof(your_adr));
        
            close(sock);
            return 0;
        }
        
        void error_handling(char* message){
            fputs(message,stderr);
            fputc('\n',stderr);
            exit(1);
        }
        

在这里插入图片描述

+ UDP数据报(Datagram):UDP套接字传输的数据包又称为数据报,数据报也属于数据包的一种,与TCP包不同,其本身可以成为1个完整数据,UDP中存在数据边界,1个数据包即可成为1个完整的数据,所以称为数据报。
  • 已连接(connected)UDP套接字与未连接(unconnected)UDP套接字:

    • TCP套接字中需注册待传输数据的目标IP和端口号,而UDP中无需注册。所以通过sendto函数传输数据的过程大致如下:
      • 第一阶段:向UDP套接字注册目标IP和端口号
      • 第二阶段:传输数据
      • 第三阶段:删除UDP套接字中注册的目标地址信息。
    • 每次调用sendto函数时重读上述过程,每次都变更目标地址,所以可以重复利用同一UDP套接字向不同目标传输数据,这种未注册目标地址信息的套接字称为未连接套接字,注册了目标地址的套接字称为连接套接字,UDP默认属于未连接套接字。
    • 如果要与同一主机进行长时间的通信,将UDP套接字变为已连接的套接字会提高效率。
  • 创建已连接UDP套接字

    • 创建已连接的UDP套接字只需要针对UDP套接字调用connect函数。
      在这里插入图片描述

    • #include <sys/socket.h>
      sock = socket(PF_INET,SOCK_DGRAM,0);
      memset(&adr,0,sizeof(adr));
      adr.sin_family = AF_INET;
      adr.sin_addr.s_addr = htonl(INADDR_ANY);
      adr.sin_port = htons(atoi(argv[1]));
      connect(sock,(struct sockaddr*)&adr,sizeof(adr));
      
    • 代码看似与TCP套接字创建过程一致,但socket函数的第二个参数是SOCK_DGRAM,即创建的是UDP套接字。针对UDP套接字调用connect函数不意味着与对方UDP套接字连接,只是向UDP套接字注册目标IP和端口信息。

    • 之后就与TCP套接字一样,每次调用sento函数只需要传输数据,因为已经指定了收发对象,不仅可以使用sendto、recvfrom函数,还可以使用write、read函数进行通信。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值