【Linux】套接字的理解 基于TCP协议的套接字编程(单/多进程 / 线程池|英汉互译 / C++)

news2024/11/18 18:17:00

文章目录

  • 1. 前言
    • 1.1 网络方面的预备知识👇
    • 1.2 了解TCP协议
  • 2. 关于套接字编程
    • 2.1 什么是套接字 Socket
    • 2.2 socket 的接口函数
    • 2.3 Udp套接字编程的步骤
    • 2.4 sockaddr 结构
  • 3. 代码实现
    • 3.1 makefile
    • 3.2 log.hpp
    • 3.3 tcp_server.hpp
      • ① 框架
      • ② service() 通信服务
      • ③ initServer()(初始化服务器)
      • ④ startServer()(启动服务器)
    • 3.4 tcp_server.cc
    • 3.5 tcp_client.cc
    • 3.6 结果演示
  • 4. 线程池版本(实现英汉互译)
    • 4.1 线程池ThreadPool的实现
      • Task.hpp
    • 4.2 对多进程代码的修改 - TcpServer
    • tcp_server.hpp
      • 框架
      • service() 通信服务
      • dictOnline()(英汉互译/网络词典)
      • initServer()(初始化服务器)
      • startServer()(启动服务器)
    • 结果演示
  • 5. 完整代码

1. 前言

1.1 网络方面的预备知识👇

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


1.2 了解TCP协议

首先我们对Tcp协议进行一个了解:

【网络基础】深入理解TCP协议:协议段、可靠性、各种机制

简单总结而言, TCP协议具有以下特点:

1.可靠传输:TCP通过三次握手建立连接,并使用序号、确认ACK和重传机制来确保数据的可靠性传输。
2.面向连接:在数据传输之前,TCP必须先建立连接,双方通过握手过程建立起通信信道。
3.数据流式传输:TCP将数据拆分成小块,并通过IP地址和端口号标识发送和接收方的数据包,保证数据的有序传输。
4.拥塞控制和流量控制:TCP具有拥塞控制和流量控制机制,通过调整发送方的发送速率和接收方的反馈机制来避免网络拥塞和资源浪费。


2. 关于套接字编程

2.1 什么是套接字 Socket

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

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


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

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

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

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

2.2 socket 的接口函数

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

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

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

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

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

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

2.3 Udp套接字编程的步骤

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

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

  3. 监听连接:对于服务器端,使用 listen() 开始监听连接请求。

  4. 接受连接:对于服务器端,使用 accept() 接受客户端的连接请求并创建新的套接字用于通信。

  5. 发送数据:使用 send() 函数发送数据到连接的另一端。

  6. 接收数据:使用 recv() 函数接收从连接的另一端发送过来的数据。

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


2.4 sockaddr 结构

首先:

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

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

sockaddr 结构定义如下:

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

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

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

3. 代码实现

3.1 makefile

首先写一个简单的makefile文件,用于后续执行程序便于测试

.PHONY:all
all:tcp_client tcp_server

tcp_client:tcp_client.cc
	g++ -o $@ $^ -std=c++11 #-lpthread
tcp_server:tcp_server.cc
	g++ -o $@ $^ -std=c++11

.PHONY:clean
clean:
	rm -f tcp_client tcp_server

3.2 log.hpp

在我们编写代码时,对于 异常情况报告或正常情况通知 ,利用log.hpp进行日志信息的记录:

#pragma once

#include <iostream>
#include <cstdio>
#include <cstdarg>

#include "log.hpp"

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

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

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

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

#ifndef DEBUG_SHOW // 将日志级别映射为对应的字符串
    if(level == DEBUG) return; // DEBUG_SHOW不存在 且 日志级别为 DEBUG时,返回
#endif
    // DEBUG_SHOW存在 则执行下面的日志信息 
    char stdBuffer[1024];
    time_t timestamp = time(nullptr);

    // 将日志级别和时间戳格式化后的字符串将会被写入到 stdBuffer 缓冲区中
    snprintf(stdBuffer, sizeof(stdBuffer), "[%s] [%ld] ", gLevelMap[level], timestamp);
    
    char logBuffer[1024];
    va_list args;`在这里插入代码片`
    va_start(args, format);

    vsnprintf(logBuffer, sizeof(logBuffer), format, args);
    va_end(args);

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

