【Linux】套接字的理解 & 基于UDP协议的套接字编程(多版本)

1. 前言

1.1 网络方面的预备知识👇

网络基础 - 预备知识(协议、网络协议、网络传输流程、地址管理)


1.2 了解 UDP协议

① 对UDP协议进行简单的认知:

  • UDP(用户数据报协议,User Datagram Protocol) 是一种 无连接的、 是一种 不可靠传输 面向数据报的传输层协议,位于OSI模型中的第四层。

② 关于UDP的详细内容在:

深入理解UDP协议:从报文格式到应用本质


2. 关于套接字编程

2.1 什么是套接字 Socket

套接字(Socket) 是计算机网络中用于实现进程间通信的一种机制。它允许在不同计算机之间或同一计算机的不同进程之间进行数据传输和通信

套接字可以看作是网络通信中的一个端点 ,它由 IP地址 端口号 组成, 用于唯一标识网络中的通信实体点 。套接字提供了一组接口(通常是API)用于创建、连接、发送、接收和关闭连接等操作,以实现数据的传输和通信。


套接字可以分为两种类型(了解) 流套接字(Stream Socket) 数据报套接字(Datagram Socket)

  1. 流套接字 :基于 传输控制协议(TCP) 的套接字,提供面向连接的、可靠的、双向的数据传输。

    • 流套接字通过建立连接来实现数据的可靠传输,适用于需要保证数据完整性和顺序性的应用,如网页浏览、文件传输等。
  2. 数据报套接字:基于 用户数据报协议(UDP) 的套接字,提供无连接的、不可靠的数据传输。

    • 数据报套接字不需要建立连接,可以直接发送数据报给目标主机,适用于实时性要求高、对数据完整性和顺序性要求不高的应用,如视频流传输、实时游戏等。

2.2 socket 的接口函数

下面列举在我们进行Tcp与Udp的套接字编程所用的API;

// 创建 socket 文件描述符 (TCP/UDP, 客户端 & 服务器)
int socket(int domain, int type, int protocol);

// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,
socklen_t address_len);

// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);

// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,
socklen_t* address_len);

// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen)

2.3 Udp套接字编程的步骤

  1. 创建套接字:使用 socket() 函数创建套接字。

  2. 绑定套接字:利用bind() 将套接字绑定到一个 IP 地址和端口上。

  3. 发送数据:使用 sendto() 函数发送数据到目标地址。

  4. 接收数据:使用 recvfrom() 函数接收数据。

  5. 关闭套接字:通信结束后,使用close()关闭套接字以释放资源


2.4 sockaddr 结构

首先:

  • IPv4、 IPv6地址类型分别定义为常数AF_INET、 AF_INET6
    • 这样只要取得某种sockaddr结构体的首地址,不需要知道具体是哪种类型的sockaddr结构体,就可以根据地址类型字段确定结构体中的内容
  • socket API 可以都用struct sockaddr *类型表示, 在使用的时候需要强制转化成sockaddr_in;
    • 好处在于程序的通用性, 可以接收IPv4, IPv6, 以及UNIX Domain Socket各种类型的sockaddr结构体指针做为参数;

sockaddr 结构是在套接字编程中表示网络地址的通用结构。本身是一个抽象的结构,最常见的使用是:用于表示 IPv4 地址。

sockaddr 结构定义如下:

struct sockaddr {
    sa_family_t sa_family; // 地址家族(如 AF_INET)
    char sa_data[14]; // 地址数据
};

在实际使用中,经常使用 struct sockaddr_in 结构来表示 IPv4 地址,定义如下:

struct sockaddr_in {
    sa_family_t sin_family; // 地址家族(AF_INET)
    in_port_t sin_port; // 端口号
    struct in_addr sin_addr; // IP 地址
    char sin_zero[8]; // 填充字段,通常为0
};

3. 代码实现

有了上面的预备知识,我们可以正式编写 Udp套接字编程 的代码:

3.1 udp_server.hpp(udp 服务器)

在该头文件中,我们实现一个基于UDP协议实现的服务器类UdpServer的代码。该类包含了 初始化服务器、启动服务器 等功能。

① 框架

下面的框架用来展示Udp服务器的成员变量和相关成员函数。

