简单的udp程序

文章目录

  • 1. 预备知识
    • 1.1 源IP地址和目的IP地址
    • 1.2 端口号
    • 1.3 套接字初识
    • 1.4 tcp协议和udp协议简单认识
    • 1.5 网络字节序
  • 2. udp程序
    • 2.1 创建套接字(socket)的系统调用
    • 2.2 bind()
      • 2.2.1 初始化一个sockaddr_in结构体
      • 2.2.2 inet_addr函数
      • 2.2.3 0.0.0.0
      • 2.2.4 127.0.0.1
    • 2.3 recvfrom 从套接字中接受数据 和 sendto 发送数据
    • 2.4 现在的代码
    • 2.5 写一下Udpclient.hpp
    • 2.6 封装一下服务端处理数据的函数
    • 2.7 windows下的客户端
    • 2.8 网络聊天室
      • 2.8.1 inet_ntoa 函数
      • 2.8.2 得到客户端的ip地址和端口号
      • 2.8.3 多线程
      • 2.8.4 不同终端打印
    • 2.9 sockaddr 结构

1. 预备知识

1.1 源IP地址和目的IP地址

在IP数据包头部中, 有两个IP地址, 分别叫做源IP地址, 和目的IP地

源IP地址(Source IP Address)

源IP地址是发送数据包的设备的IP地址。当一个设备需要向网络发送数据时,它会在数据包的头部包含自己的IP地址作为源地址。这个地址告诉网络和目标设备数据包来自哪里。

目标IP地址(Destination IP Address)

目标IP地址是接收数据包的设备的IP地址。当数据包在网络上传输时,它会被发送到目标IP地址指定的设备。目标设备使用这个地址来确定数据包是否为自己所期望接收的。


思考:我们光有IP地址就可以完成通信了嘛?

回答:

仅仅有IP地址并不足以完成网络通信,还需要其他信息来确保数据能够正确、高效地从源头传输到目的地。以下是一些其他所需要的

  1. MAC地址:在局域网(LAN)层面,数据帧的传输依赖于物理地址,即MAC地址。每个网络接口卡(NIC)都有一个全球唯一的MAC地址。当数据在局域网内传输时,交换机会使用MAC地址来决定将数据帧转发到哪个端口。
  2. 端口号:IP地址标识了网络中的设备,而端口号则标识了设备上的特定服务或应用程序。例如,HTTP服务通常使用端口80,而HTTPS使用端口443。端口号使得同一设备上的多个服务能够同时运行而不发生冲突。
  3. 传输协议:网络通信需要选择合适的传输协议,如TCP(传输控制协议)或UDP(用户数据报协议)。TCP提供可靠的、面向连接的通信,而UDP提供快速但不可靠的通信。

在某些情况下,网络通信可以被视为一种特殊的进程间通信,因为它也涉及到数据的交换和进程之间的协调。

1.2 端口号

端口号(port)是传输层协议的内容

  • 端口号是一个2字节16位的整数
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程
  • 一个端口号只能被一个进程占用 ,一个进程可以有多个端口号

1.3 套接字初识

套接字(Socket)是网络编程中一个非常基础的概念,它是一种通信端点。网络中,套接字允许设备上的程序(进程)进行双向通信。套接字是网络通信的构建块,它们为进程提供了一种方式来发送和接收数据。

套接字的组成:

套接字由以下两部分组成:

  1. IP地址:标识网络上的设备。
  2. 端口号:标识设备上的特定进程。

因此,一个套接字通常由一个IP地址和一个端口号的组合来唯一标识,例如 (IP地址:端口号)

1.4 tcp协议和udp协议简单认识

TCP(Transmission Control Protocol 传输控制协议)

  • 传输层协议
  • 有连接
  • 可靠传输
  • 面向字节流

UDP(User Datagram Protocol 用户数据报协议)

  • 传输层协议
  • 无连接
  • 不可靠传输
  • 面向数据报

1.5 网络字节序

在网络通信中,数据的字节序是一个重要的考虑因素,因为不同的计算机架构可能使用不同的字节序来存储多字节数据类型。字节序主要有两种:

  1. 大端序(Big-endian):最高有效字节(MSB)存储在最低的内存地址,最低有效字节(LSB)存储在最高的内存地址。
  2. 小端序(Little-endian):最低有效字节(LSB)存储在最低的内存地址,最高有效字节(MSB)存储在最高的内存地址。

网络协议规定,所有的多字节数值(如端口号、IP地址等)在网络中传输时都应该使用大端序,这被称为网络字节序。这种规定是为了确保数据在不同架构的计算机之间传输时能够被一致地解释。

当你在一台计算机上设置端口号或其他需要在网络上传输的数值时,你需要确保这些数值是以网络字节序的形式发送出去的。如果你的主机是小端序的,那么直接使用主机上的数值(以主机字节序存储)会导致网络另一端的计算机无法正确解释这些数值,因为它们的字节序是相反的。

为使网络程序具有可移植性,使同样的C代码在大端和小端计算机上编译后都能正常运行,可以调用以下库函数做网络字节序和主机字节序的转换

image-20241019094855449

  • 这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。
  • 例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。
  • 如果主机是小端字节序, 这些函数将参数做相应的大小端转换然后返回
  • 如果主机是大端字节序, 这些函数不做转换,将参数原封不动地返回。

2. udp程序

2.1 创建套接字(socket)的系统调用

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