对于该日志类,不再重点,根据需要可以自行调整编写,不多讲解。

3.3 tcp_server.hpp

① 框架

tcp_server.hpp 即服务器类,首先是对于该服务器的 框架

  • 该框架包含了我们实现该类中会编写的 相关函数 以及 成员变量
// 通信服务的代码
static void service(int sock, const string& clientip, const int16_t& clientport){}

class TcpServer // tcp服务器类
{
private:
    const static int gbacklog = 20;

public:
    TcpServer(uint16_t port, string ip = ""): _port(port), _ip(ip), listensock(-1) // 设定缺省值
    {}

    // 初始化服务器
    void initServer()
    {}

	// 启动服务器
    void startServer()
    {}

    ~TcpServer(){}

private:
    uint16_t _port; // 端口号
    string _ip; // ip地址
    int listensock; // 套接字
};

② service() 通信服务

该代码用于 处理客户端与服务器间的通信:

  • 读取sock的内容:
    • 读取成功,则打印出客户端信息与发送的内容
    • 读取失败,打印日志并退出
  • 最后回显内容给客户端并关闭sock
static void service(int sock, const string& clientip, const int16_t& clientport)
{
    char buffer[1024];
    while(true)
    {
        // 网络通信 可以直接使用read/write
        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
        if(s > 0) {// 成功读取
            buffer[s] = 0;
            cout << clientip << " : " << clientport << "# " << buffer << endl;
        }else if (s == 0){ // 对端关闭了连接
            logMessage(NORMAL, "%s:%d shutdown, metoo", clientip.c_str(), clientport);
            break;
        }else{ // 错误
            logMessage(ERROR, "read socket error, %d:%s", errno, strerror(errno));
            break;
        }
        // 读取成功 将buffer内容写入sock(回显)
        write(sock, buffer, strlen(buffer));
    }
    close(sock);
}

③ initServer()(初始化服务器)

下面是 初始化服务器: 的代码,简单描述其步骤:

  1. socket()创建监听套接字
  2. bind()绑定客户端端口与ip
  3. listen()设置监听状态(因为tcp是面向连接的,需要先建立连接才能进行通信)
void initServer()
    {
        // 1. 创建socket —— 进程、文件方面
        listensock = socket(AF_INET, SOCK_STREAM, 0); // ipv4协议,套接字类型,协议类型
        if(listensock < 0) // 创建套接字失败
        {
            logMessage(FATAL, "%d : %s", errno, strerror(errno));
            exit(2); // 退出进程
        }
        logMessage(NORMAL, "create socket success, sock: %d", listensock); // 创建成功,输出信息

        // 2. bind —— 网络、文件方面
        struct sockaddr_in local; // 表示ipv4地址
        memset(&local, 0, sizeof(local)); // 初始化
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());

        // 3. 设置监听状态
        // TCP面向连接,正式通信前,应先建立连接
        if(listen(listensock, gbacklog) < 0)
        {
            logMessage(FATAL, "listen error: %d:%s", errno, strerror(errno));
            exit(4);
        }
        logMessage(NORMAL, "init server success.");
    }


④ startServer()(启动服务器)

下面是启动服务器的代码,简述其步骤:
首先下面代码分为 单进程与多进程 版本,在注释有标出

  1. 通过accept() 获取连接
  2. 获取连接成功后 进行双方的通信:
    • 对于单进程,提取客户端的ip与port后直接调用之前的service()即可
    • 对于多进程,首先通过fork创建子进程,进行功能分配。
  3. 从功能上讲:
    • 单进程版本
      • 单个进程负责监听套接字并处理连接请求,在处理连接的过程中,
      • 单个进程可能会阻塞在读取或写入数据的操作上,导致无法及时处理其他连接请求。
    • 多进程版本
      • 在父进程中负责监听套接字,并循环接收连接请求。
      • 每当有新的连接请求到来时,父进程会创建一个新的子进程来处理该连接。
      • 子进程独立于父进程,负责与客户端进行通信,父进程则继续监听新的连接请求。
      • 每个子进程都有自己的资源空间,因此可以独立处理连接,避免了单进程版本中可能出现的阻塞问题,提高了并发处理能力。
