当前位置: 首页 > news >正文

Linux网络套接字

Socket 编程 UDP

本章将函数介绍和代码编写实战一起使用。
IPv4 的 socket 网络编程,sockaddr_in 中的成员 struct in_addr.sin_addr 表示 32 位 的 IP 地址
但是我们通常用点分十进制的字符串表示 IP 地址,以下函数可以在字符串表示和in_addr 表示之间转换;
字符串转 in_addr 的函数:

#include <arpa/inet.h>
int inet_aton(const char:*strptr,struct inaddr *addrptr);
int_addr_t inet_addr(const char *strptr);
int inet_pton(int family,const char *strptr,void *addrptr);

in_addr 转字符串的函数:

char *inet_ntoa(struct in addrinaddr);
const char *inet_ntop(int family,const void *addrptr, char *strptr,size t len);

查看OS所有UDP和进程信息:

netstat -naup

这个函数是创建一个套接字:

int socket(int domain, int type,int protocol);
第一个参数是套接字的域,就是确定是ipv4还是ipv6等待。
第二个是套接字的数据流类型。
第三个参数是协议类型。
返回值是:返回成功返回一个文件操作符,失败返回-1。
其实socket也就相当于创建了一个文件。

在这里插入图片描述
这个函数是绑定套接字:

int bind(int sockfd, const struct sockaddr *addr,socklen_t addrlen);
第一个参数是网络文件描述符
第二个参数是用哪一种套接字(让绑定过来的套接字实现哪一种功能)

在这里插入图片描述

将指定内存全部初始化为0的函数:

void bzero(void *s, size_t n);
第一个参数是传地址
第二个参数是缓冲区的大小

在这里插入图片描述

in_addr_t inet_addr(const char *cp);
让字符串转换成网络ip风格的四字节

在这里插入图片描述

接收信息函数:

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,struct sockaddr *src_addr, socklen_t *addrlen);
第二个参数为接收容器
第三个参数为信息长度
第四个参数为设置阻塞与非阻塞
第六个参数为套接字种类的长度

在这里插入图片描述
发送信息函数:

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,const struct sockaddr *dest_addr, socklen_t addrlen);
参数和上一个函数差不多,只有最后一个参数是不需要取地址的

在这里插入图片描述

首先有一个代码的预备工作,实现一个日志附带文件打印功能的代码

//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 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;
};
Log log;

模拟服务器

#include "udpserver.hpp"
#pragma once
#include <memory>
#include <cstring>
#include <sys/types.h>          
#include <sys/socket.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
using namespace std;

extern Log log;//声明变量log
enum{
    SOCKET_ERR = 1,//套接字创建失败
    BIND_ERR
};

uint16_t defaultport = 8080;//默认端口
//绑定端口号的时候要注意,很多端口都是被应用层协议固定使用[0,1023]这个区间:http:80 https:443等等
//建议使用1024以上,但也要注意,比如mysql:3306
string defaultip = "0.0.0.0";//默认ip,bind为0就是任意ip地址都可以进到服务器来

const int size = 1024;

class UdpServer
{
public:
    UdpServer(const uint16_t &port = defaultport, const string &ip = defaultip):sockfd_(0),port_(port),ip_(ip),isrunning_(false)
    {

    }
    void Init()
    {
        //1.创建udp socket
        sockfd_ = socket(AF_INET,SOCK_DGRAM,0);
        if(sockfd_ < 0)
        {
            log(Fatal,"socket create error, socket:%d",sockfd_);
            exit(SOCKET_ERR);
        }
        log(Info,"socket create success, socket:%d",sockfd_);
        //2.bind socket
        struct sockaddr_in local;
        bzero(&local,sizeof(local));
        local.sin_family = AF_INET;//设置自己为IPV4
        local.sin_port = htons(port_);//因为端口号需要给对方发送,所以必须要保证我的端口号是网络字节序列
        local.sin_addr.s_addr = inet_addr(ip_.c_str());
        //1.将string->uint32_t 2.必须是网络序列
        //sin_addr里面还有一个成员,s_addr才是真实的本体 
        //local.sin_addr.s_addr=INADDR_ANY;也是绑定任意IP地址的方法
        int n = bind(sockfd_,(const struct sockaddr *)&local,sizeof(local));
        if(n < 0)
        {
            log(Fatal, "bind error, error: %d, err string:%s",errno, strerror(errno));
            exit(BIND_ERR);
        }
        log(Info,"bind success, errno: %d, err string: %s",errno,strerror(errno));

    }
    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;
            }
            string info = inbuffer;
            string echo_string = "server echo#" + info;
            sendto(sockfd_,echo_string.c_str(),echo_string.size(),0,(struct sockaddr*)&client,len);
        }
    }
    ~UdpServer()
    {
        if(sockfd_ > 0) close(sockfd_); 
    }
