《TCP/IP网络编程》学习笔记 | Chapter 6:基于UDP的服务器端/客户端

《TCP/IP网络编程》学习笔记 | Chapter 6:基于UDP的服务器端/客户端

  • 《TCP/IP网络编程》学习笔记 | Chapter 6:基于UDP的服务器端/客户端
    • 理解UDP
      • UDP套接字的特点
      • UDP内部工作原理
      • UDP的高效使用
    • 实现基于UDP的服务器端/客户端
      • UDP中的服务器端和客户端没有连接
      • UDP服务器端和客户端均只需1个套接字
      • 基于UDP的数据I/O函数
      • 基于UDP的回声服务器端/客户端
      • UDP客户端套接字的地址分配
    • UDP数据传输特性和调用connect函数
      • 存在数据边界的UDP套接字
      • 调用connect函数的UDP套接字
      • 拓展:关于recvfrom函数
    • 基于 Windows 的实现
      • 数据I/O函数
      • 基于 Windows 的 UDP 回声服务器端/客户端
      • 基于 Windows 的存在数据边界的 UDP 套接字
      • 基于 Windows 的已连接 UDP 套接字
    • 习题
      • (1)UDP为什么比TCP速度快?为什么TCP数据传输可靠而UDP数据传输不可靠?
      • (2)下列不属于UDP特点的是?
      • (3)UDP数据包向对方主机的UDP套接字传递过程中,IP和UDP分别负责哪些部分?
      • (4)UDP一般比TCP快,但根据交换数据的特点,其差异可大可小。请说明何种情况下UDP的性能优于TCP?
      • (5)客户端TCP套接字调用connect函数时自动分配IP和端口号。UDP中不调用bind函数,那何时分配IP和端口号?
      • (6)TCP客户端必须调用connect函数,而UDP中可以选择性调用。请问,在UDP中调用connect函数有哪些好处?
      • (7)请参考本章给出的uecho_sever.c和uecho_client.c,编写示例使服务器端和客户端轮流收发消息。收发的消息均要输出到控制台窗口。

《TCP/IP网络编程》学习笔记 | Chapter 6:基于UDP的服务器端/客户端

本篇文章简单描述了UDP传输协议的工作原理及特点。

理解UDP

UDP和TCP一样同属于TCP/IP协议栈的第二层,即传输层。

UDP套接字的特点

  • 提供的是一种不可靠的数据传输服务。
  • 从通信速度上来讲,UDP通常是要快于TCP的;每次交换的数据量越大,TCP的传输速率就越接近于UDP。
  • UDP在通信结构上较TCP更为简洁,通常性能也要优于TCP。
  • 没有流控制机制。

区分TCP和UDP最重要的标志是流控制,流控制赋予了TCP可靠性的特点,也说TCP的生命在于流控制。

UDP内部工作原理

在这里插入图片描述

可以看出,IP的作用就是让离开主机B的UDP数据包准确传递到主机A,而UDP则是把UDP包最终交给主机A的某一UDP套接字。UDP最重要的作用就是根据端口号将传输到主机的数据包交付给最终的UDP套接字。

UDP的高效使用

TCP用于对可靠性要求较高的场景,比如要传输一个重要文件或是压缩包,这种情况往往丢失一个数据包就会引起严重的问题;而对于多媒体数据来说,丢失一部分数据包并没有太大问题,因为实时性更为重要,速度就成为了重要考虑因素。TCP慢于UDP主要在于以下两点:

  • 收发数据前后进行的连接及清理过程。
  • 收发数据过程中为保证可靠性而添加的流控制。

因此,如果收发的数据量小但需要频繁的连接时,UDP比TCP更为高效。

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

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

和TCP不同,UDP服务器端/客户端并不需要在连接状态下交换数据。不必调用listen和accept函数。UDP的通信只有创建套接字和数据交换的过程。

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

TCP套接字是一对一的关系,且服务器端还需要一个额外的TCP套接字用于监听连接请求;而UDP通信中,无论服务器端还是客户端都只需要一个套接字即可,且可以实现一对多的通信关系。

