网络编程套接字——实现简单的UDP网络程序

目录

1、预备知识

1.1、认识端口号

1.2、端口号 vs 进程pid

1.3、认识TCP协议

1.4、认识UDP协议

1.5、网络字节序

2、socket编程接口

2.1、socket常见API

2.2、sockaddr结构

3、实现一个简易的UDP服务器和客户端通信

log.hpp

UdpServer.hpp

UdpClient.cc

Main.cc

makefile


1、预备知识

1.1、认识端口号

首先我们先来解决一个问题,在进行网络通信的时候,是不是我们的两台机器在进行通信呢?

答案肯定是不是的,在网络协议中的下三层,主要解决的是,数据安全可靠的运送到远端机器,但是使我们的用户使用应用层软件,完成数据发送和接收的,软件对应的自然就是进程,所以,我们日常网络通信的本质:就是进程间通信!

那么问题又来了,我们两台主机在进行通信时,传输层的上层应用层不止有一个应用软件,该如何区分呢?用的就是端口号。

在公网上,IP地址能表示唯一的一台主机,端口号port,用来标识该主机上的唯一的一个进程,所以:IP:Port = 标识全网唯一的一个进程。

那么我们来总结一下端口号的特性;

· 端口号是一个2字节16位的整数

· 端口号用来标识一个进程,告诉操作系统,当前这个数据要交给哪个进程处理

· IP地址+端口号能够标识网络上的某一台主机的某一个进程

· 一个端口号只能被一个进程占用

1.2、端口号 vs 进程pid

pid已经能够标识一台主机上进程的唯一性了,为什么还要搞一个端口号?

这个问题也很简单,因为网络是晚于操作系统的,而操作系统的每个进程都需要有pid,但是却不失所有的进程都要网络通信,另外,也是追求软件工程低耦合高内聚思想,创建一个端口号,实现了系统和网络功能的解耦。

再做一点小小的扩充,我们的客户端是如何知道服务器的端口号是多少的?

答:每一个服务的端口号都必须是众所周知,精心设计,被客户端知晓的。

那么这该如何实现呢?

如图所示,其实在传输层,有一张哈希表存储端口号,而进程会绑定哈希表中的端口号,这样就实现了端口号的要求,但是这里还有一些细节:

一个进程可以绑定多个端口号,但是一个端口号不可以被多个进程绑定,如果被多个进程绑定就会让端口号无法决定该将信息发送给哪个进程!

1.3、认识TCP协议

此处我们先对TCP(Transmission Control Protocol 传输控制协议)有一个直观的认识,后面再讨论TCP的细节问题。

· 传输层协议

· 有连接

· 可靠传输

· 面向字节流

1.4、认识UDP协议

此处也是对UDP(User Datagram Protocol 用户数据协议)有一个直观的认识,细节后面讨论。

· 传输层协议

· 无连接

· 不可靠传输

· 面向数据报

1.5、网络字节序

我们已经知道,内存中的多字节数据相对于内存地址有大端和小端之分,磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分,网络数据流同样有大端小端之分。那么如何定义网络数据流的地址呢?

· 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出。
· 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存。
· 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址。
· TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节。
· 不管这台主机是大端机还是小端机,都会按照这个TCP/IP规定的网络字节序来发送/接收数据。
· 如果当前发送主机是小端,就需要先将数据转成大端。否则就忽略,直接发送即可。

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

#include <arpa/inet.h>

uint32_t htonl(uint32_t hostlong);

uint16_t htons(uint16_t hostshort);

uint32_t ntohl(uint32_t netlong);

uint16_t ntohs(uint16_t netshort);

这些函数名很好记,h表示host,n表示network,l表示32位长整数,s表示16位短整数。

例如htonl表示将32位的长整数从主机字节序转换为网络字节序,例如将IP地址转换后准备发送。

如果主机是小端字节序,这些函数将参数做相应的大小端转换然后返回;

如果主机是大端字节序,这些函数不做转换,将参数原封不动地返回。

2、socket编程接口

2.1、socket常见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.2、sockaddr结构

socket API是一层抽象的网络编程接口,适用于各种底层网络协议,如IPv4、IPv6,然而,各种网络协议的地址格式并不相同。

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


3、实现一个简易的UDP服务器和客户端通信

log.hpp

#pragma once

#include <iostream>
#include <time.h>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define SIZE 1024

#define Info 0
#define Debug 1
#define Warning 2
#define Error 3
#define Fatal 4

#define Screen 1
#define Onefile 2
#define Classfile 3

#define LogFile "log.txt"

