【Linux】socket网络编程

news2024/11/27 15:37:38

文章目录

  • 1. 网络进程的端口号
  • 2. 认识UDP和TCP
  • 3. 网络字节序
  • 4. socket通信
  • 5. UDP服务器和客户端
    • 5.1 基础UDP服务器和客户端
    • 5.2 群聊服务器和客户端
  • 6. TCP服务器和客户端
    • 6.1 TcpServer
    • 6.2 TcpClient
    • 6.3 TcpServer的优化
      • 引入线程池
      • 日志系统
      • 服务器守护进程化


1. 网络进程的端口号

网络通信的本质是:两个网络进程进行通信。 上节我们提到,网络中ip地址可以标识唯一的一台主机,而主机中的网络进程是通过端口号(port)确定进程唯一性的。进一步讲,在网络通信中,通过ip+port来唯一标识某个主机上的某个进程。

💭端口号是传输层协议的内容,它是一个2字节16bit的整数,用于标识一个进行网络通信的进程,告知OS当前数据传递给哪一个进程。一个端口号只能标识一个进程,一个进程可以绑定多个端口号。端口号可由用户指定,也可由OS自动分配。

  • 理解“端口号”和“进程PID”

    🔎既然需要标识网络通信中唯一一个进程,那么为什么不用系统中的进程PID,而是重新定义了一个端口号呢?

    1. 跨计算机通信: 进程PID是针对每台计算机上运行的进程的,不同计算机上可能存在相同的PID。在网络中,需要一种机制来标识不同计算机上运行的进程,因此需要使用全局唯一的标识符。
    2. 动态性: 进程在运行时可以创建和销毁,其PID也可能会更改。如果使用PID作为标识符,那么在进程重新启动后,其他进程无法识别它,这会导致通信中断。
    3. 端口号的多样性: 端口号是一种广泛用于网络通信的标识符,它不仅用于标识进程,还可以用于标识不同类型的服务。这种多样性使得不同类型的通信可以共存于同一台计算机上,而无需担心冲突。
    4. 网络层次: 在计算机网络中,通信涉及多个层次,从物理层到应用层。端口号位于传输层(通常是TCP或UDP协议),而进程PID是操作系统内核层的概念。因此,端口号更适合在传输层标识和管理不同进程之间的通信。

2. 认识UDP和TCP

此处我们先简单直观的认识一下UDP和TCP两种协议,以便更好掌握socket套接字编程。

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

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

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

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

3. 网络字节序

这里先简单复习一下系统大小端字节序的概念

  • 小端:低位在低地址,高位在高地址

  • 大端:低位在高地址,高位在低地址

下面是4字节整数0x12345678在内存中小端与大端不同的字节序。

在这里插入图片描述

不同的主机可能以不同的字节序存储多字节数据,那么,一台小端机器和一台大端机器就不能直接将数据传递给对方了,双方都不认识对方的数据。

为了解决这一问题,TPC/IP协议规定:网络数据流采用大端字节序。 即:小端机器向网络中发送数据,需要先将数据转成大端字节序,从网络中获取数据也需将数据先转成小端再使用。而大端就直接收发数据即可,无需转换。

网络通信双方传输和接收的核心数据,一般由系统调用自动做字节序的转换,而需要用户手动转换字节序的一般是通信的端口号和ip地址。

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);	// 主机转网络(4byte)

uint16_t htons(uint16_t hostshort); // 主机转网络(2byte)

uint32_t ntohl(uint32_t netlong);   // 网络转主机(4byte)

uint16_t ntohs(uint16_t netshort);  // 网络转主机(2byte)

4. socket通信

**Linux中的socket套接字是一种用于在不同进程之间进行通信的机制,它允许在同一台或不同计算机上的进程之间进行数据交换。**Socket API是一种应用层与传输层之间的接口,它使开发者能够创建网络应用程序,如客户端-服务器应用程序。

1️⃣

Linux下一切皆文件,因此网络通信本质上也是进程打开一个文件,获取一个文件描述符,并向这个文件描述符中传输或获取网络数据。这是网络通信的第一步,用到的是socket这个系统接口。

#include <sys/types.h>         
#include <sys/socket.h>

int socket(int domain, int type, int protocol); 

参数:

  • domain:通信类型,IPv4通信:AF_INET, IPV6通信:AF_INET6, 本地通信:AF_UNIX
  • type:传输数据类型,UDP:SOCK_DGRAM(数据报), TCP:SOCK_STREAM(字节流)
  • protocol:协议类型,传入0可根据type自动推导

返回值:

​ 一个套接字的文件描述符,后续通过该文件描述符传输或获取数据

在这里插入图片描述

2️⃣

第二步要绑定网络进程的地址,以便其它进程能找到该进程,实现通信。socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、 IPv6,以及UNIX Domain Socket(本地进程通信)。然而, 各种协议的地址格式并不相同。

在这里插入图片描述

绑定本地地址用到的系统接口是bind,需要用户先定义并填充一个地址结构体,再用bind接口绑定到系统当中,值得注意的是,bind接口的第二个参数addr的类型是struct sockaddr *,因为addr可能指向不同的地址结构体类型,这里传入统一类型的指针,再内部判断指针指向的空间头部的地址类型(AF_INET/AF_UNIX),即可判断地址结构体的类型,此处类似cpp多态的思想。

#include <sys/types.h>       
#include <sys/socket.h>

int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

  • sockfd:接口socket创建的文件描述符

  • addr:用户定义的地址结构体指针

  • addrlen:结构体的长度

返回值:

​ 成功返回0,失败返回-1并设置错误码errno。

在这里插入图片描述

💬struct sockaddr_in的代码结构

在这里插入图片描述


5. UDP服务器和客户端

💭UDP协议规定的是无连接的网络通信,通信双方无需连接直接通过地址找到对方并通信,传输数据面向数据报。优点是代码实现简单,缺点是传输不稳定可靠。

在这里插入图片描述