下图展示了一个UDP套接字与两台主机进行数据交换的过程。

在这里插入图片描述

基于UDP的数据I/O函数

TCP套接字建立连接之后,数据传输过程便无需额外添加地址信息,因为TCP套接字会保持与对端的连接状态;而UDP则没有这种连接状态,因此每次数据交换过程都需要添加目标地址信息。下面是UDP套接字数据传输函数,与TCP传输函数最大的区别在于,该函数需要额外添加传递目标的地址信息。

#include <sys/socket.h>

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

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

参数:

  • sock:这是套接字描述符,是一个整数值,唯一标识了一个打开的套接字。
  • buff:指向要发送的数据缓冲区的指针。
  • nbytes:要发送的数据的长度(以字节为单位)。
  • flags:这是一个选项标志,用于修改 sendto 函数的行为,没有则为0。
  • to:指向目标地址的指针,该地址结构包含了目的主机的网络地址信息。
  • addrlen:目的地址结构 (to) 的长度。

由于UDP数据的发送端并不固定,因此,UDP套接字的数据接收函数定义了存储发送端地址信息的数据结构。

#include <sys/socket.h>

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

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

参数:

  • sock:这是套接字描述符,是一个整数值,唯一标识了一个打开的套接字。
  • buff:指向一个缓冲区的指针,用于存储接收到的数据。
  • nbytes:缓冲区的长度(以字节为单位)。
  • flags:这是一个选项标志,用于修改 recvfrom 函数的行为,没有则为0。
  • from:指向一个sockaddr结构的指针,用于存储发送方的地址信息。
  • addrlen:发送方地址结构 (from) 的长度。

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

UDP通信函数调用流程:

在这里插入图片描述

UDP不同于TCP,不存在请求连接和受理连接的过程,因此某种意义上并没有明确的服务器端和客户端之分。如下是示例源码。

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];
    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);
    }
    serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (serv_sock == -1)
        error_handling("UDP socket 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]));
    if (bind(serv_sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr)) == -1)
        error_handling("bind() error");
    while (1)
    {
        clnt_adr_sz = sizeof(clnt_adr);
        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);
}

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];
    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_DGRAM, 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("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_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(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}

示例代码运行结果:

在这里插入图片描述

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

从上述示例源码来看,服务器端UDP套接字需要手动bind地址信息,而客户端UDP套接字则无此过程。我们已经知道,客户端TCP套接字是在调用connect函数的时机,由操作系统为我们自动绑定了地址信息;而客户端UDP套接字同样存在该过程,如果没有手动bind地址信息,则在首次调用sendto函数时自动分配IP和端口号等地址信息。和TCP一样,IP是主机IP,端口号则随机分配(客户的临时端口是在第一次调用sendto函数时一次性选定,不能改变;然而客户的IP地址却可以随客户发送的每个UDP数据报而变动(如果客户没有绑定一个具体的IP地址到其套接字上)。其原因在于如果客户主机是多宿的,客户有可能在两个目的地之间交替选择)。

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

之前我们介绍了TCP传输数据不存在数据边界,下面将会验证UDP数据传输存在数据边界的特性,并介绍UDP传输调用connect函数的作用。

存在数据边界的UDP套接字

UDP协议具有数据边界,这就意味着数据交换的双方输入函数和输出函数必须一一对应,这样才能保证可完整接收数据。如下是验证UDP存在数据边界的示例源码。

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_adr, your_adr;
    socklen_t adr_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_adr, 0, 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++)
    {
        sleep(5); // delay 5 sec.
        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:

#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_adr, your_adr;
    socklen_t adr_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_adr, 0, 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++)
    {
        sleep(5); // delay 5 sec.
        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);
}

示例代码运行结果:

在这里插入图片描述

发送方调用了3次sendto函数发送数据,接收方的recvfrom函数调用间隔为5s,因此,数据早就传输到了缓冲区中。如果是TCP程序,这时只需要调用一次输入函数即可读入数据。UDP则不同,必须调用3次recvfrom函数。

调用connect函数的UDP套接字

