Linux TCP多线程服务器

一、多线程开发

线程和进程

程序写好存储在硬盘介质里,CPU读取程序到内存 ,这个在内存中的可执行程序实例就叫进程。一个程序如果多次读取到内存中,那他们就是各自独立的进程

内存中的任何位置都有相应的地址方便访问,而在内存中的进程,自己内部都有一个虚拟独立的地址空间。

每个进程首先有加载的程序,通常只有一个程序计数器,用来记录当前程序执行的位置,会按照程序顺序计算,这里的一个执行流就是一个线程。如果有多个线程,就需要多个程序计数器,每个线程会独立运行。除此之外,每个进程还有寄存器、

堆栈等程序运行时的状态信息,同时线程间共享的则有地址空间、全局变量、打开的文件等等信息。

那么为什么进程中还要有更小的"进程" - 线程

假如进程是一个文档编辑器,存放着相应的程序和文档,现在用户用键盘敲下回车,交互的程序接收键盘的按下事件,布局的程序将文字重新排布渲染出来,另外每隔一段时间,写入的程序保存文档到硬盘中,所以这三个程序最好能并行执行,但他们又需要访问修改同一个文档,所以肯定是在同一个进程中,所以现在需要更轻量级的3个线程,交互线程, 渲染线程,保存线程。

因此,线程是并行的最小单位,假如计算机只有一个单核CPU,即同一时刻只能执行一个线程,对每个线程快速切换轮流执行。为了简化,CPU在内核中为每个线程提供各自虚拟的CPU,每个线程会认为自己独占着CPU,他们就不需要考虑时间片轮转的问题了。

一句人尽皆知的终结:进程是资源分配的最小单位,线程是CPU调度/程序执行的最小单位

并行和并发

假设有多个线程(CPU需要执行的任务),CPU的一个核心挨个挨个处理叫批处理。通过时间片轮转的方式快速切换,就是并发(多线程处理),虽然CPU切换速度非常快宏观上看起来多个线程是并行运行的,但实际不是。批处理和并发的实际处理时间是一样的

而现代的多核系统下,假设系统中有两个核心有两个线程,那么此时操作系统可以把两个线程分配给两个CPU核心,那么此时,这两个线程就可以同时进行了,这就是并行

并行和并发的区别就是在于是否一个核心还是多个核心吗?

实际上更加复杂,上述内容有一个前提就是假设线程之间是独立的,现在假设系统中有两个核心有两个线程,这两个线程需要持有同一把锁,拿到锁的线程才能向前运行,所以此时即使有两个核心也无法做到并行。这也是为什么高性能程序要避免使用锁。

所以说并行和并发不是说单核就是并发,多核就是并行,还要看线程之间的依赖关系。只有在多核,且任务之间没有关系的情况下,才叫真正的并行。当然,多核系统下,如果线程数多余核心数,既有并行也有并发。假设两个核心1,2,4个任务a,b,c,d,ab分配给了核心1,cd分配给了核心2,此时ab就是并发,cd也是并发,a与c就可以并行,b与d也可以并行

网络中进程之间如何通信?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的"ip地址"可以唯一标识网络中的主机,而传输层的"协议+端口"可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

多线程开发

top     ps-ef  都可以查看进程

进程

进程有独立的地址空间

Linux为每个进程创建task struct

每个进程都参与内核调度,互不影响
线程

线程没有独立的内存空间,一个进程内所有线程公用一个

线程仅仅有自己的独立栈空间
使用线程的好处

大大提高了任务切换的效率避免了额外的TLB & cache的刷新

1、创建线程 pthread_create
#include <pthread.h>
int pthread_create(pthread_t *thread, const pthread_attr_t *attr,void *(*start_routine) (void *), void *arg);
参数
pthread_t *thread: 线程id指针,存放tid的
onst pthread_attr_t *attr:	线程属性
void *(*start_routine) (void *):	线程回调函数(函数指针)
void *arg:	函数参数
c 复制代码
#include<stdio.h>
#include<pthread.h>
#include <unistd.h>

int funct(int a){
	printf("hello\n");
	pause();
}

