Linux网络编程 - 基于UDP的服务器端/客户端

2023-05-16

一 理解UDP

1.0 UDP协议简介

UDP(User Datagram Protocol,用户数据报协议) [RFC 768]

UDP协议的数据传输单元叫 UDP用户数据报,而TCP协议的数据传输单元叫 TCP报文段(segment)。

UDP在传送数据前不需要先建立连接。远地主机的运输层在收到UDP报文段后,不需要给出任何确认。虽然UDP不提供可靠交付服务,但在某些情况下却是一种最有效的工作方式

下图给出了一些应用和应用层协议主要使用的运输层协议(UDP和TCP)。

图1-1  使用UDP和TCP协议的各种应用和应用层协议
  • 关于UDP协议的特点和UDP的首部格式请参见如下链接的博文

UDP协议详解

传输层协议——UDP协议

1.1 UDP套接字的特点

        下面通过信件说明UDP的工作原理,这是讲解UDP时使用的传统示例,它与UDP特性完全相符。寄信前应先在信封上填好寄信人和收信人的地址,之后贴上邮票放进邮筒即可。当然,信件的特点使我们无法确认对方是否收到。另外,邮寄过程中也可能发生信件丢失的情况。也就是说,信件是一种不可靠的传输方式。与之类似,UDP提供的同样是不可靠的数据传输服务。

        “既然如此,TCP应该就是更优质的协议吧?

        如果只考虑传输的可靠性,TCP的确要比UDP好。但UDP在结构上比TCP更简单。UDP不会发送类似ACK确认的应答消息,也不会像SEQ那样给数据包分配序号。因此,UDP的性能有时比TCP高出很多。编程中实现UDP也比TCP简单。另外,UDP的可靠性虽比不上TCP,但也不会像想象中那么频繁地发生数据损毁或丢失。因此,在更重视性能而非可靠性的情况下,UDP是一种很好的选择。

        既然如此,UDP的作用到底是什么呢?为了提高可靠的数据传输服务,TCP在不可靠的IP层(即网络层)进行流控制,而UDP就缺少了这种流控制机制。

        “UDP和TCP的差异只在于流控制机制吗?

        是的,流控制是区分UDP和TCP的最重要标志。但若从TCP中除去流控制,所剩内容也屈指可数。也就是说,TCP的生命在于流控制机制。TCP通信两端的套接字之间建立连接以及断开连接过程也属于流控制的一部分。

提示》我们之前的博文中讲到过,把TCP通信比喻为打电话的过程,而在上文在把UDP通信过程比喻为投递信件过程。但这只是通过类比来形容两种协议的工作方式,并没有包含数据交换时的传输速率。请不要误认为“电话的速度比信件快,因此TCP的数据收发速率也比UDP快”。实际上正好相反,TCP的传输速度无法超过UDP,但在收发某些类型的数据时有可能接近UDP。例如,每次交换的数据量越大,TCP的传输速率就越接近UDP的传输速率。

1.2 UDP内部工作原理

        与TCP不同,UDP不会进行流控制。接下来具体讨论UDP的作用,如下图所示。

图1-2  数据包传输过程中UDP和IP的作用

         从上图1-2 可以看出,IP的作用就是让离开主机B的UDP用户数据报准确地传递到主机A。但把UDP用户数据报最终交付给主机A的某一UDP套接字的过程则是由UDP完成的。UDP最重要的作用就是根据端口号将传到主机的UDP用户数据报交付给最终的UDP套接字

1.3 UDP的高效使用

        虽然貌似大部分网络编程都是基于TCP实现的,但也有一些是基于UDP实现的。接下来考虑何时使用UDP更有效。我们需要知道的一点,其实UDP也是具有一定的可靠性的。计算机网络传输特性不同于传统的电信网的电话服务传输,因此在传送信息过程中有可能出现信息丢失的情况,可若要传递一个压缩文件(发送1万个数据包时,只要丢失1个就会产生问题),则必须使用TCP,因为压缩文件只要丢失一部分就很难解压。但通过网络实时传输视频或音频时的情况有所不同。对于多媒体数据而言,丢失的一部分也没有太大问题,这只会引起短暂的画面抖动,或出现细微的杂音。但因为需要提供实时服务,发送速度就成为非常重要的因素。因此,使用TCP的流控制就显得有些多余,此时需要考虑使用UDP。

        TCP比UDP慢的原因通常有以下两点。

  • 收发数据前后进行的连接建立及连接释放过程。
  • 收发数据过程中为保证可靠传输而添加的流控制。

        如果收发的数据量比较小但需要频繁连接时,UDP比TCP更高效。有机会的话,希望各位深入学习TCP/IP协议的内部构造。C语言程序员懂得计算机结构和操作系统知识就能写出更好的程序。同样,网络开发程序员若能深入理解TCP/IP协议则可以大幅提高自身的网络编程实力。