参数说明:

  1. domain:指定通信协议的域,常见的有:
    • AF_INET:IPv4 网络协议
    • AF_INET6:IPv6 网络协议
    • AF_UNIX:Unix 域套接字
    • AF_PACKET:与网络层直接交互的原始套接字
  2. type:指定套接字的类型,常见的有:
    • SOCK_STREAM:提供面向连接、可靠的字节流服务,通常用于 TCP 协议
    • SOCK_DGRAM:提供无连接的、尽最大努力交付的数据报服务,通常用于 UDP 协议
    • SOCK_RAW:原始套接字,允许直接访问较低层次的协议
    • SOCK_SEQPACKET:有序、可靠的数据包服务
  3. protocol:指定使用的特定协议,通常设置为 0,表示让系统选择一个默认协议。对于 SOCK_STREAM,通常使用 TCP 协议(IPPROTO_TCP),对于 SOCK_DGRAM,通常使用 UDP 协议(IPPROTO_UDP)。

返回值:

  • 成功时,返回新创建的套接字的文件描述符(一个非负整数)。所以创建套接字类似打开一个文件,指向的是某一个网卡设备
  • 失败时,返回 -1,并设置 errno 以指示错误原因。

2.2 bind()

bind 函数是 Linux 系统中用于将一个套接字(socket)绑定到一个特定的网络地址的系统调用。这个地址通常由一个 IP 地址和一个端口号组成。bind 函数定义在 <sys/socket.h> 头文件中。

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

参数说明:

  1. socket:这是由 socket 函数创建的套接字的文件描述符。
  2. address:这是一个指向 sockaddr 结构体的指针,该结构体包含了要绑定到套接字的地址信息。对于不同的地址族(如 IPv4、IPv6、Unix 域套接字等),这个结构体的具体内容会有所不同。例如,对于 IPv4 地址,它可能是一个 sockaddr_in 结构体。
  3. address_len:这是 address 指向的结构体的大小,以字节为单位。这个参数是必须的,因为不同的地址族可能会有不同的结构体大小。

返回值:

  • 成功时,返回 0
  • 失败时,返回 -1,并设置 errno 以指示错误原因。

2.2.1 初始化一个sockaddr_in结构体

在 Linux 系统中,sockaddr_in 结构体用于表示 IPv4 地址和端口号,通常用于与 socketbindconnect 等函数一起使用。sockaddr_in在头文件#include<netinet/in.h>或#include <arpa/inet.h>中定义。以下是如何初始化 sockaddr_in 结构体的步骤:

  1. 定义 sockaddr_in 结构体变量:首先,你需要定义一个 sockaddr_in 类型的变量。
  2. 设置地址族:将 sin_family 成员设置为 AF_INET,表示这是一个 IPv4 地址。
  3. 设置端口号:将 sin_port 成员设置为端口号,通常需要使用 htons 函数将主机字节序转换为网络字节序。
  4. 设置 IP 地址:将 sin_addr 成员设置为所需的 IPv4 地址,可以使用 inet_addr 函数将点分十进制的 IP 地址字符串转换为网络字节序的二进制形式。

2.2.2 inet_addr函数

inet_addr 函数用于将一个点分十进制的 IPv4 地址字符串转换为一个网络字节序的32位整数。这个函数定义在 <arpa/inet.h> 头文件中。

函数原型如下:

in_addr_t inet_addr(const char *cp);

参数说明:

  • cp:这是一个指向以空字符结尾的字符串的指针,该字符串包含了一个点分十进制的 IPv4 地址,例如 “192.168.1.1”。

返回值:

  • 如果转换成功,inet_addr 返回一个 in_addr_t 类型的值,这是一个适合存储 IPv4 地址的32位无符号整数,且该值以网络字节序表示。
  • 如果输入的字符串不是一个有效的 IPv4 地址,inet_addr 返回一个特殊值 INADDR_NONEINADDR_NONE 通常定义为 -1,但也可能在不同的系统上有不同的定义。

使用 inet_addr 函数时需要注意的是,它不进行错误检查,如果输入的字符串不是有效的 IPv4 地址,它将返回 INADDR_NONE。因此,在使用返回值之前,你应该检查它是否等于 INADDR_NONE


2.2.3 0.0.0.0

在网络编程中,当你使用 bind 函数将一个套接字绑定到一个地址时,如果你将 IP 地址设置为 0.0.0.0(在 IPv4 中),这意味着套接字被绑定到了一个特殊的地址,称为“ wildcard address”(通配地址)。它表示本机中所有的IPV4地址

  1. 监听所有接口:当你将服务器套接字绑定到 0.0.0.0 时,它将监听所有可用的网络接口上(网卡)的指定端口。这意味着服务器将接受发送到该端口的任何网络接口上的数据。
  2. IPv4 通配符:在 IPv4 中,0.0.0.0 用作通配符地址,表示“接受任何 IPv4 地址”。
  3. INADDR_ANY 的关系:在 C 语言的网络编程中,0.0.0.0 通常通过宏 INADDR_ANY 来表示。INADDR_ANY<netinet/in.h> 头文件中定义,通常被定义为 0x00000000(即 0)。

2.2.4 127.0.0.1

  1. 本地接口127.0.0.1 指向的是本地计算机。它是一个环回地址,意味着数据包发送到这个地址后,不会离开本地计算机,而是直接被操作系统捕获。
  2. 用于测试:由于 127.0.0.1 总是指向本地计算机,它经常被用于测试网络应用和服务,而不必担心数据在网络中传输。例如,在开发过程中,你可以使用 127.0.0.1 来测试服务器是否正确响应请求,而不需要将数据发送到网络上的其他计算机。
  3. IPv4地址127.0.0.1 是一个IPv4地址。在IPv6中,回环地址是 ::1
  4. 网络字节序127.0.0.1 在二进制中表示为 0111 0111 0000 0000 0000 0000 0000 0001,它在内存中的存储顺序(大端序)和网络字节序是一致的。
  5. 子网掩码:在使用 127.0.0.1 时,通常不需要指定子网掩码,因为它只用于本地通信。
  6. 网络库和框架:许多网络编程库和框架默认使用 127.0.0.1 作为本地测试地址。
  7. 安全性:使用 127.0.0.1 可以提高安全性,因为数据不会在网络上传输,从而减少了被网络监听和攻击的风险。