private:
    int sockfd_;//网络文件描述符
    string ip_;//服务器iP地址
    uint16_t port_;//服务器进程的端口号
    bool isrunning_;//服务器是否在运行
};

#include "log.hpp"
#include "udpserver.hpp"

void Usage(string proc)
{
    cout << "\n\rUsage:" << proc << "port[1024+]\n" << endl;
}
int main(int argc,char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(0);
    }
    uint16_t port = stoi(argv[1]);
    unique_ptr<UdpServer> svr(new UdpServer(port));
    svr->Init();
    svr->Run();
    return 0;
}

客户端

#pragma once
#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <cstring>
#include <sys/types.h>          
#include <sys/socket.h>
#include <strings.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
using namespace std;

void Usage(string proc)
{
    cout << "\n\rUsage:" << proc << "port[1024+]\n" << endl;
}
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        exit(0);
    }
    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);

    struct sockaddr_in server;
    bzero(&server, 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);
    int sockfd = socket(AF_INET,SOCK_DGRAM,0);
    if(sockfd < 0)
    {
        cout << "socket error" << endl;
        exit(1);
    }
    //客户端需要绑定,但是不需要显示绑定,由OS自己选择
    //为了防止进程端口出现冲突
    //OS什么时候给我绑定的呢?是在首次发送数据的时候
    string message;
    char buffer[1024];
    while(true)
    {
        cout << "Please Enter@ ";
        getline(cin,message);
      
        //1.数据2.发给谁
        
        sendto(sockfd,message.c_str(),message.size(),0,(struct sockaddr*)&server,len);
        
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t s = recvfrom(sockfd,buffer,1023,0,(struct sockaddr*)&temp,&len);
        if(s > 0)
        {
            buffer[s] = 0;
            cout << buffer <<endl;
        }
    }
    close(sockfd);
    return 0;
}

Socket 编程 TCP

测试服务器工具,指定服务器远程登陆:

telnet 127.0.0.1 端口号
127.0.0.1表示本地环回。

第一个函数也是将字符串的ip转换成网络四字节的ip。
在这里插入图片描述
因为TCP是面向连接的,服务器比较被动,一直处于等待链接到来的状态,所以用监听的方式查看是否有客户端到来。
这个函数是将套接字设置监听状态:

int listen(int sockfd, int backlog);
第二个参数等后面TCP协议在进行解释

在这里插入图片描述
接收消息函数,并获知哪个客户端连接上了自己:

int accept4(int sockfd, struct sockaddr *addr,socklen_t *addrlen, int flags);
这里返回值也是一个套接字,那么我们第一个参数也是套接字,有什么区别呢?
我们输入的套接字就相当于饭店外面的接待员,并不参与真正的服务当中,返回值的套接字才是真正的服务员,服务被接待过的客人。
也就是说,第一个参数的套接字仅仅是帮助我们获取新连接的工具人。

在这里插入图片描述
通过指定的套接字发送连接。

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

在这里插入图片描述
这个客户端与服务器的程序还是要用到上面log的代码:
服务器

