【Linux】网络编程套接字

简介: 【Linux】网络编程套接字

网络编程套接字

1. 认识TCP协议

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

2. 认识UDP协议

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

3. 网络字节序

不管这台主机是大端还是小端,就需要先将数据转换为大端字节序

h表示本地,n 表示 network, l表示32为长整数, s表示16位短整数

4. socket编程接口

// 创建 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);

4.1 sockaddr 结构

5. 简单的UDP网络程序

void InitServer()
{
    _sock = socket(AF_INET, SOCK_DGRAM, 0);
    if (_sock < 0)
    {
        std::cout << "create sock error" << strerror(errno) << std::endl;
        exit(SOCKET_ERR);
    }
    std::cout << "create sock success" << std::endl;
    struct sockaddr_in local;
    sizeof (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(_sock, (struct sockaddr*)&local, sizeof(local)) < 0)
    {
        std::cout << "bind socket error" << strerror(errno) << std::endl;
        exit(BIND_ERR);
    }
    std::cout << "bind socket success" << std::endl;
}

下面是实现的简单群聊服务器:

  1. 环形队列
#pragma once
#include <iostream>
#include <vector>
#include <pthread.h>
#include <semaphore.h>
static const int N = 50;
template <class T>
class RingQueue
{
private:
    void P(sem_t &s)
    {
        sem_wait(&s);
    }
    void V(sem_t &s)
    {
        sem_post(&s);
    }
    void Lock(pthread_mtuex_t &m)
    {
        pthread_mutex_lock(&m);
    }
    void Unlock(pthread_mutex_t &m)
    {
        pthread_mutex_unlock(&m);
    }
public:
    RingQueue(int num = N) : _ring(num), _cap(num)
    {
        sem_init(&_data_sem, 0, 0);
        sem_init(&_space_sem, 0, num);
        _c_step = _p_step = 0;
        pthread_mutex_init(&_c_mutex, nullptr);
        pthread_mutex_init(&_p_mutex, nullptr);
    }
    void push(const T &in)
    {
        P(_space_sem);
        Lock(_p_mutex);
        _ring[_p_step++] = in;
        _p_step %= _cap;
        Unlock(_p_mutex);
        V(_data_sem);
    }
    void pop(T *out)
    {
        P(_data_sem);
        Lock(_c_mutex);
        *out = _ring[_c_step++];
        _c_step %= _cap;
        Unlock(_c_mutex);
        V(_space_sem);
    }
    ~RingQueue()
    {
        sem_destroy(&_data_sem);
        sem_destroy(&_space_sem);
        pthread_mutex_destroy(&_p_mutex);
        pthread_mutex_destroy(&_c_mutex);
    }
private:
    std::vector<T> _ring;
    int _cap; // 环形队列的大小
    sem_t _data_sem; // 消费者关心
    sem_t _space_sem;  // 生产者关心
    int _c_step; // 消费位置
    int _p_step; // 生产位置
    pthread_mutex_t _c_mutex;
    pthread_mutex_t _p_mutex;
};

下面是实现的简单群聊服务器:

  1. 环形队列
#pragma once
#include <iostream>
#include <vector>
#include <pthread.h>
#include <semaphore.h>
static const int N = 50;
template <class T>
class RingQueue
{
private:
    void P(sem_t &s)
    {
        sem_wait(&s);
    }
    void V(sem_t &s)
    {
        sem_post(&s);
    }
    void Lock(pthread_mtuex_t &m)
    {
        pthread_mutex_lock(&m);
    }
    void Unlock(pthread_mutex_t &m)
    {
        pthread_mutex_unlock(&m);
    }
public:
    RingQueue(int num = N) : _ring(num), _cap(num)
    {
        sem_init(&_data_sem, 0, 0);
        sem_init(&_space_sem, 0, num);
        _c_step = _p_step = 0;
        pthread_mutex_init(&_c_mutex, nullptr);
        pthread_mutex_init(&_p_mutex, nullptr);
    }
    void push(const T &in)
    {
        P(_space_sem);
        Lock(_p_mutex);
        _ring[_p_step++] = in;
        _p_step %= _cap;
        Unlock(_p_mutex);
        V(_data_sem);
    }
    void pop(T *out)
    {
        P(_data_sem);
        Lock(_c_mutex);
        *out = _ring[_c_step++];
        _c_step %= _cap;
        Unlock(_c_mutex);
        V(_space_sem);
    }
    ~RingQueue()
    {
        sem_destroy(&_data_sem);
        sem_destroy(&_space_sem);
        pthread_mutex_destroy(&_p_mutex);
        pthread_mutex_destroy(&_c_mutex);
    }
private:
    std::vector<T> _ring;
    int _cap; // 环形队列的大小
    sem_t _data_sem; // 消费者关心
    sem_t _space_sem;  // 生产者关心
    int _c_step; // 消费位置
    int _p_step; // 生产位置
    pthread_mutex_t _c_mutex;
    pthread_mutex_t _p_mutex;
};