class Log
{
public:
    Log()
    {
        printMethod = Screen;
        path = "./log/";
    }
    void Enable(int method)
    {
        printMethod = method;
    }
    std::string levelToString(int level)
    {
        switch (level)
        {
        case Info:
            return "Info";
        case Debug:
            return "Debug";
        case Warning:
            return "Warning";
        case Error:
            return "Error";
        case Fatal:
            return "Fatal";
        default:
            return "None";
        }
    }

    // void logmessage(int level, const char *format, ...)
    // {
    //     time_t t = time(nullptr);
    //     struct tm *ctime = localtime(&t);
    //     char leftbuffer[SIZE];
    //     snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
    //              ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
    //              ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

    //     // va_list s;
    //     // va_start(s, format);
    //     char rightbuffer[SIZE];
    //     vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
    //     // va_end(s);

    //     // 格式:默认部分+自定义部分
    //     char logtxt[SIZE * 2];
    //     snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);

    //     // printf("%s", logtxt); // 暂时打印
    //     printLog(level, logtxt);
    // }
    void printLog(int level, const std::string &logtxt)
    {
        switch (printMethod)
        {
        case Screen:
            std::cout << logtxt << std::endl;
            break;
        case Onefile:
            printOneFile(LogFile, logtxt);
            break;
        case Classfile:
            printClassFile(level, logtxt);
            break;
        default:
            break;
        }
    }
    void printOneFile(const std::string &logname, const std::string &logtxt)
    {
        std::string _logname = path + logname;
        int fd = open(_logname.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666); // "log.txt"
        if (fd < 0)
            return;
        write(fd, logtxt.c_str(), logtxt.size());
        close(fd);
    }
    void printClassFile(int level, const std::string &logtxt)
    {
        std::string filename = LogFile;
        filename += ".";
        filename += levelToString(level); // "log.txt.Debug/Warning/Fatal"
        printOneFile(filename, logtxt);
    }

    ~Log()
    {
    }
    void operator()(int level, const char *format, ...)
    {
        time_t t = time(nullptr);
        struct tm *ctime = localtime(&t);
        char leftbuffer[SIZE];
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),
                 ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,
                 ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

        va_list s;
        va_start(s, format);
        char rightbuffer[SIZE];
        vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);
        va_end(s);

        // 格式:默认部分+自定义部分
        char logtxt[SIZE * 2];
        snprintf(logtxt, sizeof(logtxt), "%s %s", leftbuffer, rightbuffer);

        // printf("%s", logtxt); // 暂时打印
        printLog(level, logtxt);
    }

private:
    int printMethod;
    std::string path;
};

// int sum(int n, ...)
// {
//     va_list s; // char*
//     va_start(s, n);

//     int sum = 0;
//     while(n)
//     {
//         sum += va_arg(s, int); // printf("hello %d, hello %s, hello %c, hello %d,", 1, "hello", 'c', 123);
//         n--;
//     }

//     va_end(s); //s = NULL
//     return sum;
// }

UdpServer.hpp

#pragma once

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

typedef std::function<std::string(const std::string &, const std::string&, uint16_t)> func_t;

Log log;

enum{
    SOCKET_ERR=1,
    BIND_ERR=2
};

uint16_t defaultport = 8080;
std::string defaultip = "0.0.0.0";
const int size = 1024;

class UdpServer{
public:
    UdpServer(const uint16_t &port = defaultport, const std::string &ip = defaultip):sockfd_(0), port_(port), ip_(ip), isrunning_(false)
    {}
    void Init()
    {
        // 1. 创建udp socket
        //  Udp的socket是全双工的,允许被同时读写的
        sockfd_ = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd_ < 0)
        {
            log(Fatal, "socket creat error, sockfd: %d", sockfd_);
            exit(SOCKET_ERR);
        }
        log(Info, "socket create success, sockfd: %d", sockfd_);
        // 2. bind socket
        struct sockaddr_in local;
        bzero(&local, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_); //需要保证我的端口号是网络字节序列,因为该端口号是要给对方发送的
        local.sin_addr.s_addr = inet_addr(ip_.c_str()); //1. string -> uint32_t 2. uint32_t必须是网络序列的 // ??
        //local.sin_addr.s_addr = htonl(INADDR_ANY);

