云计算百科
云计算领域专业知识百科平台

Linux网络编程之UDP 回声服务器与客户端

这个Demo主要演示了如何在Linux环境下使用UDP协议实现一个简单的回声服务器(Echo Server)以及与之对应的客户端应用程序:

服务器端

#include <stdio.h>      // 引入标准输入输出库,用于打印信息
#include <stdlib.h>     // 引入标准库,提供exit()等函数
#include <string.h>     // 引入字符串处理函数库
#include <unistd.h>     // 引入UNIX标准函数库,包含如close()函数
#include <arpa/inet.h>  // 引入网络字节顺序转换和网络协议相关函数库
#include <sys/socket.h> // 引入 socket 创建和操作函数库

#define BUF_SIZE 30     // 定义缓冲区大小为30字节

void error_handling(char *message); // 错误处理函数的声明

int main(int argc, char *argv[]) // 程序主入口
{
   int serv_sock;                // 用于存储服务端socket描述符
   char message[BUF_SIZE];       // 定义缓冲区,存储接收到的消息
   int str_len;                  // 存储接收到的数据长度
   socklen_t clnt_adr_sz;        // 客户端地址结构大小

   struct sockaddr_in serv_adr, clnt_adr; // 服务端和客户端地址结构

   if(argc != 2){  // 如果命令行参数不等于2(程序名和端口号),则输出用法提示
       printf("Usage : %s <port>\\n", argv[0]);
       exit(1);  // 退出程序
  }

   // 创建 UDP socket
   serv_sock = socket(PF_INET, SOCK_DGRAM, 0);
   if(serv_sock == -1)  // 如果 socket 创建失败,则调用错误处理函数
       error_handling("UDP socket creation error");

   // 初始化服务端地址结构
   memset(&serv_adr, 0, sizeof(serv_adr));  // 清空地址结构
   serv_adr.sin_family = AF_INET;           // 设置协议族为 IPv4
   serv_adr.sin_addr.s_addr = htonl(INADDR_ANY);  // 设置服务器可以接收任意客户端发送的数据
   serv_adr.sin_port = htons(atoi(argv[1])); // 将端口号转换为网络字节顺序,并设置端口

   // 绑定 socket 到指定的 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);  // 设置客户端地址的大小
       // 接收客户端发送的数据
       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);  // 关闭 socket
   return 0;
}

// 错误处理函数
void error_handling(char *message)
{
   fputs(message, stderr);  // 将错误信息输出到标准错误流
   fputc('\\n', stderr);     // 输出换行符
   exit(1);                 // 退出程序
}

这个 UDP 回声服务器的流程可以总结为以下几个步骤:

1. 参数检查
  • 服务器启动时,会检查命令行参数是否正确,确保用户输入了端口号。如果参数不正确,程序会输出用法说明并退出。

  • 例如,如果命令行输入是 ./server 12345,12345 就是指定的端口号。

2. 创建 UDP Socket
  • 使用 socket() 函数创建一个 UDP socket。PF_INET 表示使用 IPv4 协议,SOCK_DGRAM 表示创建一个数据报(UDP)socket。

  • 如果 socket 创建失败,调用 error_handling() 函数输出错误信息并退出程序。

3. 绑定 Socket 到端口
  • 服务器构建一个sockaddr_in类型的地址结构,并设置:

    • sin_family = AF_INET:指定使用 IPv4 协议。

    • sin_addr.s_addr = htonl(INADDR_ANY):允许服务器监听来自任何 IP 地址的请求。

    • sin_port = htons(atoi(argv[1])):设置服务器监听的端口号。

  • 使用 bind() 函数将创建的 UDP socket 与该地址结构绑定,确保服务器通过指定的端口接收数据。

  • 如果绑定失败,调用 error_handling() 输出错误信息并退出。

4. 进入主循环,等待并处理客户端请求
  • 在无限循环中,服务器等待接收客户端发送的数据:

    • 使用 recvfrom() 函数从客户端接收数据。该函数将收到的数据存储在 message 缓冲区中,str_len 表示接收到的数据长度。

    • 客户端的地址信息(如 IP 地址和端口)被保存在 clnt_adr 中,clnt_adr_sz 保存客户端地址的大小。

5. 回送接收到的数据
  • 一旦接收到数据,服务器会使用 sendto() 函数将相同的数据发送回客户端。这实现了回声服务器的功能,即将客户端发送的消息原封不动地返回。

6. 重复处理
  • 服务器会继续重复步骤 4 和 5,处理下一个客户端发来的数据,直到程序被外部终止。

7. 关闭 Socket
  • 虽然这段代码进入了无限循环,但如果退出循环(例如遇到错误或程序被外部信号终止),服务器会关闭 socket,释放资源。

客户端

#include <stdio.h>      // 引入标准输入输出库,用于打印信息
#include <stdlib.h>     // 引入标准库,提供 exit() 等函数
#include <string.h>     // 引入字符串处理函数库
#include <unistd.h>     // 引入 UNIX 标准函数库,包含 close() 函数
#include <arpa/inet.h>  // 引入网络字节顺序转换和网络协议相关函数库
#include <sys/socket.h> // 引入 socket 创建和操作函数库