线程包装

#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <pthread.h>
#include <functional>
class Thread
{
public:
    typedef enum
    {
        NEW = 0, 
        RUNNING, 
        EXITED
    } ThreadStatus;
    using func_t = std::function<void ()>;
public:
    Thread(int num, func_t func) : _tid(0), _status(NEW), _func(func)
    {
        char name[128];
        snprintf(name, sizeof(name), "thread-%d", num);
        _name = name;   
    }
    int status() 
    {
        return _status;
    }
    std::string threadname()
    {
        return _name;
    }
    pthread_t pthreadid()
    {
        if (_status == RUNNING)
        {
            return _tid;
        }
        else
        {
            return 0;
        }
    }
    void operator()() // 仿函数
    {
        if (_func != nullptr) _func();
    }
    static void *runHelper(void *args)
    {
        Thread* ts = (Thread*)args;
        (*ts)();
        return nullptr;
    }
    void run()
    {
        int n = pthread_create(&_tid, nullptr, runHelper, this);
        if (n != 0) exit(1);
        _status = RUNNING;
    }
    void join()
    {
        int n = pthread_join(_tid, nullptr);
        if (n != 0)
        {
            std::cout << "man thread join thread" << _name << " error" << std::endl;
            return;
        }
        _status = EXITED;
    }
    ~Thread()
    {}
private:
    pthread_t _tid;
    std::string _name;
    func_t _func;
    ThreadStatus _status;
};

锁包装

#pragma once
#include <iostream>
#include <pthread.h>
class Mutex // 自己不维护锁,外部传入
{
private:
    pthread_mutex_t *_pmutex;
public:
    Mutex(pthread_mutex_t *mutex) : _pmutex(mutex)
    {}
    void lock()
    {
        pthread_mutex_lock(_pmutex);
    }
    void unlock()
    {
        pthread_mutex_unlock(_pmutex);
    }
    ~Mutex()
    {}
};
class lockGuard
{
private:
    Mutex _mutex;
public:
    lockGuard(pthread_mutex_t *mutex) : _mutex(mutex)
    {
        _mutex.lock();
    }
    ~lockGuard()
    {
        _mutex.unlock();
    }
};

服务器

#pragma once
#include <iostream>
#include <cerrno>
#include <cstring>
#include <cstdlib>
#include <functional>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <unordered_map>
#include "err.hpp"
#include "RingQueue.hpp"
#include "lockGuard.hpp"
#include "Thread.hpp"
const static uint16_t port = 8888;
using func_t = std::function<std::string(std::string)>;
class UdpServer
{
private:
    uint16_t _port;
    int _sock;
    std::unordered_map<std::string, struct sockaddr_in> _onlineuser;
    pthread_mutex_t _lock;
    RingQueue<std::string> _rq;
    Thread *c;
    Thread *p;
public:
    UdpServer(uint16_t port = port) : _port(port)
    {
        std::cout << "server addr " << _port << std::endl;
        pthread_mutex_init(&_lock, nullptr);
        p = new Thread(1, std::bind(&UdpServer::Recv, this));
        c = new Thread(1, std::bind(&UdpServer::Broadcast, this));
    }
    void start()
    {
        _sock = socket(AF_INET, SOCK_DGRAM, 0); // 创建套接字
        if (_sock < 0)
        {
            std::cout << "create _sock error" << std::endl;
            exit(SOCKET_ERR);
        }
        std::cout << "create _sock success" << std::endl;
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_port = htons(_port);
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY;
        if (bind(_sock, (struct sockaddr *)&local, sizeof(local)) < 0) // 绑定套接字
        {
            std::cout << "bind socket error" << std::endl;
            exit(BIND_ERR);
        }
        std::cout << "bind socket success" << std::endl;
        p->run();
        c->run();
    }
    void addUser(const std::string &name, const struct sockaddr_in &peer)
    {
        lockGuard guard(&_lock);
        auto it = _onlineuser.find(name);
        if (it != _onlineuser.end())
        {
            return;
        }
        _onlineuser.insert(std::pair<const std::string, const struct sockaddr_in>(name, peer));
    }
    void Recv()
    {
        char buf[2056];
        while (true)
        {
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int n = recvfrom(_sock, buf, sizeof(buf) - 1, 0, (struct sockaddr *)&peer, &len);
            if (n > 0)
            {
                buf[n] = '\0';
            }
            else
                continue;
            std::cout << "recv done" << std::endl;
            std::string clientip = inet_ntoa(peer.sin_addr);
            uint16_t clientport = ntohs(peer.sin_port);
            std::cout << clientip << "-" << clientport << "#" << buf << std::endl;
            std::string name = clientip;
            name += "-";
            name += std::to_string(clientport);
            addUser(name, peer);
            _rq.push(buf);
        }
    }
    void Broadcast()
    {
        while (true)
        {
            std::string sendstring;
            _rq.pop(&sendstring);
            std::vector<struct sockaddr_in> v;
            {
                lockGuard guard(&_lock);
                for (auto user : _onlineuser)
                {
                    v.push_back(user.second);
                }
            }
            for (auto user : v)
            {
                sendto(_sock, sendstring.c_str(), sendstring.size(), 0, (struct sockaddr*)&(user), sizeof(user));
                std::cout << "send done" << sendstring << std::endl;
            }
        }
    }
    ~UdpServer()
    {
        pthread_mutex_destroy(&_lock);
        c->join();
        p->join();
        delete p, c;
    }
};

