【Linux】网络部分——Socket编程 UDP实现网络云服务器与本地虚拟机的基本通信

33. Socket 编程 UDP------实现网络云服务器与本地虚拟机的基本通信

文章目录

API

  • socket:创建用于通信的套接字

    c 复制代码
    #include <sys/socket.h>
    
    int socket(int domain, int type, int protocol);
    • domain参数指定通信域:AF_INET表示IPv4网络通信
    • type指定的类型:SOCK_DGRAM表示使用UDP协议,数据包格式
    • protocol指定套接字使用的特定协议,默认设为0
    • 返回值是一个文件描述符,int类型------returns a file descriptor that refers to that endpoint.
  • bind绑定

    c 复制代码
    #include <sys/socket.h>
    
    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
    • sockfd------文件描述符

    • addr------指向sockaddr结构体的指针,这个结构体包含指定通信域、IP、端口号

      • 我们使用网络通信一般定义sockaddr_in结构体,之后强转为sockaddr类型

        c 复制代码
        #define CONV(v) (struct sockaddr *)(v)
      • 结构体结构

        c 复制代码
        struct sockaddr
          {
            __SOCKADDR_COMMON (sa_);	/* Common data: address family and length.  */
            char sa_data[14];		/* Address data.  */
          };
        
        struct sockaddr_in
          {
            __SOCKADDR_COMMON (sin_);
            in_port_t sin_port;			/* Port number.  */
            struct in_addr sin_addr;		/* Internet address.  */
        
            /* Pad to size of `struct sockaddr'.  */
            unsigned char sin_zero[sizeof (struct sockaddr)
        			   - __SOCKADDR_COMMON_SIZE
        			   - sizeof (in_port_t)
        			   - sizeof (struct in_addr)];
          };
      • 使用

        c 复制代码
        struct sockaddr_in local;
        bzero(&local, sizeof(local));//把结构体中的空间全部初始化为0
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);//这里需要用htons函数将主机的16位无符号短整型数转换为网络字节序,类似的函数还有htonl/ntohs/ntohl
        local.sin_addr.s_addr = inet_addr(_ip.c_str());//sin_addr是一个结构体,这个结构体中只有s_addr一个成员变量,我们需要对这个赋值
    • 成功返回0,失败返回-1,同时errno被设置

    • 在C语言中,结构体可以在定义时整体初始化,但不能在赋值语句中整体赋值(除非使用memcpy或类似)

  • recvfrom:从套接字中接收消息

    c 复制代码
    #include <sys/socket.h>
    
    ssize_t recvfrom(int sockfd, void buf[restrict .len], size_t len,
                     int flags,
                     struct sockaddr *_Nullable restrict src_addr,
                     socklen_t *_Nullable restrict addrlen);
    • 函数参数依次是:文件描述符、字符串数组、字符串数组长度、设置位、结构体指针、结构体大小指针。后面两个是输出型参数表示接收到的数据来自哪个套接字,flags一般设为0
    • 返回值为接受到的字节,发射错误返回-1
  • sendto:向套接字发送消息

    c 复制代码
     #include <sys/socket.h>
    
    ssize_t sendto(int sockfd, const void buf[.len], size_t len, int flags,
                   const struct sockaddr *dest_addr, socklen_t addrlen);
    • 函数参数依次是:文件描述符、字符串数组、字符串数组长度、设置位、结构体指针、结构体大小指针。flags一般设为0
    • 如果成功,这些调用将返回发送的字节数。失败返回-1,同时errno被设置

echo server程序

实现UDP服务器类的核心功能,把从客户端client接收到的数据以日志的方式打印,并重新发送回客户端。

cpp 复制代码
//UdpServer.hpp
#pragma once

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <cerrno>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "Common.hpp"
#include "Log.hpp"

using namespace My_Log;

const in_port_t defport = 8080;
const std::string defip = "127.0.0.1";