二 实现基于UDP的服务器端/客户端

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

        UDP服务器端/客户端不像TCP那样在连接状态下交换数据,因此与TCP不同,无需经过连接过程。也就是说,不必调用TCP连接过程中调用的listen函数和accept函数。UDP中只有创建套接字的过程和数据交换过程。

2.2 UDP服务器端和客户端均只需1个套接字

        TCP中,套接字之间是一对一的关系。若要向10个客户端提供服务,则除了充当门卫的服务器套接字外,还需要创建10个新的服务器套接字。但在UDP中,不管是服务器端还是客户端都只需要1个套接字。之前解释UDP原理时举了信件的例子,收发信件时使用的邮筒就好比为UDP套接字。只要附近有1个邮筒,就可以通过它向任意地址基寄出信件。同样,只需1个套接字就可以向任意主机传输数据,如下图所示。

图2-3  UDP套接字通信模型

         上图2-3展示了1个UDP套接字与2个不同主机交换数据的过程。也就是说,只需1个UDP套接字就能和多台主机通信。

2.3 基于UDP的数据 I/O 函数

        创建好TCP套接字后,传输数据时无需再添加地址信息(IP地址和端口号)。因为TCP套接字将保持与对方套接字的连接状态。换言之,TCP套接字知道目的地址信息。但UDP套接字不会保持连接状态(UDP套接字只有简单的邮筒功能),因此每次传输数据都要添加目标地址信息。这相当于寄信前在信件中填写地址。接下来介绍填写地址并传送数据时调用的UDP相关函数。

  •  sendto() — 用于将数据由指定的套接字传递给通信对端。
#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto(int sock, const void *buff, size_t nbytes, int flags,
                struct sockaddr *to, socklen_t addrlen);

/*参数说明
sock: 用于传输数据的UDP套接字文件描述符
buff: 保存待传输数据的缓冲地址值
nbytes: 待传输数据的长度,以字节为单位
flags: 可选项参数,若没有则设置为0
to: 存有目的通信地址信息的sockaddr结构体变量的地址值
addrlen: 传递给第5个参数to的网络地址值结构体变量长度
*/

//返回值: 成功时返回传输的字节数,失败时返回-1

函数说明》sendto()函数与之前的TCP发送函数send最大的区别在于,此函数需要传递目的主机的网络地址信息。

        接下来介绍接收UDP数据的 recvfrom()函数。UDP数据的发送端并不固定,因此该函数定义为可接收发送端信息的形式,也就是将同时返回UDP数据包中的发送端网络地址信息。

  • recvfrom() — 用于接收由指定套接字发送给接收端的数据。
#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom(int sock, void *buff, size_t nbytes, int flags,
                    struct sockaddr *from, socklen_t *addrlen);

/*参数说明
sock: 用于接收数据的UDP套接字文件描述符
buff: 保存接收数据的缓存区地址值
nbytes: 可接收的最大字节数,故无法超过参数buff所指向的缓冲区大小
flags: 可选项参数,若没有则传入0
from: 存有发送端通信地址信息的sockaddr结构体变量的地址值
addrlen: 保存第5个参数from的结构体变量长度的变量地址值
*/

//返回值: 成功时返回接收的字节数,失败时返回-1

编写UDP程序时最核心的部分就在于上述两个函数,这也说明了二者在UDP数据传输中的地位。

参考链接

TCP、UDP 通信常用函数send,sendto,recv,recvfrom详解

2.4 实现基于UDP的回声服务器端/客户端

        下面结合之前博文中实现的基于TCP的回声服务器端/客户端程序,我们实现一下基于UDP的回声服务器端/客户端程序。需要注意的是,UDP不同于TCP,UDP服务器端不存在请求连接和受理的过程,因此在某种意义上无法明确区分服务器端和客户端。只是因其提供服务而称为服务器端,希望各位不要无解。

  • 服务器端程序 uecho_server.c