地址转换函数

inet_ntoa 是把返回结果放到了静态区,这个时候不需要手动释放。如果多次调用,会覆盖掉上一次的值

6. 简单的TCP网络程序

TCP由于是全双工的,所以初始化工作一共有五步

  1. socket
  2. bind
  3. listen
  4. accept
  5. connect

listen 声明socket fd处

  1. 于监听状态,并且允许多个客户端来连接等待状态
    accept 三次握手完成后,调用服务器连接,

connect 客户端需要调用这个函数连接服务器

TCP 简单服务器:

#pragma once
#include <iostream>
#include <cstdlib>
#include <cstring>
#include <functional>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
#include <pthread.h>
#include "err.hpp"
static const int defaultport = 8888;
static const int backlog = 32;
using func_t = std::function<std::string(const std::string &)>;
class TcpServer;
class ThreadData
{
public:
    int _sock;
    std::string _clientip;
    uint16_t _port;
    TcpServer *_current;
public: 
    ThreadData(int fd, const std::string &ip, const uint16_t &port, TcpServer *ts)
    : _sock(fd), _clientip(ip), _port(port), _current(ts)
    {}
};
class TcpServer
{
public:
    TcpServer(func_t func, uint16_t port = defaultport) : _port(port), _func(func)
    {
    }
    void initServer()
    {
        _listensock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listensock < 0)
        {
            std::cout << "create socket error" << std::endl;
            exit(SOCKET_ERR);
        }
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = htonl(INADDR_ANY);
        local.sin_port = htons(_port);
        if (bind(_listensock, (struct sockaddr*)&local, sizeof(local)) < 0)
        {
            std::cout << "create bind error" << std::endl;
            exit(BIND_ERR);
        }
        if (listen(_listensock, backlog) < 0)
        {
            std::cout << "create listen error" << std::endl;
            exit(LISTEN_ERR);
        }
    }
    static void* threadRuntinue(void* args)
    {
        pthread_detach(pthread_self());
        ThreadData *td = static_cast<ThreadData*>(args);
        td->_current->server(td->_sock, td->_clientip, td->_port);
        delete td;
        return nullptr;
    }
    void start()
    {
        _quit = false;
        while (true)
        {
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sock = accept(_listensock, (struct sockaddr*)&client, &len);                                                  
            if (sock < 0)
            {
                std::cout << "create accept error" << std::endl;
                continue;
            }
            std::string clientip = inet_ntoa(client.sin_addr);
            uint16_t clientport = ntohs(client.sin_port);
            std::cout << "accept success" << clientip << " " << clientport << std::endl;
            // server(sock, clientip, clientport); 第一个版本
            // 多线程版本
            pthread_t tid;
            ThreadData *threadDate = new ThreadData(sock, clientip, clientport, this);
            pthread_create(&tid, nullptr, threadRuntinue, threadDate);
        }
    }
    void server(int sock, const std::string ip, uint16_t port)
    {
        std::string who = ip + "-" + std::to_string(port);
        char buffer[1024];
        while (true)
        {
            size_t s = read(sock, buffer, sizeof(buffer) - 1);
            if (s > 0)
            {
                buffer[s] = 0;
                std::string res = _func(buffer); // 回调传进来的函数
                std::cout << who << ">>>" << res << std::endl;
                write(sock, res.c_str(), res.size());
            }
            else if (s == 0)
            {
                close(sock);
                std::cout << who << "quit, me too" << std::endl;
                break;
            }
            else
            {
                close(sock);
                std::cout << "read error" << std::endl;
                break;
            }
        }
    }
    ~TcpServer() {}