端口号[0, 1023]都是系统内定的端口号,一般不要绑定这些端口号


上面两步(2.1和2.2)的代码汇总, log是创建的用来打日志的对象

/* 1.创建udp套接字 */ 
_socketFd = socket(AF_INET, SOCK_DGRAM, 0);        
if(_socketFd == -1) {
    // 创建失败
    log(FATAL, "创建套接字失败, fd: %d, 原因: %s\n", _socketFd, strerror(errno));
    exit(SOCKET_ERR);
}
else {
    // 创建成功
    log(INFO, "创建套接字成功, fd: %d\n", _socketFd);
    /* 2.  将一个套接字绑定到一个特定的网络地址 */
    struct sockaddr_in local;
    memset(&local, 0, sizeof(local));
    local.sin_family = AF_INET;
    local.sin_port = htons(_port);
    in_addr_t ipTmp = inet_addr(_ip.c_str());
    if(ipTmp == INADDR_NONE) {
        // IP非法
        log(FATAL, "IP非法\n");
        exit(IP_ERR);
    }
    // sin_addr是个in_addr结构体类型的变量,里面有个in_addr_t类型的成员s_addr
    local.sin_addr.s_addr = ipTmp;		// 如果想绑定统配地址,也可以使用INADDR_ANY
    // 开始绑定
    if(bind(_socketFd, (const struct sockaddr*)&local, sizeof(local)) == -1) {
        log(FATAL, "绑定失败, 原因: %s\n", strerror(errno));
        exit(BIND_ERR);
    }
    log(INFO, "绑定成功\n");
}

image-20241021210730345

2.3 recvfrom 从套接字中接受数据 和 sendto 发送数据

recvfrom 函数是在 Linux 系统中用于从套接字接收数据的系统调用。它可以用于不同类型的套接字,包括面向连接的(如 TCP)和无连接的(如 UDP)协议。recvfrom 函数定义在 <sys/socket.h> 头文件中。

函数原型如下:

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                 struct sockaddr *src_addr, socklen_t *addrlen);

参数说明:

  1. sockfd:这是一个有效的套接字文件描述符,该套接字已经用 socket 函数创建。
  2. buf:这是一个指向缓冲区的指针,用于存储接收到的数据。
  3. len:这是缓冲区的大小,即你希望接收的最大数据量。
  4. flags:这个参数通常设置为0,或者可以设置特定的标志来修改接收操作的行为。例如,MSG_DONTWAIT 可以使 recvfrom 调用非阻塞。
  5. src_addr:输出型参数,这是一个可选的指针,指向 sockaddr 结构体,用于存储发送方的地址信息。如果不需要发送方的地址信息,可以设置为 NULL
  6. addrlen:输出型参数,这是一个指向 socklen_t 类型的变量的指针,该变量在调用前应该初始化为 src_addr 指向的缓冲区的大小。在函数返回时,它将被设置为实际接收到的地址结构体的大小。

返回值:

  • 成功时,返回接收到的字节数。如果远端正常关闭了连接,并且没有更多的数据可读,返回0。
  • 失败时,返回 -1,并设置 errno 以指示错误原因。

sendto 函数是在 Linux 系统中用于发送数据到套接字的系统调用,特别是用于无连接的协议如 UDP。这个函数定义在 <sys/socket.h> 头文件中。

函数原型如下:

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
               const struct sockaddr *dest_addr, socklen_t addrlen);

参数说明:

  1. sockfd:这是一个有效的套接字文件描述符,该套接字已经用 socket 函数创建。
  2. buf:这是一个指向要发送数据的缓冲区的指针。
  3. len:这是要发送的数据的长度,以字节为单位。
  4. flags:这个参数通常设置为0,或者可以设置特定的标志来修改发送操作的行为。例如,MSG_DONTWAIT 可以使 sendto 调用非阻塞。
  5. dest_addr:输入型参数,这是一个指向 sockaddr 结构体的指针,该结构体包含了接收方的地址信息。对于不同的地址族(如 IPv4、IPv6、Unix 域套接字等),这个结构体的具体内容会有所不同。
  6. addrlen:输入型参数,这是 dest_addr 指向的结构体的大小,以字节为单位。

返回值:

  • 成功时,返回发送的字节数。
  • 失败时,返回 -1,并设置 errno 以指示错误原因。

sendto 函数通常用于 UDP 套接字,因为 UDP 是无连接的,每次发送数据时都需要指定接收方的地址。对于 TCP 套接字,通常使用 send 函数,它不需要指定接收方地址,因为连接已经建立。

2.4 现在的代码

// udpServer.hpp
#pragma once
#include <iostream>
#include <memory>
#include <string>
#include <sys/types.h>        
#include <sys/socket.h>
#include <arpa/inet.h>
#include <string.h>     // memeset
#include "log.hpp"
using namespace std;

const string defaultIP = "0.0.0.0";    // 默认IP地址
const uint16_t defaultPort = 8080;     // 默认端口号
const size_t BUFFERSIZE = 1024;
Log log;    // 方便打日志

enum 
{
    SOCKET_ERR,
    BIND_ERR,
};