#define SIZE 1024 // 默认缓冲区大小
class UdpServer
{
public:
	// 构造
    UdpServer(uint16_t port, std::string ip = ""):_port(port),_ip(ip) //可以初始化ip为"0.0.0.0"
    {}
    
	// 析构
    ~UdpServer()
    {
		if(_sock >= 0)
			close(_sock); // 关闭_sock
	}
	// 初始化服务器
    void InitServer()
    {}   
    
	// 启动服务器
    void Start()
  	{}

private:
    // 一个服务器一定需要 端口号和ip
    uint16_t _port;  // 端口号
    std::string _ip; // ip
    int _sock; // 表示 UDP 套接字的文件描述符
};

② Init() - 初始化服务器

实现 InitServer()的步骤:

  1. 创建套接字
  2. 创建并初始化 struct sockaddr_in 变量 local
  3. 将初始化后的 localip、端口与 _sock 绑定
  4. 输出日志信息

具体的操作,下面代码中的注释都有解释。

void InitServer()
{
    // 1. 创建套接字
    _sock = socket(AF_INET, SOCK_DGRAM, 0);
    if(_sock < 0) // _sock<0 创建失败
    {
        logMessage(FATAL, "%d:%s", errno, strerror(errno)); // 输出错误信息
        exit(2); // 退出程序
    }   

  	// 2. 创建 struct sockaddr_in 变量 并初始化其变量
    struct sockaddr_in local;
    bzero(&local, sizeof(local)); // bzero(s,n), 将 s 指向的内存区域的前 n 个字节全部设置为 0(同memset)
    local.sin_family = AF_INET; // AF_INET 地址族,表示 IPv4 地址
    // 网络通信时,我们服务器的ip和port一样需要发送给对方主机
    local.sin_port = htons(_port); // 将本地端口号转为网络字节序
    // I. 先将点分十进制字符串风格的ip 转为 4字节
    // II. 再 4字节主机序列 -> 网络序列
    local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str()); // inet_addr() 直接完成上述两步操作
    // 将_sock 绑定local的ip、端口号
    if(bind(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
    {
        logMessage(FATAL, "%d:%s", errno, strerror(errno));
        exit(2);
    }
    // 初始化成功 输出信息
    logMessage(NORMAL, "init udp server done >>> %s", strerror(errno));
}   

在进行 ip地址 的转换时:

local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

我们知道:

  • local 是一个 sockaddr_in 结构体变量,用于 存储服务器地址信息 。其中,sin_addr.s_addr 表示 IPv4 地址。
  • _ip为空,将 _ip 赋值为 INADDR_ANY,即将 服务器绑定到本地任意可用地址 上。
  • _ip不为空时,将 字符串形式的 _ip 转换为网络字节序的 IP 地址
  • 最后,bind() 绑定完毕,初始化结束。

③ Start() - 启动服务器

启动服务器具体有以下步骤:

  1. 创建struct sockaddr_in peer用于存储客户端信息
  2. 接收客户端信息 并输出打印相关内容(ip、端口、信息)
  3. 将从客户端接收到的数据写回
void Start()
    {
        // 网络服务器端是永不退出的
        // 服务器启动 -> 进程 -> 进程常驻
        char buffer[SIZE]; // 用于存储接收的客户端信息
        for( ; ; )
        {
            struct sockaddr_in peer; // 存储客户端的地址信息
            bzero(&peer, sizeof(peer)); // 初始化peer所占内存区域
            
            socklen_t len = sizeof(peer); // peer大小
            // 接收客户端的数据信息,并存储到buffer中
            ssize_t s = recvfrom(_sock, buffer, sizeof buffer-1, 0, (struct sockaddr*)&peer, &len);
            if(s > 0) // 处理接受的数据
            {
                buffer[s] = 0; // 将 数据末尾置为 数据字符串结束符
                uint16_t cli_port = ntohs(peer.sin_port); // 网络字节序的端口号 -> 为本机字节序的端口号
                std::string cli_ip = inet_ntoa(peer.sin_addr); // 网络序列ip -> 本地风格ip
                printf("[%s:%d]# %s\n", cli_ip.c_str(), cli_port, buffer); // 服务端打印出 客户端的ip、端口号以及发出的信息
            }
            // 将从客户端接收到的数据写回
            sendto(_sock, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
        }
    }

3.2 upd_server.cc

思路:

  • 用于执行整个程序,main函数中初始化并启动了服务器
  • 确保正确传入参数(udp_server ip port),直接创建出udpServer对象,调用其初始化函数,以及启动函数即可。(详细在注释中体现)
static void usage(const char* programName) {
    std::cerr << Invalid command line arguments.\n Usage:\t" << programName << " ip port" << std::endl;
}

int main(int argc, char* argv[])
{
    // 参数数量错误
    if(argc != 3)
    {
        // 输出正确用法
        usage(argv[0]);
        exit(1);
    }

    std::string ip = argv[1];
    uint64_t port = atoi(argv[2]);
    // 通过端口号创建 UdpServer 实例
    // 使用智能指针 当main函数结束后会 自动释放udpServer对象,无需手动调用delete
    std::unique_ptr<UdpServer> svr(new UdpServer(port));
    // 进行初始化+启动操作
    svr->InitServer();
    svr->Start();

    return 0;
}

3.3 udp_client.cc(udp 客户端)

该文件为客户端的代码:有以下 主要功能 / 实现步骤:

  1. 创建UDP套接字 用于与服务器进行通信
  2. 在主循环中,等待用户输入消息
  3. 接收用户输入的消息并发送给服务器
  4. 接收并打印 服务器返回的响应消息
  5. 关闭套接字 释放资源

① 整体代码

udp_client.cc的整体代码实现:

// 单线程
// usage() - 输出程序正确用法
static void usage(const char* programName) { 
    std::cerr << "Invalid command line arguments.\n Usage:\t" << programName << " ip port" << std::endl;
}

int main(int argc, char *argv[]) // agrv[0] 代表可执行参数的name,argv[1]代表第一个参数...i
{
    if(argc != 3) // argc命令行参数的数量
    {
        usage(argv[0]);
        exit(1);
    }

    int sock = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
    if(sock < 0)
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    // client 一般不会显式的进行bind,让OS 自动随机选择
    // client 是客户端 -> 一般用户 下载安装启动使用
    // client 需要bind 固定的ip 和 port 以防 其他的进程占用port
    // std::unique_ptr<>
    std::string message;
    struct sockaddr_in server;
    memset(&server, 0, sizeof server); // 对 server 结构体进行初始化,将其所有成员变量置零 / 可替换bzero
    server.sin_family = AF_INET; // 地址族设置为 IPv4
    server.sin_port = htons(atoi(argv[2])); // 主机字节序转换为网络字节序
    server.sin_addr.s_addr = inet_addr(argv[1]); // 将字符串形式的 IP 地址转换为网络字节序的二进制 IP 地址

    char buffer[1024];
    while(true)
    {
        std::cout << "请输入你的信息# ";
        std::getline(std::cin, message); // 接收信息
        if(message == "quit") break; // 如果用户输入quit,则退出程序
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof server); // 将 message 字符串的内容发送给服务器

        struct sockaddr_in temp;
        socklen_t len = sizeof temp;
        ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr*)&temp, &len); // 接收server回应的消息到buffer中
        if(s > 0) // 接受成功
        {
            buffer[s] = 0;
            std::cout << "server 回应#  " << buffer << std::endl; // 打印回应信息
        }
    }
    close(sock); // 关闭 sock
    return 0;
}