int main()
{
	pthread_t tid;
	int ret=pthread_create(&tid,NULL,funct,NULL);
	if(ret<0){
		printf("create thread error\n");
	}
	sleep(2);
	pause(); //进程结束,线程也就结束了
	return 0;
}
查看线程 ps -eLf | grep progressNAme
查看自己的tid pthread_t pthread_self(void)
2、线程的退出
  • 主动退出
    return 退出,
    void pthread_exit(void* retval) //类似于exit(),直接可以NULL
  • 被动退出
    pthread_cancel(tid);
  • 资源回收
    int pthread_join(pthread_t thread,void **retval); 需要父进程回收,是一个阻塞函数,即会等待线程结束
    int pthread_detach(pthread_t thread);
3、线程同步

访问共享资源之前上锁,结束之后上锁。但频繁的加锁解锁是低效的,会完全打破多线程的并发运行;另外多个锁的嵌套使用很有可能导致锁现象

二、Linux多线程TCP服务器

socket函数

int socket(int domain, int type, int protocol);  创建一个通信端点,并返回一个指向该端点的文件描述符

domain :	选择用于通信的协议族。如AF_INET   :    IPv4 Internet protocols                    

type:	套接字具有指定的类型,该类型指定了通信语义。 如 SOCK_STREAM  提供有序、可靠、双向、基于连接的字节流。 可支持带外数据传输机制

protocl:	指定与套接字一起使用的特定协议。 通常情况下,在给定的协议族中,只有一种协议支持特定的套接字类型,
		在这种情况下,协议可以指定为 0。然而也可能存在多种协议,在这种情况下,必须在本手册中指定特定的协议

memset函数

void *memset(void *s, int c, size_t n); 初始化内存
The memset() function fills the first n bytes of the memory area pointed to by s with the constant byte c

bind函数

给socket帮 ip和端口号

 int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
 
当使用 socket() 创建套接字时,它存在于name space中,但没有分配地址。bind() 将 addr 指定的地址分配给文件描述符 sockfd 所指向的套接字。 addrlen 指定 addr 指向的地址结构体的大小。传统上,这一操作被称为 "为套接字分配name"。

On success, zero is returned.  On error, -1 is returned

listen函数

socket变为监听模式

int listen(int sockfd, int backlog);  listen for connections on a socket
listen() 会将 sockfd 所指向的套接字标记为可连接套接字,即使用 accept() 接受传入连接请求的套接字。

sockfd :文件描述符,指向 SOCK_STREAM 或 SOCK_SEQPACKET 类型的套接字。

backlog :定义了 sockfd 的待处理连接队列可能增长的最大长度。  如果连接请求在队列已满时到达,客户端可能会收到
带有 ECONNREFUSED 指示的错误信息,或者,如果底层协议支持重传,该请求可能会被忽略,以便以后重新尝试连接时获得成功。

accept函数

等待客户端连接,阻塞状态

int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);   accept a connection on a socket

accept()  用于基于连接的套接字类型(SOCK_STREAM、SOCK_SEQPACKET)。 它从监听套接字、sockfd 的待处理连接队列中
提取第一个连接请求,创建一个新的连接套接字,并返回一个指向该套接字的新文件描述符。 新创建的套接字不处于监听状态。 原始
套接字 sockfd 不受此调用影响。

参数
 sockfd :是用 socket() 创建的套接字,用 bind() 绑定到本地地址,并在 listen() 之后监听连接。
 
addr :指向 sockaddr 结构的指针。 该结构由通信层已知的对等套接字地址填充。 addr 返回地址的具体格式由套接字的地址系列决定。 当 addr 				
       为 NULL 时,将不填写任何内容;在这种情况下,addrlen 将不被使用,也应为 NULL。

addrlen :一个值-结果参数:调用者必须初始化它,使其包含 addr 指向的结构体的大小(字节);

返回:返回一个指向该套接字的新文件描述符。它将包含对等地址的实际大小。如果提供的缓冲区太小,返回的地址将被截断;在这种情况下,addrlen 返回的值将大于调用时提供的值。

recv函数

size_t recv(int sockfd, void *buf, size_t len, int flags);  recv, recvfrom, recvmsg - receive a message from a socket
用于接收来自套接字的信息。 它们可用于接收无连接和面向连接套接字上的数据。
recv() 与 read() 的唯一区别在于是否有标志。 如果 flags 参数为零,recv() 通常等同于 read()