#pragma once
#include "log.hpp"
#include <memory>
#include <sys/socket.h>
#include <cstdlib>
#include <cstdlib>
#include <cstring>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string>
#include <pthread.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
extern Log log;
const int defaultfd = -1;
const string defaultip = "0.0.0.0";
const int backlog = 10;
enum 
{
    UsageError = 1,
    SocketError,
    BindError,
    ListenError
};
class TcpServer;
class ThreadData
{
public:
    ThreadData(int fd, const string &ip, const uint16_t &p, TcpServer *t): sockfd(fd), clientip(ip), clientport(p), tsvr(t)
    {}
public:
    int sockfd;
    string clientip;
    uint16_t clientport;
    TcpServer *tsvr;
};
class TcpServer
{
public:
    TcpServer(const uint16_t &port,const string &ip = defaultip):listensock_(defaultfd),port_(port),ip_(ip)
    {

    }
    void InitServer()
    {
        listensock_ = socket(AF_INET,SOCK_STREAM,0);
        if(listensock_ < 0)
        {
            log(Fatal,"create socket,errno: %d,errstring: %s",errno,strerror(errno));
            exit(SocketError);
        }
        log(Info,"create socket success, sockfd:%d",listensock_);
        struct sockaddr_in local;
        memset(&local,0,sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port_);
        inet_aton(ip_.c_str(),&(local.sin_addr));
        if(bind(listensock_,(struct sockaddr*)&local,sizeof(local)) < 0)
        {
            log(Fatal,"bind error, errno: %d, errstring:%s",errno,strerror(errno));
            exit(BindError);
        }
        if(listen(listensock_,backlog)<0)
        {
            log(Fatal, "listen error, errno: %d, errstring: %s", errno, strerror(errno));
            exit(ListenError);
        }
        log(Info, "listen socket success, listensock_: %d", listensock_);
    }
    static void *Routine(void *args)
    {
        pthread_detach(pthread_self());//分离状态不用让主线程去等待,互不影响
        ThreadData *td = static_cast<ThreadData *>(args);
        td->tsvr->Service(td->sockfd, td->clientip, td->clientport);
        delete td;
        return nullptr;
    }
    void Start()
    {
        log(Info, "tcpServer is running....");
        for(;;)
        {
            //1.获取新连接
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = accept(listensock_,(struct sockaddr*)&client,&len);
            if(sockfd < 0)
            {
                log(Warning, "accept error, errno: %d, errstring: %s", errno, strerror(errno)); //获取一个失败不必推出,再次进行下一个获取即可
                continue;
            }
            uint16_t clientport = ntohs(client.sin_port);
            char clientip[32];
            inet_ntop(AF_INET, &(client.sin_addr), clientip, sizeof(clientip));
            //2. 根据新连接来进行通信
            log(Info, "get a new link..., sockfd: %d, client ip: %s, client port: %d", sockfd, clientip, clientport);
            //单进程版本,无法让多个客户端进行连接
            /*Service(sockfd, clientip,clientport);
            close(sockfd);*/
            //多进程版,让子进程去处理客户端,父进程继续向下执行。创建多个进程就能连接多个客户端
            /*pid_t id = fork();
            if(id == 0)
            {
                //child
                close(listensock_);//这个是父进程用的fd,防止误操作
                if(fork() > 0) exit(0);//因为是阻塞等待,所以让子进程再创建子进程去处理客户端
                Service(sockfd, clientip, clientport); //孙子进程来处理客户端,system 领养
                close(sockfd);
                exit(0);
            }
            close(sockfd);//这里必须关闭,因为sockfd已经交给子进程处理了,父进程不需要在管理了,不然父进程的文件描述符会越用越少
            // father
            pid_t rid = waitpid(id, nullptr, 0);
            (void)rid;*/
            //多线程版
            ThreadData *td = new ThreadData(sockfd, clientip, clientport, this);
            pthread_t tid;
            pthread_create(&tid, nullptr, Routine, td);
        }
    }
    void Service(int sockfd,const string& clientip,const uint16_t &clientport)//因为是面向字节流的,所以用read和write对网络文件进行读写即可
    {
        char buffer[4096];
        while (true)
        {
            ssize_t n = read(sockfd, buffer, sizeof(buffer));
            if (n > 0)
            {
                buffer[n] = 0;
                cout << "client say# " << buffer << endl;
                string echo_string = "tcpserver echo# ";
                echo_string += buffer;
    
                write(sockfd, echo_string.c_str(), echo_string.size());
            }
            else if (n == 0)//客户端退出,就会关闭套接字
            {
                log(Info, "%s:%d quit, server close sockfd: %d", clientip.c_str(), clientport, sockfd);
                break;
            }
            else
            {
                log(Warning, "read error, sockfd: %d, client ip: %s, client port: %d", sockfd, clientip.c_str(), clientport);
                break;
            }
        }
    }
    ~TcpServer()
    {

    }
private:
    int listensock_;
    string ip_;
    uint16_t port_;
};
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
using namespace std;
void Usage(const string &proc)
{
    cout << "\n\rUsage: " << proc << " serverip serverport\n"
              << endl;
}
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        Usage(argv[0]);
        exit(1);
    }
    string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);

    //TCP方式的客户端bind实在什么时候呢?
    //是在connect的时候OS自动绑定
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &(server.sin_addr));
    int sockfd = 0;
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if (sockfd < 0)
    {
        cerr << "socket error" << endl;
        return 1;
    }
    int n = connect(sockfd, (struct sockaddr *)&server, sizeof(server));
    if (n < 0)
    {
        cerr << "connect error..., reconnect: "<< endl;
    }
    while(true)
    {
        string message;
        cout << "Please Enter# ";
        getline(cin, message);
        int n = write(sockfd, message.c_str(), message.size());
        if (n < 0)
        {
            cerr << "write error..." << endl;
            
        }
        char inbuffer[4096];
        n = read(sockfd, inbuffer, sizeof(inbuffer));
        if (n > 0)
        {
            inbuffer[n] = 0;
            cout << inbuffer << endl;
        }
    }
    close(sockfd);
    return 0;
}