⭕UDP协议用于收发数据的系统接口:

  1. 接收数据recvfrom

    #include <sys/types.h>
    #include <sys/socket.h>
       
    ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
    

    参数:

    • sockfd:套接字文件描述符,用于指定要从哪个套接字接收数据。

    • buf:一个指向用于存储接收数据的缓冲区的指针。

    • len:接收缓冲区的长度,即可接收的最大字节数。

    • flags:控制接收操作的标志位,通常设置为0。

    • src_addr:一个指向 struct sockaddr 类型的指针,用于填充发送数据方的地址信息。这个参数可以为NULL,如果不关心对方的地址信息。

    • addrlen:一个指向 socklen_t 类型的指针,用于指定 src_addr 缓冲区的长度。在调用 recvfrom 之前,你需要将 addrlen 设置为 src_addr 缓冲区的大小。

    返回值:

    ​ 成功返回值接收到的字节数。如果发生错误,返回值为 -1,并且可以使用 errno 来获取错误代码。

  2. 发送数据sendto

    #include <sys/types.h>
    #include <sys/socket.h>
    
    ssize_t sendto(int sockfd, const void *buf, size_t len, int flags, const struct sockaddr *dest_addr, socklen_t addrlen);
    

    参数:

    • sockfd:套接字文件描述符,用于指定要发送数据的套接字。
    • buf:一个指向包含要发送数据的缓冲区的指针。
    • len:要发送的数据的长度(以字节为单位)。
    • flags:控制发送操作的标志位,通常设置为0。
    • dest_addr:一个指向 struct sockaddr 类型的指针,用于指定数据的目标地址。这个参数通常用于指定接收方的地址信息。
    • addrlen:一个 socklen_t 类型的整数,用于指定 dest_addr 缓冲区的长度。

    返回值:

    ​ 成功返回发送的字节数。如果发生错误,返回值为 -1,并且可以使用 errno 来获取错误代码。

5.1 基础UDP服务器和客户端

  • 服务器
// server.cc
#include <iostream>
#include <memory>
#include <unistd.h>
#include <cstdio>
#include "server.hpp"
#include "err.hpp"

// 该服务器完成工作:将客户端数据接收并原封不动地发挥给客户端即可
void Usage()
{
    // 使用手册
    std::cout << "Please enter the correct format: "
              << "./server [port]" << std::endl;
}

std::string EchoService(const std::string& msg)
{
    return msg;
}

// ./server [port] (port为该网络进程的端口号)
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        Usage();
        exit(USAGE_ERR);
    }
	
    // 将服务器封装成一个类,先理清调用逻辑
    
    // 向服务器传入用户指定的端口号,以及业务处理函数(只需将消息收到并返回给即可)
    std::unique_ptr<UdpServer> us_ptr(new UdpServer(atoi(argv[1]), EchoService));

    us_ptr->Initial(); // 初始化服务器
    
    us_ptr->Start();   // 启动服务器

    return 0;
}
// server.hpp
#pragma once

#include <iostream>
#include <string>
#include <functional>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"

const int MSG_BUF_SIZE = 1024;
using serv_func_t = std::function<std::string(const std::string &str)>;

class UdpServer
{
public:
    // 构造函数
    UdpServer(int port, serv_func_t service) : _port(port), _service(service)
    {
    }
	
    // 初始化服务器
    void Initial()
    {
        // 1. 创建socket套接字(协议族,声明是哪种通信、哪种协议)
        int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd < 0)
        {
            // 创建套接字失败
            std::cerr << strerror(errno) << std::endl;
            exit(SOCKET_ERR);
        }
        _sockfd = sockfd;

        std::cout << "socket creat success: " << _sockfd << std::endl;

        // 2. 绑定IP地址和端口号(地址族,标定网络通信地址)
        
        // 2.1 填充sockaddr_in各个字段
        struct sockaddr_in sin;
        bzero(&sin, sizeof(sin));
        sin.sin_family = AF_INET;
        sin.sin_port = htons(_port); // 主机转网络字节序

        // sin.sin_addr.s_addr = inet_addr(_ip.c_str());
        // in_addr_t inet_addr(const char *cp);
        // 将点分字符串形式的ip地址转成四字节整数形式,并且从主机字节序转换为网络字节序

        // 云服务器可能有多个ip地址,不允许用户指定某一个,用INADDR_ANY表示该服务器的任意ip
        // 表示只要发到该服务器上的信息都可以接收
        sin.sin_addr.s_addr = INADDR_ANY;

        // 2.2 调用bind绑定到系统
        if (bind(_sockfd, (struct sockaddr *)&sin, sizeof(sin)) < 0)
        {
            // 绑定失败
            std::cerr << strerror(errno) << std::endl;
            exit(BIND_ERR);
        }

        std::cout << "socket bind success: "
                  << "port->" << _port << std::endl;
    }

    // 启动服务器
    void Start()
    {
        // 不断地接收客户端数据,并将数据返回给客户端
        while (true)
        {
            // 1. 数据接收
            char msg_buf[MSG_BUF_SIZE];
            memset(msg_buf, 0, MSG_BUF_SIZE);
			
            // 客户端地址信息,recv会自动填充
            struct sockaddr_in cln;
            bzero(&cln,sizeof(cln));
            socklen_t len;

            ssize_t rn = recvfrom(_sockfd, msg_buf, sizeof(msg_buf) - 1, 0, (struct sockaddr *)&cln, &len);
            if (rn < 0)
            {
                // 接收失败
                std::cerr << strerror(errno) << std::endl;
                exit(RECV_ERR);
            }
            msg_buf[rn] = '\0';

            // 2. 业务处理
            std::cout << "[" << inet_ntoa(cln.sin_addr) << ":" << ntohs(cln.sin_port) << "] ";
            std::cout << "#用户输入指令# " << msg_buf << std::endl;
            std::string respond = _service(msg_buf);

            // 3. 数据传回客户端
            ssize_t sn = sendto(_sockfd, respond.c_str(), respond.size(), 0, (struct sockaddr *)&cln, sizeof(cln));
            if (sn < 0)
            {
                std::cerr << errno << " " << strerror(errno) << std::endl;
                exit(SEND_ERR);
            }
        }
    }

private:
    int _sockfd; 		 // 套接字文件描述符
    uint16_t _port; 	 // 服务器端口号
    serv_func_t _service;// 业务处理接口
};
  • 客户端
//client.cc
#include <iostream>
#include <memory>
#include "client.hpp"
#include "err.hpp"

void Usage()
{
    std::cout << "Please enter the correct format: "
              << "./client [server's ip] [server's port]" << std::endl;
}