void startServer()
{
	//此signal : 多线程代码
    signal(SIGCHLD, SIG_IGN); // 忽略SIGCHLD信号
    while(true)
    {
        // 4. 获取连接
        struct sockaddr_in src;
        socklen_t len = sizeof(src);
        int servicesock = accept(listensock, (struct sockaddr*)&src, &len);
        if(servicesock < 0)
        {
            logMessage(ERROR, "accept error, %d:%s", errno, strerror(errno));
            continue;
        }
        // 获取连接成功
        uint16_t client_port = ntohs(src.sin_port);
        string client_ip = inet_ntoa(src.sin_addr);
        logMessage(NORMAL, "link success, servicesock: %d| %s : %d |\n", servicesock, client_ip.c_str(), client_port);
        // 开始通信:
        // v1: 单进程循环
        // service(servicesock, client_ip, client_port);

        // v2: 多进程
        // 子进程用于给新的连接提供服务(service),父进程继续执行循环接收连接
        pid_t id = fork();
        assert(id != -1);
        if(id == 0)
        {
            // 子进程: 用于提供服务,无需监听socket
            close(listensock);
            service(servicesock, client_ip, client_port);
            exit(0); // 此时僵尸状态
        }
        close(servicesock); // 父进程不提供服务
    }
}

3.4 tcp_server.cc

  • tcp_server.cc 用于 形成最后的可执行程序 ,在main函数中 初始化服务器并启动服务器即可
  • 对于TcpServer对象的创建,我们可以使用unique_ptr智能指针进行创建对象,用于在动态内存中分配对象,可以在不需要时自动释放内存。
  • 通过获取到的参数,创建对象
using std::cout;
using std::endl;

static void usage(string proc)
{
    cout << "\nUsage:" << proc << " port" << endl;
}

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

    uint16_t port = atoi(argv[1]); // 获取端口号
    // 智能指针创建 TcpServer对象
    std::unique_ptr<TcpServer> svr(new TcpServer(port));
    svr->initServer();
    svr->startServer();

    return 0; 
}

3.5 tcp_client.cc

  • 同理对于tcp_client.cc,首先获取传来的ip与端口号
  • 循环内:
    • 如果还未建立连接 创建套接字socksockaddr_in 结构体,后进行connect() 连接
    • 建立连接后 :持续读取用户输入的内容,并接收来自客户端的回显信息。
// 打印正确的程序使用方法
void Usage(std::string proc)
{
    std::cout << "\nUsage: " << proc << " <ip> <port>" << std::endl;
}

// ./tcp_client 192.168.1.100 8080
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }

    std::string serverIp = argv[1];
    uint16_t serverPort = atoi(argv[2]);
    
    bool connectAlive = false; // 是否已经建立了连接
    int sock = 0;
    while(true)
    {
        if(!connectAlive)
        {
            sock = socket(AF_INET, SOCK_STREAM, 0);
            if(sock < 0) { // 创建套接字失败
                std::cerr << "socket error" << std::endl;
                exit(2);
            }

            struct sockaddr_in server;
            memset(&server, 0, sizeof(server));
            server.sin_port = htons(serverPort);
            server.sin_family = AF_INET;
            server.sin_addr.s_addr = inet_addr(serverIp.c_str());

            if(connect(sock, (struct sockaddr*)&server, sizeof(server)) < 0)
            { // 连接失败
                std::cerr << "connect error: " << errno << " " << strerror(errno) << std::endl;
                exit(3);
            }
            // 建立连接成功,讲connectALive 设为true
            std::cout << "connect success." << std::endl;
            connectAlive = true;
        }

        std::cout << "请输入# " << std::endl;
        std::string line;
        std::getline(std::cin, line);
        if(line == "quit") break;

        ssize_t s = send(sock, line.c_str(), line.size(), 0);
        if(s > 0) 
        { // send 成功
            char buffer[1024];
            ssize_t r = recv(sock, buffer, sizeof(buffer), 0);
            if(r > 0) { // recv 成功
                buffer[r] = '\0';
                std::cout << "server response: " << buffer << std::endl;
            } else { // recv 失败
                std::cerr << "recv() error: client receiving message failed." << std::endl;
                close(sock);
                connectAlive = false;
            }
        } 
        else {
            std::cerr << "send() error: client sending message failed." << std::endl;
            close(sock);
        }
    }

    return 0;
}