客户端

#include "tcpserver.hpp"
void Usage(std::string proc)
{
    std::cout << "\n\rUsage: " << proc << " port[1024+]\n" << std::endl;
}
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        exit(UsageError);
    }
    uint16_t port = std::stoi(argv[1]);
    std::unique_ptr<TcpServer> tcp_svr(new TcpServer(port));
    tcp_svr->InitServer();
    tcp_svr->Start();
    return 0;
}

守护进程

两个人在使用一个服务器,那么服务器就会生成两个“会话”(session),会话里面包含了命令行解释器(bash),和多个进程。
在这里插入图片描述
眼下,两个bash都是前台进程,其他的都是后台进程,并且一个会话当中只能有一个前台进程。
无论前台还是后台进程都可以向显示器进行打印,但是能用键盘的只有前台。(比如说将bash变成后台,另一个进程变成前台进程,Ctrl+C就能让这个进程停止,然后将bash换回到前台)——谁拥有键盘文件谁就是前台。

如何将进程后台运行呢?
只需要在启动可执行文件的时候加上一个&即可。
如果想让进程变成前台需要

fg+任务号

用jobs指令来查看后台任务号。

如果某一个前台进程被暂停之后放到后台了,想让这个后台继续运行:

bg+任务号

进程间的关系
在这里插入图片描述
这里的PGID是进程组的ID,SID是会话的ID。
会话也是要被OS组织管理的。
第一个进程PID和PGID是一样的,说明这个进程自成一组。
剩下三个PGID一样说明他们三个是一组。(一般来说,第一个进程是组长)
组长是组内中PID和PGID相同的进程。

平时所有任务其实都是进程组在完成!
也就是说,前台进程和后台进程其实并不正确,应该叫做前台任务和后台任务!

那么SID的ID是谁呢?
其实就是bash。

如果客户端退出了呢?
在这里插入图片描述

TTY全变成了?也就是说跟终端无关了。
TPGID变成了1。
并且退出的客户端的进程全都被OS给领养了。
也就是说这些进程收到了客户端登录和退出的影响。

如果不想让这些进程受到客户端的影响,那么这就叫守护进程化。(也叫精灵进程)

注销
windowsOS党总有一个操作叫做注销,注销就是将整个会话给关闭。
当重新登录的时候就相当于重新创建一个会话。

守护进程
如何进行守护进程化呢?
那就是让一个会话当中的某个进程脱离当前会话,自成一个会话,上一个会话进行销毁也就和这个进程无关了。

函数接口

#include <unistd.h>
pid_t setsid(void);
谁调用这个函数谁就被设置成为守护进程,成功返回一个新的pid,失败返回-1.