class UdpServer
{
public:
    UdpServer(in_port_t port = defport, std::string ip = defip)
        : _sokfd(-1), _port(port), _ip(ip), _isrunning(false)
    {
    }
    void InitServer()
    {
        _sokfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sokfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket err: " << strerror(errno);
            exit(SOCKET_ERR);
        }

        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());
        //_net_addr.sin_addr.s_addr = INADDR_ANY;//上面这一句可以改成这个,表示允许任何远程主机连接

        int n = bind(_sokfd, CONV(&local), sizeof(local));
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind: " << strerror(errno);
            exit(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success";
    }
    void Start()
    {
        _isrunning = true;

        while (true)
        {
            char recbuff[1024] = {0};
            struct sockaddr_in cliaddr;
            socklen_t len = sizeof(cliaddr);
            int n = recvfrom(_sokfd, recbuff, 1024, 0, CONV(&cliaddr), &len);
            if(n < 0)
            {
                LOG(LogLevel::ERROR) << "recvfrom err" << strerror(errno);
                exit(REC_ERR);
            }

            std::string message = recbuff;
            LOG(LogLevel::DEBUG) << "Server receive message# " << message;

            sendto(_sokfd, recbuff, n, 0, CONV(&cliaddr), len);
        }
        _isrunning = false;
    }
    ~UdpServer()
    {
        if(_sokfd > -1)
            close(_sokfd);
    }

private:
    int _sokfd;
    in_port_t _port;
    std::string _ip;

    bool _isrunning;
};
  • 服务端socket编程的一般顺序:socket创建套接字,bind绑定套接字IPh和port,发送接收数据
cpp 复制代码
//UdpServerMain.cc
#include "UdpServer.hpp"
#include <memory>


int main()
{
    ENABLE_CONSOLE_LOG();

    std::unique_ptr<UdpServer> ser_utpr = std::make_unique<UdpServer>();

    ser_utpr->InitServer();
    ser_utpr->Start();

    return 0;
}
cpp 复制代码
//UdpClientMain.hpp
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>

#include "Common.hpp"
#include "Log.hpp"

using namespace My_Log;