// ./client [ip] [port] (由用户指定服务器的ip和port)
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage();
        exit(USAGE_ERR);
    }

    std::unique_ptr<UdpClient> uc_ptr(new UdpClient(argv[1], atoi(argv[2])));
    uc_ptr->Initial();

    while (true)
    {
        std::cout << "ENTER:> ";
        std::string msg;
        std::getline(std::cin, msg);
        // 用户不断发送消息并接收从服务器返回的数据
        uc_ptr->Send(msg);
        uc_ptr->Recv();
    }

    return 0;
}
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"

const int MSG_BUF_SIZE = 1024;

static struct sockaddr_in tmp;
static socklen_t len;

class UdpClient
{
public:
    UdpClient(std::string svr_ip, uint16_t svr_port)
        : _svr_ip(svr_ip), _svr_port(svr_port)
    {
        // 填充服务器的地址信息
        bzero(&_svr, sizeof(_svr));
        _svr.sin_family = AF_INET;
        _svr.sin_port = htons(_svr_port);
        _svr.sin_addr.s_addr = inet_addr(_svr_ip.c_str());
    }

    void Initial()
    {
        // 1. 创建套接字
        int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd < 0)
        {
            // 创建套接字失败
            std::cerr << strerror(errno) << std::endl;
            exit(SOCKET_ERR);
        }
        _sockfd = sockfd;

        // 2. 绑定(由操作系统自动绑定地址,端口号由OS分配)
    }

    // 向服务端发送消息
    void Send(std::string &msg)
    {
        ssize_t n = sendto(_sockfd, msg.c_str(), msg.size(), 0, (struct sockaddr *)&_svr, sizeof(_svr));
        if (n < 0)
        {
            std::cerr << strerror(errno) << std::endl;
            exit(SEND_ERR);
        }
    }

    // 接收服务端发回的消息
    void Recv()
    {
        char msg_buf[MSG_BUF_SIZE];
        memset(msg_buf, 0, MSG_BUF_SIZE);

        int n = recvfrom(_sockfd, msg_buf, sizeof(msg_buf) - 1, 0, (struct sockaddr *)&tmp, &len);
        if (n < 0)
        {
            std::cerr << strerror(errno) << std::endl;
            exit(RECV_ERR);
        }
        msg_buf[n] = '\0';
        std::cout << "[" << inet_ntoa(_svr.sin_addr) << ":" << ntohs(_svr.sin_port) << "] " << std::endl
                  << msg_buf;
    }

private:
    int _sockfd;             // 套接字文件描述符
    std::string _svr_ip;     // 服务器ip
    uint16_t _svr_port;      // 服务器port
    struct sockaddr_in _svr; // 服务器地址信息
};

tips:

  • 服务器是稳定的,长期运行的,因此其端口号需要在启动时指定且运行时一直保持不变,才能让客户端能准确地找到服务器。

  • 客户端是动态的,随时可能退出与重连,而且可能会有多个客户端同时存在。因此客户端的端口号不应该由用户指定,而是OS动态分配,避免端口冲突,提高并发性能。OS在客户端第一次调用Socket Api完成地址的绑定工作。客户端分配的端口号是临时的,在连接关闭后释放。

5.2 群聊服务器和客户端

实现客户端能模拟类似微信群聊的功能。在服务器中设置一个环形队列cirQueue,并设置两个线程,一个用于接收用户消息,一个用于广播用户消息(即向每位用户发送head消息)。为了满足向用户广播消息的需求,服务器里还需储存当前在线用户的信息。

在这里插入图片描述

  • 服务器
// GroupChatServer.hpp
#pragma once

#include <iostream>
#include <string>
#include <unordered_map>
#include <list>
#include <functional>
#include <mutex>
#include <cstring>
#include <cstdlib>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"
#include "cirQueue.hpp"
#include "Thread.hpp"

const int BUF_SIZE = 1024;
using serv_func_t = std::function<std::string(const std::string &str)>;

// 接收客户端的信息,并传回客户端

class UdpServer
{
public:
    UdpServer(uint16_t port, serv_func_t service)
        : _port(port), _service(service)
    {
        // 调用之前写的Thread组件,两个线程,一个负责收消息,一个负责广播数据
        _p = Thread(1, RecvThreadRoutine, this);
        _c = Thread(2, BoardcastThreadRoutine, this);
    }
    
    static void *RecvThreadRoutine(void *args)
    {
        UdpServer *ts = static_cast<UdpServer *>(args);
        while (true)
        {
            ts->Recv();
        }
        return nullptr;
    }

    static void *BoardcastThreadRoutine(void *args)
    {
        UdpServer *ts = static_cast<UdpServer *>(args);
        while (true)
        {
            ts->Boardcast();
        }
        return nullptr;
    }

    void Initial()
    {
        // 1. 创建socket套接字(协议族,声明是哪种通信、哪种协议)
        int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd < 0)
        {
            // 创建套接字失败
            std::cerr << strerror(errno) << std::endl;
            exit(SOCKET_ERR);
        }
        _sockfd = sockfd;

        std::cout << "socket creat success: " << _sockfd << std::endl;

        // 2. 绑定IP地址和端口号(地址族,标定网络通信地址)
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;

        if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            // 绑定失败
            std::cerr << strerror(errno) << std::endl;
            exit(BIND_ERR);
        }
        std::cout << "socket bind success: "
                  << "port->" << _port << std::endl;
    }
    
    // 服务器启动,即两个线程开始运行,主线程等待即可
    void Start()
    {
        _p.run();
        _c.run();

        _p.join();
        _c.join();
    }

    void Recv()
    {
        // 1.1 创建接收数据的缓冲区
        char buf[BUF_SIZE];
        memset(buf, 0, sizeof(buf));

        // 1.2 创建套接字
        struct sockaddr_in client;
        bzero(&client, sizeof(client));
        socklen_t len = sizeof(client);

        // 1.3 从sock中接收客户端数据
        ssize_t n = recvfrom(_sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr *)&client, &len);
        if (n < 0)
        {
            std::cerr << "recv error: " << strerror(errno) << std::endl;
            exit(RECV_ERR);
        }
        buf[n] = '\0';

        // 1.4 打包用户信息,并添加新用户
        std::string client_info = inet_ntoa(client.sin_addr);
        client_info += '-';
        client_info += std::to_string(ntohs(client.sin_port));
        AddOnlineUser(client_info, client);

        // 1.5 将消息投放到公共聊天窗口(即环形缓冲区)中
        std::string message = "[" + client_info + "] " + buf;
        _messages.push(message);
    }

    void Boardcast()
    {
        // 1. 取出环形缓冲区的头部数据,这是我们这次要广播的消息
        std::string message;
        _messages.pop(&message);

        // 2. 发给每一个在线用户

        // 2.1 先加锁拷贝一份在线用户信息副本(公有->私有)
        list<struct sockaddr_in> sins;
        {
            std::unique_lock<std::mutex> lck(_mtx);
            for (auto &usr : _online_users)
            {
                std::cout << "send to " << usr.first << ": " << message << std::endl;
                sins.push_back(usr.second);
            }
        }
        // 2.2 再用线程私有的副本进行网络IO将信息传给客户端
        for (auto &sin : sins)
        {
            ssize_t n = sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)&sin, sizeof(sin));
            if (n < 0)
            {
                std::cerr << "send error: " << strerror(errno) << std::endl;
                exit(SEND_ERR);
            }
        }
    }