② 步骤拆析

此部分为对于整体代码的拆分解释(为什么要这么写) 👇

Ⅰ. Usage(输出函数正确用法)
  • 根据上面 udp_server.cc 的代码实现,如何执行得到的可执行文件? 要给出连接的ip地址与端口号。

在这里插入图片描述

当我们错误给出参数后,我们希望程序给出错误提示,如下图所示:

在这里插入图片描述

  • 下面的代码当用户错误给出参数时,打印正确的执行方式。
// 打印出 程序的正确执行用法
static void usage(const char* programName) {
    std::cerr << "Invalid command line arguments.\n Usage:\t" << programName << " ip port" << std::endl;
}

int main(int argc, char *argv[]) // agrv[0] 代表可执行参数的name,argv[1]代表第一个参数...i
{
    if(argc != 3) // argc命令行参数的数量
    {
        usage(argv[0]);
        exit(1);
    }
}

main函数有 (int argc, char *argv[]) 两参数,argc用来判断命令行参数的数量argv[]用于读取命令行参数,便于后续使用。


Ⅱ. 套接字的创建与初始化

参数前提保证后,下面进行 套接字的创建 ,以及sockaddr_in 类型server的创建(用于与服务器连接发送信息);

code_segment1() // 这里用代码段1表示,实际上代码依然在main函数中,没有实际意义
{
	// 创建套接字
	int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if(sock < 0) // 如果创建失败,打印错误信息
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }	

    std::string message; // 用于存储发送的信息
    struct sockaddr_in server; // 连接服务器 与 信息发送
    memset(&server, 0, sizeof server); // 可替换bzero
    server.sin_family = AF_INET; // 初始化server 
    server.sin_port = htons(atoi(argv[2]));
    server.sin_addr.s_addr = inet_addr(argv[1]);
}