#define BUF_SIZE 30     // 定义缓冲区大小为 30 字节

void error_handling(char *message); // 错误处理函数的声明

int main(int argc, char *argv[]) // 程序主入口
{
   int sock;                  // 用于存储客户端 socket 描述符
   char message[BUF_SIZE];    // 定义缓冲区,存储发送的消息
   int str_len;               // 存储接收到的数据长度
   socklen_t adr_sz;          // 存储服务器地址的大小

   struct sockaddr_in serv_adr, from_adr;  // 服务器和接收数据的地址结构
   
   if(argc != 3){  // 检查命令行参数是否为 3 个(程序名、服务器 IP 和端口号)
       printf("Usage : %s <IP> <port>\\n", argv[0]);
       exit(1);  // 如果参数不正确,则退出程序
  }
   
   // 创建 UDP socket
   sock = socket(PF_INET, SOCK_DGRAM, 0);  
   if(sock == -1)  // 如果 socket 创建失败,则调用错误处理函数
       error_handling("socket() error");
   
   // 初始化服务器地址结构
   memset(&serv_adr, 0, sizeof(serv_adr));  // 清空地址结构
   serv_adr.sin_family = AF_INET;           // 设置协议族为 IPv4
   serv_adr.sin_addr.s_addr = inet_addr(argv[1]);  // 将命令行提供的 IP 地址转换为网络字节顺序
   serv_adr.sin_port = htons(atoi(argv[2]));  // 将命令行提供的端口号转换为网络字节顺序
   
   // 进入发送消息循环
   while(1)
  {
       fputs("Insert message(q to quit): ", stdout);  // 提示用户输入消息
       fgets(message, sizeof(message), stdin);  // 从标准输入读取用户消息
       
       // 如果用户输入 "q" 或 "Q",则退出循环
       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);  // 关闭 socket
   return 0;

}

// 错误处理函数
void error_handling(char *message)
{
   fputs(message, stderr);  // 将错误信息输出到标准错误流
   fputc('\\n', stderr);     // 输出换行符
   exit(1);                 // 退出程序
}

客户端的流程如下:

1.命令行参数检查

首先检查输入的参数是否正确(需要提供服务器的 IP 地址和端口号)。如果不正确,则输出用法说明并退出。

2.创建 UDP Socket

通过 socket() 函数创建一个 UDP socket,准备与服务器进行通信。如果创建失败,则调用 error_handling() 输出错误信息并退出。

3.配置服务器地址

设置 sockaddr_in 结构体,配置服务器的 IP 地址和端口号。inet_addr() 函数将输入的 IP 地址转换为网络字节顺序,htons() 函数将端口号转换为网络字节顺序。

4.消息发送与接收
  • 进入一个无限循环,提示用户输入消息。

  • 用户输入的消息通过 sendto() 发送到服务器。如果输入的是 "q" 或 "Q",则退出循环。

  • 客户端等待服务器回显消息,通过 recvfrom() 函数接收服务器的回应。

  • 接收到服务器的消息后,输出到屏幕上。

5.关闭 Socket

当退出循环后,关闭 socket 连接,结束客户端程序。

程序执行

void main(char* argv0) {   // 程序的主入口,接收程序的名称作为参数(argv0)
   pid_t pid = fork();    // 创建子进程,fork() 会返回两次,父进程返回子进程的 PID,子进程返回 0

   if (pid > 0) {  // 如果 pid 大于 0,表示当前代码在父进程中执行
       // 主进程部分
       int argc = 3;  // 主进程的参数个数设为 3(程序名、IP 地址、端口号)
       
       // 构造主进程的参数数组,用于调用 client 函数
       char* argv[] = {
           argv0,          // 程序名(从参数传入)
          (char*)"127.0.0.1",  // 服务器的 IP 地址
          (char*)"9999",  // 服务器的端口号
           NULL  // 确保数组以 NULL 结尾,符合 C 字符串数组的规范
      };
       
       // 调用 client 函数,传入主进程的参数(argc 和 argv)
       client(argc, argv);
       
       int status = 0;  // 存储子进程的退出状态
       
       // 等待子进程结束并获取其退出状态
       wait(&status);
  }
   else if (pid == 0) {  // 如果 pid 等于 0,表示当前代码在子进程中执行
       // 子进程部分
       int argc = 2;  // 子进程的参数个数设为 2(程序名、端口号)
       
       // 构造子进程的参数数组,用于调用 server 函数
       char* argv[] = {
           argv0,          // 程序名(从参数传入)
          (char*)"9999",  // 监听的端口号
           NULL  // 确保数组以 NULL 结尾
      };
       
       // 调用 server 函数,传入子进程的参数(argc 和 argv)
       server(argc, argv);
  }
   else {  // 如果 pid 小于 0,表示 fork 失败
       // 打印错误信息,表示 fork 调用失败
       perror("fork failed");
  }
}

重要概念解释

serv_adr.sin_family = AF_INET;

功能概述