3.6 结果演示

如下图所示,当启动客户端后,客户端会有一个监听套接字

此时我们可以有多个客户端同时进行连接通信,并且能正确的接收到来自服务器的回显信息。
在这里插入图片描述


4. 线程池版本(实现英汉互译)

4.1 线程池ThreadPool的实现

对于线程池版本,首先我们需要自实现一个线程池,有关线程池的详细内容/ 代码在下面👇:

ThreadPool代码实例 与 理解

  • 而线程池版本的套接字实现,我们需要 对上面链接代码部分进行修改
    • 需要根据要求(在这里即英汉互译)对Task.hpp(任务类)进行更改:

Task.hpp

对于任务类,由于我们通信时需要:套接字,对方的ip、端口号,以及处理方法,所以 对Task类的变量和成员函数进行修改 即可:

#pragma once

#include <iostream>
#include <string>
#include <functional>
#include "log.hpp"

// typedef std::function<int(int, int)> func_t;
using func_t = std::function<void(int, const std::string& ip, const uint16_t& port, const std::string& name)>;

class Task
{
public:
    // 构造
    Task(){}
    Task(int sock, const std::string ip, const uint16_t port, func_t func)
    :_sock(sock),_ip(ip),_port(port),_func(func)
    {}

    void operator()(const std::string& name)
    {
        _func(_sock, _ip, _port, name);
    }

private:
    int _sock;
    uint16_t _port;
    std::string _ip;
    func_t _func;
};

4.2 对多进程代码的修改 - TcpServer

由于我们将改为线程池的版本,所以这里我们对服务器的逻辑tcp_server.hpp作修改:

tcp_server.hpp

框架

  • 框架用于展示整个文件中包含的函数,以及类的实现,省略具体实现:
#pragma once

#include <iostream>
#include <string>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <assert.h>
#include <signal.h>
#include <memory>
#include <unordered_map>

#include "ThreadPool/threadPool.hpp"
#include "ThreadPool/log.hpp"
#include "ThreadPool/Task.hpp"

// 通信服务
static void service(int sock, const std::string& clientip, const uint16_t& clientport, const std::string& thread_name)
{}

// 英汉互译
static void dictOnline(int sock, const std::string& clientip, const uint16_t& clientport, const std::string& thread_name)
{}

class TcpServer
{
private:
    const static int gbacklog = 20;

public: 
    // 构造 + 析构
    TcpServer(const uint16_t& port, const std::string& ip = "0.0.0.0")
    : _ip(ip), _port(port),
      _listensock(-1), _threadpool_ptr(ThreadPool<Task>::getThreadPool())
    {}

    ~TcpServer()
    {}

    // 功能函数
    // 启动服务器
    void initServer()
    { }

    void startServer()
    { }

private:
    std::string _ip;
    uint16_t _port;
    int _listensock;
    std::unique_ptr<ThreadPool<Task>> _threadpool_ptr;
};

service() 通信服务

对于通信服务部分,总体和多进程版本无异,区别在于参数上 传入了线程名 ,可以在输出消息时加上线程名。