class UdpServer
{
public:
    UdpServer(const uint16_t& port, const string& ip);
    ~UdpServer();
    void init();
    void run();
private:
    int _socketFd = 0;      
    string _ip;
    uint16_t _port;     // 服务器进程端口号
    bool _isRunning = false;    
};

UdpServer::UdpServer(const uint16_t& port = defaultPort, const string& ip = defaultIP) : _port(port), _ip(ip)
{}

UdpServer::~UdpServer()
{
    close(_socketFd);
}

void UdpServer::init()
{
    /* 1.创建udp套接字 */ 
    _socketFd = socket(AF_INET, SOCK_DGRAM, 0);        
    if(_socketFd == -1) {
        // 创建失败
        log(FATAL, "创建套接字失败, fd: %d, 原因: %s\n", _socketFd, strerror(errno));
        exit(SOCKET_ERR);
    }
    else {
        // 创建成功
        log(INFO, "创建套接字成功, fd: %d\n", _socketFd);
        /* 2.  将一个套接字绑定到一个特定的网络地址 */
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        in_addr_t ipTmp = inet_addr(_ip.c_str());
        // sin_addr是个in_addr结构体类型的变量,里面有个in_addr_t类型的成员s_addr
        local.sin_addr.s_addr = ipTmp;
        // 开始绑定
        if(bind(_socketFd, (const struct sockaddr*)&local, sizeof(local)) == -1) {
            log(FATAL, "绑定失败, 原因: %s\n", strerror(errno));
            exit(BIND_ERR);
        }
        log(INFO, "绑定成功\n");
    }

}

void UdpServer::run()
{
    /* 从客户端拿数据,处理后再发给客户端 */
    _isRunning = true;
    while (_isRunning) {
        // 接受数据
        sockaddr_in client;
        socklen_t len = sizeof(client);  
        string inBuffer(BUFFERSIZE, 0);
        if(recvfrom(_socketFd, (char*)inBuffer.c_str(), inBuffer.size(), 0, (sockaddr*)&client, &len) == -1) {
            log(WARNING, "接受数据失败, 原因: %s\n", strerror(errno));
        }
        cout << inBuffer << '\n';
        // 服务端进行数据处理
        string echoString = "处理器处理数据。。。: " + inBuffer;
        // 发回客户端
        if (sendto(_socketFd, echoString.c_str(), echoString.size(), 0, (const sockaddr*)&client, len) == -1) {
            log(WARNING, "发送数据失败, 原因: %s\n", strerror(errno));
        }
    }
}


// main.cc
#include "udpServer.hpp"

void Usage(const char* string)
{
    cout << "\n\rUsage: " << string << " port(1024+)\n\n";  
}

int main(int argc, char* argv[])
{
    if(argc != 2) {
        Usage(argv[0]);
    } else {
        uint16_t port = stoi(argv[1]);
        //  使用智能指针管理资源
        unique_ptr<UdpServer> svr(new UdpServer(port));
        svr->init();
        svr->run();
    }

    return 0;
}

image-20241022140150171

2.5 写一下Udpclient.hpp

注意下面几点:

  • 对于像 UDP 这样的无连接协议,客户端不需要建立一个持久的连接。它们只是简单地向服务器发送数据包,而不需要监听任何入站连接或数据。因此,在这种情况下,客户端不需要绑定到一个特定的端口。
  • 当创建套接字时,如果没有明确地绑定到一个端口,操作系统会自动为客户端分配一个临时的源端口,这是随机的。这个端口用于发送或接受数据,并且只有在套接字的生命周期内有效。这意味着客户端不需要关心使用哪个端口。减少了冲突
// udpClient.cc
#include <iostream>
#include <sys/types.h>          
#include <sys/socket.h>
#include <string.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
using namespace std;

Log log;

enum {
    SOCKET_ERR,
};

void Usage(const char* string)
{
    cout << "\n\rUsage: " << string << " serverIp serverPort\n\n";  
}

int main(int argc, char* argv[])
{
    if(argc != 3) {
        Usage(argv[0]);
    }
    // 创建套接字
    int socketFd = socket(AF_INET, SOCK_DGRAM, 0);
    if(socketFd == -1) {
        // 创建失败
        log(FATAL, "创建套接字失败, fd: %d, 原因: %s\n", socketFd, strerror(errno));
        exit(SOCKET_ERR);
    }
    // 输入服务端的ip和端口号
    string serverIp = argv[1];
    uint16_t serverPort = stoi(argv[2]);
    // 构建服务器信息
    sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverPort);
    server.sin_addr.s_addr = inet_addr(serverIp.c_str());
    socklen_t len = sizeof(server);
    string message;
    while(true) {
        // 发送数据
        cout << "@Please enter: ";
        getline(cin, message);
        sendto(socketFd, message.c_str(), message.size(), 0, (sockaddr*)&server, len); 
        // 拿到服务端处理后的数据
        string buffer(1024, 0);
        sockaddr_in tmpServer;  
        socklen_t tmpLen;
        if(recvfrom(socketFd, (char*)buffer.c_str(), buffer.size(), 0, (sockaddr*)&tmpServer, &tmpLen) > 0) {
            cout << buffer << endl << endl;
        }
    }
    return 0;
}

服务端处理来自客户端的数据,之后再发给客户端

image-20241022161115715

2.6 封装一下服务端处理数据的函数

// udpServer.hpp
using func_t = function<string(const string &)>;
// ...
void UdpServer::run(func_t fun)
{
    /* 从客户端拿数据,处理后再发给客户端 */
    // ...
    string echoString = fun(inBuffer);      // 回调fun函数
}

