网络编程套接字,Linux下实现echo服务器和客户端

目录

1、一些网络中的名词

[1.1 IP地址](#1.1 IP地址)

[1.2 端口号port](#1.2 端口号port)

[1.3 "端口号" 和 "进程ID"](#1.3 "端口号" 和 "进程ID")

[1.4 初始TCP协议](#1.4 初始TCP协议)

[1.5 UDP协议](#1.5 UDP协议)

2、socket编程接口

[2.1 socket 常见API](#2.1 socket 常见API)

[2.2 sockaddr结构](#2.2 sockaddr结构)

3、简单的网络程序

[3.1 udp实现echo服务器和客户端](#3.1 udp实现echo服务器和客户端)

[3.1.1 echo服务器实现](#3.1.1 echo服务器实现)

[3.1.2 echo客户端实现](#3.1.2 echo客户端实现)

[3.1.3 运行结果](#3.1.3 运行结果)

[3.2 tcp实现echo服务器和客户端](#3.2 tcp实现echo服务器和客户端)

[3.2.1 多进程的echo服务器](#3.2.1 多进程的echo服务器)

[3.2.2 基于线程池tcp的echo服务器](#3.2.2 基于线程池tcp的echo服务器)

[3.3 代码中的一些函数](#3.3 代码中的一些函数)

[3.3.1 地址转换函数](#3.3.1 地址转换函数)

[3.3.2 udp使用的的函数](#3.3.2 udp使用的的函数)

[3.3.3 tcp使用的函数](#3.3.3 tcp使用的函数)

4、结语


1、一些网络中的名词

1.1 IP地址

IP地址就和我们现实中的地址是一个概念,只不过一个在网络中定位,一个在现实中定位,

在一台服务器往另一台服务器发送数据的时候,IP数据包的头部中,有两个IP地址,一个是源IP地址,另一个是目的IP地址,

1.2 端口号port

端口号(port)是传输层协议的内容.

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

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

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

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

1.3 "端口号" 和 "进程ID"

pid 表示唯一一个进程; 此处我们的端口号也是唯一表示一个进程。一个进程可以绑定多个端口号; 但是一个端口号不能被多个进程绑定。

1.4 初始TCP协议

传输层协议

有连接

可靠传输

面向字节流

1.5 UDP协议

传输层协议

无连接

不可靠传输

面向数据报

网络字节序

在计算机的内存中的多字节数据相对于内存地址有大端和小端之分, 磁盘文件中的多字节数据相对于文件中的偏移地址也有大端小端之分, 网络数据流同样有大端小端之分. 那么如何定义网络数据流的地址呢?

发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;

接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;

因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址.

TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.

不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据;

如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可;

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

cpp 复制代码
#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

cpp 复制代码
// 创建 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,以及UNIX Domain Socket. 然而, 各种网络协议的地址格式并不相同.

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

sockaddr 结构

cpp 复制代码
struct sockaddr
  {
    __SOCKADDR_COMMON (sa_);	/* Common data: address family and length.  */
    char sa_data[14];		/* Address data.  */
  };

sockaddr_in 结构

cpp 复制代码
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)];
  };

虽然socket api的接口是sockaddr, 但是我们真正在基于IPv4编程时, 使用的数据结构是sockaddr_in; 这个结构里主要有三部分信息: 地址类型, 端口号, IP地址.

in_addr结构

cpp 复制代码
struct in_addr
  {
    in_addr_t s_addr;
  };

in_addr用来表示一个IPv4的IP地址. 其实就是一个32位的整数;

3、简单的网络程序

3.1 udp实现echo服务器和客户端

3.1.1 echo服务器实现

cpp 复制代码
//udp_server.hpp
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <unordered_map>
#include <vector>

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

class udpserver{
public:
    udpserver(std::string ip, int16_t port):_fd(-1), _ip(ip), _port(port),_users(0)
    {}

    ~udpserver(){
        if (_fd > 0) {
            close(_fd);
        }
    }

    void initServer() {
        _fd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_fd < 0) {
            perror("注册socket失败");
            exit(2);
        }
        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(_fd, (struct sockaddr*)&local, sizeof(local)) < 0) {
            perror("绑定失败!");
            exit(3);
        }
        //std::cout << "绑定成功!"<< std::endl;
    }

    void startServer(){

        //准备用来接收客户端发送的消息的缓冲区
        char buffer[1024];
        while (1) {
            
            //准备用来接收发送消息的客户端信息
            memset(buffer, '\0', sizeof(buffer));
            struct sockaddr_in peer;
            memset(&peer, 0, sizeof(peer));
            socklen_t len = sizeof(peer);
            //接收数据,以及接收发送数据的客户端信息
            //std::cout << "正在接收!" << std::endl;
            ssize_t recv_size = recvfrom(_fd, buffer, sizeof(buffer) - 1, 0, (struct sockaddr*)&peer, &len);
            
            //打印客户端发送来的数据
            //std::cout << "接收成功!正在打印:" << std::endl;
            // if (recv_size > 0) {
            //     buffer[recv_size] = 0;
            //     std::string ip = inet_ntoa(peer.sin_addr);
            //     int16_t port = ntohs(peer.sin_port);
            //     std::cout << "[" << ip << ":" << port << "]:";
            //     std::cout << buffer << std::endl;
            // }
            //处理数据
            buffer[recv_size] = 0;
            std::string massage;
            massage += inet_ntoa(peer.sin_addr);
            massage += ":";
            massage += ntohs(peer.sin_port);

            //_users.insert(make_pair<std::string,struct sockaddr_in>(massage, peer);
            _users.insert({massage, peer});

            massage += "#";
            massage += buffer;
            //_users.insert(makepair(, peer);

            //回写数据
            //sendto(_fd, buffer, strlen(buffer), 0, (struct sockaddr*)&peer, len);
            for (auto &s : _users) {
                sendto(_fd, massage.c_str(), massage.size(),0 ,(struct sockaddr*)&(s.second), sizeof(s.second));
            }
        }
    }

private:
    int _fd;
    std::string _ip;
    int16_t _port;
    std::unordered_map<std::string,struct sockaddr_in> _users;
};
cpp 复制代码
//udp_server.cpp
#include "udpserver.hpp"
#include <memory>

int main(int argc, char* args[]) {

    std::string ip;
    int16_t port = 0;
    if (argc == 3) {
        ip = args[1];
        port = atoi(args[2]);
    }
    else if (argc == 2) {
        ip = "0.0.0.0";
        port = atoi(args[1]);
    }
    else{
        perror("输入错误!");
        return 1;
    }

    std::unique_ptr<udpserver> server(new udpserver(ip,port));
    server->initServer();
    server->startServer();
    return 0;
}

3.1.2 echo客户端实现

cpp 复制代码
//udp_client.cpp
#include <iostream>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <pthread.h>
#include <cstdio>

#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <arpa/inet.h>

struct sendData{
    int _sock;
    struct sockaddr_in *server;
};

void* sending(void *arg) {
    struct sendData* data = (struct sendData*)arg;
    int sock = data->_sock;
    struct sockaddr_in server = *(data->server);

    while (1) {
        std::string massage;
        std::cerr << "请输入内容:" ;
        std::getline(std::cin, massage);
        //发送数据
        sendto(sock, massage.c_str(), massage.size(), 0, (struct sockaddr*)&server, sizeof(server));
    }
}

void* receive(void *arg) {
    struct sendData* data = (struct sendData*)arg;
    int sock = data->_sock;
    char buffer[1024];
    while (1) {
        memset(buffer, '\0', sizeof(buffer));
        struct sockaddr_in from;
        socklen_t len = sizeof(from);
        ssize_t recv_size = recvfrom(sock, buffer, sizeof(buffer), 0, (struct sockaddr*)&from, &len);
        if(recv_size < 1) {
            continue;
        }
        buffer[recv_size] = '\0';
        //printf("[%s:%u]#%s\n",inet_ntoa(from.sin_addr),ntohs(from.sin_port),buffer);
        std::cout << buffer << std::endl;
    }
}

//客户端,负责给服务端发送消息
int main(int argc, char* args[]) {

    if (argc != 3) {
        std::cerr << "请正确输入参数!" << std::endl;
        exit(1);
    }
    std::string ip = args[1];
    int16_t port = atoi(args[2]);

    //创建套接字
    int _sock = socket(AF_INET, SOCK_DGRAM, 0);
    //这里依然会绑定,但是不需要手动绑定,回自动绑定,在第一次send的时候自动绑定,  
    if (_sock < 0) {
        exit(2);
    }

    struct sockaddr_in server;
    server.sin_addr.s_addr = inet_addr(ip.c_str());
    server.sin_family = AF_INET;
    server.sin_port = htons(port);
    socklen_t len = sizeof(server);

    sendData data;
    data._sock = _sock;
    data.server = &server;

    //创建线程,让线程1负责发送,线程2负责接收
    pthread_t send,recv;
    pthread_create(&send,nullptr,sending,(void*)&data);
    pthread_create(&send,nullptr,receive,(void*)&data);

    pthread_join(send,nullptr);
    pthread_join(recv,nullptr);

    close(_sock);
    return 0;
}

3.1.3 运行结果

3.2 tcp实现echo服务器和客户端

3.2.1 多进程的echo服务器

cpp 复制代码
#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <signal.h>

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

static void servise(int serviseSock, std::string userip, int16_t userport) {
    char buffer[1024];
    while (1) {
        memset(buffer, 0, sizeof(buffer));
        size_t s = read(serviseSock,buffer,sizeof(buffer));    

        if (s > 0) {
            buffer[s] = '\0';
            std::cout << userip.c_str() << ":" << userport << "#" << buffer << std::endl;
        }
        else if (s == 0) {
            //表示对方关闭了连接
            std::cerr << userip << ":" << userport << " shutdowm,me too!" << std::endl;
            break;
        }
        else {
            std::cerr << "read socket error," << errno << strerror(errno) << std::endl;
            break;
        }

        write(serviseSock, buffer, strlen(buffer));
    }
}


class tcpServer{
public:
    tcpServer(int16_t port, std::string ip = "")
    :_ip(ip),
    _port(port),
    _listenSock(-1)
    {}
    
    ~tcpServer(){
        if (_listenSock > 0) {
            close(_listenSock);
        }
    }

    void initServer(){
        //backlog不能太大也不能太小
        static int gbacklog = 20;
        //申请描述符
        _listenSock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listenSock < 0) {
            std::cerr << "注册socket失败" << std::endl;
            exit(2);
        }
        //绑定端口号和IP地址
        struct sockaddr_in local;
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        local.sin_port = htons(_port);

        if (bind(_listenSock, (struct sockaddr*)&local, sizeof(local)) < 0) {
            std::cerr << "绑定ip和端口号失败" << std::endl;
            exit(3);
        }

        //设置监听状态
        if (listen(_listenSock, gbacklog) < 0) {
            std::cerr << "设置监听失败" << std::endl;
            exit(4);
        }
    }

    void start() {

        //将子进程的信号改为忽略
        signal(SIGCHLD, SIG_IGN);
        
        while (1) {

            struct sockaddr_in user;
            socklen_t len = sizeof(user);
            int serviseSock = accept(_listenSock, (struct sockaddr*)&user, &len);
            std::string userip = inet_ntoa(user.sin_addr);
            int16_t userport = ntohs(user.sin_port);
            
            //servise(serviseSock,userip,userport);
            int pid = fork();
            if (pid == 0) {
                close(_listenSock);
                servise(serviseSock,userip,userport);
                close(serviseSock);
                exit(0);
            }
            close(serviseSock);
        }
    }

private:
    std::string _ip;
    int16_t _port;
    int _listenSock;
};

但是我们都知道,在操作系统中,进程是资源分配的基本单位,如果使用多进程的方案的话,就非常的浪费资源,所以,相比之下,使用多线程的方式回更好,我们在实现一个基于线程池的实现方式。

3.2.2 基于线程池tcp的echo服务器

cpp 复制代码
//自己实现的循环队列,当中使用的锁和信号都是自己封装的,这里就不放代码了
//ringqueue.hpp
#include <iostream>
#include <vector>
#include "sem.hpp"
#include "mutex.hpp"

template<class T>
class ringqueue {

public:
    ringqueue(int capacity = 10)
    :_ring_queue(capacity),
    _start(0),
    _tail(0),
    _space_sem(capacity),
    _data_sem(0),
    _mtx()
    {}

    void push(const T &in){

        _space_sem.p();
        _mtx.lock();
        _ring_queue[_start++] = in;
        _start %= _ring_queue.size();
        _data_sem.v();
        _mtx.unlock();
    }

    void pop(T & out){
        _data_sem.p();
        _mtx.lock();
        out = _ring_queue[_tail++];
        _tail %= _ring_queue.size();
        _space_sem.v();
        _mtx.unlock();
    }

    ~ringqueue()
    {
        
    }

private:
    std::vector<T> _ring_queue;
    int _start;
    int _tail;
    sem _space_sem;
    sem _data_sem;
    mutex _mtx;
};
cpp 复制代码
//单例模式的线程池
//其中的线程也是自己进行封装的,不做代码展示
//thread_pool.hpp
#include "thread.hpp"
#include "ringQueue.hpp"
#include <ctime>
#include <unistd.h>

template <class T>
struct poolData
{
    Thread* _self;
    ringqueue<T>* _rq;
};

template <class T>
class Pool
{

public:
    static Pool<T>* getpool(int num = 10){
        if (nullptr == _pool) {
            pthread_mutex_lock(&mtx);
            if (nullptr == _pool) {
                _pool = new Pool<T>(num);
            }
            pthread_mutex_unlock(&mtx);
        }
        return _pool;
    }

private:
    Pool(int num) 
    :_consumer(num),
    _rq(10)
    {}
    Pool(const Pool& pool) = delete;
    Pool& operator=(const Pool& pool) = delete;
public:
    void strat()
    {
        poolData<T> condata[_consumer.size()];
        for (int i = 0; i < _consumer.size(); ++i) {
            _consumer[i] = new Thread(i);
            condata[i]._self = _consumer[i];
            condata[i]._rq = &_rq;
            _consumer[i]->create(consumer,&condata[i]);
        }
    }

    // 生产者
    void pushTask(T task)
    {
        _rq.push(task);
    }
    // 消费者
    static void *consumer(void *args)
    {
        poolData<T> *pd = (poolData<T>*)args;
        Thread *self = pd->_self;
        ringqueue<T> *rq = pd->_rq;
        std::cout << self->name() << " Successfully started!" << std::endl;
        while (true) {
            T t;
            rq->pop(t);
            (*t)();
            delete t;
        }
    }

    ~Pool()
    {
        for (int i = 0; i < _consumer.size(); ++i) {
            _consumer[i]->join();
            delete _consumer[i];
        }
        
    }

private:
    
    ringqueue<T> _rq;
    std::vector<Thread*> _consumer;
    static pthread_mutex_t mtx;
    static Pool<T>* _pool;
};

template<class T>
Pool<T>* Pool<T>::_pool = nullptr;

template<class T>
pthread_mutex_t Pool<T>::mtx = PTHREAD_MUTEX_INITIALIZER;
cpp 复制代码
//tcp_server.hpp
#include "thread_pool.hpp"
#include "Task.hpp"

#include <iostream>
#include <string>
#include <cerrno>
#include <cstring>
#include <signal.h>

#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

static void servise(int serviseSock, std::string & userip, int16_t userport) {
    char buffer[1024];
    while (1) {
        memset(buffer, 0, sizeof(buffer));
        size_t s = read(serviseSock,buffer,sizeof(buffer));    
        if (s > 0) {
            buffer[s] = '\0';
            std::cout << userip.c_str() << ":" << userport << "#" << buffer << std::endl;
        }
        else if (s == 0) {
            //表示对方关闭了连接
            std::cerr << userip << ":" << userport << " shutdowm,me too!" << std::endl;
            break;
        }
        else {
            std::cerr << "read socket error," << errno << strerror(errno) << std::endl;
            break;
        }

        write(serviseSock, buffer, strlen(buffer));
    }
    close(serviseSock);
}

class tcpServer{
public:
    tcpServer(int16_t port, std::string ip = "")
    :_ip(ip),
    _port(port),
    _listenSock(-1),
    _pool_ptr(Pool<Task*>::getpool())
    {}
    
    ~tcpServer(){
        if (_listenSock > 0) {
            close(_listenSock);
        }
    }

    void initServer(){
        //backlog不能太大也不能太小
        static int gbacklog = 20;
        //申请描述符
        _listenSock = socket(AF_INET, SOCK_STREAM, 0);
        if (_listenSock < 0) {
            std::cerr << "注册socket失败" << std::endl;
            exit(2);
        }
        //绑定端口号和IP地址
        struct sockaddr_in local;
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = _ip.empty() ? INADDR_ANY : inet_addr(_ip.c_str());
        local.sin_port = htons(_port);

        if (bind(_listenSock, (struct sockaddr*)&local, sizeof(local)) < 0) {
            std::cerr << "绑定ip和端口号失败" << std::endl;
            exit(3);
        }
        //设置监听状态
        if (listen(_listenSock, gbacklog) < 0) {
            std::cerr << "设置监听失败" << std::endl;
            exit(4);
        }
    }

    void start() {
        _pool_ptr->strat();
        while (1) {
            struct sockaddr_in user;
            socklen_t len = sizeof(user);
            int serviseSock = accept(_listenSock, (struct sockaddr*)&user, &len);
            std::string userip = inet_ntoa(user.sin_addr);
            int16_t userport = ntohs(user.sin_port);
            Task *task = new Task(serviseSock, userip, userport, servise);
            _pool_ptr->pushTask(task);
        }
    }

private:
    std::string _ip;
    int16_t _port;
    int _listenSock;
    Pool<Task*>* _pool_ptr;
};
cpp 复制代码
//服务器入口,
//tcp_server.cpp
#include "tcp_server.hpp"
#include <memory>

int main(int argc, char* args[]) {
    std::string ip;
    int16_t port;
    if (argc == 2) {
        ip = "";
        port = atoi(args[1]);
    }
    else if (argc == 3) {
        ip = args[1];
        port = atoi(args[2]);
    }
    else {
        std::cerr << "输入错误!" << std::endl;
        exit(1);
    }
    std::unique_ptr<tcpServer> server(new tcpServer(port,ip));
    server->initServer();
    server->start();

    return 0;
}

3.2.3 运行结果

3.3 代码中的一些函数

3.3.1 地址转换函数

本节基于IPv4的socket网络编程,sockaddr_in中的成员struct in_addr sin_addr表示32位 的IP 地址但是我们通常用点分十进制的字符串表示IP 地址,以下函数可以在字符串表示 和in_addr表示之间转换;

字符串与in_addr的一些函数:

cpp 复制代码
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

int inet_aton(const char *cp, struct in_addr *inp);
in_addr_t inet_addr(const char *cp);
in_addr_t inet_network(const char *cp);
char *inet_ntoa(struct in_addr in);
struct in_addr inet_makeaddr(int net, int host);
in_addr_t inet_lnaof(struct in_addr in);
in_addr_t inet_netof(struct in_addr in);

3.3.2 udp使用的的函数

发送函数sendto:

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                const struct sockaddr *dest_addr, socklen_t addrlen);

接收函数recvfrom:

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                  struct sockaddr *src_addr, socklen_t *addrlen);

3.3.3 tcp使用的函数

发送函数

cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

ssize_t send(int sockfd, const void *buf, size_t len, int flags);
cpp 复制代码
#include <unistd.h>

ssize_t write(int fd, const void *buf, size_t count);

接收函数

cpp 复制代码
#include <unistd.h>

ssize_t read(int fd, void *buf, size_t count);
cpp 复制代码
#include <sys/types.h>
#include <sys/socket.h>

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

4、结语

本文中若有错误,请私信或评论指出,谢谢!

相关推荐
是小李呀~11 分钟前
websocket实战
网络·websocket·网络协议
天上掉下来个程小白29 分钟前
请求响应-08.响应-案例
java·服务器·前端·springboot
IChen.1 小时前
解决centos 删除文件后但空间没有释放
linux·运维·centos
多恩Stone2 小时前
【vs code(cursor) ssh连不上服务器】但是 Terminal 可以连上,问题解决 ✅
运维·服务器·ssh
何陈陈2 小时前
【Linux】线程池
linux·服务器·开发语言·c++
S hh2 小时前
【Linux 】文件描述符fd、重定向、缓冲区(超详解)
linux·运维·服务器
DuoRuaiMiFa2 小时前
Linux系统性能调优实战指南
linux
沉登c2 小时前
Javascript客户端时间与服务器时间
服务器·javascript
憧憬一下2 小时前
线程池的实现和讲解:解决多线程并发服务器创建销毁线程消耗过大的问题
linux·线程池·c/c++·嵌入式linux