        int n = bind(sockfd_, (const struct sockaddr *)&local, sizeof(local));
        if(n < 0)
        {
            log(Fatal, "bind error, errno: %d, err string: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
        log(Info, "bind success, errno: %d, err string: %s", errno, strerror(errno));
    }

    void CheckUser(const struct sockaddr_in &client, const std::string clientip, uint16_t clientport)
    {
        auto iter = online_user_.find(clientip);
        if(iter == online_user_.end())
        {
            online_user_.insert({clientip, client});
            std::cout << "[" << clientip << ":" << clientport << "] add to online user. " << std::endl;
        }
    }
    
    void Broadcast(const std::string&info, const std::string clientip, uint16_t clientport)
    {
        for(const auto &user : online_user_)
        {
            std::string message = "[";
            message += clientip;
            message += ":";
            message += std::to_string(clientport);
            message += "]# ";
            message += info;
            socklen_t len = sizeof(user.second);
            sendto(sockfd_, message.c_str(), message.size(), 0, (struct sockaddr*)(&user.second), len);
        }
     }

    void Run() //对代码进行分层
    {
        isrunning_ = true;
        char inbuffer[size];
        while(isrunning_)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            ssize_t n = recvfrom(sockfd_, inbuffer, sizeof(inbuffer) - 1, 0,  (struct sockaddr*)&client, &len);
            if (n < 0)
            {
                log(Warning, "recvfrom error, errno: %d, err string: %s", errno, strerror(errno));
                continue;
            }

            uint16_t clientport = ntohs(client.sin_port);
            std::string clientip = inet_ntoa(client.sin_addr);
            CheckUser(client, clientip, clientport);

            std::string info = inbuffer;
            Broadcast(info, clientip, clientport);
         
        }
    }
    ~UdpServer()
    {
        if(sockfd_ > 0) close(sockfd_);
    }

private:
    int sockfd_;      // 网络文件描述符
    std::string ip_; // 任意地址bind 0
    uint16_t port_;   // 表明服务器进程的端口号
    bool isrunning_;
    std::unordered_map<std::string, struct sockaddr_in> online_user_;
};

UdpClient.cc

#include <iostream>
#include <unistd.h>
#include <string.h>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include "Terminal.hpp"

using namespace std;

void Usage(std::string proc)
{
    std::cout << "\n\tUsage:" << proc << "serverip serverport\n" << std::endl;
}

struct ThreadData
{
    struct sockaddr_in server;
    int sockfd;
    std::string serverip;
};

void *recv_message(void *args)
{
    //OpenTerminal();
    ThreadData *td = static_cast<ThreadData *>(args);
    char buffer[1024];
    while(true)
    {
        memset(buffer, 0, sizeof(nuffer));
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);

        ssize_t s = recvfrom(td->sockfd, buffer, 1023, 0, (struct sockaddr*)&temp, &len);
        if(s > 0)
        {
            buffer[s] = 0;
            cerr << buffer << endl;
        }
    }
}

void *send_message(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    socklen_t len = sizeof(td->server);
    string message;

    std::string welcome = td->serverip;
    welcome += "coming...";
    sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&(td->server), len);

    while(true)
    {
         cout << "Please Enter@ ";
        getline(cin, message);

     
        // 1. 数据 2. 给谁发
        sendto(td->sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&(td->server), len);
    }
}

//多线程
// ./udpclient serverip serverport
int main(int argc, char *argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    std::string serverip = argv[1];
    uint16_t serverport = std::stoi(argv[2]);

    struct ThreadData td;

    bzero(&td.server, sizeof(td.server));
    td.server.sin_family = AF_INET;
    td.server.sin_port = htons(serverport);
    td.server.sin_addr.s_addr = inet_addr(serverip.c_str());

    td.sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (td.sockfd < 0)
    {
        std::cout << "socker error" << endl;
        return 1;
    }

    td.serverip = serverip;

    // client 要bind吗?要!只不过不需要用户显式的bind!一般由OS自由随机选择!
    // 一个端口号只能被一个进程bind,对server是如此,对于client,也是如此!
    // 其实client的port是多少,其实不重要,只要保证主机上的唯一性就可以!
    // 系统什么时候给我bind呢?首次发送数据的时候
    pthread_t recvr, sender;
    pthread_create(&recvr, nullptr, recv_message, &td);
    pthread_create(&sender, nullptr, send_message, &td);

    pthread_join(recvr, nullptr);
    pthread_join(sender, nullptr);

    close(td.sockfd);
    return 0;
}

Main.cc

#include "UdpServer.hpp"
#include <memory>
#include <cstdio>
#include <vector>

void Usage(std::string proc)
{
    std::cout << "\n\tUsage:" << proc << "port[1024]\n" << std::endl;
}