如果套接字上没有可用的消息,接收调用将等待消息的到来,除非套接字是非阻塞的,在这种情况下将返回值-1,并将外部变量 errno 设为 EAGAIN 或 EWOULDBLOCK。  
接收调用通常会返回任何可用的数据,最多不超过请求的数量,而不是等待收到请求的全部数据。

参数:
int sockfd:已连接的套接字confd,
void *buf:	接收缓冲区
size_t len:希望接收数据的最大字节长度
int flags:

返回值: return the number of bytes received, or -1 if an error occurred. 连接套接字关闭时,返回0

实现单线程

c 复制代码
#include <stdio.h>      // 引入标准输入输出库  
#include <stdlib.h>     // 引入标准库,用于exit等函数  
#include <string.h>     // 引入字符串处理库,用于strcmp等函数  
#include <unistd.h>     // 引入POSIX操作系统API,用于close等函数  
#include <sys/socket.h> // 引入套接字编程库  
#include <netinet/in.h> // 引入IPv4和IPv6地址定义  
#include <arpa/inet.h>  // 引入地址转换函数,如inet_addr  
  
#define MAXLINE 4096    // 定义接收缓冲区的大小  
  
int main(int argc, char** argv)  
{  
    int listenfd, connfd; // 分别用于监听和连接的套接字文件描述符  
    struct sockaddr_in serveraddr; // 存储服务器地址信息的结构体  
    char buff[MAXLINE]; // 接收消息的缓冲区  
    int n, ret; // 分别用于存储接收到的字节数和比较结果  
  
    // 创建TCP套接字  
    //netstat   a 不仅显示套接字内容的命令,还显示尚未开始通信等状态的所有套接字 
    //			n 显示ip地址和端口号 
    // 			o 显示使用该套接字的程序PID
    if ((listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {  
        printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);  
        exit(0); // 创建失败则退出  
    }  
  
    // 初始化服务器地址信息  
    memset(&serveraddr, 0, sizeof(serveraddr));  
    serveraddr.sin_family = AF_INET; // 使用IPv4  
    serveraddr.sin_addr.s_addr = inet_addr("10.47.158.12"); // 设置服务器IP地址  
    serveraddr.sin_port = htons(6666); // 设置服务器端口号,htons用于主机字节序到网络字节序的转换  
  
    // 绑定套接字到服务器地址和端口  
    if (bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {  
        printf("bind socket error: %s(errno:%d)\n", strerror(errno), errno);  
        exit(0); // 绑定失败则退出  
    }  
  
    // 监听连接请求  
    if (listen(listenfd, 10) < 0) {  
        printf("listen socket error: %s(errno:%d)\n", strerror(errno), errno);  
        exit(0); // 监听失败则退出  
    }  
  
    printf("=========waiting for client's request.....=====\n"); // 打印等待客户端请求的消息  
  
    // 无限循环,接受并处理客户端连接  
    while(1) {  
    	//如果已建立连接,则不再调用accept,否则调用accept会生成新的connfd ,结果只会打印一次数据
    	if(connfd==0){
    	        if ((connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1) {  
            		printf("accept socket error:%s(errno:%d)\n", strerror(errno), errno);      
            		continue;  
        	}  
    	}

        // 从连接套接字接收消息  
        n = recv(connfd, buff, MAXLINE, 0);  
        if (n <= 0) {  
            // 如果n <= 0,表示连接已关闭或发生错误  
            close(connfd);  
            continue;  
        }  
        buff[n] = '\0'; // 在接收到的字符串末尾添加空字符  
  
        // 比较接收到的消息是否为"exit\n"  
        ret = strcmp(buff, "exit\n");  
        if (ret == 0) {  
            // 如果是,则关闭监听套接字并退出循环  
            close(listenfd);  
            close(connfd); // 注意:虽然此时connfd可能不再需要,但关闭是个好习惯  
            break;  
        }  
        printf("recv msg from client: %s\n", buff); // 打印接收到的消息  
  
        // 关闭连接套接字(在实际应用中,可能会在此处处理更多数据或保持连接)  
        close(connfd);  
    }  
  
    // 退出循环后,关闭监听套接字(注意:在上面的"exit"情况下,这一步会被跳过)  
    close(listenfd);  
  
    return 0; // 程序正常退出  
}
c 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<unistd.h>
#include <arpa/inet.h>

#define MAXLINE 4096

int main(int argc, char** argv)
{
        int listenfd, connfd;
        struct sockaddr_in serveraddr;
        char buff[MAXLINE];
        int n, ret;

        if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // third param is 0,means select default protocol
                printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
                exit(0);
        }

        memset(&serveraddr, 0, sizeof(serveraddr));
        serveraddr.sin_family = AF_INET; //ipv4网络类型
        serveraddr.sin_addr.s_addr = (inet_addr("192.168.72.128"));  // INADDR_ANY: set addr 0.0.0.0, means all addr
        serveraddr.sin_port = htons(1234);  // 绑定端口号

        if ( bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {
                printf("bind socket error: %s(errno:%d)\n", strerror(errno), errno);
                exit(0);
        }

        if (listen(listenfd, 10) < 0) {
                printf("listen socket error: %s(errno:%d)\n", strerror(errno), errno);
                exit(0);
        }

        printf("=========waiting for client's request.....=====\n");
        

    //    if ( (connfd = accept(listenfd, (struct sockaddr*)NULL, NULL)) == -1) {
      //          printf("accept socket error:%s(errno:%d)\n", strerror(errno), errno);
                //continue;
      //  }

        while(1) {
        	// ccept(listenfd, (struct sockaddr*)&clientaddr, &addrlen);  //clientaddr  ip地址, &addrlen 端口号
		connfd = accept(listenfd, (struct sockaddr*)NULL, NULL);
		if(connfd<0){
			printf("accept socket error:%s(errno:%d)\n", strerror(errno), errno);
			return -1;
		}
		
		if(connfd>0){
			while(1){
			n = recv(connfd, buff, MAXLINE, 0); //断开连接后则不再阻塞
			if(n==0){
				printf("connect %d close\n",connfd);
				printf("=========waiting for client's request.....=====\n");
				close(connfd);
				
				break;
			}
                	buff[n] = '\0';
                	printf("recv msg from client: %s\n", buff);
                	
                	ret = strcmp(buff, "exit\n"); //相等返回0,客户端发送exit\n则断开连接
                	if (ret == 0){
                		close(connfd);
                		break;
                	}
			
			}

                      	  
		}
        }
        close(listenfd);
        return 0;
}

实现多线程

TCP服务器:该代码实现了一个基本的TCP服务器,能够监听指定端口上的连接请求,并接受来自客户端的数据。

多线程处理:服务器使用多线程来同时处理多个客户端连接。每个新连接都会触发一个新线程的创建,该线程负责与该客户端的通信。主线程(main函数中的循环)继续监听新的连接请求,并为每个新请求创建新的线程。这个过程会一直持续下去,直到程序被外部方式(如用户中断)终止。

数据接收与响应:服务器能够接收来自客户端的数据,并打印到标准输出。如果接收到特定的字符串(exit\n),则关闭与该客户端的连接。

错误处理:代码中包含了对套接字操作(如创建、绑定、监听、接受连接)的错误处理,如果操作失败,则会打印错误信息并退出程序(或继续监听其他连接)

c 复制代码
#include<stdio.h>  
#include<stdlib.h>  
#include<string.h>  
#include<errno.h>  
#include<sys/types.h>  
#include<sys/socket.h>  
#include<netinet/in.h>  
#include<unistd.h>  
#include <arpa/inet.h>  
#include <pthread.h>  
  
#define MAXLINE 4096  
  
void* handle_client(void* arg) {  
    int connfd = *(int*)arg;  
    free(arg);  // 释放传递的指针  
    char buff[MAXLINE];  
    int n;  
  
    while (1) {  
        n = recv(connfd, buff, MAXLINE, 0);  
        if (n == 0) {  
            printf("Client %d disconnected\n", connfd);  
            close(connfd);  
            break;  
        }  
        buff[n] = '\0';  
        printf("Received from client: %s\n", buff);  
  
        if (strcmp(buff, "exit\n") == 0) {  
            close(connfd);  
            break;  
        }  
  
        // 在这里可以添加对客户端消息的处理逻辑  
    }  
  
    return NULL;  
}  
  
int main(int argc, char** argv)  
{  
    int listenfd, connfd;  
    struct sockaddr_in serveraddr;  
    pthread_t tid;  
    int *new_sock;  
    char buff[MAXLINE];
    int n, ret;

     if ( (listenfd = socket(AF_INET, SOCK_STREAM, 0)) == -1) { // third param is 0,means select default protocol
     	printf("create socket error: %s(errno: %d)\n", strerror(errno), errno);
        exit(0);
     }

     memset(&serveraddr, 0, sizeof(serveraddr));
     serveraddr.sin_family = AF_INET; //ipv4网络类型
     serveraddr.sin_addr.s_addr = (inet_addr("192.168.72.128"));  // INADDR_ANY: set addr 0.0.0.0, means all addr
     serveraddr.sin_port = htons(1234);  // 绑定端口号

     if ( bind(listenfd, (struct sockaddr*)&serveraddr, sizeof(serveraddr)) == -1) {
         printf("bind socket error: %s(errno:%d)\n", strerror(errno), errno);
         exit(0);
     }

     if (listen(listenfd, 10) < 0) {
         printf("listen socket error: %s(errno:%d)\n", strerror(errno), errno);
         exit(0);
     }

  
    printf("=========Waiting for client's request.....=====\n");  
  
    while (1) {  
        connfd = accept(listenfd, NULL, NULL);  
        if (connfd < 0) {  
            printf("Accept error: %s (errno: %d)\n", strerror(errno), errno);  
            continue;  
        }  
  
        // 为新连接创建新线程  
        new_sock = malloc(sizeof(int));  
        *new_sock = connfd;  
        if (pthread_create(&tid, NULL, handle_client, (void*)new_sock) != 0) {  
            perror("Failed to create thread");  
            close(connfd);  
            free(new_sock);  
            continue;  
        }  
  
        // 可以在这里继续处理其他连接,或者什么都不做(由主线程循环负责)  
    }  
  
    close(listenfd);  
    return 0;  
}

mysql部分

mysql_real_connect

MYSQL *mysql_real_connect(MYSQL *mysql, const char *host, const char *user,  
                          const char *passwd, const char *db, unsigned int port,  
                          const char *unix_socket, unsigned long clientflag);
  • mysql: 指向 MYSQL 结构的指针,这个结构由 mysql_init 函数初始化。这个结构包含了连接所需的所有信息。
  • host: MySQL 服务器的地址,可以是 IP 地址或者域名。如果 host 是 NULL 或 "localhost",客户端将尝试通过 UNIX socket 连接。
  • user: 连接 MySQL 服务器所使用的用户名。
  • passwd: 用户的密码。如果密码是 NULL,客户端将尝试使用空密码进行连接。
  • db: 尝试连接后立即使用的默认数据库名。如果不需要默认数据库,可以将其设置为 NULL。
  • port: MySQL 服务器的端口号。如果设置为 0,客户端将使用默认的 MySQL 端口(通常是 3306)。
  • unix_socket: UNIX socket 文件的路径(如果服务器运行在 UNIX 上)。如果 host 参数是 NULL 或 "localhost",并且 unix_socket 不是 - NULL,客户端将尝试通过 UNIX socket 而不是 TCP/IP 连接到服务器。
  • clientflag: 一个位掩码,用于修改客户端的默认行为。这些标志可以组合使用,以提供对连接行为的更细粒度控制。例如MYSQL_CLIENT_COMPRESS 可以用来请求使用压缩协议。
  • 返回值: 成功,mysql_real_connect 返回一个指向 MYSQL 结构的指针,该结构与传递给函数的指针相同;失败,返回 NULL。你可以通过调用 mysql_error 函数来获取关于连接失败的错误消息。

在使用完 MYSQL 结构后,应使用 mysql_close 函数来关闭连接并释放相关资源。

mysql_real_connect 是线程安全的,但 MYSQL 结构本身不是线程安全的。如果你在多线程环境中工作,每个线程都应该有自己的 MYSQL 结构实例。

mysql_error (指定要使用的 MySQL 连接标识符)

返回值

成功时,如果上一个操作没有错误,则返回空字符串("")。

失败时,返回描述上一个 MySQL 操作的错误的字符串。

c 复制代码
#include <mysql.h>  
#include <stdio.h>  
#include <stdlib.h>  
  
int main() {  
    MYSQL *conn;  //MYSQL 是一个结构体类型,它包含了与 MySQL 数据库服务器建立连接所需的所有信息
    MYSQL_RES *res;  //指向MYSQL_RES的指针,存储结果
    MYSQL_ROW row;  //一个字符指针数组(char **)的别名,它用于表示结果集中的一行数据
  
    // 初始化 MySQL 连接  
    conn = mysql_init(NULL);  
  
    // 连接到 MySQL 数据库  
    if (!mysql_real_connect(conn, "localhost", "your_username", "your_password", "testdb", 0, NULL, 0)) {  
   		 //fprintf 函数是 C 语言标准库中的一个非常有用的函数,它用于向文件或标准输
        fprintf(stderr, "%s\n", mysql_error(conn));  出(如屏幕)写入格式化的数据。
        exit(1);  
    }  
  
    // 插入数据  
    if (mysql_query(conn, "INSERT INTO users (name) VALUES ('John Doe')")) {  
        fprintf(stderr, "%s\n", mysql_error(conn));  
        mysql_close(conn);  
        exit(1);  
    }  
  
    // 查询数据  
    if (mysql_query(conn, "SELECT * FROM users")) {  
        fprintf(stderr, "%s\n", mysql_error(conn));  
        mysql_close(conn);  
        exit(1);  
    }  
  
    res = mysql_use_result(conn);  
    while ((row = mysql_fetch_row(res)) != NULL) {  
        printf("%s\n", row[1]); // 假设 name 是第二列(索引从 0 开始)  
    }  
    mysql_free_result(res);  
  
    // 更新数据  
    if (mysql_query(conn, "UPDATE users SET name = 'Jane Doe' WHERE id = LAST_INSERT_ID()")) {  
        fprintf(stderr, "%s\n", mysql_error(conn));  
        mysql_close(conn);  
        exit(1);  
    }  
  
    // 再次查询以验证更新  
    if (mysql_query(conn, "SELECT * FROM users")) {  
        fprintf(stderr, "%s\n", mysql_error(conn));  
        mysql_close(conn);  
        exit(1);  
    }  
  
    res = mysql_use_result(conn);  
    while ((row = mysql_fetch_row(res)) != NULL) {  
        printf("%s\n", row[1]);  
    }  
    mysql_free_result(res);  
  
    // 删除数据  
    if (mysql_query(conn, "DELETE FROM users WHERE id = LAST_INSERT_ID()")) {  
        fprintf(stderr, "%s\n", mysql_error(conn));  
        mysql_close(conn);  
        exit(1);  
    }  
  
    // 关闭数据库连接  
    mysql_close(conn);  
  
    return 0;  
}
相关推荐
姜西西_7 分钟前
[网络编程]通过java用TCP实现网络编程
java·网络·tcp/ip
司职在下20 分钟前
828华为云征文|Flexus云服务器X实例快速部署在线测评平台,适用各种信息学教学
运维·服务器·华为云
银氨溶液1 小时前
DNS解析域名详解
linux·服务器·apache·域名解析
乱搭巴士1 小时前
【IP网址正则表达式匹配】java,IPv4网址正则表达式匹配
java·网络协议·tcp/ip
阿瑾06181 小时前
【Linux】进程间通信——System V共享内存
linux·运维·服务器
leSerein_1 小时前
【Docker】docker的一些常用命令
linux·运维·docker·容器
不会代码的小徐1 小时前
Shell脚本监控Centos 7系统运行状态
linux·运维·centos
十一吖i1 小时前
uniapp实现下拉刷新
linux·服务器·uni-app
SofterICer2 小时前
Test-Specification-v2_3_1-1
linux·运维·服务器
哲伦贼稳妥2 小时前
一天认识一个硬件之服务器
运维·服务器·经验分享·硬件工程