int main(int argc, char *argv[])
{
    if (argc < 3)
    {
        std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;
        exit(USAGE_ERR);
    }

    in_port_t serport = std::stoi(argv[2]);
    std::string serip = argv[1];
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        LOG(LogLevel::FATAL) << "socket err: " << strerror(errno);
        exit(SOCKET_ERR);
    }

    struct sockaddr_in seraddr;
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = ntohs(serport);
    seraddr.sin_addr.s_addr = inet_addr(serip.c_str());

    while (true)
    {
        std::string sendstr;
        std::cin >> sendstr;
        sendto(sockfd, sendstr.c_str(), sendstr.size(), 0, CONV(&seraddr), sizeof(seraddr));


        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        int n = ::recvfrom(sockfd, buffer, sizeof(buffer)-1, 0, CONV(&temp), &len);
        if(n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return 0;
}
  • 客户端使用scoket时不需要bind,虽然client也必须要有自己的ip和端口,但是客户端,不需要自己显示的调用bind,客户端首次sendto消息的时候,由OS自动进行bind。这种自动绑定的机制避免了不同客户端之间的端口号冲突,确保客户端程序能够顺利启动。服务器端需要显式绑定IP地址和端口号,因为服务器需要稳定的端口号供客户端访问。服务器端口号的稳定性至关重要,因为客户端需要知道固定的端口号才能访问服务器。
  • 一个端口号只能被一个进程绑定,但一个进程可以绑定多个端口号。

Chat聊天室

服务端部分

基于echo server扩展实现群聊功能,核心思路是维护用户信息表并广播消息。当多个客户端(如张三、李四、王五)同时发送消息时,服务器需记录消息内容及发送者信息(IP+端口),然后向所有在线用户转发。因此在用户向服务器发送信息的时候需要记录用户的IP和端口,便于后面的统一转发。基本的过程也就是:某一个用户向服务器发送第一次消息,服务器接收消息后注册这个用户,也就是保存这个用户的IP和端口,之后向已经保存的其他用户统一转发这条消息。

  • 实现这个项目主要有三个模块:消息接收模块,消息转发模块、线程池

  • 消息转发模块由用户管理模块实现,消息接收模块由服务器模块实现,线程池使用之前包装的单例线程池,用于并行发送消息

IP与端口的统一管理

在用户向服务器发送信息的时候需要记录用户的IP和端口,使用时需要考虑网络字节序的转化问题,所以为了方便处理,我们也把IP与端口进行封装,将一套IP地址,端口号,以及对应的sockaddr_in进行管理,给出通用的网络字节序与普通主机序列的转化和接口:

cpp 复制代码
//IntAddr.hpp
#pragma once
#include <iostream>
#include <string>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "Common.hpp"

class InetAddr
{
private:
    void Port_N2H()															//网络序列转主机序列
    {
        _port = ntohs(_net_addr.sin_port);
    }

    void IP_N2H()															//主机序列转网络序列
    {
        char buf[20];
        ::inet_ntop(AF_INET, &_net_addr.sin_addr, buf, sizeof(buf));
        _ip = buf;
    }

public:
    InetAddr()
    {
    }
    InetAddr(const struct sockaddr_in &addr) : _net_addr(addr)
    {
        Port_N2H();
        IP_N2H();
    }
    InetAddr(uint16_t port) : _port(port)
    {
        _net_addr.sin_family = AF_INET;
        _net_addr.sin_port = htons(_port);
        // _net_addr.sin_addr.s_addr = ::inet_addr(_ip.c_str());
        _net_addr.sin_addr.s_addr = INADDR_ANY; // 上面这一句可以改成这个,表示允许任何远程主机连接
    }
    struct sockaddr *NetAddr() { return CONV(&_net_addr); }

    socklen_t NetAddrLen() { return sizeof(_net_addr); }

    std::string Ip() const { return _ip; }

    uint16_t Port() const { return _port; }

    std::string GetStrAddr() const { return Ip() + ":" + std::to_string(Port()); }

    bool operator==(const InetAddr &_addr)
    {
        return (_ip == _addr._ip);
    }

    ~InetAddr()
    {
    }

private:
    std::string _ip;
    uint16_t _port;
    struct sockaddr_in _net_addr;
};
用户管理模块

由上面的基本概括可知,我们需要一个模块来管理参与群聊的用户信息。为实现消息转发功能,需构建用户管理模块(User.hpp)维护在线用户列表。用户唯一性标识采用IP地址或IP+端口组合

cpp 复制代码
//User.hpp
#pragma once

#include <iostream>
#include <string>
#include <memory>
#include <list>

#include "InetAddr.hpp"
#include "Log.hpp"
#include "Mutex.hpp"

using namespace My_Log;
using namespace My_Mutex;

class UserInterface
{
public:
    virtual ~UserInterface() = default;
    virtual void SendTo(int sokfd, const std::string &message) = 0;
    virtual bool operator==(const InetAddr &other) = 0;
    virtual std::string Id() = 0;
};

// 单个用户对象
class User : public UserInterface
{
public:
    User(const InetAddr &id) : _id(id)
    {
    }
    void SendTo(int sokfd, const std::string &message) override
    {
        LOG(LogLevel::DEBUG) << "send message to: " << _id.GetStrAddr() << "info: " << message;
        ::sendto(sokfd, message.c_str(), message.size(), 0, _id.NetAddr(), _id.NetAddrLen());
    }
    bool operator==(const InetAddr &other) override
    {
        return _id == other;
    }
    std::string Id() override
    {
        return _id.GetStrAddr();
    }
    ~User()
    {
    }

private:
    InetAddr _id;
};

// 管理用户
class UserManager
{
public:
    UserManager()
    {
    }
    void AddUser(const InetAddr &id)
    {
        LockGuard lockguard(_mutex);
        for (auto &u : _online_ptr)
        {
            if (*u == id)
            {
                LOG(LogLevel::DEBUG) << id.GetStrAddr() << "该用户已存在";
                return;
            }
        }
        LOG(LogLevel::DEBUG) << "新增用户: " << id.GetStrAddr();
        _online_ptr.push_back(std::make_shared<User>(id));
    }
    void DelUser(const InetAddr &id)
    {
        LockGuard lockguard(_mutex);
        auto pos = std::remove_if(_online_ptr.begin(), _online_ptr.end(), [&id](std::shared_ptr<UserInterface> &user){
            return *user == id;
        });
        _online_ptr.erase(pos, _online_ptr.end());
        PrintUser();
    }
    void PrintUser()
    {
        for(auto user : _online_ptr)
        {
            LOG(LogLevel::DEBUG) <<"在线用户-> "<<  user->Id();
        }
    }
    void Route(int sokfd, std::string &message)
    {
        LockGuard lockguard(_mutex);
        for(auto& user:_online_ptr)
        {
            user->SendTo(sokfd, message);
        }
    }
    ~UserManager()
    {
    }

private:
    std::list<std::shared_ptr<UserInterface>> _online_ptr;
    Mutex _mutex;
};
  • 该模块包含三个主要类:用户类、用户管理类和用户接口类

    • 用户类(class user)的设计需要考虑用户的唯一性,由于没有用户名注册机制,使用IP地址作为唯一标识 。在class user中,首先定义用户的基本属性,包括IP地址(InetAddr)作为唯一标识符。用户类需要包含构造方法和析构方法,以及一个公共的消息发送方法(SendTo)。消息发送方法的功能是将消息发送给指定的用户,在服务器端表现为消息的转发。
    • 用户管理类(class UserManager)负责管理所有在线用户,以链表形式组织用户信息。该类需要提供新增用户(AddUser)和删除用户(DelUser)的功能,以及消息路由(Route)功能------也就是转发消息。新增用户时需要检查用户是否已存在,避免重复添加。用户管理类使用智能指针(shared_ptr)管理用户对象,存储在std::list容器中。路由功能负责将消息广播给所有在线用户,需要遍历用户链表并调用每个用户的SendTo方法。
    • 用户接口类(class UserInterface)作为基类,定义了一个纯虚方法SendTo,用于发送消息。子类user 继承自class UserInterface,必须实现SendTo方法。用户接口类定义消息发送的规范,用户实现类完成具体的消息发送逻辑
  • 新增用户时需要构建user对象并将其插入到在线用户链表中。为了实现用户地址的比较,需要对inet addr结构体重载等号运算符。

  • 消息路由功能的具体实现需要遍历在线用户列表,对每个用户调用SendTo方法发送消息。在AddUser方法的实现中,首先创建user对象,然后将其插入到_online_ptr链表中。插入前需要检查用户是否已存在,避免重复添加。用户管理类使用std::list容器存储用户信息,虽然查找效率不高,但适合广播消息的场景。

  • 在这段代码中,成员变量的list是存放UserInterface类型的智能指针,但是在AddUser函数中插入的却是User类型的智能指针,能不能这样做,为什么要这样做?

    • 在C++中,我们可以将派生类的指针赋值给基类的指针,这是多态的常见用法。这里,std::list<std::shared_ptr<UserInterface>> 存储的是基类 UserInterface 的智能指针,而 std::make_shared<User>(id) 创建的是派生类 User 的智能指针。由于 UserUserInterface 的派生类,所以这样做是合法的。``User继承自UserInterface,因此 std::shared_ptr可以隐式转换为std::shared_ptr`,C++ 智能指针支持派生类到基类的隐式转换
    • 利用多态性,我们可以通过基类指针来操作派生类对象,从而在 UserManager 中统一管理所有满足 UserInterface 接口的用户对象。
    • 如果未来有其他类型的用户(比如不同的用户类,但都实现了 UserInterface 接口),也可以加入到同一个列表中管理,增加了代码的扩展性。
    • Route 函数中,我们调用 user->SendTo 时,实际上会调用到具体用户类(如 User)的 SendTo 方法,这就是多态的作用。
    • 需要注意的是,UserInterface 必须定义虚析构函数,这样当通过基类指针删除对象时,会正确调用派生类的析构函数。在代码中,UserInterface 的析构函数已经被定义为虚函数(virtual ~UserInterface() = default;),所以这是安全的。
服务器模块

服务器模块提供初始化网络接口,之后循环接收消息并将消息转发任务注册进线程池中

cpp 复制代码
//UdpServer.hpp
#pragma once

#include <iostream>
#include <string>
#include <cstdio>
#include <cstring>
#include <cerrno>
#include <functional>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"

using namespace My_Log;
using namespace My_ThreadPool;

using adduser_t = std::function<void(const InetAddr &id)>;
using route_t = std::function<void(int sokfd, std::string &message)>;
using deluser_t = std::function<void(const InetAddr &id)>;
using task_t = std::function<void()>;

const in_port_t defport = 8080;
const std::string defip = "127.0.0.1";

class UdpServer
{
public:
    UdpServer(in_port_t port = defport)
        : _sokfd(-1), _isrunning(false), _addr(port)
    {
    }
    void RegisterService(adduser_t adduser, route_t route, deluser_t deluser)
    {
        _adduser = adduser;
        _route = route;
        _deluser = deluser;
    }
    void InitServer()
    {
        _sokfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sokfd < 0)
        {
            LOG(LogLevel::FATAL) << "socket err: " << strerror(errno);
            exit(SOCKET_ERR);
        }

        int n = bind(_sokfd, _addr.NetAddr(), _addr.NetAddrLen());
        if (n < 0)
        {
            LOG(LogLevel::FATAL) << "bind: " << strerror(errno);
            exit(BIND_ERR);
        }
        LOG(LogLevel::INFO) << "bind success";
    }
    void Start()
    {
        _isrunning = true;

        while (true)
        {
            // 获取发送消息的客户端信息以及消息内容
            char recbuff[1024] = {0};
            struct sockaddr_in cliaddr;
            socklen_t len = sizeof(cliaddr);
            int n = recvfrom(_sokfd, recbuff, 1024, 0, CONV(&cliaddr), &len);
            if (n < 0)
            {
                LOG(LogLevel::ERROR) << "recvfrom err" << strerror(errno);
                exit(REC_ERR);
            }

            // 构建用户信息
            InetAddr cli(cliaddr);
            std::string message;

            if (strcmp(recbuff, "QUIT") == 0)
            {
                // 用户退出
                _deluser(cli); // 删除用户
                message = std::string(cli.GetStrAddr() + " status: ") + "用户退出";
            }
            else
            {
                // 正常通信
                _adduser(cli); // 创建用户(如果没有就创建)
                message = std::string(cli.GetStrAddr() + " info: ") + recbuff;
            }

            // 执行全局发送(将发送任务注册进线程池中)
            task_t task = bind(_route, _sokfd, message);
            ThreadPool<task_t>::getinstance()->Equeue(task);
        }
        _isrunning = false;
    }
    ~UdpServer()
    {
        if (_sokfd > -1)
            close(_sokfd);
    }

private:
    int _sokfd;
    InetAddr _addr;
    bool _isrunning;

    adduser_t _adduser;
    route_t _route;
    deluser_t _deluser;
};
  • UdpServer收到网络消息后,需将消息内容与客户端身份分离,客户端信息(包括IP和端口)已通过预定义方法(如InetAddr)自动转换。新增用户时,调用class UserManagerAddUser方法将客户端对象传递给UserManager模块,若用户已存在则直接返回。代码中通过日志打印用户新增状态(如"用户已存在"或"该用户新增")。网络服务器启动时需将UserManager模块的AddUser方法注册为回调函数,确保新用户消息能触发观察者插入逻辑。
mian函数
cpp 复制代码
#include "UdpServer.hpp"
#include "User.hpp"
#include <memory>

int main(int argc, char *argv[])
{
    ENABLE_CONSOLE_LOG();

    std::shared_ptr<UserManager> um_sptr = std::make_shared<UserManager>();
    std::unique_ptr<UdpServer> ser_utpr = std::make_unique<UdpServer>(std::stoi(argv[1]));

    ser_utpr->InitServer();
    ser_utpr->RegisterService([&um_sptr](const InetAddr &id)
                              { um_sptr->AddUser(id); },
                              [&um_sptr](int sokfd, std::string &message)
                              { um_sptr->Route(sokfd, message); },
                              [&um_sptr](const InetAddr &id)
                              { um_sptr->DelUser(id); });
    ser_utpr->Start();

    return 0;
}
客户端部分
cpp 复制代码
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cstring>
#include <pthread.h>
#include <signal.h>

#include "Common.hpp"
#include "Log.hpp"

using namespace My_Log;

int sockfd = -1;
struct sockaddr_in seraddr;

void Quit(int signo)
{
    (void)signo;

    std::string mes = "QUIT";
    ::sendto(sockfd, mes.c_str(), mes.size(), 0, CONV(&seraddr), sizeof(seraddr));
    exit(0);
}

void *Recver(void *args)
{
    (void)args; // 防止编译器报警告

    while (true)
    {
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        char buffer[1024];
        int n = ::recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, CONV(&temp), &len);
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << buffer << std::endl;
        }
    }

    return nullptr;
}

int main(int argc, char *argv[])
{
    // 接受命令行参数
    if (argc < 3)
    {
        std::cerr << "Usage: " << argv[0] << " serverip serverport" << std::endl;
        exit(USAGE_ERR);
    }
    in_port_t serport = std::stoi(argv[2]);
    std::string serip = argv[1];

    signal(2, Quit);

    // 创建套接字
    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        LOG(LogLevel::FATAL) << "socket err: " << strerror(errno);
        exit(SOCKET_ERR);
    }

    // 填充server服务端信息
    seraddr.sin_family = AF_INET;
    seraddr.sin_port = ntohs(serport);
    seraddr.sin_addr.s_addr = inet_addr(serip.c_str());

    // 创建线程,子线程用于接受消息,主线程用于发送消息
    pthread_t pid;
    pthread_create(&pid, nullptr, Recver, nullptr);

    // 开始运行,发送第一个信息给服务端
    std::string mes = "上线";
    int n = ::sendto(sockfd, mes.c_str(), mes.size(), 0, CONV(&seraddr), sizeof(seraddr));
    if (n == -1)
    {
        LOG(LogLevel::FATAL) << "sendto err: " << strerror(errno);
    }

    // 循环发送消息
    while (true)
    {
        std::cout << "Please Enter# ";
        std::string sendstr;
        std::cin >> sendstr;
        int n = sendto(sockfd, sendstr.c_str(), sendstr.size(), 0, CONV(&seraddr), sizeof(seraddr));
        if (n == -1)
        {
            LOG(LogLevel::FATAL) << "sendto err: " << strerror(errno);
        }
    }

    return 0;
}
  • 客户端同样需要发送和接收消息,但是不需要向每个用户转发,只需要与服务端之间发送和接收消息即可。因此,我们创建两个线程,主线程用于阻塞等待键盘消息,并将消息发送到服务端,子线程用于循环接收服务器发送来的消息
  • 我们还自定义捕捉了二号信号作为客户端的退出信号,收到信号之后执行Quit函数内容并退出进程
运行效果

可以实现网络云服务器与本地虚拟机的基本通信

网络云服务器服务端:

网络云服务器客户端:

本地虚拟机客户端:

日志和线程池部分有关的代码在之前的博客中可以找一找

相关推荐
Ching·4 小时前
RK3568入门之VScode远程连接开发板,直接开发板上面编程和实验
linux·ide·vscode·编辑器·rk3568
iconball5 小时前
个人用云计算学习笔记 --20 (Nginx 服务器)
linux·运维·笔记·学习·云计算
十碗饭吃不饱5 小时前
WebClient工具调用HTTP接口报错远程主机断开连接
网络·网络协议·http
Wang's Blog5 小时前
Linux小课堂: 在 VirtualBox 虚拟机中安装 CentOS 7 的完整流程与关键技术详解
linux·运维
liu****5 小时前
基于websocket的多用户网页五子棋(九)
服务器·网络·数据库·c++·websocket·网络协议·个人开发
liu****5 小时前
基于websocket的多用户网页五子棋(八)
服务器·前端·javascript·数据库·c++·websocket·个人开发
馨谙5 小时前
Linux中权限系统
linux·运维·服务器
jieyu11195 小时前
虚拟专用网络
linux·网络
报错小能手6 小时前
linux学习笔记(19)进程间通讯——消息队列
linux·笔记·学习