popen

popen() 函数通过创建一个管道,调用 fork 产生一个子进程,执行一个 shell 以运行命令来开启一个进程

这个进程必须由 pclose() 函数关闭,而不是 fclose() 函数。pclose() 函数关闭标准 I/O 流,等待命令执行结束,然后返回 shell 的终止状态。如果 shell 不能被执行,则 pclose() 返回的终止状态与 shell 已执行 exit 一样。

函数原型如下:

FILE *popen(const char *command, const char *type);

参数说明:

  • command:要执行的命令字符串。
  • type:一个字符串,指定管道的方向。它必须是以下两个值之一:
    • "r":这意味着你将从子进程的标准输出中读取数据。换句话说,子进程的输出(stdout)将连接到 popen 调用进程的标准输入(stdin)。这样,你就可以像从文件中读取数据一样,从子进程的输出中读取数据。
    • "w":意味着:父进程将向子进程的标准输入写入数据。子进程的输入(stdin)连接到父进程的输出(stdout)。

返回值:

  • 如果成功,popen 返回一个指向新打开的管道的 FILE 指针。
  • 如果失败,返回 NULL

使用 popen 时,通常需要配合 pclose 函数来关闭管道:

// 检查命令的安全性
bool cmdCheck(const string& s) 
{
    vector<string> dict = {
        "rm", 
        "mv",
        "cp",
        "sudo",
        "yum",
        "unlink",
        "uninstall",
        "top",
        "while"
    };
    for(auto& e : dict) {
        // 如果有关键字中的任意一个,就return false
        if(s.find(e) != string::npos)   return false;
    }
    return true;
}

struct Deleter 
{
    void operator()(FILE* p) 
    {
        if(p != nullptr)    pclose(p);
    }
};

// 执行命令
string exCmd(const string& s) 
{
    cout << "Server get a commad: " << s << '\n'; 
    if(cmdCheck(s) == false)    return "Bad Cmd!";
    // FILE* fp = popen(s.c_str(), "r");
    // unique_ptr<FILE, decltype(&pclose)> fp(popen(s.c_str(), "r"), pclose);
    // unique_ptr<FILE, function<int(FILE*)>> fp(popen(s.c_str(), "r"), pclose);
    // unique_ptr<FILE, function<void(FILE*)>> fp(popen(s.c_str(), "r"), [](FILE* p)->void { if(p) pclose(p); });
    unique_ptr<FILE, Deleter> fp(popen(s.c_str(), "r"));
    if(fp == nullptr) {
        perror("poen: ");
        return "Popen Error!";
    }
    string res;
    while(true) {
        // 将命令输出的字符串读入到buffer中
        char buffer[2*BUFFERSIZE];
        char* p = fgets(buffer, sizeof(buffer), fp.get());
        if(p != nullptr)  res += buffer;
        else break;
    }
    // pclose(fp);
    return res; 
}

image-20241026141949859

2.7 windows下的客户端

代码差别不大

#define _WINSOCK_DEPRECATED_NO_WARNINGS
#include <stdio.h>
#include <tchar.h>
#include <iostream>
#include <WinSock2.h>
#include <Windows.h>
#include <WS2tcpip.h>
#include <string>
using namespace std;

#pragma comment(lib, "ws2_32.lib")

int main()
{
	WSAData wsd;           // 初始化信息
    //启动Winsock
    if (WSAStartup(MAKEWORD(2, 2), &wsd) != 0) { /*进行WinSocket的初始化,
        windows 初始化socket网络库,申请2,2的版本,windows socket编程必须先初始化。*/
        cerr << "WinSocket的初始化失败" << endl;
        return -1;
    }
    // 创建套接字
    SOCKET socketFd = socket(AF_INET, SOCK_DGRAM, 0);
    if (socketFd == -1) {
        // 创建失败
        cerr << "创建套接字失败" << endl;
        return -2;
    } 
    // 服务端的ip和端口号
    string serverIp = "124.70.203.1";
    uint16_t serverPort = 9000;
    // 构建服务器信息
    sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverPort);
    server.sin_addr.s_addr = inet_addr(serverIp.c_str());
    // inet_pton(AF_INET, "127.0.0.1", (void*)&server.sin_addr.s_addr);
    string message;
    while (true) {
        // 发送数据
        cout << "Please enter$ ";
        getline(cin, message);
        sendto(socketFd, message.c_str(), (int)message.size(), 0, (sockaddr*)&server, sizeof(server));
        // 拿到服务端处理后的数据
        string buffer(1024, 0);
        sockaddr_in tmpServer;
        int tmpLen = sizeof(tmpServer);
        if (recvfrom(socketFd, (char*)buffer.c_str(), (int)buffer.size(), 0, (sockaddr*)&tmpServer, &tmpLen) > 0) {
            cout << buffer << endl;
        }
    }
    // 关闭socket连接
    closesocket(socketFd);
    // 清理
    WSACleanup();
	return 0;
}

image-20241026154035226

2.8 网络聊天室

2.8.1 inet_ntoa 函数

inet_ntoa 函数是 C 语言标准库中用于将网络字节序的 IPv4 地址转换为点分十进制字符串的函数。这个函数定义在 <arpa/inet.h> 头文件中。

函数原型如下:

char *inet_ntoa(struct in_addr in);

参数说明:

  • in:这是一个 struct in_addr 类型的参数,它包含了一个网络字节序的 IPv4 地址。

返回值:

  • inet_ntoa 返回一个指向以 null 结尾的字符的指针,该字符串表示点分十进制的 IPv4 地址。如果转换失败,返回 NULL。