private:
    uint16_t _port;
    int _listensock;
    bool _quit;
    func_t _func;
};

TCP 简单的客户端

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "err.hpp"
int main(int argc, char *argv[])
{
    std::string serverip = argv[1];
    uint16_t serverport = atoi(argv[2]);
    int sock = socket(AF_INET, SOCK_STREAM, 0);
    // 要不要bind? 要
    // 要不要自己bind? 不要,因为client要让OS自动给用户进行bind
    // 要不要listen?不要 要不要accept?不需要
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_aton(serverip.c_str(), &(server.sin_addr));
    int cnt = 5;
    while (connect(sock, (struct sockaddr*)&server, sizeof(server)) != 0)
    {
        sleep(1);
        std::cout << "正在尝试重新连接" << std::endl;
        cnt--;
        if (cnt < 0) break;
    }
    if (cnt <= 0) {
        std::cout << "连接失败" << std::endl;
        exit(CONNECT_ERR);
    }
    char buffer[1024];
    while (true)
    {
        std::string line;
        std::getline(std::cin, line);
        write(sock, line.c_str(), line.size());
        size_t s = read(sock, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "server echo" << std::endl;
        }
        else if (s == 0)
        {
            std::cout << "server quit" << std::endl;
            break;
        } else {
            std::cout << "read error" << std::endl;
            break;
        }
    }
    close(sock);
    return 0;
}

6.1 TCP socket的封装

#pragma once
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstring>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <unistd.h>
#include "Log.hpp"
#include "Err.hpp"
static const int gbacklog = 32;
static const int defaultfd = -1;
class Sock
{
public:
    Sock() : _sock(defaultfd) {}
    void Socket()
    {
        _sock = socket(AF_INET, SOCK_STREAM, 0);
        if (_sock < 0)
        {
            logMessage(Fatal, "socket error, code : %d", errno);
            exit(SOCKET_ERR);
        }
    }
    void Bind(const uint16_t &port)
    {
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;
        if (bind(_sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(Fatal, "bind error, code: %d, errstring: %s", errno, strerror(errno));
            exit(BIND_ERR);
        }
    }
    void Listen()
    {
        if (listen(_sock, gbacklog) < 0)
        {
            logMessage(Fatal, "listen error, code: %d, errstring: %s", errno, strerror(errno));
            exit(LISTEN_ERR);
        }
    }
    int Accept(std::string *clientip, uint16_t *clientport)
    {
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        int sock = accept(_sock, (struct sockaddr *)&temp, &len);
        if (sock < 0)
        {
            logMessage(Warning, "accept error, code: %d, errstring: %s", errno, strerror(errno));
        }
        else
        {
            *clientip = inet_ntoa(temp.sin_addr);
            *clientport = ntohs(temp.sin_port);
        }
        return sock;
    }
    int Connect(const std::string &serverip, const uint16_t &serverport)
    {
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;
        server.sin_port = htons(serverport);
        server.sin_addr.s_addr = inet_addr(serverip.c_str());
        return connect(_sock, (struct sockaddr *)&server, sizeof(server));
    }
    int Fd()
    {
        return _sock;
    }
    ~Sock()
    {
        if (_sock != defaultfd)
            close(_sock);
    }
private:
    int _sock;
};

6.2 TCP协议通讯流程

服务器初始化:

  • 调用socket,创建文件描述符
  • 调用bind,将当前的文件描述符和ip / port 绑定在一起,如果这个端口被占用了,就会bind失败
  • 调用listen,声明这个文件描述符是服务器的文件描述符,为后面的accept做好准备
  • 调用accept并阻塞,等待客户端连接

建立连接的过程:

  • 调用socket,创建文件描述符
  • 调用connect,向服务器发起连接请求
  • connect会发出SYN并阻塞等待服务器应答
  • 服务器收到客户端的SYN,会应答一个SYN-ACK,表示同意建立连接
  • 客户端收到SYN-ACK后会从connect()返回,同时应答一个ACK
    这个建立过程称为三次握手

TCPVSUDP