关于socekt传入的参数:

  • AF_INET:表示套接字的地址族为 IPv4。
  • SOCK_DGRAM:表示套接字的类型为数据报套接字,即用于支持无连接、不可靠的消息传输,与 TCP 协议的流式套接字不同。
  • 0:表示使用默认协议。

接下来进行 “接收用户信息发送给服务端,并读取服务端 的回显信息” ,最后关闭套接字。

code_segment2()
{
	char buffer[1024]; // 存储接受消息的缓冲区
    while(true) // 循环接收用户信息
    {
        std::cout << "请输入你的信息# " << std::endl;
        std::getline(std::cin, message); // 接收读取信息
        if(message == "quit") break; // 判断退出信号
        // 向server套接字发送信息
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof server);

        struct sockaddr_in temp; // 用于存储发送方的地址信息
        socklen_t len = sizeof temp;
        // 接收sock信息存储到buffer
        ssize_t s = recvfrom(sock, buffer, sizeof buffer, 0, (struct sockaddr*)&temp, &len);
        if(s > 0) // 接受成功
        {
            buffer[s] = 0; // 输出信息
            std::cout << "server echo#  " << buffer << std::endl;
        }
    }
    close(sock);
    return 0;
}

需要注意的是sendto 与 recvfrom 的使用。


3.3 log.hpp(日志库)

log.hpp用于记录日志,根据代码的不同情况输出不同信息:

// 宏定义 日志级别
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

// 全局字符串数组 : 将日志级别映射为对应的字符串
const char *gLevelMap[] = {
    "DEBUG",
    "NORMAL",
    "WARNING",
    "ERROR",
    "FATAL"
};

#define LOGFILE "./threadpool.log" // LOGFILE: 表示日志文件的路径

void logMessage(int level, const char* format, ...)
{
    // 判断DEBUG_SHOW 是否定义,分别执行操作

#ifndef DEBUG_SHOW // 根据需要选择是否输出 DEBUG_SHOW 级别的日志i信息
    if(level == DEBUG) return; // DEBUG_SHOW不存在 且 日志级别为 DEBUG时,返回
#endif
    // DEBUG_SHOW存在 则执行下面的日志信息 
    char stdBuffer[1024];
    time_t timestamp = time(nullptr);

    // 将日志级别和时间戳格式化后的字符串将会被写入到 stdBuffer 缓冲区中
    snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%ld] ", gLevelMap[level], timestamp);
    
    char logBuffer[1024];
    va_list args; // 可变参数列表
    va_start(args, format);
	// 将格式化后的日志信息写入到 logBuffer 缓冲区中
    vsnprintf(logBuffer, sizeof(logBuffer), format, args);
    va_end(args);

    printf("%s%s\n", stdBuffer, logBuffer);
}

4. 演示最终效果

在这里插入图片描述
从上面的结果演示:udp_client 发出的信息被 udp_server 所接收并返回消息。


5. 其他版本

5.1 服务端 接收命令 并执行

  • 上面实现的服务端代码 用来接收客户端的信息并进行回显

  • 接下来实现一个 接收客户端的命令并执行且返回执行结果 的服务端:

大致思路如下:

  1. 接收 客户端传来的指令信息相关套接字地址信息
  2. 执行 接收的命令将执行结果返回客户端
  3. 其中涉及了一些函数如(popen 等,注释概括了其作用,请自行查阅函数具体功能)
void Start()
{
  char buffer[SIZE]; // 存储客户端传来的指令信息
  for( ; ; )
  {
      struct sockaddr_in peer; // 存储与客户端通信的套接字地址信息
      bzero(&peer, sizeof(peer));
      socklen_t len = sizeof(peer);

      char ret[256]; // 存储命令执行的结果
      std::string cmd_echo; // 每次执行命令后,将结果追加到cmd_echo中

      //1. 读取数据
      ssize_t s = recvfrom(_sock, buffer, sizeof buffer-1, 0, (struct sockaddr*)&peer, &len);
      if(s > 0) // 处理接受的数据
      {
          buffer[s] = 0; // 将指令输入到buffer
          // 接受的字符串为指令 exp.ls -l -a
          // 如果客户端执行删除命令(rm,rmdir),我们取消执行,并打印+回复错误信息
          if(strcasestr(buffer, "rm") != nullptr || strcasestr(buffer, "rmdir") != nullptr)
          {
              // 得到客户端ip,port,将该用户和相关信息传给server
              uint16_t cli_port = ntohs(peer.sin_port); // 得到客户端
              std::string cli_ip = inet_ntoa(peer.sin_addr); // 网络序列ip -> 本地风格ip

              std::string err_message = "检测到删除操作.. 取消执行";
              err_message += cli_ip + " ";
              err_message += std::to_string(cli_port) + " | ";
              std::cout << err_message << buffer << std::endl;
              
              // 写回数据
              sendto(_sock, err_message.c_str(), err_message.size(), 0, (struct sockaddr*)&peer, len);
              continue;
          }

          FILE* fp = popen(buffer, "r"); // popen() 执行客户端发来的命令
          if(fp == nullptr) // 此次执行失败,输出日志信息
          {
              logMessage(ERROR, "popen: %d:%s", errno, strerror(errno));
              continue;
          }

          while(fgets(ret, sizeof ret, fp) != nullptr) // ret接收读取的数据
          {
              cmd_echo += ret; // 将每次读取的结果追加到字符串变量cmd_echo
          }

      }
      // 分析&&处理 数据
      //end 写回数据
      sendto(_sock, cmd_echo.c_str(), cmd_echo.size(), 0, (struct sockaddr*)&peer, len); // 将执行结果写回 客户端
  }
}

演示效果:

下面的结果可以看出:客户端输入ls命令后,接收到了server回应的执行结果,当客户端输入rm指令时,server取消执行命令,并返回相关错误信息

在这里插入图片描述


5.2 服务器接收 多个客户端的消息并发送给每个客户端(类似群组)

实现server 接收多个客户端信息并发送给每个客户端时,成员变量如下:

private:
uint16_t _port;  // 端口号
std::string _ip; // ip
int _sock;
std::unordered_map<std::string, struct sockaddr_in> _users; // 键值对的方式存储用户信息地址信息
std::queue<std::string> messageQueue; // 存储消息 的队列容器

start():

步骤如下:

  1. 正常创建 sturct sockaddr_in 变量

  2. 通过 recvfrom 函数从套接字 _sock 中接收数据,存储在 buffer 缓冲区中,并根据客户端的 IP 地址和端口号创建套接字地址 peer

  3. 通过 ntohs 函数将网络字节序的端口号转换为本机字节序的端口号,并使用 inet_ntoa 函数将网络序列 IP 地址转换为本地风格的 IP 地址。

  4. 使用 snprintf 函数将格式化的字符串输出到 key 缓冲区中,并将其作为用户的唯一标识符。

  5. 如果没有找到具有相应标识符的用户,则将其添加到 _users 容器中。

  6. 遍历 _users 容器中的所有用户并使用 sendto 函数将消息发送到每个用户。

  7. 根据 _users 容器中存储的套接字地址,使用 sendto 函数将信息发送到特定客户端。