注意点:

  1. 返回的字符串是只读的,并且存储在静态分配的内存中。因此,多次调用 inet_ntoa 可能会导致不同的调用覆盖之前返回的字符串。
  2. 由于返回的字符串是指向静态内存的指针,因此不应尝试修改该字符串,也不应在程序的生命周期中依赖该字符串,因为它可能会在后续的调用中被覆盖。
  3. inet_ntoa 函数不执行任何错误检查,如果输入的 in_addr 结构体不是有效的 IPv4 地址,返回的字符串将不可靠。

2.8.2 得到客户端的ip地址和端口号

// udpServer.hpp
// ...
using func_t2 = function<string(const string &, const string&, const uint16_t&)>;     // 用于网络聊天室

enum {
    SOCKET_ERR,
    BIND_ERR,
};

class UdpServer
{
public:
    UdpServer(const uint16_t& port, const string& ip);
    ~UdpServer();
    void init();
    void run(func_t fun);
    void run2(func_t2 fun);
private:
    int _socketFd = 0;      
    string _ip;
    uint16_t _port;     // 服务器进程端口号
    bool _isRunning = false;    
};

// ...

void UdpServer::run2(func_t2 fun)
{
      /* 从客户端拿数据,处理后再发给客户端 */
    _isRunning = true;
    while (_isRunning) {
        // 接受数据
        sockaddr_in client;
        socklen_t len = sizeof(client);  
        string inBuffer(BUFFERSIZE, 0);
        if(recvfrom(_socketFd, (char*)inBuffer.c_str(), inBuffer.size(), 0, (sockaddr*)&client, &len) == -1) {
            log(WARNING, "接受数据失败, 原因: %s\n", strerror(errno));
        }

        // 拿到客户端的ip和端口号
        string ip = inet_ntoa(client.sin_addr);
        uint16_t cPort = client.sin_port;
        
        string echoString = fun(inBuffer, ip, cPort);      // 回调fun函数, 这里有ip和端口号, 方便标识
        // 发回客户端
        if (sendto(_socketFd, echoString.c_str(), echoString.size(), 0, (const sockaddr*)&client, len) == -1) {
            log(WARNING, "发送数据失败, 原因: %s\n", strerror(errno));
        }
    }
}
// main.cc
#include "udpServer.hpp"
#include <vector>

// ...
string stringHandler2(const string& s, const string& ip, const uint16_t& port) 
{
    cout << "From [" << ip << ' ' << port << "]# " << s << endl;
    string ret;
    // 将所有的字符变成大写
    for(auto& e : s) {
        ret += toupper(e);
    }
    return ret;
}

int main(int argc, char* argv[])
{
    if(argc != 2) {
        Usage(argv[0]);
    } else {
        uint16_t port = stoi(argv[1]);
        //  使用智能指针管理资源
        unique_ptr<UdpServer> svr(new UdpServer(port));
        svr->init();
        svr->run2(stringHandler2);
    }
    return 0;
}

image-20241026164157000

2.8.3 多线程

// udpServer.hpp
void UdpServer::userOperation(const sockaddr_in& client, const string& ip, const uint16_t& port)
{
    /* 如果该用户不在哈希表中,就插入 */
    if(_onlineUser.count(ip) == true)   return;
    _onlineUser.insert({ip, client});
     cout << "用户[" << ip << ' ' << port << "]# 已经添加到用户列表中!"  << endl;
}

void UdpServer::broadcastUser(const string&info, const string& ip, const uint16_t& port)
{
    /* 让所有的在线用户看到同一份消息 */
    for(const auto& e : _onlineUser) {
        string echoString;
        echoString += "[";
        echoString += ip;
        echoString += " ";
        echoString += to_string(port);
        echoString += "]# ";
        echoString += info;
        // 发回客户端
        socklen_t len = sizeof(e.second);
        if (sendto(_socketFd, echoString.c_str(), echoString.size(), 0, (sockaddr*)(&e.second), len) == -1) {
            log(WARNING, "发送数据失败, 原因: %s\n", strerror(errno));
        } else {
            cout << "客户端发送给:" << e.first <<"消息成功" << endl;
        }
    }
}

void UdpServer::run3()
{
    /* 网络聊天室,从客户端拿到信息,转发给所有人 */
    _isRunning = true;
    while (_isRunning) {
        // 接受数据
        sockaddr_in client;
        socklen_t len = sizeof(client);  
        string inBuffer(BUFFERSIZE, 0);
        if(recvfrom(_socketFd, (char*)inBuffer.c_str(), inBuffer.size(), 0, (sockaddr*)&client, &len) == -1) {
            log(WARNING, "接受数据失败, 原因: %s\n", strerror(errno));
            continue;
        }
        // 拿到客户端的ip和端口号
        string ip = inet_ntoa(client.sin_addr);
        uint16_t cPort = ntohs(client.sin_port);
        // 如果该用户不在哈希表中,就插入
        userOperation(client, ip, cPort);
        // 服务端打印一下
        cout << "用户[" << ip << ' ' << cPort << "]说# " << inBuffer << endl;
        // 让所有的在线用户看到同一份消息
        broadcastUser(inBuffer, ip, cPort);
    }
}
// main.cc
// ...
int main(int argc, char* argv[])
{
    if(argc != 3) {
        Usage(argv[0]);
    }
    // 创建套接字
    int socketFd = socket(AF_INET, SOCK_DGRAM, 0);
    if(socketFd == -1) {
        // 创建失败
        log(FATAL, "创建套接字失败, fd: %d, 原因: %s\n", socketFd, strerror(errno));
        exit(SOCKET_ERR);
    }
    // 输入服务端的ip和端口号
    string serverIp = argv[1];
    uint16_t serverPort = stoi(argv[2]);
    // 构建服务器信息
    sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverPort);
    server.sin_addr.s_addr = inet_addr(serverIp.c_str());
    socklen_t len = sizeof(server);
    string message;
    // 改成多线程,一个读取,一个发送。在UDP中,一个socket可以同时进行读写操作。
    thread t1([&] {
        while(true) {
            // 发送数据
            cout << "@Please enter: ";
            getline(cin, message);
            sendto(socketFd, message.c_str(), message.size(), 0, (sockaddr*)&server, len); 
        }
    });
    thread t2([&] {
        while(true) {
            // 拿到服务端处理后的数据
            string buffer(1024, 0);
            sockaddr_in tmpServer;  
            socklen_t tmpLen;
            if(recvfrom(socketFd, (char*)buffer.c_str(), buffer.size(), 0, (sockaddr*)&tmpServer, &tmpLen) > 0) {
                lock_guard<mutex> lock(coutMutex);
                cout << "Received: " << buffer << endl;
            }
        }
    });
    t1.join();
    t2.join();
    return 0;
}