static void service(int sock, const std::string& clientip, const uint16_t& clientport, const std::string& thread_name)
{
    char buffer[1024];
    while(true)
    {
        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
        if(s > 0)
        {
            buffer[s] = 0;
            std::cout << thread_name << " | " << clientip << ":" << clientport << " | " << buffer << std::endl; 
        }
        else if (s == 0)
        {
            logMessage(DEBUG, "client quit, me too.");
            break;
        }
        else
        {
            logMessage(ERROR, "read error, me too.");
            break;
        }
        write(sock, buffer, strlen(buffer)); // 回写内容
    }
    close(sock);
}

dictOnline()(英汉互译/网络词典)

对于该部分,主要包含以下步骤:

  • 创建一个哈希类,作为词典用于对应中英文
  • 随后循环读取客户端的内容:
    • 如果可以在词典中找到,就记录该对应的词汇
    • 如果找不到,就打印错误信息
  • 最后将结果写回客户端
static void dictOnline(int sock, const std::string& clientip, const uint16_t& clientport, const std::string& thread_name)
{
    char buffer[1024];
    static std::unordered_map<std::string, std::string> dict = {
        {"hello", "你好"},
        {"world", "世界"},
        {"tcp", "传输控制协议"},
        {"udp", "用户数据报协议"}
    };
    // 添加4组数据
    dict["rpc"] = "远程过程调用";
    dict["ssh"] = "安全外壳";
    dict["http"] = "超文本传输协议";
    dict["https"] = "超文本传输协议安全";

    while(true)
    {
        ssize_t s = read(sock, buffer, sizeof(buffer) - 1);
        if(s > 0)
        {
            buffer[s] = 0;
            std::string message;
            auto iter = dict.find(buffer);
            if(iter == dict.end()) message = "not found:(";
            else message = iter->second;
            std::cout << thread_name << " | " << clientip << ":" << clientport << " | " << buffer << " | " << message << std::endl;
            write(sock, message.c_str(), message.size()); // 将结果写回
        }
        else if (s == 0)
        {
            logMessage(DEBUG, "client quit, me too.");
            break;
        }
        else
        {
            logMessage(ERROR, "read error | %d : %s", errno, strerror(errno));
            break;
        } 
    }
    close(sock);
}

initServer()(初始化服务器)

初始化服务器的代码与多进程版本一致:

void initServer()
    {
        // 创建listensock
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if(_listensock < 0) {
            logMessage(ERROR, "socket error, %d : %s", errno, strerror(errno));
            exit(1);
        }

        // 绑定
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = inet_addr(_ip.c_str());
        if(bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            logMessage(ERROR, "bind error, %d : %s", errno, strerror(errno));
            exit(2);
        }


        // 监听
        if(listen(_listensock, gbacklog) < 0)
        {
            logMessage(ERROR, "listen error, %d : %s", errno, strerror(errno));
            exit(3);
        }

        logMessage(NORMAL, "init server success.");
    }

startServer()(启动服务器)

  • 对于启动服务器,首先需要 运行线程池 ,在正确建立连接后,将客户端的ip端口号与英汉互译的功能一同 传入Task对象
  • 最后将任务类添加到线程池中
void startServer()
{
    // 使用线程池
    _threadpool_ptr->run();
    while(true)
    {
        // 等待客户端连接
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int sock = accept(_listensock, (struct sockaddr*)&peer, &len);
        if(sock < 0)
        {
            logMessage(ERROR, "accept error, %d : %s", errno, strerror(errno));
            continue;
        }
        // 成功获取连接
        uint16_t client_port = ntohs(peer.sin_port);
        std::string client_ip = inet_ntoa(peer.sin_addr);
        logMessage(NORMAL, "get a new client, ip: %s, port: %d", client_ip.c_str(), client_port);
        // 创建线程
        Task t(sock, client_ip, client_port, dictOnline);
        _threadpool_ptr->pushTask(t); // 添加 任务
    }
}

结果演示

通过下图,可以看出,启动服务器后,服务器可以接收多个客户端的信息,并正确发出反馈。

在这里插入图片描述


5. 完整代码

上述关于Tcp套接字通信的完整代码在👇:

Tcp套接字编程 - 实例代码

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

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

相关文章