这行代码设置 serv_adr 结构体中的 sin_family 成员为 AF_INET,表示使用 IPv4 协议族。在网络编程中,sin_family 字段用于指定地址族,不同的地址族对应不同的网络协议。

知识点详解
  • struct sockaddr_in 结构体:这是一个用于存储 IPv4 地址信息的结构体,定义在 <arpa/inet.h> 头文件中。serv_adr 就是这个结构体类型的变量。

  • sin_family 成员:该成员用于指定地址族,AF_INET 是一个常量,表示 IPv4 协议族。常见的地址族还有 AF_INET6 表示 IPv6 协议族。

serv_adr.sin_addr.s_addr = inet_addr(argv[1]);

功能概述

这行代码将命令行提供的 IP 地址(argv[1])转换为网络字节顺序,并存储在 serv_adr 结构体的 sin_addr.s_addr 成员中。

知识点详解
  • sin_addr 成员:struct sockaddr_in 结构体中的 sin_addr 成员也是一个结构体,其定义为 struct in_addr sin_addr;,用于存储 IP 地址。sin_addr.s_addr 是一个 32 位的无符号整数,用于存储 IPv4 地址的二进制表示。

  • inet_addr 函数

    该函数位于<arpa/inet.h>头文件中,其原型为in_addr_t inet_addr(const char *cp);

    • 参数:cp 是一个指向点分十进制表示的 IPv4 地址字符串的指针,这里是 argv[1],即命令行中的第二个参数。

    • 返回值:返回一个 in_addr_t 类型的值,表示转换后的网络字节顺序的 IP 地址。如果输入的字符串不是有效的 IP 地址,inet_addr 会返回 INADDR_NONE。不过需要注意的是,inet_addr 在处理 255.255.255.255 时可能会有问题,现在更推荐使用 inet_pton 函数。

serv_adr.sin_port = htons(atoi(argv[2]));

功能概述

这行代码将命令行提供的端口号(argv[2])转换为网络字节顺序,并存储在 serv_adr 结构体的 sin_port 成员中。

知识点详解
  • sin_port 成员:struct sockaddr_in 结构体中的 sin_port 成员是一个 16 位的无符号整数,用于存储端口号。

  • atoi 函数:该函数位于 <stdlib.h> 头文件中,其原型为 int atoi(const char *nptr);。它的作用是将字符串转换为整数,这里将 argv[2](命令行中的第三个参数)转换为整数类型的端口号。

  • htons 函数

    该函数位于<arpa/inet.h>头文件中,其原型为uint16_t htons(uint16_t hostshort);

    • 功能:htons 表示 “host to network short”,即从主机字节顺序转换为网络字节顺序。在不同的计算机系统中,字节序可能不同(大端序或小端序),而网络通信中要求使用大端序(网络字节顺序),因此需要使用 htons 函数进行转换。

    • 参数:hostshort 是一个 16 位的无符号整数,表示主机字节顺序的端口号。

    • 返回值:返回网络字节顺序的端口号。

UDP网络编程一般步骤

同时也可以总结出UDP网络编程的一般步骤:

  • 创建UDP Socket
    • 使用 socket() 函数指定协议族(通常为 PF_INET 或 AF_INET)和通信类型(使用 SOCK_DGRAM 表示UDP),建立一个UDP套接字。

    • 此步骤是整个通信的起点,套接字充当网络通信的端点。

  • 配置地址结构
    • 定义并初始化 sockaddr_in 结构体,用来存储IP地址和端口信息。

    • 对于服务器端,可以将 IP 地址设置为 INADDR_ANY 以允许接收任意源的数据;对于客户端,则需要指定目标服务器的具体IP地址和端口号。

    • 通过 inet_addr() 和 htons() 等函数进行网络字节序转换,确保数据传输的一致性。

  • 绑定Socket(仅对服务器)

    • 服务器端需调用 bind() 函数,将创建的socket绑定到指定的IP地址和端口上。

    • 绑定后,服务器可以接收发往该端口的UDP数据报。

  • 数据发送与接收
    • 接收数据(服务器端): 使用 recvfrom() 函数接收来自客户端的数据报,同时获取客户端的地址信息。

    • 发送数据:

      • 对于服务器端:将接收到的数据通过 sendto() 函数返回给客户端,实现回声或响应功能。

      • 对于客户端:使用 sendto() 将消息发送到服务器指定的地址,然后使用 recvfrom() 接收服务器回复的数据。

    • UDP不保证数据传输的可靠性,因此在实际应用中可能需要设计重发、丢包检测等机制。

  • 错误处理

    每个系统调用(如 socket()、bind()、sendto()、recvfrom() 等)都应检查其返回值,发现错误时,通过适当的错误处理机制(例如调用自定义的 error_handling() 函数)输出错误信息并安全退出程序。

  • 资源关闭与清理

    当数据传输完成或程序退出前,务必调用 close() 函数关闭socket,释放系统资源,避免内存泄露和端口占用问题。

  • 赞(0)
    未经允许不得转载:网硕互联帮助中心 » Linux网络编程之UDP 回声服务器与客户端
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!