void Start()
    {
        char buffer[SIZE]; // 保存从套接字接收的信息
        for( ; ; )
        {
            struct sockaddr_in peer; // 存储与客户端通信的套接字地址信息
            bzero(&peer, sizeof(peer)); // 将peer的参数初始化为0

            // 输入: peer的缓冲区大小
            // 输出: 实际所读到的peer
            socklen_t len = sizeof(peer);
            char result[256];
            char key[64];
            std::string cmd_echo;

            ssize_t s = recvfrom(_sock, buffer, sizeof buffer-1, 0, (struct sockaddr*)&peer, &len); // 从_sock接收信息。存储在buffer
            
            if(s > 0) // 处理接受的数据
            {
                buffer[s] = 0;
                // 
                uint16_t cli_port = ntohs(peer.sin_port); // 网络字节序的端口号 -> 为本机字节序的端口号
                std::string cli_ip = inet_ntoa(peer.sin_addr); // 网络序列ip -> 本地风格ip
                snprintf(key, sizeof(key), "%s-%u", cli_ip.c_str(), cli_port); // 将格式化的字符串输出到指定的缓冲区中
                logMessage(NORMAL, "key: %s", key); // 更新信息
                auto it = _users.find(key);
                // 如果没有该用户对应的key,则添加新用户
                if(it == _users.end())
                {
                    logMessage(NORMAL, "add new user : %s", key); // 日志 添加信息
                    _users.insert({key, peer});
                }
            }

            for(auto &iter : _users)
            {
                // 将从一个客户端接收到的信息 发送给各个客户端
                std::string sendMessage = key;
                sendMessage += "# ";
                sendMessage += buffer; // 设置发送信息
                logMessage(NORMAL, "push message to %s", iter.first.c_str()); 
                sendto(_sock, sendMessage.c_str(), sendMessage.size(), 0 ,(struct sockaddr*)&(iter.second), sizeof(iter.second)); // 发送操作
            }
        }
    }


client.cc

client.cc 的多线程版本:

我们封装出两个函数,udpSendudpRecv

udpSend函数是用于 发送UDP消息的线程函数,它不断从用户输入中读取消息并通过套接字发送给服务器
udpRecv函数是用于 接收UDP消息的线程函数,它不断地从套接字中接收消息并将接收到的消息输出到控台

uint16_t serverport = 0; // 服务器端口
std::string serverip; // 服务器地址

// 多线程
static void usage(const char* programName) { // 打印正确用法
    std::cerr << "Invalid command line arguments.\n Usage:\t" << programName << " ip port" << std::endl;
}

// 发送udp信息的函数
static void* udpSend(void* args)
{
    // 获取套接字描述符与名称
    int sock = *(int*)((threadData*)args)->_args;
    std::string name = ((threadData*)args)->_name;
    // 创建sockaddr_in
    std::string message;
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server)); // 初始化server
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());
    while(true)
    {
        std::cerr << "请输入你的信息# ";
        std::getline(std::cin, message); // 读取信息
        if(message == "quit")
            break;

        // 当client首次发送消息给服务器时,操作系统会自动bind 其ip和port
        sendto(sock, message.c_str(), message.size(), 0, (struct sockaddr*)&server, sizeof(server));
    }

    return nullptr;
} 

// 接受udp信息的线程函数
static void* udpRecv(void* args)
{
    // 从传入的参数args中获取套接字描述符sock和名称name
    int sock = *(int*)((threadData*)args)->_args;
    std::string name = ((threadData*)args)->_name;

    char buffer[1024];
    while(true)
    {
        memset(buffer, 0, sizeof(buffer)); // 每次循环开始前,清空buffer信息,保证不会重复上次信息
        struct sockaddr_in temp; // 发送方(客户端)的地址结构体
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&temp, &len);
        if(s > 0)
        {
            // 接收成功
            buffer[s] = 0; // 确保字符串以null结尾
            std::cout << buffer << std::endl; // 打印接收的信息
        }
    }
}