HTML静态网页成品作业(HTML+CSS)——利物浦足球俱乐部介绍网页设计制作(5个页面)

&#x1f389;不定期分享源码&#xff0c;关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 &#x1f3f7;️本套采用HTMLCSS&#xff0c;共有5个页面。 二、作品演示 三、代码目录 四、网站代码 HTML部分代…

【webrtc】内置opus解码器的移植

m98 ,不知道是什么版本的opus,之前的交叉编译构建: 【mia】ffmpeg + opus 交叉编译 【mia】ubuntu22.04 : mingw:编译ffmpeg支持opus编解码 看起来是opus是1.3.1 只需要移植libopus和opus的webrtc解码部分即可。 linux构建的windows可运行的opus库 G:\NDDEV\aliply-0.4\C…

0407放大电路的频率响应

放大电路的频率响应 单时间常数RC电路的频率响应中频响应高频响应低频响应全频域响应 放大电路频率响应概述1. 直接耦合放大电路频域响应阻容耦合放大电路频域响应 4.7.1 单时间常数RC电路的频率响应 4.7.2 放大电路频率响应概述 4.7.3 单级共射极放大电路的频率响应 4.7.4 单级…

51单片机-数码管显示多个

书接上回 51单片机-数码管显示单个 http://t.csdnimg.cn/Ii6x0 一.简单全亮 ps:有好几次没电亮,是没加阻值,看了这方面的基础要好好补补了. 这种是不需要控制哪个亮,全部接了电.直接用上一篇的代码,就可以每个都动态变化 #include <reg52.h> #include <intrins.h…

aws eks集成wasm运行时并启动pod

参考资料 WebAssembly 在云原生中的实践指南&#xff0c;https://cloud.tencent.com/developer/article/2324065 作为一种通用字节码技术&#xff0c;wasm的初衷是在浏览器中的程序实现原生应用性能。高级语言将wasm作为目标语言进行编译并运行在wasm解释器中。和nodejs类似的…

Unity Render Streaming 云渲染 外网访问

初版&#xff1a; 日期&#xff1a;2024.5.20 前言&#xff1a;临时思路整理&#xff0c;后期会详细补充 环境&#xff1a; 1. 阿里云服务器 需要安装好nodejs 、npm 2. windows电脑&#xff0c;需安装好 nodejs 、npm 3.Unity 2021.3.15f1 4.Unity Render Streaming …

景源畅信电商:抖店怎么提高店铺的权重?

在竞争激烈的电商市场&#xff0c;如何提升抖店店铺权重成为商家关注的焦点。店铺权重是决定商品搜索排名的关键因素之一&#xff0c;直接关系到店铺流量和销量。提高店铺权重并非一蹴而就&#xff0c;而是一个系统工程&#xff0c;需要从多个维度着手优化。 一、优化商品标题和…

Dubbo生态之dubbo功能

1.Dubbo生态功能的思考 dubbo具有哪些功能呢&#xff1f;我们要根据dubbo的架构和本质是用来干什么的来思考? 首先对于分布式微服务&#xff0c;假设我们有两个服务A和B&#xff0c;并且都是集群部署的。那么按照我们正常的流程应该是启动两个微服务项目&#xff08;启动时应该…

大模型落地竞逐,云计算大厂“百舸争流”

作者 | 辰纹 来源 | 洞见新研社 从ChatGPT到Sora&#xff0c;从图文到视频&#xff0c;从通用大模型到垂直大模型……经过了1年多时间的探索&#xff0c;大模型进入到以落地为先的第二阶段。 行业的躁动与资本的狂热相交汇&#xff0c;既造就了信仰派的脚踏实地&#xff0c;也…

二十五篇:嵌入式系统揭秘:基础理论与实践探索

嵌入式系统揭秘&#xff1a;基础理论与实践探索 1. 嵌入式系统的定义与特性 1.1 详细解释 嵌入式系统&#xff0c;作为一种特殊的计算机系统&#xff0c;其设计目的是为了执行一个或多个特定的功能。与通用计算机相比&#xff0c;嵌入式系统通常被集成到更大的设备中&#xf…