#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] = {0};
    int str_len;
    socklen_t clnt_addr_sz;
    
    struct sockaddr_in serv_addr, clnt_addr;
    if(argc!=2){
        printf("Usage: %s <port>\n", argv[0]);
        exit(1);
    }
    
    serv_sock=socket(PF_INET, SOCK_DGRAM, 0);         //创建一个面向消息类型的套接字,注意第二个实参SOCK_DGRAM
    if(serv_sock==-1)
        error_handling("UDP socket creation error");
    
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi(argv[1]));
    
    if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)  //将通信地址信息绑定到套接字上,构成UDP套接字
        error_handling("bind() error");

    while(1)
    {
        clnt_addr_sz=sizeof(clnt_addr);
        str_len=recvfrom(serv_sock, message, BUF_SIZE, 0, 
                        (struct sockaddr*)&clnt_addr, &clnt_addr_sz);
        sendto(serv_sock, message, str_len, 0, 
                        (struct sockaddr*)&clnt_addr, clnt_addr_sz);
    }
    close(serv_sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

        接下来介绍与上述服务器端协同工作的客户端。这部分代码与TCP客户端不同,不存在connect函数。

  • 客户端程序 uecho_client.c
#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 sock;
    char message[BUF_SIZE] = {0};
    int str_len;
    socklen_t addr_sz;
    
    struct sockaddr_in serv_addr, from_addr;
    if(argc!=3){
        printf("Usage: %s <IP> <port>\n", argv[0]);
        exit(1);
    }
    
    sock=socket(PF_INET, SOCK_DGRAM, 0);   //创建一个面向消息类型的套接字,注意第二个实参SOCK_DGRAM
    if(sock==-1)
        error_handling("socket() error");
    
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
    serv_addr.sin_port=htons(atoi(argv[2]));
    
    while(1)
    {
        fputs("Insert message(q to quit): ", stdout);
        fgets(message, sizeof(message), stdin);                 //从终端输入字符串信息
        if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))
            break;
        
        sendto(sock, message, strlen(message), 0, 
                    (struct sockaddr*)&serv_addr, sizeof(serv_addr));
        addr_sz=sizeof(from_addr);
        str_len=recvfrom(sock, message, BUF_SIZE, 0, 
                    (struct sockaddr*)&from_addr, &addr_sz);

        message[str_len]=0;
        printf("Message from server: %s\n", message);
    }
    close(sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
  • 运行结果
  • 服务器端:uecho_server.c

编译程序:gcc uecho_server.c -o userver

运行程序:./userver 9190

  • 客户端:uecho_client.c

编译程序:gcc uecho_client.c -o uclient

运行结果:./uclient 127.0.0.1 9190

Insert message(q to quit): Hi UDP Server?

Message from server: Hi UDP Server?

Insert message(q to quit): Nice to meet you!

Message from server: Nice to meet you!

Insert message(q to quit): Good bye~

Message from server: Good bye~

Insert message(q to quit): q

程序说明

        服务器端/客户端程序的启动顺序并不重要,客户端程序可以在服务器端程序之前启动,只需保证在调用 sendto 函数前,sendto 函数的目标主机程序已经开始运行。

思考题:我们已经知道,TCP客户端套接字在调用connect函数时,操作系统会自动给它分配IP地址和端口号,既然如此,UDP客户端是何时分配IP地址和端口号呢?所有套接字都应分配IP地址和端口号,问题在于是在程序代码中直接分配还是自动分配呢?

2.5 UDP客户端套接字的地址分配

        前面讲解了UDP服务器端/客户端的实现方法。但如果仔细观察UDP客户端程序代码会发现,它缺少把IP地址和端口号分配给UDP套接字的过程。TCP客户端调用connect函数自动完成此过程,而UDP客户端中连承担相同功能的函数调用语句都没有。究竟在何时分配IP和端口号呢?
        在UDP程序中,调用 sendto 函数传输数据前应完成对套接字的通信地址分配工作,因此调用bind函数。当然,bind函数在TCP程序中出现过,但bind函数不区分TCP和UDP,也就是说,在UDP程序中同样可以调用。另外,如果调用 sendto 函数时发现尚未分配通信地址信息,则在首次调用 sendto 函数时给相应套接字自动分配IP地址和端口号。而且此时分配的通信地址一直保留到程序结束为止,因此可用来与其他UDP套接字进行数据交换。当然,IP用主机IP地址,端口号选尚未使用的任意端口号。

        综上所述,调用 sendto 函数时自动分配IP和端口号,因此,UDP客户端中通常无需额外的通信地址分配过程。所以客户端实现代码中省略了该过程,这也是普遍的实现方式。

三 UDP的数据传输特性和调用 connect 函数

        我们在之前的博文中验证了TCP传输的数据不存在数据边界,下面我们将验证UDP数据传输中存在数据边界。最后讨论在UDP中connect函数的调用问题。

3.1 存在数据边界的UDP套接字

        前面说过TCP数据传输中不存在数据边界,这表示“数据传输过程中调用I/O函数的次数不具有任何意义。

        相反,UDP数据传输中是具体数据边界的,传输中调用I/O函数的次数非常重要。因此,发送函数的调用次数应和接收函数的调用次数完全一直,这样才能保证接收全部已发送数据。例如,调用3次发送函数发送的数据必须通过3次接收函数才能接收完。

        下面通过简单示例进行验证。

  • bound_host1.c
#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 sock;
    char message[BUF_SIZE];
    struct sockaddr_in my_addr, your_addr;
    socklen_t addr_sz;
    int str_len, i;

    if(argc!=2){
        printf("Usage: %s <port>\n", argv[0]);
        exit(1);
    }
    
    sock=socket(PF_INET, SOCK_DGRAM, 0);
    if(sock==-1)
        error_handling("socket() error");
    
    memset(&my_addr, 0, sizeof(my_addr));
    my_addr.sin_family=AF_INET;
    my_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    my_addr.sin_port=htons(atoi(argv[1]));
    
    if(bind(sock, (struct sockaddr*)&my_addr, sizeof(my_addr))==-1)
        error_handling("bind() error");
    
    for(i=0; i<3; i++)                 //每隔5秒调用一次recvfrom函数
    {
        sleep(5);                      //睡眠5秒
        addr_sz=sizeof(your_addr);
        str_len=recvfrom(sock, message, BUF_SIZE, 0, 
                            (struct sockaddr*)&your_addr, &addr_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_host1.c 传输数据的,该示例程序共调用3次 sendto 函数以传输字符串数据。

  • bound_host2.c
#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 sock;
    char msg1[]="Hi!";
    char msg2[]="I'm another UDP host!";
    char msg3[]="Nice to meet you.";

    struct sockaddr_in your_addr;
    socklen_t your_addr_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_addr, 0, sizeof(your_addr));
    your_addr.sin_family=AF_INET;
    your_addr.sin_addr.s_addr=inet_addr(argv[1]);
    your_addr.sin_port=htons(atoi(argv[2]));
    
    //连续调用3次sendto函数
    sendto(sock, msg1, sizeof(msg1), 0, 
                    (struct sockaddr*)&your_addr, sizeof(your_addr));
    sendto(sock, msg2, sizeof(msg2), 0, 
                    (struct sockaddr*)&your_addr, sizeof(your_addr));
    sendto(sock, msg3, sizeof(msg3), 0, 
                    (struct sockaddr*)&your_addr, sizeof(your_addr));
    close(sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
  • 运行结果
  • bound_host1.c

编译程序:gcc bound_host1.c -o host1

运行程序:./host1 9190

Message 1: Hi!
Message 2: I'm another UDP host!
Message 3: Nice to meet you.

  • bound_host2.c

编译程序:gcc bound_host2.c -o host2

运行程序:./host2 127.0.0.1 9190

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

《结果说明》从运行结果,特别是 bound_host1.c 的运行结果中可以看出,共调用了3次recvfrom函数。这就证明了必须在UDP通信过程中使I/O函数调用保持一致。

《提示》UDP数据报(Datagram)

        UDP 套接字传输的数据包又称为用户数据报,实际上数据报也属于数据包的一种,TCP数据包则称为报文段。只是与TCP不同,其本身可以成为一个完整数据。这与UDP的数据传输特性有关,UDP中存在数据边界,1个数据包即可成为一个完整数据,因此称为数据报。

3.2 已连接(connected)UDP套接字与未连接(unconnected)UDP套接字

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

  • 第1阶段:向UDP套接字注册目标IP和端口号。
  • 第2阶段:传输数据。
  • 第3阶段:删除UDP套接字中注册的目的通信地址信息。

        每次调用sendto 函数时重复上述过程。每次都变更目标地址信息,因此可以重复利用同一UDP套接字向不同目标传输数据。这种未注册目标地址信息的套接字称为无连接套接字,反之,注册了目标地址信息的套接字称为连接套接字。显然,UDP套接字默认属于无连接套接字。但UDP套接字在下述情况下显得不太合理:

IP为211.210.147.82 的主机 82端口 共准备了3个数据,调用3次sendto函数进行传输。

        此时需重复3次上述三阶段。因此,当要与同一主机进行长时间通信时,将UDP套接字变成连接套接字就会提高传输效率。上述三个阶段中,第一个和第三个阶段占整个通信过程近 1/3 的时间,缩短这部分时间将大大提高整体性能。也就是说,当使用UDP进行一对一通信时,使用连接套接字将大大提高数据传输效率

3.3 创建已连接UDP套接字

创建已连接UDP套接字的过程格外简单,只需要对UDP套接字调用connect函数。

sock = socket(PF_INET, SOCK_DGRAM, 0);
memset(&addr, 0, sizeof(addr));
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = ...
addr.sin_port = ...
connect(sock, (struct sockaddr*)&addr, sizeof(addr));

        上述代码看似与TCP套接字创建过程一致,但socket函数的第二个实参是 SOCK_DGRAM。也就是说,创建的的确是UDP套接字。当然,针对UDP套接字调用connect函数并不意味着要与对方UDP套接字连接,这只是向UDP套接字注册目标IP和端口信息。

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

        下列示例将之前的 uecho_client.c 程序改写成基于连接UDP套接字的程序,因此可以结合 uecho_server.c 程序运行。另外,为了便于说明,未直接删除 uecho_client.c 的I/O函数,而是将其注释掉了。

  • uecho_con_client.c
#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 sock;
    char message[BUF_SIZE];
    int str_len;
    socklen_t addr_sz;
    
    struct sockaddr_in serv_addr, from_addr;
    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(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
    serv_addr.sin_port=htons(atoi(argv[2]));
    
    connect(sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));  //向服务器端注册本端的通信地址信息(IP+端口号)

    while(1)
    {
        fputs("Insert message(q to quit): ", stdout);
        fgets(message, sizeof(message), stdin);     
        if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))    
            break;
        /*
        sendto(sock, message, strlen(message), 0, 
                    (struct sockaddr*)&serv_addr, sizeof(serv_addr));
        */
        write(sock, message, strlen(message));                  //发送数据

        /*
        addr_sz=sizeof(from_addr);
        str_len=recvfrom(sock, message, BUF_SIZE, 0, 
                    (struct sockaddr*)&from_addr, &addr_sz);
        */
        str_len=read(sock, message, sizeof(message)-1);         //接收数据

        message[str_len]=0;
        printf("Message from server: %s", message);
    }
    close(sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

《代码说明》UDP的客户端调用connect函数的作用是在进行数据交互之前,提前为客户端的UDP套接字分配IP和端口号,那么在调用sendto 函数时,在该函数内部执行逻辑中,就会省略第一、三阶段的判断执行过程,提高了数据传输的消息。当然必须指出的是,在UDP中使用connect函数只适用于一对一通信的场景。

四 习题

1、UDP为什么比TCP传输速度快?为什么TCP数据传输可靠而UDP数据传输不可靠?

:UDP与TCP不同,不进行流控制。由于该控制涉及到TCP套接字的连接建立和连接释放,以及整个数据收发过程,因此,TCP的传输速度受流控制机制的约束,但是这种机制却提供了可靠的数据传输服务;而UDP由于没有流控制机制,无法保证数据集传输的可靠性,但是传输速率却不受影响,因此比TCP的快。

2、下列不属于UDP特点的是?

a. UDP不同于TCP,不存在连接的概念,所以不像TCP那样只能进行一对一的数据传输。

b. 利用UDP传输数据时,如果有2个目标,则需要2个套接字。

c. UDP套接字中无法使用已分配给TCP的同一端口号。

d. UDP套接字和TCP套接字可以共存。若需要,可以在同一主机进行TCP和UDP数据传输。

e. 针对UDP函数也可以调用connect函数,此时UDP套接字跟TCP套接字相同,也需要经过3次握手。

:上述不属于UDP特点的是:bce。解释如下:

b:UDP支持一对多的数据传输,由于UDP是无连接的,因此只需一个套接字就可与多个不同的目标主机进行通信。

c:TCP和UDP的协议是互相独立地,所以它们的端口号也是互相独立。因此,UDP套接字可以使用已分配给TCP的同一端口号。例如:如果某TCP套接字使用了9190号端口,则其他TCP套接字就无法使用该端口号,但UDP套接字可以使用。

e:虽然UDP函数也可以使用connect函数,但是UDP套接字仍然是无连接的,不需要经过3次握手过程。

参考链接

TCP套接字和UDP套接字可以共用相同端口号

【程序设计艺术】TCP和UDP为何可以共用同一端口

一个端口号可以同时被两个进程绑定吗?

3、UDP数据报向对方主机的UDP套接字传递过程中,IP和UDP分别负责哪些部分?

:UDP负责端到端的逻辑链路传输。IP负责传输路径的选择,即路由选择。

4、UDP一般比TCP快,但根据交换数据的特点,其差异可大可小。请说明何种情况下UDP的性能优于TCP。

:UDP与TCP不同,它是无连接的,不需要经过连接建立以及连接释放的过程,因此,在频繁的连接建立及连接关闭的情况下,UDP的数据收发能力会凸显出更好的性能。

5、客户端 TCP 套接字调用 connect 函数时自动分配IP和端口号。UDP 中不调用 bind 函数,那何时分配 IP 和端口号?

:在UDP程序中,如果调用sendto 函数时发现UDP套接字尚未分配地址信息(即IP和端口号),则在首次调用sendto 函数时先给相应套接字自动分配IP和端口号,而且此时分配的地址信息会一直保留到程序结束为止。简言之,调用sendto函数时自动分配IP和端口号,因此UDP客户端中通常无需额外的地址信息分配过程。

6、TCP 客户端必须调用 connect 函数,而 UDP 中可以选择性调用。请问,在 UDP 中调用 connect 函数有哪些好处?

:每当以UDP套接字为对象调用sendto函数时,都要经过以下三个过程:

  • 第一阶段:向UDP套接字注册目标IP和端口号。
  • 第二阶段:传输数据。
  • 第三阶段:删除UDP套接字中注册的目的通信地址信息。

        每次调用sendto函数都要重复上述3个过程,但是在调用sendto函数之前调用connect函数,就可以忽略每次传输数据时反复进行的第一阶段和第三阶段。然而,调用connect函数并不意味着需要经过连接过程,只是将IP地址和端口号指定在UDP的发送对象上。这样connect函数使用后,还可以用write、read函数进行数据处理,而不必使用sendto、recvfrom。

        简言之,当UDP使用一对一通信方式时,调用connect函数,可以将UDP套接字变成连接套接字,这样会提高数据传输的效率。因为三个阶段中,第一个阶段和第三个阶段占用了部分时间,调用 connect 函数可以节省这些时间。

7、请参考本篇博文中给出的 uecho_server.c 和 uecho_client.c,编写示例程序使服务器和客户端轮流收发消息。收发的消息均要输出到控制台窗口。

  • 服务器端程序 uchat_server.c
#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] = {0};
    int str_len;
    socklen_t clnt_addr_sz;
    
    struct sockaddr_in serv_adr, clnt_addr;
    if(argc!=2){
        printf("Usage: %s <port>\n", argv[0]);
        exit(1);
    }
    
    serv_sock=socket(PF_INET, SOCK_DGRAM, 0);
    if(serv_sock==-1)
        error_handling("UDP socket creation error");
    
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_addr.sin_port=htons(atoi(argv[1]));
    
    if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr))==-1)
        error_handling("bind() error");
    
    clnt_addr_sz=sizeof(clnt_addr);
    while(1)
    {
        str_len=recvfrom(serv_sock, message, BUF_SIZE, 0, 
                        (struct sockaddr*)&clnt_addr, &clnt_addr_sz);   //接收来自客户端的字符串消息
        message[str_len]='0';
        printf("Message from client: %s", message);

        fputs("Insert message(q to quit): ", stdout);
        fgets(message, sizeof(message), stdin);                         //从控制台输入一行字符串
        if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))    
            break;

        sendto(serv_sock, message, strlen(message), 0, 
                (struct sockaddr*)&clnt_addr, clnt_addr_sz);            //发送字符串消息给客户端
    }   
    close(serv_sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}
  • 客户端程序 uchat_client.c