image-20241026211800848

2.8.4 不同终端打印

dev/pts路径下有不同终端的编号

image-20241026214843884

写一下测试代码

const char* path = "/dev/pts/0";

int main() {
    int fd = open(path, O_WRONLY);
    // 检查文件描述符是否有效
    if(fd == -1) {
        perror("open");
        return -1;
    }
    
    dup2(fd, 1);        // 用该文件描述符替换标准输出
    printf("hello world");
    close(fd);
    return 0;
}

image-20241026221211515

我们让udpClient.hpp的读端重定向到另一个终端中

// udpClient.hpp
int openTerm() 
{
    int fd = open(path, O_WRONLY);
    // 检查文件描述符是否有效
    if(fd == -1) {
        perror("open");
        return -1;
    }
    dup2(fd, 2);        // 用该文件描述符替换标准错误
    close(fd);
    return 0;
}
// ...
thread t2([&] {
        while(true) {
            openTerm();     // 重定向终端,现在使用标准错误输出就是像另一个终端打印
            // 拿到服务端处理后的数据
            string buffer(1024, 0);
            sockaddr_in tmpServer;  
            socklen_t tmpLen;
            if(recvfrom(socketFd, (char*)buffer.c_str(), buffer.size(), 0, (sockaddr*)&tmpServer, &tmpLen) > 0) {
                lock_guard<mutex> lock(coutMutex);
                cerr << "Received:" << buffer << endl;
            }
        }
    });

image-20241027102239578


另一种做法是不需要写openTerm(),而是使用命令行直接重定向,因为t2线程中使用的是cerr

image-20241027103830344

2.9 sockaddr 结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,以及后面要讲的UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同

image-20220205080027660

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

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

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

相关文章

深入解析东芝TB62261FTG,步进电机驱动方案

TB62261FTG是一款由东芝推出的两相双极步进电机驱动器&#xff0c;采用了BiCD工艺&#xff0c;能够提供高效的电机控制。这款芯片具有多种优秀的功能&#xff0c;包括PWM斩波、内置电流调节、低导通电阻的MOSFET以及多种步进操作模式&#xff0c;使其非常适合用于需要精确运动控…

一步一步从微信小程序获取asp.net Core API的数据

前面我们说过&#xff0c;如何使用微信小程序获取asp.net的数据&#xff0c;这里我们继续介绍如何获取asp.net core api的数据。两者之间还是有一些差别的。本篇博文旨在详细介绍如何一步一步从微信小程序获取asp.net Core API的数据。 文章目录 一、建立并了解asp.net core we…

RabbitMQ集群搭建及使用

1. 概述 前提条件&#xff1a;linux服务器下已经安装好了docker服务。 本文档将搭建一个三台RabbitMQ的集群&#xff0c;包括三个RabbitMQ容器安装在同一服务器和三台不同的服务器。 2. 集群搭建 在一台服务器上创建三个RabbitMQ容器。 2.1.1. 创建容器 执行以下命令创建三…

合理利用IPIDEA代理IP,优化数据采集效率!

一、前言 在全球化与信息化交织的当代社会&#xff0c;数据已成为驱动商业智慧与技术革新的核心引擎。网络&#xff0c;作为信息汇聚与交流的枢纽&#xff0c;不仅是人们获取知识的窗口&#xff0c;更是商业活动与技术创新的广阔舞台。在这个信息繁荣的时代&#xff0c;Python…

Docker 实践与应用举例教程:从入门到精通

Docker 实践与应用举例教程&#xff1a;从入门到精通 引言 在现代软件开发中&#xff0c;Docker 已成为一种不可或缺的工具。它通过容器化技术简化了应用的部署、管理和扩展&#xff0c;极大地提高了开发和运维的效率。本文将详细介绍 Docker 的基本概念、安装步骤、常用命令…

开放式耳机哪个品牌音质好?音质最好的开放式耳机推荐!

如今&#xff0c;开放式耳机市场日益繁荣&#xff0c;成为了众多音乐爱好者和追求舒适佩戴体验者的新宠。然而&#xff0c;面对琳琅满目的品牌和产品&#xff0c;消费者往往陷入选择的困境。音质&#xff0c;作为衡量一款耳机优劣的关键因素&#xff0c;更是备受关注。究竟哪个…

反编译华为-研究功耗联网监控日志