web压力测试,要不要过滤掉JS,CSS等请求?

在进行性能测试&#xff08;压测&#xff09;时&#xff0c;是否过滤掉对JavaScript、CSS等静态资源的请求&#xff0c;取决于你测试的目标和目的。 是测试服务端的性能还是前端的性能。这两种目的所涉及到的测试场景和工具等方法是不一样的。 一般的web产品&#xff0c;像cs…

dify:开源 LLMOps平台。

单纯笔记&#xff1a; 一、关于 Dify dify/README_CN.md at main langgenius/dify GitHub Dify 是一款开源的大语言模型&#xff08;LLM&#xff09;应用开发平台。它融合了后端即服务&#xff08;Backend as Service&#xff09;和 LLMOps 的理念&#xff0c;使开发者可以…

vite+ts+mock+vue-router+pinia实现vue的路由权限

0.权限管理 前端的权限管理主要分为如下&#xff1a; 接口权限路由权限菜单权限按钮权限 权限是对特定资源的访问许可&#xff0c;所谓权限控制&#xff0c;也就是确保用户只能访问到被分配的资源 1.项目搭建 创建vite项目 yarn create vite配置别名 npm install path -…

不用从头训练,通过知识融合创建强大的统一模型

在自然语言处理&#xff08;NLP&#xff09;领域&#xff0c;大型语言模型&#xff08;LLMs&#xff09;的开发和训练是一个复杂且成本高昂的过程。数据需求是一个主要问题&#xff0c;因为训练这些模型需要大量的标注数据来保证其准确性和泛化能力&#xff1b;计算资源也是一个…

关于基础的流量分析(1)

1.对于流量分析基本认识 1&#xff09;简介&#xff1a;网络流量分析是指捕捉网络中流动的数据包&#xff0c;并通过查看包内部数据以及进行相关的协议、流量分析、统计等来发现网络运行过程中出现的问题。 2&#xff09;在我们平时的考核和CTF比赛中&#xff0c;基本每次都有…

【Linux取经路】进程通信——共享内存

文章目录 一、直接原理1.1 共享内存的的申请1.2 共享内存的释放 二、代码演示2.1 shmget2.1.1 详谈key——ftok 2.2 创建共享内存样例代码2.3 获取共享内存——进一步封装2.4 共享内存挂接——shmat2.5 共享内存去关联——shmdt2.6 释放共享内存——shmctl2.7 开始通信2.7.1 pr…

安全攻防基础

一、安全是什么&#xff1f;就是三个基础原则 安全就是保护数据 1. 机密性 对未授权的主体不可见 开发人员不能拥有敏感数据的访问权限 密钥要复杂 显示器伤的数据被别有用心的人窥探 2. 完整性 没授权的人不可修改数据 3. 可用性 被授权的主体可读 二、如何解决安全问题…

Rust面试宝典第14题:旋转数组

题目 给定一个数组&#xff0c;将数组中的元素向右移动k个位置&#xff0c;其中k是非负数。要求如下&#xff1a; &#xff08;1&#xff09;尽可能想出更多的解决方案&#xff0c;至少有三种不同的方法可以解决这个问题。 &#xff08;2&#xff09;使用时间复杂度为O(n)和空间…

微服务远程调用 RestTemplate

Spring给我们提供了一个RestTemplate的API&#xff0c;可以方便的实现Http请求的发送。 同步客户端执行HTTP请求&#xff0c;在底层HTTP客户端库(如JDK HttpURLConnection、Apache HttpComponents等)上公开一个简单的模板方法API。RestTemplate通过HTTP方法为常见场景提供了模…

VisualStudio2022的使用

Visual Studio 2022 的安装、卸载和使用方法详解 一、安装Visual Studio 2022 1. 下载Visual Studio 2022 要安装Visual Studio 2022&#xff0c;需要先下载安装程序。可以从微软的官方网站&#xff08;Visual Studio下载页面&#xff09;下载免费的社区版&#xff08;Commun…