int main(int argc, char *argv[]) // agrv[0] 代表可执行参数的name,argv[1]代表第一个参数...i
{
    if(argc != 3) // argc命令行参数的数量
    {
        usage(argv[0]);
        exit(1);
    }
    // 创建 套接字
    int sock = socket(AF_INET, SOCK_DGRAM, 0);
    if(sock < 0) // 创建失败
    {
        std::cerr << "socket error" << std::endl;
        exit(2);
    }

    serverport = atoi(argv[2]); // 获取服务器ip,端口
    serverip = argv[1];

    // sender线程调用udpSend函数,用于向指定的服务器发送UDP消息。
    // recver线程调用udpRecv函数,用于接收来自指定服务器的UDP消息
    std::unique_ptr<Thread> sender(new Thread(1, udpSend, (void*)&sock)); // 创建线程对象sender,udpSender是线程执行函数
    std::unique_ptr<Thread> recver(new Thread(2, udpRecv, (void*)&sock)); // 同sender
    sender->start(); // 启动两线程
    recver->start();

    sender->join(); // 等待线程结束
    recver->join();


    close(sock); // 关闭套接字
    return 0;
}

演示效果:

在这里插入图片描述
上面的图例可以看到,我们创建两个客户端,当两个客户端发消息成功并可以正确被server添加

在这里插入图片描述

  • 11
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
套接字(Socket)是一种通信机制,它允许不同的进程在同一主机或不同主机之间进行通信。Linux下的套接字编程可以使用C语言的套接字接口库函数来实现。 以下是一个简单的Linux套接字编程教程: 1. 创建套接字 使用socket()函数创建一个套接字。socket()函数的原型如下: ```c int socket(int domain, int type, int protocol); ``` 其中,domain指定协议族,type指定套接字类型,protocol指定协议。常用的协议族有AF_INET(IPv4)和AF_INET6(IPv6),套接字类型有SOCK_STREAM(TCP)和SOCK_DGRAM(UDP),协议有IPPROTO_TCP和IPPROTO_UDP等。 例如,创建一个TCP协议套接字: ```c int sockfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); ``` 2. 绑定地址 使用bind()函数将套接字与一个地址绑定。bind()函数的原型如下: ```c int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen); ``` 其中,sockfd是要绑定的套接字描述符,addr是一个指向地址结构体的指针,addrlen是地址结构体的长度。 例如,绑定到本地IP地址和指定端口号: ```c struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_addr.s_addr = htonl(INADDR_ANY); addr.sin_port = htons(8080); int ret = bind(sockfd, (struct sockaddr *)&addr, sizeof(addr)); ``` 3. 监听连接 使用listen()函数将套接字设置为监听状态。listen()函数的原型如下: ```c int listen(int sockfd, int backlog); ``` 其中,sockfd是要监听的套接字描述符,backlog是在内核中排队等待的连接的最大数量。 例如,设置最大排队数量为5: ```c int ret = listen(sockfd, 5); ``` 4. 接受连接 使用accept()函数接受客户端连接请求,并创建一个新的套接字用于与客户端进行通信。accept()函数的原型如下: ```c int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen); ``` 其中,sockfd是监听套接字描述符,addr是一个指向地址结构体的指针,用于存储客户端的地址信息,addrlen是地址结构体的长度。 例如,接受客户端连接请求,并创建一个新的套接字描述符: ```c struct sockaddr_in client_addr; socklen_t client_addrlen = sizeof(client_addr); int newfd = accept(sockfd, (struct sockaddr *)&client_addr, &client_addrlen); ``` 5. 发送和接收数据 使用send()函数向对端发送数据,使用recv()函数从对端接收数据。send()和recv()函数的原型如下: ```c ssize_t send(int sockfd, const void *buf, size_t len, int flags); ssize_t recv(int sockfd, void *buf, size_t len, int flags); ``` 其中,sockfd是要发送或接收数据的套接字描述符,buf是要发送或接收的缓冲区,len是缓冲区的长度,flags是可选的标志参数。 例如,发送数据: ```c char msg[] = "Hello World!"; int ret = send(newfd, msg, sizeof(msg), 0); ``` 例如,接收数据: ```c char buf[1024]; int ret = recv(newfd, buf, sizeof(buf), 0); ``` 6. 关闭套接字 使用close()函数关闭套接字。close()函数的原型如下: ```c int close(int fd); ``` 其中,fd是要关闭的套接字描述符。 例如,关闭套接字: ```c close(sockfd); ``` 以上是一个简单的Linux套接字编程教程。套接字编程涉及到许多细节和复杂的操作,需要仔细学习和实践。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值