TCP套接字需要手动注册传输数据的目标IP和端口号,而UDP则是调用sendto函数时自动完成目标地址信息的注册,该过程如下:

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

每次调用sendto函数都会重复执行以上过程,这也是为什么同一个UDP套接字可和不同目标进行数据交换的原因。像UDP这种未注册目标地址信息的套接字称为未连接套接字,而TCP这种注册了目标地址信息的套接字称为连接connected套接字。当需要和同一目标主机进行长时间通信时,UDP的这种无连接的特点则会非常低效。通过调用connect函数使UDP变为已连接套接字则会有效改善这一点,因为上述的第一阶段和第三阶段会占用整个通信过程近1/3的时间。

已连接的UDP套接字不仅可以使用之前的sendto和recvfrom函数,还可以使用没有地址信息参数的write和read函数。

需要注意的是,调用connect函数的已连接UDP套接字并非真的与目标UDP套接字建立连接,仅仅是向本端UDP套接字注册了目标IP和端口号信息而已。

修改之前的uecho_client代码为已连接UDP套接字,代码如下所示:

#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 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_DGRAM, 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]));

    connect(sock, (struct sockaddr *)&serv_adr, sizeof(serv_adr));

    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_adr, sizeof(serv_adr));
        */
        write(sock, message, strlen(message));

        /*
        adr_sz=sizeof(from_adr);
        str_len=recvfrom(sock, message, BUF_SIZE, 0,
                    (struct sockaddr*)&from_adr, &adr_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);
}

拓展:关于recvfrom函数

recvfrom是一个阻塞函数,那么该函数的返回时机是怎样的?

显然如果客户端正常收到应答数据,recvfrom自然可以返回。但如果发生其他情况呢?

对端调用close函数关闭UDP套接字时是否会发送EOF信息,本端recvfrom函数又会有什么动作吗?是否会像TCP套接字的read函数那样收到EOF信息而返回0?

由于UDP套接字无连接的特性,即使对端调用close函数关闭套接字,本端也不会有任何感知,recvfrom自然不会返回。那如果是调用了connect函数的已连接UDP套接字呢,服务端的close函数调用是否会使客户端的recvfrom函数退出阻塞状态?

仍然不会。因为调用connect函数的已连接UDP套接字并非真的像TCP套接字那样建立了连接,仅仅是为了数据交换的便利性向本端UDP套接字注册了目标地址信息而已;而对端并不能感知到这些,close函数自然也不会向TCP那样向本端发送文件结束标志EOF。因此,正常情况下,只有接收到发送端消息的recvfrom函数才会退出阻塞状态而返回。

如果一个客户端数据报丢失(譬如说,被客户主机与服务主机之间的某个路由器丢弃),客户端将永远阻塞于recvfrom 调用,等待一个永远不会到达的服务器应答。类似地,如果客户端数据报到达服务器,但是服务器的应答丢失了,客户端也将永远阻塞于recvfrom 调用。防止这样永久阻塞的一般方法是给客户端的recvfrom 调用设置一个超时时间。

基于 Windows 的实现

数据I/O函数

sendto 函数和 recvfrom函数的参数及其含义与 Linux 无太大区别。

#include <winsock2.h>

int sendto(SOCKET s, const char *buf, int len, int flags, const struct sockaddr *to, int tolen);

成功时返回传输的字节数,失败时返回SOCKET_ERROR。

#include <winsock2.h>

int recvfrom(SOCKET s, char *buf, int len, int flags, struct sockaddr *from, int fromlen);

成功时返回接收的字节数,失败时返回SOCKET_ERROR。

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

uecho_server_win.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#define BUF_SIZE 30

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

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    SOCKET serverSock;
    SOCKADDR_IN serverAddr, clientAddr;
    int clientAddrSize;

    int strLen;
    char message[BUF_SIZE];

    if (argc != 2)
    {
        printf("Usage: %s <port>\n", argv[0]);
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHanding("WSAStartup() error!");

    serverSock = socket(PF_INET, SOCK_DGRAM, 0);
    if (serverSock == INVALID_SOCKET)
        ErrorHanding("socket() error!");

    memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serverAddr.sin_port = htons(atoi(argv[1]));

    if (bind(serverSock, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
        ErrorHanding("bind() error!");

    while (1)
    {
        clientAddrSize = sizeof(clientAddr);
        strLen = recvfrom(serverSock, message, BUF_SIZE, 0, (SOCKADDR *)&clientAddr, &clientAddrSize);
        sendto(serverSock, message, strLen, 0, (SOCKADDR *)&clientAddr, clientAddrSize);
    }

    closesocket(serverSock);
    WSACleanup();

    return 0;
}

uecho_client_win.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#define BUF_SIZE 1024

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

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    SOCKET sock;
    SOCKADDR_IN serverAddr, fromAddr;
    int fromAddrSize;

    int strLen;
    char message[BUF_SIZE];

    if (argc != 3)
    {
        printf("Usage: %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHanding("WSAStartup() error!");

    sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (sock == INVALID_SOCKET)
        ErrorHanding("sock() error!");

    memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = inet_addr(argv[1]);
    serverAddr.sin_port = htons(atoi(argv[2]));

    while (1)
    {
        fputs("Insert 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, (SOCKADDR *)&serverAddr, sizeof(serverAddr));
        fromAddrSize = sizeof(fromAddr);
        strLen = recvfrom(sock, message, BUF_SIZE, 0, (SOCKADDR *)&fromAddr, &fromAddrSize);

        message[strLen] = '\0';
        printf("Message from server: %s", message);
    }

    closesocket(sock);
    WSACleanup();

    return 0;
}

编译:

gcc uecho_server_win.c -lwsock32 -o uechoServWin
gcc uecho_client_win.c -lwsock32 -o uechoClntWin

运行结果:

// 服务器端
C:\Users\81228\Documents\Program\TCP IP Project\Chapter 6>uechoServWin 9190

// 客户端
C:\Users\81228\Documents\Program\TCP IP Project\Chapter 6>uechoClntWin 127.0.0.1 9190
Insert message(q to quit): Hi UDP Server?
Message from server: Hi UDP Server?
Insert message(q to quit): Nicew to meet you!
Message from server: Nicew to meet you!
Insert message(q to quit): Good bye~
Message from server: Good bye~
Insert message(q to quit): q

基于 Windows 的存在数据边界的 UDP 套接字

bound_host1_win.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#define BUF_SIZE 30

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

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    SOCKET serverSock;
    SOCKADDR_IN serverAddr, clientAddr;
    int clientAddrSize;

    int strLen;
    char message[BUF_SIZE];

    if (argc != 2)
    {
        printf("Usage: %s <port>\n", argv[0]);
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHanding("WSAStartup() error!");

    serverSock = socket(PF_INET, SOCK_DGRAM, 0);
    if (serverSock == INVALID_SOCKET)
        ErrorHanding("socket() error!");

    memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serverAddr.sin_port = htons(atoi(argv[1]));

    if (bind(serverSock, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
        ErrorHanding("bind() error!");

    for (int i = 0; i < 3; i++)
    {
        Sleep(5000);
        clientAddrSize = sizeof(clientAddr);
        strLen = recvfrom(serverSock, message, BUF_SIZE, 0, (SOCKADDR *)&clientAddr, &clientAddrSize);
        printf("Message %d: %s\n", i + 1, message);
    }

    closesocket(serverSock);
    WSACleanup();

    return 0;
}

bound_host2_win.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

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

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    SOCKET sock;
    SOCKADDR_IN serverAddr;

    char msg1[] = "Hi!";
    char msg2[] = "I'm another UDP host!";
    char msg3[] = "Nice to meet you.";

    if (argc != 3)
    {
        printf("Usage: %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHanding("WSAStartup() error!");

    sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (sock == INVALID_SOCKET)
        ErrorHanding("sock() error!");

    memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = inet_addr(argv[1]);
    serverAddr.sin_port = htons(atoi(argv[2]));

    sendto(sock, msg1, sizeof(msg1), 0, (SOCKADDR *)&serverAddr, sizeof(serverAddr));
    sendto(sock, msg2, sizeof(msg2), 0, (SOCKADDR *)&serverAddr, sizeof(serverAddr));
    sendto(sock, msg3, sizeof(msg3), 0, (SOCKADDR *)&serverAddr, sizeof(serverAddr));

    closesocket(sock);
    WSACleanup();

    return 0;
}

编译:

gcc bound_host1_win.c -lwsock32 -o boundHost1
gcc bound_host2_win.c -lwsock32 -o boundHost2

运行结果:

// 服务器端
C:\Users\81228\Documents\Program\TCP IP Project\Chapter 6>boundHost1 9190
Message 1: Hi!
Message 2: I'm another UDP host!
Message 3: Nice to meet you.

// 客户端
C:\Users\81228\Documents\Program\TCP IP Project\Chapter 6>boundHost2 127.0.0.1 9190

基于 Windows 的已连接 UDP 套接字

uecho_con_client_win.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#define BUF_SIZE 1024

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

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    SOCKET sock;
    SOCKADDR_IN serverAddr, fromAddr; // 不再需要 fromAddr
    int fromAddrSize;                 // 多余变量

    int strLen;
    char message[BUF_SIZE];

    if (argc != 3)
    {
        printf("Usage: %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHanding("WSAStartup() error!");

    sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (sock == INVALID_SOCKET)
        ErrorHanding("sock() error!");

    memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = inet_addr(argv[1]);
    serverAddr.sin_port = htons(atoi(argv[2]));

    connect(sock, (SOCKADDR *)&serverAddr, sizeof(serverAddr));

    while (1)
    {
        fputs("Insert 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, (SOCKADDR *)&serverAddr, sizeof(serverAddr));
        */
        send(sock, message, strlen(message), 0);

        /*
        fromAddrSize = sizeof(fromAddr);
        strLen = recvfrom(sock, message, BUF_SIZE, 0, (SOCKADDR *)&fromAddr, &fromAddrSize);
        */
        strLen = recv(sock, message, sizeof(message) - 1, 0);

        message[strLen] = '\0';
        printf("Message from server: %s", message);
    }

    closesocket(sock);
    WSACleanup();

    return 0;
}

