简单的udp程序

news2024/10/28 6:20:26

文章目录

  • 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结构体指针做为参数

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

如何搭建AI智能化招聘平台?招聘系统源码与小程序开发技术方案探讨

本篇文章&#xff0c;小编将深入探讨如何搭建一个AI智能化招聘平台&#xff0c;分析其背后的招聘系统源码架构以及APP开发的技术方案。 一、AI智能化招聘平台的核心功能 在设计AI招聘平台时&#xff0c;必须考虑其核心功能&#xff0c;以确保平台具备高效的招聘能力和智能化的…

shodan4,挂黑网站查找,弱口令网站搜索

myip参数 shodan myip&#xff08;查看自己的出口IP,哪个地址链接的公网)挂黑网站查找 我们今天看一看找一下&#xff0c;有些已经被黑的网站好吧&#xff0c;就是利用shodan查看一下哪些网站已经被黑了。 shodan search -limit 10 -fields ip_str,port http.title:hacked b…

iOS静态库(.a)及资源文件的生成与使用详解(OC版本)

引言 iOS静态库&#xff08;.a&#xff09;及资源文件的生成与使用详解&#xff08;Swift版本&#xff09;_xcode 合并 .a文件-CSDN博客 在前面的博客中我们已经介绍了关于iOS静态库的生成步骤以及关于资源文件的处理&#xff0c;在本篇博客中我们将会以Objective-C为基础语言…

十八、【智能体】数据库:未来科技的大脑

在上一篇中我们讲到了 **变量 ** &#xff0c; 变量 的作用是保存用户个人信息&#xff0c;让 Bot记住用户的特征&#xff0c;使回复更加个性化。 上一篇内容为&#xff1a;https://blog.csdn.net/qq_40585384/article/details/143272599 但变量有一个缺点——存储的信息太单…

【数据结构与算法】《Java 算法宝典:探秘从排序到回溯的奇妙世界》

目录 标题&#xff1a;《Java 算法宝典&#xff1a;探秘从排序到回溯的奇妙世界》一、排序算法1、冒泡排序2、选择排序3、插入排序4、快速排序5、归并排序 二、查找算法1、线性查找2、二分查找 三、递归算法四、动态规划五、图算法1. 深度优先搜索&#xff08;DFS&#xff09;2…

【Linux系统编程】——Linux入门指南:从零开始掌握操作系统的核心(指令篇)

文章目录 查看 Linux 主机 ip以及登录主机Linux基础文件操作指令man&#xff1a;查看命令的手册页&#xff0c;了解命令的详细用法。pwd&#xff1a;显示当前目录路径。cd&#xff1a;切换目录。ls&#xff1a;列出当前目录下的文件和文件夹。mkdir&#xff1a;创建新目录。 文…

ArrayList和Array、LinkedList、Vector 间的区别

一、ArrayList 和 Array 的区别 ArrayList 内部基于动态数组实现&#xff0c;比 Array&#xff08;静态数组&#xff09; 使用起来更加灵活&#xff1a; ArrayList 会根据实际存储的元素动态地扩容或缩容&#xff0c;而 Array 被创建之后就不能改变它的长度了。ArrayList 允许…

el-table相关的功能实现

1. 表格嵌套表格时&#xff0c;隐藏父表格的全选框 场景&#xff1a;当table表格设置复选&#xff08;多选&#xff09;功能时&#xff0c;如何隐藏表头的复选框&#xff0c;不让用户一键多选。 <el-table :header-cell-class-name"cellClass">// 表头复选框禁…

102. 管道漫游案例

通过一个轨迹线生成一个管道几何体&#xff0c;然后相机沿着该轨迹线移动&#xff0c;注意相机的方向要沿着轨迹线的切线方向&#xff0c;这样会形成一个管道漫游的效果。 管道几何体TubeGeometry、纹理贴图相机对象Camera的.position属性和.lookAt()方法 管道模型 课件源码“…

动态规划算法专题(九):完全背包问题

目录 1. 【模板】完全背包 1.1 算法原理 1.2 算法代码 1.3 空间优化 1.4 空间优化版本代码 2. 零钱兑换 2.1 算法原理 2.2 算法代码 3. 零钱兑换 II 3.1 算法原理 3.2 算法代码 4. 完全平方数 4.1 算法原理 4.2 算法代码 完全背包问题的初始化与 01 背包的初…