  • 可靠传输 VS 不可靠传输
  • 有连接 VS 无连接
  • 字节流 VS 数据报

7. 守护进程

// 1. setsid();
// 2. setsid(), 调用进程,不能是组长!我们怎么保证自己不是组长呢?
// 3. 守护进程a. 忽略异常信号 b. 0,1,2要做特殊处理 c. 进程的工作路径可能要更改 /
#include <cstdlib>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include "log.hpp"
#include "err.hpp"
//守护进程的本质:是孤儿进程的一种!
void Daemon()
{
    // 1. 忽略信号
    signal(SIGPIPE, SIG_IGN);
    signal(SIGCHLD, SIG_IGN);
    // 2. 让自己不要成为组长
    if (fork() > 0)
        exit(0);
    // 3. 新建会话,自己成为会话的话首进程
    pid_t ret = setsid();
    if ((int)ret == -1)
    {
        logMessage(Fatal, "deamon error, code: %d, string: %s", errno, strerror(errno));
        exit(SETSID_ERR);
    }
    // 4. 可选:可以更改守护进程的工作路径
    // chdir("/")
    // 5. 处理后续的对于0,1,2的问题
    int fd = open("/dev/null", O_RDWR);
    if (fd < 0)
    {
        logMessage(Fatal, "open error, code: %d, string: %s", errno, strerror(errno));
        exit(OPEN_ERR);
    }
    dup2(fd, 0);
    dup2(fd, 1);
    dup2(fd, 2);
    close(fd);
}
相关文章
|
11天前
|
监控 安全 Linux
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景
在 Linux 系统中,网络管理是重要任务。本文介绍了常用的网络命令及其适用场景,包括 ping(测试连通性)、traceroute(跟踪路由路径)、netstat(显示网络连接信息)、nmap(网络扫描)、ifconfig 和 ip(网络接口配置)。掌握这些命令有助于高效诊断和解决网络问题,保障网络稳定运行。
36 2
|
2月前
|
安全 Linux 网络安全
Web安全-Linux网络协议
Web安全-Linux网络协议
78 4
|
23天前
|
域名解析 网络协议 安全
|
29天前
|
运维 监控 网络协议
|
25天前
|
存储 Ubuntu Linux
2024全网最全面及最新且最为详细的网络安全技巧 (三) 之 linux提权各类技巧 上集
在本节实验中,我们学习了 Linux 系统登录认证的过程,文件的意义,并通过做实验的方式对 Linux 系统 passwd 文件提权方法有了深入的理解。祝你在接下来的技巧课程中学习愉快,学有所获~和文件是 Linux 系统登录认证的关键文件,如果系统运维人员对shadow或shadow文件的内容或权限配置有误,则可以被利用来进行系统提权。上一章中,我们已经学习了文件的提权方法, 在本章节中,我们将学习如何利用来完成系统提权。在本节实验中,我们学习了。
|
30天前
|
Java
[Java]Socket套接字(网络编程入门)
本文介绍了基于Java Socket实现的一对一和多对多聊天模式。一对一模式通过Server和Client类实现简单的消息收发;多对多模式则通过Server类维护客户端集合,并使用多线程实现实时消息广播。文章旨在帮助读者理解Socket的基本原理和应用。
23 1
|
1月前
|
Ubuntu Linux 虚拟化
Linux虚拟机网络配置
【10月更文挑战第25天】在 Linux 虚拟机中,网络配置是实现虚拟机与外部网络通信的关键步骤。本文介绍了四种常见的网络配置方式:桥接模式、NAT 模式、仅主机模式和自定义网络模式,每种模式都详细说明了其原理和配置步骤。通过这些配置,用户可以根据实际需求选择合适的网络模式,确保虚拟机能够顺利地进行网络通信。
|
1月前
|
网络协议 安全 Ubuntu
Linux中网络连接问题
【10月更文挑战第3天】
33 1
|
1月前
|
网络协议 Linux
linux学习之套接字通信
Linux中的套接字通信是网络编程的核心,允许多个进程通过网络交换数据。套接字提供跨网络通信能力,涵盖本地进程间通信及远程通信。主要基于TCP和UDP两种模型:TCP面向连接且可靠,适用于文件传输等高可靠性需求;UDP无连接且速度快,适合实时音视频通信等低延迟场景。通过创建、绑定、监听及读写操作,可以在Linux环境下轻松实现这两种通信模型。
39 1
|
1月前
|
监控 Linux 测试技术
Linux系统命令与网络,磁盘和日志监控总结
Linux系统命令与网络,磁盘和日志监控总结
58 0
下一篇
无影云桌面