private:
    void AddOnlineUser(const std::string &user, struct sockaddr_in &sin)
    {
        std::unique_lock<std::mutex> lck(_mtx);
        // if user is a new client, add it into online_users, else do nothing
        size_t size = _online_users.size();
        _online_users[user] = sin;
        if (_online_users.size() > size)
            std::cout << "新用户加入: " << user << std::endl;
    }

private:
    int _sockfd;                                                       // 套接字文件fd
    uint16_t _port;                                                    // 服务器端口号
    serv_func_t _service;                                              // 业务处理函数
    cirQueue<std::string> _messages;                                   // 存储用户消息的环形队列
    std::unordered_map<std::string, struct sockaddr_in> _online_users; // 在线用户信息表(用户ip和port)
    Thread _p;                                                         // recv线程(生产者)
    Thread _c;                                                         // boardcast线程(消费者)
    std::mutex _mtx;                                                   // 保护_online_users的锁
};
  • 客户端
//client.cc(无封装版本)
#include <iostream>
#include <cstring>
#include <cerrno>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Thread.hpp"
#include "err.hpp"

const int BUF_SIZE = 1024;

void Usage()
{
    std::cout << "Please enter the correct format: " << std::endl
              << "      ./client [server's ip] [server's port]" << std::endl;
}

struct ThreadData
{
    ThreadData(int sockfd, struct sockaddr_in *psvr) : _sockfd(sockfd), _psvr(psvr)
    {
    }
    int _sockfd;
    struct sockaddr_in *_psvr;
};

void *SendThreadRountine(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    while (true)
    {
        // 1. 用户输入消息
        std::string message;
        std::cout << "Please Enter# ";
        std::getline(std::cin, message);
        // 2. 发送消息到服务端
        ssize_t n = sendto(td->_sockfd, message.c_str(), message.size(), 0, (struct sockaddr *)td->_psvr, sizeof(*td->_psvr));
        if (n < 0)
        {
            std::cerr << "send error: " << strerror(errno) << std::endl;
            exit(SEND_ERR);
        }
    }
    delete td;
    return nullptr;
}

void *RecvThreadRountine(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    while (true)
    {
        // 接收其它客户端的消息(来自服务端)
        char buf[BUF_SIZE];
        memset(buf, 0, sizeof(buf));
        struct sockaddr_in tmp;
        bzero(&tmp, sizeof(tmp));
        socklen_t len = sizeof(tmp);

        ssize_t n = recvfrom(td->_sockfd, buf, sizeof(buf) - 1, 0, (struct sockaddr *)&tmp, &len);
        if (n < 0)
        {
            std::cerr << "recv error: " << strerror(errno) << std::endl;
            exit(RECV_ERR);
        }
        buf[n] = '\0';

        // 群聊信息打印到2号文件描述符上,方便重定向观察输出结果
        std::cerr << buf << std::endl;
    }
    delete td;
    return nullptr;
}

// ./client [server's ip] [server's port]
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage();
        exit(USAGE_ERR);
    }

    // 1. 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        std::cerr << strerror(errno) << std::endl;
        exit(SOCKET_ERR);
    }
    // IP和端口号由OS自动分配绑定

    // 2. 获取服务端ip和port
    std::string server_ip = argv[1];
    uint16_t server_port = atoi(argv[2]);

    // 指明服务端的地址族(ip and port)
    struct sockaddr_in server;
    server.sin_family = AF_INET;
    server.sin_port = htons(server_port);
    server.sin_addr.s_addr = inet_addr(server_ip.c_str());

    // 一个线程负责让用户输入并发送消息,一个线程负责接收群聊服务器的消息
    Thread send_thread(1, SendThreadRountine, new ThreadData(sockfd, &server));
    Thread recv_thread(2, RecvThreadRountine, new ThreadData(sockfd, &server));

    send_thread.run();
    recv_thread.run();

    send_thread.join();
    recv_thread.join();
    return 0;
}

呈现效果如下

在这里插入图片描述


6. TCP服务器和客户端

💭TCP协议规定面向连接的网络通信,传输数据面向字节流。TCP服务器除了完成socket创建套接字和bind绑定本机地址外,还需要做如下两件事:

  1. 设置套接字为监听状态,等待客户端的连接请求

    #include <sys/types.h> 
    #include <sys/socket.h>
    
    int listen(int sockfd, int backlog);
    

    参数:

    • sockfd:服务器的套接字文件描述符
    • backlog:用于指定等待连接队列的最大长度

    返回值:

    ​ 成功返回0,失败返回-1,错误码errno被设置

  2. 接收客户端的连接请求

    #include <sys/types.h>
    #include <sys/socket.h>
    
    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
    

    参数:

    • sockfd:服务器的套接字文件描述符
    • addr:用于存储接收到的客户端的地址信息
    • 一个指向 socklen_t 类型的指针,用于指定 src_addr 缓冲区的长度。在调用 accept 之前,你需要将 addrlen 设置为 addr 缓冲区的大小。

    返回值:

    ​ 返回一个套接字文件描述符,该描述符面向已接收到的客户端,服务器通过该描述符与此客户端进行通信。也就是说,TCP服务器为每一个已连接的客户端创建一个专属的套接字。

🔗服务器等待客户端的连接请求,客户端调用connect函数向指定的服务器发送连接请求。

#include <sys/types.h>
#include <sys/socket.h>

int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

参数:

  • sockfd:客户端的套接字文件描述符
  • addr:指定的服务器地址信息
  • addrlen:addr指向空间的长度

返回值:

​ 成功返回0,失败返回-1,错误码errno被设置

在这里插入图片描述