// ./udpserver port
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }

    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<UdpServer> svr(new UdpServer(port));

    svr->Init(/**/);
    svr->Run();

    return 0;
}

makefile

.PHONY:all
all:udpserver udpclient

udpserver:Main.cc
	g++ -o $@ $^ -std=c++11
udpclient:UdpClient.cc 
	g++ -o $@ $^ -lpthread -std=c++11

.PHONY:clean
clean:
	rm -f udpserver udpclient

这里实现了一个小型的聊天室

所有人发的聊天消息都会显示在客户端。

每有一位新用户,服务器就会显示添加信息。

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

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

相关文章

【Maven入门篇】(2)IDEA集成Maven环境的具体操作

&#x1f38a;专栏【Maven入门篇】 &#x1f354;喜欢的诗句&#xff1a;更喜岷山千里雪 三军过后尽开颜。 &#x1f386;音乐分享【The truth that you leave】 &#x1f970;欢迎并且感谢大家指出我的问题 文章目录 &#x1f354;配置Maven环境⭐方法一&#xff08;当前工程&…

EPICS和Arduino Uno之间基于串行文本协议的控制开发

Arduino Uno的串口服务程序设置如文本的串口通信协议设计以及在Arduino上的应用-CSDN博客中所示。通过在串口上发送约定的文本协议&#xff0c;它实现的功能如下&#xff1a; 实现功能&#xff1a; 读取三路0.0V~5.0V模拟量输入&#xff0c;读取端口A0~A2设置三路0.0V~5.0V的模…

光伏电站信息化管理系统如何优化能源管理

随着可再生能源的快速发展&#xff0c;光伏电站作为其中的重要组成部分&#xff0c;其运营管理面临着越来越多的挑战。为了提升光伏电站的能源管理效率&#xff0c;信息化管理系统成为了不可或缺的工具&#xff0c;那么如何优化能源管理呢&#xff1f; 1.数据实时监控 信息化管…

一站式解决方案:uni-app条件编译及多环境配置,appid动态修改攻略!

前言 这篇文章主要介绍uniapp在Hbuilderx 中&#xff0c;通过工程化&#xff0c;区分不同环境、动态修改小程序appid以及自定义条件编译&#xff0c;解决代码发布和运行时手动切换到问题。 背景 在企业级的应用中&#xff0c;通常会分为&#xff0c;开发、联调、生产等多个环…

GPT-1, GPT-2, GPT-3, InstructGPT / ChatGPT and GPT-4 总结

1. GPT-1 What the problem GPT-1 solve? 在 GPT-1 之前&#xff0c;NLP 通常是一种监督模型。 对于每个任务&#xff0c;都有一些标记数据&#xff0c;然后根据这些标记数据开发监督模型。 这种方法存在几个问题&#xff1a;首先&#xff0c;需要标记数据。 但 NLP 不像 CV&…

从嵌套事务的日志看MyBatis的sqlSession生命周期