#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 sock;
    char message[BUF_SIZE];
    int str_len;
    socklen_t addr_sz;
    
    struct sockaddr_in serv_addr, from_addr;
    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(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family=AF_INET;
    serv_addr.sin_addr.s_addr=inet_addr(argv[1]);
    serv_addr.sin_port=htons(atoi(argv[2]));
    
    while(1)
    {
        fputs("Insert message(q to quit): ", stdout);
        fgets(message, sizeof(message), stdin);                     //从控制台输入一行字符串消息
        if(!strcmp(message,"q\n") || !strcmp(message,"Q\n"))    
            break;
        
        sendto(sock, message, strlen(message), 0, 
                (struct sockaddr*)&serv_addr, sizeof(serv_addr));   //发送字符串消息给服务器端

        addr_sz=sizeof(from_addr);
        str_len=recvfrom(sock, message, BUF_SIZE, 0, 
                        (struct sockaddr*)&from_addr, &addr_sz);    //接收来自服务器端的消息

        message[str_len]='0';
        printf("Message from server: %s", message);
    }
    close(sock);
    return 0;
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

参考

《TCP-IP网络编程(尹圣雨)》第6章 - 基于UDP的服务器端/客户端

《计算机网络(第7版-谢希仁)》第5章 - 运输层

《TCP/IP网络编程》课后练习答案第一部分6~10章 尹圣雨

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Linux网络编程 - 基于UDP的服务器端/客户端 的相关文章

  • Tiny210(S5PV210) U-BOOT(六)----DDR内存配置

    上次讲完了Nand Flash的低级初始化 xff0c 然后Nand Flash的操作主要是在board init f nand xff0c 中 xff0c 涉及到将代码从Nand Flash中copy到DDR中 xff0c 这个放到后面实
  • NAND FLASH命名规则

    基于网络的一个修订版 三星的pure nandflash xff08 就是不带其他模块只是nandflash存储芯片 xff09 的命名规则如下 xff1a 1 Memory K 2 NANDFlash 9 3 Small Classifi
  • s3c6410 DMA

    S3C6410中DMA操作步骤 xff1a 1 决定使用安全DMAC SDMAC 还是通用DMAC DMAC xff1b 2 开始相应DMAC的系统时钟 xff0c 并关闭另外一组的时钟 xff08 系统默认开启SDMA时钟 xff09 x
  • Visual Studio和VS Code的区别

    1 Visual Studio简介 xff1a 是一个集成开发环境 IDE xff0c 安装完成后就能直接用 xff0c 编译工具 xff0c 调试工具 xff0c 各个语言的开发工具 xff0c 都是已经配置好的 xff0c 开箱即用 适
  • 博客转移

    由于CSDN 文章不间断的会丢失图片 xff0c 然后逼格也不够高 xff0c 导致几年都没有写博客 xff0c 全部是记录至印象笔记中 xff0c 但是久了也不太好 xff0c 所以最近搞了一个自己的个人博客 xff0c 以后文章全部写至
  • 安装qt-everywhere-opensource-src-4.8.6

    1 下载 qt everywhere opensource src 4 8 6 http mirrors hust edu cn qtproject official releases qt 4 8 4 8 6 qt everywhere
  • CentOS6.5上安装qt-creator-opensource-linux-x86-3.1.2.run

    1 qt creator opensource linux x86 3 1 2 run的下载 wget http mirrors hustunique com qt official releases qtcreator 3 1 3 1 2
  • atoi()函数

    atoi 函数 原型 xff1a int atoi xff08 const char nptr xff09 用法 xff1a include lt stdlib h gt 功能 xff1a 将字符串转换成整型数 xff1b atoi 会扫描
  • ubuntu中printk打印信息

    1 设置vmware添加seria port 使用文件作为串口 2 启动ubuntu xff0c 修改 etc default grub GRUB CMDLINE LINUX DEFAULT 61 34 34 GRUB CMDLINE LI
  • 静态库、共享库、动态库概念?

    通常库分为 xff1a 静态库 共享库 xff0c 动态加载库 下面分别介绍 一 静态库 xff1a 1 概念 xff1a 静态库就是一些目标文件的集合 xff0c 以 a结尾 静态库在程序链接的时候使用 xff0c 链接器会将程序中使用
  • 链表——怎么写出正确的链表?

    链表 相比数组 xff0c 链表不需要一块连续的内存空间 xff0c 而是通过指针将一组零散的内存块串联起来使用 xff0c 而这里的内存块就叫做节点 xff0c 一般节点除了保存data还会保存下一个节点的地址也就是指针 单链表 头节点
  • 【STM32】STM32 变量存储在片内FLASH的指定位置

    在这里以STM32L4R5为例 xff08 官方出的DEMO板 xff09 xff0c 将变量存储在指定的片内FLASH地址 xff08 0x081F8000 xff09 一 MDK Keil软件操作 uint8 t version spa
  • 【STM32】 利用paho MQTT&WIFI 连接阿里云

    ST联合阿里云推出了云接入的相关培训 xff08 基于STM32的端到端物联网全栈开发 xff09 xff0c 所采用的的板卡为NUCLEO L4R5ZI板 xff0c 实现的主要功能为采集温湿度传感器上传到阿里云物联网平台 xff0c 并
  • 【STM32 】通过ST-LINK utility 实现片外FLASH的烧写

    目录 前言 一 例程参考及讲解 1 1 Loader Src c文件 1 2 Dev Inf c文件 二 程序修改 三 实测 参考 前言 在单片机的实际应用中 xff0c 通常会搭载一些片外FLASH芯片 xff0c 用于存储系统的一些配置
  • 基于FFmpeg的推流器(UDP推流)

    一 推流器对于输入输出文件无要求 1 输入文件可为网络流URL xff0c 可以实现转流器 2 将输入的文件改为回调函数 xff08 内存读取 xff09 的形式 xff0c 可以推送内存中的视频数据 3 将输入文件改为系统设备 xff08
  • 十进制转十六进制的C语言实现

    include lt stdio h gt include lt stdlib h gt include lt string h gt void reversestr char source char target unsigned int
  • Linux中断处理的“下半部”机制

    前言 中断分为硬件中断 xff0c 软件中断 中断的处理原则主要有两个 xff1a 一个是不能嵌套 xff0c 另外一个是越快越好 在Linux中 xff0c 分为中断处理采用 上半部 和 下半部 处理机制 一 中断处理 下半部 机制 中断
  • Linux中的workqueue机制

    转载与知乎https zhuanlan zhihu com p 91106844 一 前言 Linux中的workqueue机制是中断底半部的一种实现 xff0c 同时也是一种通用的任务异步处理的手段 进入workqueue队列处理的任务
  • 嵌入式编程通用Makefile

    一 根目录下Makefile 这个Makefile为主Makefile CROSS COMPILE span class token operator 61 span AS span class token operator 61 span
  • Hi3798 PWM输出控制背光

    一 PWM配置说明 Hi3798 具有3个PWM输出端口 通过查阅 Hi3798M V200 低功耗方案 使用指南 pdf 可得 xff1a 通过查阅Hitool工具可以查看到三个PWM端口的寄存器分别为 xff1a 通过原理图可得 xff

随机推荐

  • Hi3798移植4G模块(移远EC20)

    Hi3798移植4G模块 xff08 移远EC20 xff09 一 前言二 USB驱动修改2 1 添加VID和PID信息2 2 添加空包处理机制2 3 添加复位重连机制2 4 修改内核配置 三 GoBiNet测试程序 一 前言 本次系统采用
  • uniapp小视频项目:滑动播放视频

    文章目录 1 监听视频滑动2 播放和暂停3 增加播放 暂停视频功能4 增加双击点赞5 控制首个视频自动播6 动态渲染视频信息 1 监听视频滑动 给 swiper 增加 64 change 61 34 change 34 xff0c 这个时间
  • Vue github用户搜索案例

    文章目录 完成样式请求数据完善使用 vue resource 完成样式 1 public 下新建 css 文件夹 xff0c 放入下载好的 bootstrap css xff0c 并在 index html 中引入 2 新建 Search
  • 【敬伟ps教程】平移、缩放、移动、选区

    文章目录 平移抓手工具旋转抓手 缩放工具移动工具详解选区选区工具详解 平移 抓手工具 当打开一张大图时 xff0c 可以通过修改底部的百分比或使用抓手工具 xff08 H或在任何时候按住空格键来使用抓手工具 xff09 来查看更多细节 使用
  • 【敬伟ps教程】套索、魔棒工具、快速选择工具、选区的编辑和调整

    文章目录 套索工具自由套索多边形套索磁性套索工具 魔棒工具快速选择工具选区的编辑和调整 套索工具 自由套索 套索工具的用法 xff0c 点击鼠标左键拖动鼠标建立选区 当选区没闭合时 xff0c 松开鼠标会自动闭合选区 套索工具灵活快速但不够
  • Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

    项目是 vite 43 vue 43 ts 运行 npm run dev可以正常运行 xff0c 运行 npm run build 报错 解决办法 xff1a 1 先打开cmd全局命令窗口 xff0c 输入 npm install g in
  • 安卓手机投屏到win10电脑

    PC端操作 手机端操作 xff08 Mi6为例 xff09 pc端弹出提示 xff0c 选择是
  • 用Windows自带画图软件吸取色值

    1 打开画图windows自带画图软件 2 用qq截图要吸取颜色的图片 xff0c ctrl 43 v粘贴到画图软件中 3 点击取色器 xff0c 吸取颜色 xff0c 这是会看到吸取成功的颜色 4 打开编辑颜色 5 这样就得到了RGB颜色
  • 打开浏览器默认是360导航解决办法

    Chrome Chrome的设置中已经设置了百度为启动页 但是打开Chrome显示的还是360 解决办法很简单就是把桌面的快捷方式删除 xff0c 然后在安装目录重新生成快捷方式到桌面即可 Microsoft Edge 这个360修改的就更
  • 打开项目报错Error:Could not get unknown property 'mave' for project ':app' of type org.gradle.api.Project.

    今天打开项目 xff0c 报错如下 xff1a 打开gradle properties xff0c 发现最后的MAVEN URL地址错乱 xff0c 改完就可以了
  • Eclipse自带的抓包工具

    打开Eclipse window show view other 现在访问之前写的项目 http localhost 8888 android jsp flight index jsp 查看Eclipse
  • Fiddler抓取手机端APP接口数据(包括https)

    下载安装Fiddler https pan baidu com s 12zAt0r8lcHTszekOOcqeLg 环境要求 PC机和手机连接在同一网络下 设置 1 记录pc端地址 2 如果不显示这个工具栏 xff0c 可以设置View S
  • 【Git】Git撤销add操作

    Git add错了文件怎么办 xff1f 可以查看以下两篇 https git scm com book zh v1 Git 基础 记录每次更新到仓库 https git scm com book zh v1 Git 基础 撤消操作 我们来
  • Cygwin安装教程

    简介 cygwin是一个在windows平台上运行的unix模拟环境 xff0c 是cygnus solutions公司开发的自由软件 Cygwin就是一个windows软件 xff0c 该软件就是在windows上仿真linux操作系统
  • 学习Kalibr工具--Camera标定过程

    这里介绍用kalibr工具对相机进行单目和双目的标定 xff1b 在kalibr中不仅提供了IMU与camera的联合标定工具 xff0c 也包含了camera的标定工具箱 xff1b 准备 安装好kalibr之后 xff0c 开始准备标定
  • 常用NMEA0183的报文解析

    NMEA0183报文包括GPGGA GPRMC GPVTG等报文 xff0c 本文主要介绍NMEA0183语句报文的格式以及解析 xff0c 方便有关位置信息编程或者有关位置获取的其他方面 1 GPGGA GPGGA消息包含详细的GNSS定
  • 03 - 雷达的基本组成

    目录 1 雷达发射机 2 雷达天线 3 雷达接收机 4 雷达信号处理机 5 雷达终端设备 以典型单基地脉冲雷达为例来说明雷达的基本组成及其作用 如图1 5所示 xff0c 它主要由天线 发射机 接收机 信号处理机和终端设备等组成 1 雷达发
  • 相机模型详解

    相机模型 数码相机图像拍摄的过程实际上是一个光学成像的过程 相机的成像过程涉及到四个坐标系 xff1a 世界坐标系 相机坐标系 图像坐标系 像素坐标系 以及这四个坐标系的转换 理想透视模型 针孔成像模型 相机模型是光学成像模型的简化 xff
  • Socket通讯实验总结

    网络编程的第一个实验入门比较难 因为要理解透彻套接字的工作原理 xff0c 服务器与客户端通讯的过程 不过经过几天的仔细研究 xff0c 实验还是完成了 以下对几个实验的知识点总结一下 xff1a 1 Socket和线程 在实验中一定要弄清
  • Linux网络编程 - 基于UDP的服务器端/客户端

    一 理解UDP 1 0 UDP协议简介 UDP User Datagram Protocol xff0c 用户数据报协议 RFC 768 UDP协议的数据传输单元叫 UDP用户数据报 xff0c 而TCP协议的数据传输单元叫 TCP报文段