习题

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

UDP是面向报文、无连接的传输层协议。UDP尽最大努力交付数据但不保证可靠传输。

TCP有流控制和重传机制保证数据传输可靠,而UDP没有这些机制。

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

a. UDP不同于TCP,不存在连接的概念,所以不像TCP那样只能进行一对一的数据传输。
b. 利用UDP传输数据时,如果有2个目标,则需要2个套接字。
c. UDP套接字中无法使用已分配给TCP的同一端口号。
d. UDP套接字和TCP套接字可以共存。若需要,可以在同一主机进行TCP和UDP数据传输。
e. 针对UDP可以调用connect函数,此时UDP套接字和TCP套接字相同,也需要经过3次握手过程。

答:b、c、e。

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

IP负责链路选择,让UDP数据包准确传递到目的主机。

UDP负责端到端的传输,根据端口号将传输到主机的数据包交付给最终的UDP套接字。

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

TCP于UDP传输过程最大不同就是TCP要先建立连接,数据传输结束还要断开连接。所以在传输数据少,又要频繁传输数据的情况下,UDP简单轻巧的优势就凸显出来了。

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

如果没有手动bind地址信息,则在首次调用sendto函数时自动分配IP和端口号等地址信息(IP是主机IP,端口号则随机分配)。

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

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

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