在这里插入图片描述
但是这个函数不会让这个进程成为新会话的组长。
可是新的会话只有这个进程,那怎么办呢?只要不让这个进程是第一个进程就好了。

if(fork()>0) exit(0);
srtsid();

所以守护进程的本质也是孤儿进程。

如果程序生成一个守护进程(以服务器举例),分为以下几个步骤:

1.忽略部分异常信号
2.将自己变成独立会话
3.更改当前调用进程的工作目录
4.标准输入输出错误不要在打印到屏幕上,重定向到/dev/null(也可以放在一个文件里形成文件的日志)

这样就能让一个服务器在后台持久运行了。
注意:守护进程命名习惯是后面以d为结尾。

让进程和以上效果相同的函数:
第一个参数是设置为0是将工作目录设置为根目录,否则就是当前目录,
第二个参数是设置为0是将标准输入输出错误重定向到/dev/null。
在这里插入图片描述

TCP简单的特性

三次握手与四次挥手
TCP会三次握手来进行链接的建立:
在这里插入图片描述
通过四次挥手进行释放:
在这里插入图片描述

注意:TCP是全双工的。(可以互相通信)
那么为什么不会相互收到影响呢?
在这里插入图片描述
因为在两个客户端当中,双方网络文件的上层都有自己的两种缓冲区,下层也是,所以不会冲突。(双方资源是隔离的)
也就是说我们上面用的read和write都是在对网络上下的两个缓冲区之间进行拷贝。

但TCP是面向字节流的,我们如何保证都上来的是一个完整的报文?
在用read和write的时候,TCP会有一个传输控制协议!
什么时候发,发多少,出错了如何解决?
也就是说write写的时候,其实从自己的缓冲区发送到网络的缓冲区就直接返回了,我们并不知道到底有没有发送到对方手里,因为这是TCP决定的。(其实就是给了OS,因为TCP也是在OS当中实现的,也就是TCP网络模块)
同理,read也是一样的。

所以这就需要协议定制,序列化和反序列化。

http://www.dtcms.com/a/113685.html

相关文章:

  • 【C++11】lambda
  • C# WPF 命令机制(关闭CanExecute自动触发,改手动)
  • Apifox接口测试工具详细解析
  • C# 多线程并发编程基础
  • 【Block总结】PagFM,像素注意力引导融合模块|即插即用
  • 基于STM32的智能门禁系统设计与实现
  • 05-Spring Security 认证与授权机制源码解析
  • python爬虫爬取淘宝热销(热门)零食商品加数据清洗、销量、店铺及词云数据分析_源码及相关说明文档;售后可私博主
  • 【学Rust写CAD】27 双线性插值函数(bilinear_interpolation.rs)
  • python爬虫:DrissionPage实战教程
  • 基于FAN网络的图像识别系统设计与实现
  • 【软考-高级】【信息系统项目管理师】【论文基础】范围管理过程输入输出及工具技术的使用方法
  • linux提取 Suid提权入门 Sudo提权入门
  • (二)使用Android Studio开发基于Java+xml的安卓app之环境搭建
  • 状态机思想编程练习
  • 【学习笔记】pytorch强化学习
  • flutter 专题 七十三Flutter打包未签名的ipa
  • Media streaming mental map
  • 马吕斯定律(Malus‘s Law)
  • [Hot 100] 221. 最大正方形 215. 数组中的第K个最大元素 208. 实现 Trie (前缀树) 207. 课程表
  • Nmap全脚本使用指南!NSE脚本全详细教程!Kali Linux教程!(五)
  • 7-12 最长对称子串(PTA)
  • verilog状态机思想编程流水灯
  • VMware 安装 Ubuntu 全流程实战指南:从零搭建到深度优化
  • 医药档案区块链系统
  • 强引用,弱引用,软引用,虚引用,自旋锁,读写锁
  • 基于springboot放松音乐在线播放系统(源码+lw+部署文档+讲解),源码可白嫖!
  • Linux驱动-①电容屏触摸屏②音频③CAN通信
  • client-go如何监听自定义资源
  • 2011-2019年各省地方财政资源勘探电力信息等事务支出数据