电动汽车与软件定义汽车(SDV)时代的汽车行业变革

我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 屏蔽力是信息过载时代一个人的特殊竞争力,任何消耗你的人和事,多看一眼都是你的不对。非必要不费力证明自己,无利益不试图说服别人,是精神上的节…

LeetCode437. 路径总和 III(2024秋季每日一题 50)

给定一个二叉树的根节点 root &#xff0c;和一个整数 targetSum &#xff0c;求该二叉树里节点值之和等于 targetSum 的 路径 的数目。 路径 不需要从根节点开始&#xff0c;也不需要在叶子节点结束&#xff0c;但是路径方向必须是向下的&#xff08;只能从父节点到子节点&am…

.NET Core WebApi第3讲:第一个Web Api项目

一、.NEt Core 1、运行模板项目 1&#xff09;仍然有controllers&#xff0c;说明WebApi是基于MVC模式的&#xff0c;只是对比之下这里没有MVC中的views。 因为WebApi只会向前台发送数据&#xff0c;不会向前台发送HTML页面。 2、验证模板项目的api 1&#xff09;法1&#xf…

第12次CCF CSP认证真题解

1、最小差值 题目链接&#xff1a;https://sim.csp.thusaac.com/contest/12/problem/0 100分代码&#xff1a; #include <iostream> #include <algorithm> using namespace std; int main(int argc, char *argv[]) {int n;cin >> n;int a[1010],b[1010];f…

【模型学习】

https://zhuanlan.zhihu.com/p/522344841 from transformers import AutoTokenizer tokenizer AutoTokenizer.from_pretrained(model_checkpoint) tokenizer("男女主角亦有专属声优这一模式是由谁改编的&#xff1f;", "任天堂游戏谜之村雨城") { input_…

数通自学——计算机网络基础知识IP地址、局域网、广域网、NAT、端口映射、子网掩码、网关、IPV4、IPV6

计算机网络基础知识IP地址、局域网、广域网、NAT、端口映射、子网掩码、网关、IPV4、IPV6 一、IP地址1、概念引入2、概念3、组成及分类 二、局域网和广域网1、局域网2、广域网 三、NAT与端口映射四、公网IP、私网IP五、IPV4与IPV6 一、IP地址 1、概念引入 现在思考一个问题&a…

IntelliJ IDEA 查看类class的结构Structure轮廓outline窗口, 快捷键是Alt+7

IntelliJ IDEA 查看类class的结构Structure轮廓outline窗口, 快捷键是Alt7 idea的结构Structure窗口相当于Eclipse的outline 快捷键是: Alt7 或者点击左上角主菜单面包屑,打开主菜单 然后菜单找到-视图&#xff08;View&#xff09;→ 工具窗口&#xff08;Tool Windows&…

鸿蒙开发--点击下拉菜单,同时最下面出现遮罩层的实现方法

效果展示 实现 除去最上面的Naviation标题&#xff08;房源列表&#xff09;&#xff0c;该页面有两个SearchFilter搜索筛选&#xff08;包括其中的下拉菜单&#xff09;&#xff0c;RoomList房源列表 根目录容器 显然&#xff0c;两个组件之间存在覆盖关系&#xff0c;所以…

【密码学】全同态加密张量运算库解读 —— TenSEAL

项目地址&#xff1a;https://github.com/OpenMined/TenSEAL 论文地址&#xff1a;https://arxiv.org/pdf/2104.03152v2 TenSEAL 是一个在微软 SEAL 基础上构建的用于对张量进行同态加密操作的开源Python库&#xff0c;用于在保持数据加密的状态下进行机器学习和数据分析。 Ten…

ssm旅游网页开发与设计+jsp

系统包含&#xff1a;源码论文 所用技术&#xff1a;SpringBootVueSSMMybatisMysql 免费提供给大家参考或者学习&#xff0c;获取源码请私聊我 需要定制请私聊 目 录 摘 要 I 目 录 III 第1章 绪论 1 1.1 研究背景 1 1.2目的和意义 1 1.3 论文研究内容 1 第2章 程序…