如果调用connect函数,就可以忽略每次传输数据时反复进行的第一阶段和第三阶段。然而,调用connect函数并不意味着经过连接过程,只是将IP地址和端口号绑定在UDP套接字上。这样connect函数使用后,还可以用write、read函数进行数据处理,不需要sendto、recvfrom函数指定目的地址。

(7)请参考本章给出的uecho_sever.c和uecho_client.c,编写示例使服务器端和客户端轮流收发消息。收发的消息均要输出到控制台窗口。

定义协议:

  • 客户端先发,服务端先收
  • 收发的消息均要输出到控制台窗口

6_7_server.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#define BUF_SIZE 30

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

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    SOCKET serverSock;
    SOCKADDR_IN serverAddr, clientAddr;
    int clientAddrSize;

    int strLen;
    char message[BUF_SIZE];

    if (argc != 2)
    {
        printf("Usage: %s <port>\n", argv[0]);
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHanding("WSAStartup() error!");

    serverSock = socket(PF_INET, SOCK_DGRAM, 0);
    if (serverSock == INVALID_SOCKET)
        ErrorHanding("socket() error!");

    memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = htonl(INADDR_ANY);
    serverAddr.sin_port = htons(atoi(argv[1]));

    if (bind(serverSock, (SOCKADDR *)&serverAddr, sizeof(serverAddr)) == SOCKET_ERROR)
        ErrorHanding("bind() error!");

    while (1)
    {
        clientAddrSize = sizeof(clientAddr);
        strLen = recvfrom(serverSock, message, BUF_SIZE, 0, (SOCKADDR *)&clientAddr, &clientAddrSize);
        printf("Message from client: %s\n", message);

        fputs("Insert message(q to quit): ", stdout);
        fgets(message, BUF_SIZE, stdin);
        if (!strcmp(message, "q\n") || !strcmp(message, "Q\n"))
            break;

        sendto(serverSock, message, strlen(message), 0, (SOCKADDR *)&clientAddr, clientAddrSize);
        printf("Message send to client: %s\n", message);
    }

    closesocket(serverSock);
    WSACleanup();

    return 0;
}