service层业务代码 Override public void test(){QueryWrapper<StoreRebateCalculateLog> queryWrapper;queryWrapper new QueryWrapper<>();queryWrapper.eq("delete_flag", 0);//执行查询A,A事务开启List<StoreRebateCalculateLog> storeRebat…

区块链推广海外市场怎么做,CloudNEO服务商免费为您定制个性化营销方案

随着区块链技术的不断发展和应用场景的扩大&#xff0c;区块链项目希望能够进入海外市场并取得成功已成为越来越多公司的目标之一。然而&#xff0c;要在海外市场推广区块链项目&#xff0c;需要采取有效的营销策略和措施。作为您的区块链项目营销服务商&#xff0c;CloudNEO将…

深度学习——SAM(Segment-Anything)代码详解

目录 引言代码目录segment-anything 代码详解build_sam.pypredictor.pyautomatic_mask_generator.py 引言 从去年年初至今&#xff0c;SAM(Segment Anything )已经问世快一年了&#xff0c;SAM凭借其强大而突出的泛化性能在各项任务上取得了优异的表现&#xff0c;广大的研究者…

详解MySql索引

目录 一 、概念 二、使用场景 三、索引使用 四、索引存在问题 五、命中索引问题 六、索引执行原理 一 、概念 索引是一种特殊的文件&#xff0c;包含着对数据表里所有记录的引用指针。暂时可以理解成C语言的指针,文章后面详解 二、使用场景 数据量较大&#xff0c;且…

【图像分割】使用Otsu 算法及迭代计算最佳全局阈值估计并实现图像分割(代码实现与分析)

本实验要求理解全局阈值分割的概念&#xff0c;并实现文本图像分割。需要大家深入理解Ostu 算法的实现过程及其迭代原理&#xff0c;同时通过学习使用Otsu 算法及其迭代&#xff0c;实践图像分割技术在文本图像处理中的应用。 以下将从实验原理、实验实现、实验结果分析三部分对…

老阳视频号带货项目,究竟是一个怎样的选择呢?

近年来&#xff0c;随着网络技术的飞速发展&#xff0c;直播带货已经成为电商行业的新宠。其中&#xff0c;网红老阳以其独特的风格和专业度&#xff0c;成功吸引了大量粉丝的关注&#xff0c;并带动了一波视频号带货的热潮。那么&#xff0c;现在跟随老阳的步伐&#xff0c;投…

sqllab第二十七A关通关笔记

知识点&#xff1a; 双引号闭合union select 大小写绕过 Union Select这里不能进行错误注入&#xff0c;无回显 经过测试发现这是一个双引号闭合 构造payload:id1"%09and%091"1 页面成功回显 构造payload:id0"%09uNion%09SElect%091,2,3%09"1 页面成功…

AI论文速读 | TPLLM:基于预训练语言模型的交通预测框架

论文标题&#xff1a;TPLLM: A Traffic Prediction Framework Based on Pretrained Large Language Models 作者&#xff1a;Yilong Ren&#xff08;任毅龙&#xff09;, Yue Chen, Shuai Liu, Boyue Wang&#xff08;王博岳&#xff09;,Haiyang Yu&#xff08;于海洋&#x…

Mysql 索引、锁与MVCC等相关知识点

文章目录 Mysql锁的类型锁使用MVCC快照读和当前读读视图【Read View】串行化的解决 索引类型存储方式区分逻辑区分实际使用区分索引失效情况 索引建立规范SQL编写规范exlpain字段解析ACID的原理日志引擎慢SQL整合SpringBoot博客记录 Mysql锁的类型 MySQL中有哪些锁&#xff1a…

2024-3-13,14(CSS)

1.复合选择器 有两个或者多个基础选择器&#xff0c;通过不同的方式组合而成。 目的是更加准确高效的选择目标元素&#xff08;标签&#xff09; 分类&#xff1a; 后代选择器&#xff1a;选中某个元素的所有后代元素 写法&#xff1a;父选择器 子选择器 {CSS属性}&#x…

软考高级:软件工程单元测试(驱动模块、被测模块、桩模块)概念和例题

作者&#xff1a;明明如月学长&#xff0c; CSDN 博客专家&#xff0c;大厂高级 Java 工程师&#xff0c;《性能优化方法论》作者、《解锁大厂思维&#xff1a;剖析《阿里巴巴Java开发手册》》、《再学经典&#xff1a;《Effective Java》独家解析》专栏作者。 热门文章推荐&am…

爱奇艺 CTR 场景下的 GPU 推理性能优化

01 背景介绍 GPU 目前大量应用在了爱奇艺深度学习平台上。GPU 拥有成百上千个处理核心&#xff0c;能够并行的执行大量指令&#xff0c;非常适合用来做深度学习相关的计算。在 CV&#xff08;计算机视觉&#xff09;&#xff0c;NLP&#xff08;自然语言处理&#xff09;的模型…

Spring炼气之路(炼气一层)

目录 一、IOC 1.1 控制反转是什么&#xff1f; 1.2 什么是IOC容器&#xff1f; 1.3 IOC容器的作用 1.4 IOC容器存放的是什么&#xff1f; 二、DI 2.1 依赖注入是什么&#xff1f; 2.2 依赖注入的作用 三、IOC案例实现 3.1下载Maven 3.2 配置Maven中的settings.xml文…

考研C语言复习进阶(2)

目录 1. 字符指针 2. 指针数组 3. 数组指针 3.1 数组指针的定义 3.2 &数组名VS数组名 4. 函数指针 5. 函数指针数组 6. 指向函数指针数组的指针 7. 回调函数 8.三步辗转法 9. 指针和数组笔试题解析 10. 指针笔试题 指针的主题&#xff0c;我们在初级阶段的《指…

​​SQLiteC/C++接口详细介绍之sqlite3类(十一)

返回目录&#xff1a;SQLite—免费开源数据库系列文章目录 上一篇&#xff1a;​​SQLiteC/C接口详细介绍之sqlite3类&#xff08;十&#xff09; 下一篇&#xff1a;​​SQLiteC/C接口详细介绍之sqlite3类&#xff08;十二&#xff09;&#xff08;未发表&#xff09; 33.sq…