摘要 待机功耗中联网目前已知的盲点&#xff1a;App自己都不知道的push类型的被动联网、app下载场景所需时长、组播联网、路由器打醒AP。 竞品 策略 华为 灭屏使用handler定时检测&#xff08;若灭屏30分钟内则周期1分钟&#xff0c;否则为2分钟&#xff09;&#xff0c;检…

【Unity踩坑】UWP应用未通过Windows应用认证:API不支持

在将Unity项目导出为XAML类型的UWP项目后&#xff0c;通过Visual Studio打包成功&#xff0c;但在进行Windows应用认证时结果是Failed。 其中的错误是某些dll里用到了Windows SDK不支持的API。 本次问题中涉及到的具体dll有两个&#xff1a;gilzoide-sqlite-net.dll和D3D12Cor…

【Linux网络】传输层协议UDP与TCP

W...Y的主页 &#x1f60a; 代码仓库分享 &#x1f495; 目录 传输层 再谈端口号 ​编辑 端口号范围划分 认识知名端口号(Well-Know Port Number) netstat pidof UDP协议 UDP协议端格式 UDP的特点 面向数据报 UDP的缓冲区 UDP使用注意事项 基于UDP的应用层协议…

开源实时数仓的构建

设计计思路 基本思路 开源数据平台的设计思路是通过 Flink SQL Batch、StartRocks SQL 、StartRocks物化视图 的能力实现一个离线任务的开发&#xff1b;使用 DolphinScheduler 进行离线工作流编排和调度&#xff1b;通过 Flink CDC 和 Flink SQL 实现流处理能力&#xff0c;进…

Redis+Lua限流的四种算法

1. 固定窗口&#xff08;Fixed Window&#xff09; 原理&#xff1a; 固定窗口算法将时间划分为固定的时间段&#xff08;窗口&#xff09;&#xff0c;比如 1 秒、1 分钟等。在每个时间段内&#xff0c;允许最多一定数量的请求。如果请求超出配额&#xff0c;则拒绝。 优点…

软工毕设开题建议

文章目录 &#x1f6a9; 1 前言1.1 选题注意事项1.1.1 难度怎么把控&#xff1f;1.1.2 题目名称怎么取&#xff1f; 1.2 开题选题推荐1.2.1 起因1.2.2 核心- 如何避坑(重中之重)1.2.3 怎么办呢&#xff1f; &#x1f6a9;2 选题概览&#x1f6a9; 3 项目概览题目1 : 深度学习社…

文档解析与向量化技术加速 RAG 应用落地

在不久前举办的 AICon 全球人工智能开发与应用大会上&#xff0c;合合信息智能创新事业部研发总监&#xff0c;复旦博士常扬从 RAG 应用落地时常见问题与需求&#xff08;文档解析、检索精度&#xff09;出发&#xff0c;分享了针对性的高精度、高泛化性、多版面多元素识别支持…

Linux系统下串口AT指令控制EC20连接华为云物联网平台

一、前言 在当今万物互联的时代背景下&#xff0c;物联网技术的快速发展极大地推动了智能化社会的构建。作为其中的关键一环&#xff0c;设备与云端平台之间的通信变得尤为重要。本文介绍如何在Linux操作系统环境下&#xff0c;利用串口通信来实现EC20模块与华为云物联网平台的…

Word中Normal.dotm样式模板文件

Normal.dotm文档 首先将自己电脑中C:\Users\自己电脑用户名\AppData\Roaming\Microsoft\Templates路径下的Normal.dotm文件做备份&#xff0c;在下载本文中的Normal.dotm文件&#xff0c;进行替换&#xff0c;重新打开word即可使用。 字体样式如下&#xff08;可自行修改&#…

基于opencv答题卡识别判卷

项目源码获取方式见文章末尾&#xff01; 回复暗号&#xff1a;13&#xff0c;免费获取600多个深度学习项目资料&#xff0c;快来加入社群一起学习吧。 **《------往期经典推荐------》**项目名称 1.【基于DDPG算法的股票量化交易】 2.【卫星图像道路检测DeepLabV3Plus模型】 3…

蓝桥杯 单片机 DS1302和DS18B20

DS1302 时钟 时钟试题 常作为实验室考核内容 控制三个引脚 P17 时钟 P23输入 P13复位 其他已经配置好 寄存器原理 定位地址 0x80地址 固定格式 0x57 5*107*1 57 小时写入格式 不同 首位区分 A上午 P下午 0为24小时制 1为12小时制 写入8小时 0x87 //1000 7 十二小时制 7…

H5的Canvas绘图——使用fabricjs绘制一个可多选的随机9宫格

&#x1f4e2;欢迎点赞 &#xff1a;&#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff0c;赐人玫瑰&#xff0c;手留余香&#xff01;&#x1f4e2;本文作者&#xff1a;由webmote 原创&#x1f4e2;作者格言&#xff1a;新的征程&#xff0c;最近一直被测试…

Django+Vue全栈开发旅游网项目首页

一、前端项目搭建 1、使用脚手架工具搭建项目 2、准备静态资源&#xff08;图片资源&#xff09; 将准备好的静态资源拖至public目录下 3、调整生产项目结构 公共的样式 公共js 首页拆解步骤 ①分析页面结构 标题、轮播图、本周推荐、精选景点、底部导航 ②新建页面组…

VAE中的“变分”什么

写在前面 VAE&#xff08;Variational Autoencoder&#xff09;&#xff0c;中文译为变分自编码器。其中AE&#xff08;Autoencoder&#xff09;很好理解。那“变分”指的是什么呢?—其实是“变分推断”。变分推断主要用在VAE的损失函数中&#xff0c;那变分推断是什么&#x…