6_7_client.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <winsock2.h>

#define BUF_SIZE 30

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

int main(int argc, char *argv[])
{
    WSADATA wsaData;
    SOCKET sock;
    SOCKADDR_IN serverAddr, fromAddr;
    int fromAddrSize;

    int strLen;
    char message[BUF_SIZE];

    if (argc != 3)
    {
        printf("Usage: %s <IP> <port>\n", argv[0]);
        exit(1);
    }

    if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
        ErrorHanding("WSAStartup() error!");

    sock = socket(PF_INET, SOCK_DGRAM, 0);
    if (sock == INVALID_SOCKET)
        ErrorHanding("sock() error!");

    memset(&serverAddr, 0, sizeof(serverAddr));
    serverAddr.sin_family = AF_INET;
    serverAddr.sin_addr.s_addr = inet_addr(argv[1]);
    serverAddr.sin_port = htons(atoi(argv[2]));

    while (1)
    {
        fputs("Insert 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, (SOCKADDR *)&serverAddr, sizeof(serverAddr));
        printf("Message send to server: %s\n", message);

        fromAddrSize = sizeof(fromAddr);
        strLen = recvfrom(sock, message, BUF_SIZE, 0, (SOCKADDR *)&fromAddr, &fromAddrSize);

        message[strLen] = '\0';
        printf("Message from server: %s\n", message);
    }

    closesocket(sock);
    WSACleanup();

    return 0;
}

编译:

gcc 6_7_server.c -lwsock32 -o 6_7_server
gcc 6_7_client.c -lwsock32 -o 6_7_client

运行结果:

// 服务器端
C:\Users\81228\Documents\Program\TCP IP Project\Chapter 6>6_7_server 9199
Message from client: Hi!I'm client.

Insert message(q to quit): Hello!I'm server.
Message send to client: Hello!I'm server.

Message from client: Nice to meet you.

Insert message(q to quit): Nice to meet you too.
Message send to client: Nice to meet you too.

Message from client: Good bye!
et you too.

Insert message(q to quit): Bye bye!
Message send to client: Bye bye!

// 客户端
C:\Users\81228\Documents\Program\TCP IP Project\Chapter 6>6_7_client 127.0.0.1 9199
Insert message(q to quit): Hi!I'm client.
Message send to server: Hi!I'm client.

Message from server: Hello!I'm server.

Insert message(q to quit): Nice to meet you.
Message send to server: Nice to meet you.

Message from server: Nice to meet you too.

Insert message(q to quit): Good bye!
Message send to server: Good bye!

Message from server: Bye bye!

Insert message(q to quit): q

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/915037.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux也有百度云喔~

一、写在前面 经常有粉丝向我抱怨&#xff0c;为什么每次发放资料都用百度云&#xff0c;自己下载了一遍之后还得再上传一遍服务器才能分析。其实大家大可不必这么周转&#xff0c;百度云也有Linux的发行版本&#xff0c;利用python包bypy来管理/传输百度云盘资源也很方便(别问…

从0开始机器学习--Day23--支持向量机

经过前面的学习&#xff0c;我们已经知道在解决问题时&#xff0c;重要的不仅仅是要在算法A或算法B中选择更优的&#xff0c;而是考虑怎么选择用于学习算法的特征和正则化参数&#xff0c;相比神经网络和逻辑回归&#xff0c;支持向量机在这两个方面做得更好。 优化目标(Optimi…

JavaScript 中实例化生成对象的相关探讨

JavaScript 中实例化生成对象的相关探讨 在 JavaScript 世界中&#xff0c;对象的实例化是一个关键且基础的概念。当我们使用构造函数创建对象时&#xff0c;会引发一系列关于对象之间联系、原型链以及相关概念的思考。 让我们通过一段代码来深入探讨这些问题&#xff1a; f…

MatSci-LLM ——潜力和挑战以及大规模语言模型在材料科学中的应用

概述 大规模语言模型的出现正在从根本上改变技术开发和研究的方式。大规模语言模型不仅对自然语言处理领域产生了重大影响&#xff0c;而且对许多相关领域也产生了重大影响&#xff0c;例如从文本生成图像的计算机视觉&#xff08;Zhang 等人&#xff0c;2023 年&#xff09;。…

【C++】C++11特性(上)

✨✨欢迎大家来到Celia的博客✨✨ &#x1f389;&#x1f389;创作不易&#xff0c;请点赞关注&#xff0c;多多支持哦&#x1f389;&#x1f389; 所属专栏&#xff1a;C 个人主页&#xff1a;Celias blog~ 目录 一、列表初始化 二、std::initializer_list 三、右值引用和移…

Linux(光速安装+ubuntu镜像 serve live-serve desktop)

ubuntu镜像_ubuntu下载地址_ubuntu安装教程-阿里巴巴开源镜像站 Index of /ubuntu-releases/ | 清华大学开源软件镜像站 | Tsinghua Open Source Mirror 清华大学的镜像好一点速度比较快&#xff01; 下载镜像 都是推荐使用服务器版&#xff0c;桌面版一般自己用 amd64 就…

Linux命令详解,全网最详细,看这一篇就够了

声明&#xff01; 学习视频来自B站up主 泷羽sec 有兴趣的师傅可以关注一下&#xff0c;如涉及侵权马上删除文章&#xff0c;笔记只是方便各位师傅的学习和探讨&#xff0c;文章所提到的网站以及内容&#xff0c;只做学习交流&#xff0c;其他均与本人以及泷羽sec团队无关&#…

机器情绪及抑郁症算法

&#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f916;编程探索专栏&#xff1a;点击&#xff01; ⏰️创作时间&#xff1a;2024年11月12日17点02分 点击开启你的论文编程之旅https://www.aspiringcode.com/content?id17230869054974 计算机来理解你的情绪&a…

美食网的设计与实现

摘 要 随着科技的发展、生活水平的提升&#xff0c;人们更加注重饮食搭配和饮食健康。通过网络技术来加强美食与健康知识的普及是当前一种可行的措施。通过网页浏览美食网&#xff0c;不仅可以普及每道美食的做法&#xff0c;通过制作美食来缓解心情&#xff0c;还可以通过美…

Ubuntu[无桌面]——修改Docker镜像源文件

下载镜像的时候&#xff0c;一般有两种方式&#xff1a; &#xff08;1&#xff09;在宿主主机配置相应的文件/etc/docker/daemon.json&#xff0c;配置镜像源环境地址 &#xff08;2&#xff09;进入https://quay.io/search中&#xff0c;输入搜索需要下载的镜像名称&#xff…

ODOO学习笔记(8):模块化架构的优势

灵活性与可定制性 业务流程适配&#xff1a;企业的业务流程往往因行业、规模和管理方式等因素而各不相同。Odoo的模块化架构允许企业根据自身的具体业务流程&#xff0c;选择和组合不同的模块。例如&#xff0c;一家制造企业可以启用采购、库存、生产和销售模块&#xff0c;并通…

Git的使用(基础语句)

首先如果想要使用git的各项功能&#xff0c;我们要下载Git-2.40.1-64-bit.exe这个驱动程序&#xff0c;并安装它&#xff0c;这个资源我没有办法上传是因为有的博主已经上传过了&#xff0c;所以有VIP的或者有钱哥可以去csdn上自行下载&#xff0c;实在不行加我qq我发你4925396…

labview用sql server数据库存取数据到一个单元格

最近有一个项目上需要一个庞大的数据量&#xff0c;需要很多列&#xff0c;但是百度查了一下sqi server最多支持1024列&#xff0c;这一限制适用于大多数表类型&#xff0c;包括常规表&#xff0c;临时表和表变量&#xff0c;要注意的是如果超出这一限制可能会导致数据的完整性…

Lucene 和 Elasticsearch 中更好的二进制量化 (BBQ)

作者&#xff1a;来自 Elastic Benjamin Trent Lucene 和 Elasticsearch 中更好的二进制量化 (BBQ)。 嵌入模型输出 float32 向量&#xff0c;通常对于高效处理和实际应用来说太大。Elasticsearch 支持 int8 标量量化&#xff0c;以减小向量大小&#xff0c;同时保持性能。其他…

库打包工具 rollup

库打包工具 rollup 摘要 **概念&#xff1a;**rollup是一个模块化的打包工具 注&#xff1a;实际应用中&#xff0c;rollup更多是一个库打包工具 与Webpack的区别&#xff1a; 文件处理&#xff1a; rollup 更多专注于 JS 代码&#xff0c;并针对 ES Module 进行打包webpa…

2024中国游戏出海情况

01 哪里出海更花钱&#xff1f; 报告显示&#xff0c;中国手游在全球不同市场的获客成本不同&#xff0c;整体来看北美市场竞争更加激烈&#xff0c;其安卓和iOS获客成本是拉丁美洲的12倍和7倍。 按具体市场划分&#xff0c;获客成本最高的TOP 3为韩国、美国和日本&#xff0c…

【达梦数据库】MYSQL迁移到DM字符集转换问题-UTF8mb4|转UTF8(UTF8mb3)

目录 背景现象问题原因原因1&#xff1a;字符集不同原因2&#xff1a;以字节为单位 解决办法方法1&#xff1a;扩大长度 结果验证MYSQLDTSDM 背景 迁移过程环境信息如下&#xff1a; 数据库版本字符集补充MYSQL8.0.xxUTF8mb4DM8.1.3.162UTF8&#xff08;UTF8mb3的简称&#x…

Qt_day10_程序打包(完结)

目录 1. 设置图标 2. Debug和Release版本 3. 动态链接库 4. 打包 5. 联系项目要求 Qt开发的程序最终都是要给用户使用的&#xff0c;用户的电脑上不可能装一个Qt的开发环境导入项目使用。因此项目项目开发完成后需要打包——制作成安装包&#xff0c;用户直接下载并安装即可使用…

RT-DETR融合[ECCV2024]自调制特征聚合SMFA模块及相关改进思路

RT-DETR使用教程&#xff1a; RT-DETR使用教程 RT-DETR改进汇总贴&#xff1a;RT-DETR更新汇总贴 《SMFANet: A Lightweight Self-Modulation Feature Aggregation Network for Efficient Image Super-Resolution》 一、 模块介绍 论文链接&#xff1a;https://link.springer.…

postman变量和脚本功能介绍

1、基本概念——global、collection、environment 在postman中&#xff0c;为了更好的管理各类变量、测试环境以及脚本等&#xff0c;创建了一些概念&#xff0c;包括&#xff1a;globals、collection、environment。其实在postman中&#xff0c;最上层还有一个Workspaces的概…