6.1 TcpServer

  1. TcpServer的成员变量

    private:
        int _sockfd;        // 服务器的套接字文件描述符
        uint16_t _svr_port; // 服务器端口号
        func_t _service;    // 业务处理函数
    
  2. TcpServer的初始化

    void Initial()
    {
        // 1.创建套接字,TCP的传输数据类型是SOCK_STREAM
        if ((_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
        {
            exit(SOCKET_ERR);
        }
    
    	// 2.绑定服务器本地地址
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_svr_port);
        local.sin_addr.s_addr = INADDR_ANY;
    
        if (bind(_sockfd, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            exit(BIND_ERR);
        }
    
        // 3.设置服务器套接字为监听状态
        if (listen(_sockfd, backlog) < 0)
        {
            exit(LISTEN_ERR);
        }
    }
    
  3. TcpServer的启动工作

    // 3.1 多进程版本
    void Start()
    {
        while (true)
        {
            struct sockaddr_in client;
            bzero(&client, sizeof(client));
            socklen_t len = sizeof(client);
    
            // 1.接收请求连接的客户端
            int cfd = accept(_sockfd, (struct sockaddr *)&client, &len);
            if (cfd < 0)
            {
                // no client connect, continue try to accept
                continue;
            }
            std::string client_info = std::string(inet_ntoa(client.sin_addr)) + "-" + std::to_string(ntohs(client.sin_port));
    
            std::cout << "新用户已接入" << client_info << " cfd: " << cfd << std::endl;
    
            // 2. 收发数据的工作交给子进程做,父进程只负责监听与接收客户端
            
            // 2.1 父进程不关心子进程的退出结果,不等待子进程
        	signal(SIGCHLD, SIG_IGN);
            
            // 2.2 创建子进程
            pid_t id = fork();
            assert(id >= 0);
            if (id == 0)
            {
                while (true)
                {
                    std::string respond = Recv(cfd, client_info);
                    Send(cfd, respond);
                }
            }
            // 2.3 父进程不再需要维护当前客户端的sockfd,直接close,并继续accept其它客户端,这样做可减少文件描述符的消耗
            close(cfd);
        }
    }
    
    // 3.2 多线程版本
    void Start()
    {
        std::cout << "server start!" << std::endl;
        while (true)
        {
            struct sockaddr_in client;
            bzero(&client, sizeof(client));
            socklen_t len = sizeof(client);
    
            // 1.接受监听的客户端
            int cfd = accept(_sockfd, (struct sockaddr *)&client, &len);
            if (cfd < 0)
            {
                // no client connect, continue try to accept
                continue;
            }
            std::string client_info = std::string(inet_ntoa(client.sin_addr)) + "-" + std::to_string(ntohs(client.sin_port));
            
            std::cout << "### 新用户已接入" << client_info << " cfd: " << cfd << " ###" << std::endl;
    
            // 2. 收发数据的工作交给新线程做,主线程只负责监听与接收客户端
            
            // 一个客户端创建一个服务线程
            pthread_t pid;
            pthread_create(&pid, nullptr, threadRoutine, new ThreadData(cfd, client_info, this));
        }
    }
    
    struct ThreadData
    {
        ThreadData() = default;
    
        ThreadData(int cfd, std::string cinfo, TcpServer *ts)
            : _cfd(cfd), _cinfo(cinfo), _ts(ts)
        {
        }
    
        int _cfd;
        std::string _cinfo;
        TcpServer *_ts;
    };
    
    static void *threadRoutine(void *args)
    {
        ThreadData *td = static_cast<ThreadData *>(args);
        pthread_detach(pthread_self());  // 分离线程,这样主线程无需等待该线程,提高服务器效率
        while (true)
        {
            std::string respond = td->_ts->Recv(td->_cfd, td->_cinfo);
            td->_ts->Send(td->_cfd, respond);
        }
    }
    

    ⭕注意:多进程版本,因为子进程拷贝了父进程的文件描述符表,每个进程拥有一张独立的文件描述符表,所以父进程可以关闭已经交给子进程的客户端sockfd。而多线程版本,各线程共享同一张文件描述符表,主线程在客户端退出之前不能关闭客户端sockfd,否则会导致工作线程找不到对接的客户端

  4. TcpServer的Recv函数(接收客户端数据)

    由于TCP是面向字节流传输的,所以TcpServer的数据传输本质上就是对文件的读写,调用 readwrite ,操作的是客户端套接字文件描述符。与管道文件的同步机制类似,如果套接字对接的客户端退出,服务器read的返回值就是0。

    std::string Recv(int cfd, const std::string &ci)
    {
        // read读取客户端数据
        char buf[BUF_SIZE];
        memset(buf, 0, sizeof(buf));
        std::string respond;
        ssize_t n = read(cfd, buf, sizeof(buf) - 1);
        if (n < 0)
        {
            // 读取失败,当前子执行流退出
            std::cerr << "read from client fail: " << strerror(errno) << std::endl;
    
            // exit(READ_ERR); // 多进程版
            pthread_exit(nullptr);
        }
        else if (n == 0)
        {
            // 客户端已退出
            close(cfd);
            // 多进程版可以不close,子进程exit也就回收了,对父进程没有影响
            // 多线程版必须close,防止文件fd泄漏
    
            // exit(0); // 多进程版
            pthread_exit(nullptr);
        }
        else
        {
            // 读取成功,将数据业务处理后返回
            buf[n] = '\0';
            return _service(buf);
        }
    }
    
  5. TcpServer的Send函数(发送数据到客户端)

    void Send(int cfd, std::string respond)
    {
        // 向客户端发回数据
        ssize_t n = write(cfd, respond.c_str(), respond.size());
        if (n < 0)
        {
            // write fail
            std::cerr << "write to client fail: " << strerror(errno) << std::endl;
            exit(WRITE_ERR);
        }
    }
    

6.2 TcpClient

#pragma once

#include <iostream>
#include <cstring>
#include <cerrno>
#include <strings.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "err.hpp"

class TcpClient
{
    static const size_t BUF_SIZE = 1024;

public:
    TcpClient(uint16_t svr_port, std::string svr_ip)
        : _svr_port(svr_port), _svr_ip(svr_ip)
    {
        bzero(&_svr, sizeof(_svr));
    }

    void Initial()
    {
        // 1.创建套接字
        if ((_sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
        {
            std::cerr << "socket create fail: " << strerror(errno) << std::endl;
            exit(SOCKET_ERR);
        }

        // 由OS自动绑定ip和port

        // 2.指定客户端地址族
        _svr.sin_family = AF_INET;
        _svr.sin_port = htons(_svr_port);
        _svr.sin_addr.s_addr = inet_addr(_svr_ip.c_str());
    }

    void Start()
    {
        // 1. 向服务器发送connect请求
        int times = 5;
        while (times > 0 && connect(_sockfd, (struct sockaddr *)&_svr, sizeof(_svr)) < 0)
        {
            std::cout << "正在尝试重新连接服务器..." << times-- << std::endl;
            sleep(1);
        }
        if (times == 0)
        {
            std::cout << "连接失败!" << std::endl;
            exit(CONNECT_ERR);
        }
        // connect success
        std::cout << "连接成功!" << std::endl;

        // 2. 开始工作
        while (true)
        {
            // 2.1 向服务端发送消息
            std::string message;
            std::cout << "ENTER:> ";
            std::getline(std::cin, message);

            if (message == "quit")
            {
                close(_sockfd);
                return;
            }

            ssize_t n = write(_sockfd, message.c_str(), message.size());
            if (n < 0)
            {
                // write fail
                std::cerr << "write to client fail: " << strerror(errno) << std::endl;
                exit(WRITE_ERR);
            }

            // 2.2 接收服务端返回的消息
            char buf[BUF_SIZE];
            memset(buf, 0, sizeof(buf));
            n = read(_sockfd, buf, sizeof(buf) - 1);
            if (n < 0)
            {
                // read fail
                std::cerr << "read from server fail: " << strerror(errno) << std::endl;
                exit(READ_ERR);
            }
            else if (n == 0)
            {
                // 同样的,服务器退出,客户端read返回值为0
                std::cout << "server quit" << std::endl;
                close(_sockfd);
                break;
            }
            else
            {
                buf[n] = '\0';
                std::cout << "server sent to: " << buf << std::endl;
            }
        }
    }

private:
    int _sockfd;             // 客户端套接字文件描述符
    uint16_t _svr_port;      // 服务器端口号
    std::string _svr_ip;     // 服务器IP
    struct sockaddr_in _svr; // 服务器地址信息
};

6.3 TcpServer的优化

引入线程池

TcpServer的多进程版,频繁创建子进程,开销大,效率低。多线程版相较于多进程版提高了效率,减少创建子进程的效率损耗和资源浪费,但频繁创建线程依然开销不低。因此可以引入线程池,减少频繁创建和销毁线程的开销,提高并发效率。

// 引入线程池的版本(仅展示与其它版本不同之处)

class Task
{
using func_t = function<void(int cfd, const std::string &client_info)>;
public:
    Task() = default;

    Task(int cfd, const std::string &client_info, func_t cb)
        : _cfd(cfd), _client_info(client_info), _cb(cb)
    {
    }
    
	// 线程池中调用Task::operator()执行任务
    void operator()()
    {
        _cb(_cfd, _client_info);
    }

private:
    int _cfd;                 // 客户端套接字文件描述符
    std::string _client_info; // 客户端信息
    func_t _cb;               // 回调函数
};

void Start()
{
    while (true)
    {
        struct sockaddr_in client;
        bzero(&client, sizeof(client));
        socklen_t len = sizeof(client);

        // 1.接受监听的客户端
        int cfd = accept(_sockfd, (struct sockaddr *)&client, &len);
        if (cfd < 0)
        {
            // no client connect, continue try to accept
            LogMessage(WARNING, "accept fail: %s\n", strerror(errno));
            sleep(1);
            continue;
        }
        std::string client_info = std::string(inet_ntoa(client.sin_addr)) + "-" + std::to_string(ntohs(client.sin_port));
        
        // 2.创建一个与客户端交互的任务t,并交给线程池
        Task t(cfd, client_info,
               std::bind(&TcpServer::ServerThreadRountine, this, std::placeholders::_1, std::placeholders::_2));
        // 回调函数需要绑定this指针,否则无法调用TcpServer::ServerThreadRountine
        threadPool<Task>::get_instance()->pushTask(t);
    }
}

void ServerThreadRountine(int cfd, const std::string &client_info)
{
    bool quit = false;   // 判断客户端是否已退出的标志
    std::string respond; // 输出型参数
    while (true)
    {
        Recv(cfd, client_info, respond, quit); // Recv内部检查客户端是否已退出
        if (quit)
        {
            break;
        }
        Send(cfd, respond);
    }
}

void Recv(int cfd, const std::string &ci, std::string &respond, bool &quit)
{
    char buf[BUF_SIZE];
    memset(buf, 0, sizeof(buf));
    
    ssize_t n = read(cfd, buf, sizeof(buf) - 1);
    if (n < 0)
    {
        // read fail
        quit = true;
        return;
    }
    else if (n == 0)
    {
        close(cfd);
        quit = true;
        return;
    }
    else
    {
        buf[n] = '\0';
        respond = _service(buf);
    }
}

void Send(int cfd, const std::string &respond)
{
    ssize_t n = write(cfd, respond.c_str(), respond.size());
    if (n < 0)
    {
        // write fail
        std::cerr << "write to client fail: " << strerror(errno) << std::endl;
    }
}

日志系统

服务器需要有日志系统,方便开发者对于服务器的维护工作。

#pragma once

#include <iostream>
#include <string>
#include <map>
#include <cstdio>
#include <cstring>
#include <time.h>
#include <stdarg.h>
#include <unistd.h>

static const char *filename = "server.log";

// 日志等级
enum loglevel_t
{
    TRACE,
    DEBUG,
    INFO,
    WARNING,
    ERROR,
    FATAL
};
static std::map<loglevel_t, std::string> ltos = {{TRACE, "TRACE"}, {DEBUG, "DEBUG"}, {INFO, "INFO"}, {WARNING, "WARNING"}, {ERROR, "ERROR"}, {FATAL, "FATAL"}};

// 日志格式:log = title(log level, time, pid) + body
void LogMessage(loglevel_t lv, const char *format, ...)
{
    // 1. title
    time_t t = time(nullptr);
    struct tm *tp = localtime(&t);
    // 2. time = y-m-d h:m:s
    char timestr[64];
    memset(timestr, 0, sizeof(timestr));
    snprintf(timestr, sizeof(timestr), "%d-%d-%d %d:%d:%d", tp->tm_year + 1900, tp->tm_mon + 1, tp->tm_mday, tp->tm_hour, tp->tm_min, tp->tm_sec);
    std::string logtitle = ltos[lv] + " " + timestr + " " + std::to_string(getpid());

    // 3. body
    va_list ap;
    char logbody[128];
    memset(logbody, 0, sizeof(logbody));
    va_start(ap, format);
    vsnprintf(logbody, sizeof(logbody), format, ap);
    va_end(ap);
	
    // 4. 输出
    
    // 输出到终端
    // combine and output
    // printf("[%s] %s", logtitle.c_str(), logbody);

    // 保存到文件
    FILE *fp = fopen(filename, "a");

    fprintf(fp, "[%s] %s", logtitle.c_str(), logbody);

    fclose(fp);
}

💭about C语言函数的可变参数

函数的传输列表中,...表示可变参数。拿上述日志系统中void LogMessage(loglevel_t lv, const char *format, ...)进行分析。

C语言提供一组宏函数来对可变参数进行操作,包含头文件#include <stdarg>

🔎参考文章:

va_list:指向可变参数首地址的指针类型

void va_start(va_list ap, last):以固定参数last(参数列表...前的最后一个参数)的地址为起点确定变参的内存起始地址,获取第一个参数的首地址赋值给ap

type va_arg(va_list ap, type):获取下一个参数的地址(跳转字节数sizeof type

void va_end(va_list ap): 将ap指针置空

而有了格式控制字符串format,可以更好地使用可变参数

int vsnprintf(char *str, size_t size, const char *format, va_list ap)

函数解释:ap指向可变参数列表的首地址,根据格式控制format,将长度为size的字符串拷贝到str中。

服务器守护进程化

🔎先介绍一个概念,Linux中的会话

会话(Session)是一个用于管理和组织进程的概念。会话是一个抽象层级,用于将相关进程分组在一起,以便它们可以协同工作并共享某些属性。以下是有关Linux会话的详细解释。

  1. 一个会话可以与一个控制终端相关联。 我们平时用shell时启动的一个终端窗口,实际上就关联了某个独立的会话。
  2. 每个会话都有一个唯一的标识符,称为SID(Session ID),它是一个整数值。这个SID与会话中第一个创建的进程(也称话首进程)的PID相同。
  3. 一个会话中可以有一个或多个进程组。 进程组是一组相关进程的集合,它们通常用于完成同一项任务,一个进程组中的进程都在同一个会话中。进程组有一个唯一的标识符PGID,进程组的PGID=进程组组长的PID。
  4. 一个会话至多有一个前台进程,可以有多个后台进程。

🔎我们在shell以用户身份登录时,Linux操作系统中执行了哪些动作。

  1. 建立一个专属的会话
  2. 在该会话中启动一个bash进程,这个bash进程就是该会话的话首进程,bash pid = 会话sid
  3. bash进程自成一个进程组,在bash下启动的进程都是bash的子进程

在这里插入图片描述

💭回到服务器的层面上。服务器一般都是一直在运行,不分昼夜,就如我们三更半夜也能刷b站、发微信。而我们刚刚写的服务器,启动在与某个终端相关联的会话中,一旦该会话关闭,服务器也随之退出,客户端将无法找到服务器。那么,我们需要将服务器与终端分离,创建一个专属于服务器的、不依赖于某个终端的会话,使之一直在系统中运行,这个过程称为服务器的守护进程化Daemon)。

⭕核心的系统调用接口:setsid

#include <unistd.h>

pid_t setsid(void);

功能:创建一个新会话,并将调用进程设为新会话的话首进程。新会话不与任何终端产生关联,

参数:无

返回值:成功返回新会话的SID,失败返回(pid_t)-1,错误码被设置。

需要注意的是,调用setsid的进程不能是某个进程组的组长进程,否则创建新会话失败。

// daemon.hpp
#include <iostream>
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void Daemon()
{
    // 1.创建子进程,父进程退出(保证服务器非组长进程)
    if (fork() > 0)
        exit(0);

    // 2.子进程创建新会话
    pid_t id = setsid();

    // 3.修改工作路径(可选做)

    // 4. 处理文件描述符0/1/2,因为守护进程没有关联终端

    // 方法1:重定向文件描述符0/1/2到/dev/null(因为守护进程没有关联终端)
    int fd = open("/dev/null", O_RDWR);
    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);
    close(fd);
    
    // 方法2:直接close
    // close(0);
    // close(1);
    // close(2);
}

tcp服务器与客户端完成代码已push到本人gitee,需要的小伙伴可以自取~

「tcp服务器与客户端、日志系统、守护进程代码」


Ending…

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

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

相关文章

Unity Game FrameWork—框架学习—ab打包流程解析

UGF资源更新与管理 https://www.jianshu.com/p/80bff8c9004a 打包配置 ResourceBuilder.xml文件保存了打包配置信息 参数&#xff1a; InternalResourceVersion&#xff1a;内部版本号 Platforms&#xff1a;生成的ab资源所对应平台编号&#xff0c;二进制左移&#xff0c;与…

中睿天下受邀参加2023北京数字交通大会暨博览会并发表主题演讲

2023年9月11号由中国交通报社、中国交通运输协会联合主办的2023北京数字交通大会暨博览会&#xff08;简称大会暨博览会&#xff09;在北京中国国际展览中心&#xff08;新馆&#xff09;举行。这次大会主题是“数字新时代、交通新未来”&#xff0c;将聚焦数字交通创新发展&am…

国货拟人AI绘图;500+AI岗位合辑;百川x亚马逊AI黑客松;企业级AI行业图谱;100+LLM面试题与答案 | ShowMeAI日报

&#x1f440;日报&周刊合集 | &#x1f3a1;生产力工具与行业应用大全 | &#x1f9e1; 点赞关注评论拜托啦&#xff01; &#x1f525; 上万人涌入抖音国货直播间&#xff0c;朴实「商战」带火国民品牌 谁能想到&#xff0c;李佳琦「华西子事件」意外带火了一众国货品牌的…

C#源码 LIS实验室(检验科)信息系统源码 SaaS模式的Client/Server架构

LIS实验室&#xff08;检验科&#xff09;信息系统&#xff0c;一体化设计&#xff0c;与其他系统无缝连接&#xff0c;全程化条码管理。集申请、采样、核收、计费、检验、审核、发布、质控、查询、耗材控制等检验科工作为一体的网络管理系统。 技术细节&#xff1a; 体系结构…

什么是RPA机器人流程自动化软件?

泽众RPA机器人流程自动化软件&#xff0c;是一种能够模拟人类来执行重复性任务的软件&#xff1b;它通过驱动对于系统业务进行统筹安排、协调处理、自动执行以此提升业务处理效率。借助RPA用户可以提高工作效率、节省成本、降低出錯率、节省时间、并从重复性的后台任务中解放劳…

《学术小白学习之路12》进阶-基于Python实现中文文本的DTM主题动态模型构建

《学术小白学习之路》基于Python实现中文文本的DTM主题动态模型构建 一、数据选择二、数据预处理三、输入数据ID映射词典构建四、文档加载成构造语料库五、DTM模型构建与结果分析六、结果进行保存七、保存模型一、数据选择 所选取的数据集是论文摘要,作为实验数据集,共计12条…

中国人民大学与加拿大女王大学金融硕士为何占据在职读研人的心?一起来看看

说起北京地区的中外合作办学在职硕士项目哪个最受欢迎呢&#xff1f;无疑是中国人民大学与加拿大女王大学金融硕士项目&#xff0c;它已经深深占据在职读研人的心。项目历经十年的风雨&#xff0c;有口皆碑。一起去人大女王金融硕士项目为什么这么受青睐。 一、名校光环 女王金…

基于微信小程序的校园代送跑腿系统(源码+lw+部署文档+讲解等)

文章目录 前言系统主要功能&#xff1a;具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计…

Python和Scrapy构建可扩展的框架

构建一个可扩展的网络爬虫框架是利用Python和Scrapy实现高效数据采集的重要技能。在本文中&#xff0c;我将为您介绍如何使用Python和Scrapy搭建一个强大灵活的网络爬虫框架。我们将按照以下步骤展开&#xff1a; 1. 安装Scrapy&#xff1a; 首先&#xff0c;确保您已经安装了…

QT配置FFmpeg出现错误原因

文章目录 QT配置ffmpeg出现&#xff1a; undefined reference to "avcodec_version"没有配置环境变量QT和FFmpeg的版本不对应直接添加FFmpeg的头文件没有在.pro文件添加路径 QT 程序异常退出没有在debug文件里面存放dll库 QT配置ffmpeg出现&#xff1a; undefined re…

优思学院|怎样制定有效的质量管控措施?要善用六西格玛思维!

要看质量管控措施行不行&#xff0c;关键在于这些措施是不是经过认真分析才定的。分析要严谨&#xff0c;就可以用DMAIC这六个步骤&#xff0c;它是六西格玛方法的核心。DMAIC是六西格玛分析过程中的步骤&#xff0c;就是&#xff1a;定义&#xff08;Define&#xff09;、测量…

华为云云耀云服务器L实例使用教学|Unbelievable, 带你用云服务器部署Windows12

&#x1f4cb; 前言 &#x1f5b1; 博客主页&#xff1a;在下马农的碎碎念&#x1f917; 欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐️留言&#x1f4dd;✍ 本文由在下马农原创&#xff0c;首发于CSDN&#x1f4c6; 首发时间&#xff1a;2023/09/26&#x1f4c5; 最近更新时…

Bitcoin Consensus Mechanism——SYSU SSE Blockchain 5th lecture(English Version)

目录 Part 1: What is Consensus? Definition Distributed Consensus Consensus in Bitcoin CAP Theorem Definitions Trade-offs ("Pick Two" Dilemma) Part 2: Why Bitcoin needs Consensus Types of Consensus Mechanisms Why Bitcoin Needs Consensus…

LaTex排版系统:TeX Live+Visual Studio Code的配置

配置Tex排版系统需要安装编译器编辑器&#xff0c;编译器通常安装TeX Live&#xff0c;编辑器主要有TeXworks、TexStudio、WinEdt、Visual Studio Code等&#xff0c;目前VS Code普遍评价不错。下面介绍Windows平台安装LaTex的详细步骤。 1 安装TeX Live 1.1 下载地址选择国内…

babel的配置执行顺序

babel配置文件&#xff1a; //.babelrc {"presets": ["babel/preset-env","babel/preset-react"],"plugins": ["babel/plugin-proposal-class-properties",["babel/plugin-transform-runtime",{"corejs&quo…

R | R及Rstudio安装、运行环境变量及RStudio配置

R | R及Rstudio安装、运行环境变量及RStudio配置 一、介绍1.1 R介绍1.2 RStudio介绍 二、R安装2.1 演示电脑系统2.2 R下载2.3 R安装2.4 R语言运行环境设置&#xff08;环境变量&#xff09;2.4.1 目的2.4.2 R-CMD测试2.4.3 设置环境变量 2.5 R安装测试 三、RStudio安装3.1 RStu…

Windows下conda安装pytorch GPU版

1.安装miniconda,不细讲了,自己去百度,miniconda自带python,可以通过conda创建虚拟python环境,安装Pytorch的话建议python版本大于3.8,完成后注意配置环境变量。 2.安装CUDA: 查看自己CUDA版本,Nvidia控制面板中找,不再赘述。根据查看的版本,下载 CUDA Toolkit并安装…

ChatGPT重磅升级:可以看图、听声音、说话啦!

美东时间9月25日&#xff0c;OpenAI在官网宣布&#xff0c;对ChatGPT进行重磅升级实现看图、听声音、输出语音内容三大功能。 早在今年3月OpenAI发布GPT-4模型时&#xff0c;就展示过看图的功能&#xff0c;但由于安全、功能不完善等原因一直没有开放。现在不仅开放了看图&…

《玩转smardaten | 无代码开发移动端APP需要几步?最全操作!》

看完这么多免编程、无代码APP软件开发的文章&#xff0c;还是一脸茫然&#x1f62e;...只讲能开发出什么玩意&#xff0c;不讲到底怎么开发&#xff0c;我怎么学会&#xff1f; 来了&#xff0c;无代码的移动端操作教程&#xff0c;从PC移动端一体式开发&#xff0c;到移动端单…

python+nodejs+php+springboot+vue 导师双选系统

为了直观显示系统的功能&#xff0c;运用用例图这样的工具显示分析的结果。分析的导师功能如下。导师管理导师选择信息&#xff0c;管理项目&#xff0c;管理项目提交并对学员提交的项目进行指导。 为了直观显示系统的功能&#xff0c;运用用例图这样的工具显示分析的结果。分析…