IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> 《TCP/IP网络编程》第10,11章知识点汇总 -> 正文阅读

[网络协议]《TCP/IP网络编程》第10,11章知识点汇总

10. 多进程服务器

实现服务器端并发的方法

  • 多进程
  • 多路复用
  • 多线程

多进程服务器不适应于Windows,适用于linux平台

cpu核数<进程数时,将启用分时机制

进程ID从2开始,1被分配给OS启动后的首个进程,用于协助OS

ps au 查看正在进程的详细信息

10.1 fork创建进程

#include <unistd.h>
pid_t fork(void);
//成功时返回进程ID,失败时返回-1

fork创建调用它的进程的副本,被创建的副本将从调用fork()语句的后一句开始执行(子进程当前的fork()语句返回值规定为0)

  • 父进程:fork()返回子进程ID
  • 子进程:fork()返回0

父子进程相当于完全独立的两个进程,互不影响,只是共享同样的代码,只是注意fork的返回值

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main(void){
    pid_t pid ;
    pid = fork();  
    if(pid == 0) printf("this is child process\n");
    else printf("this is parent process\n");
    
    return 0;
}

在这里插入图片描述

10.2 僵尸进程

僵尸进程

产生原因:fork产生的子进程结束后,并未被完全销毁,会留下一个称为僵尸进程(Zombie)的数据结构。这是因为子进程的返回值和状态信息等不能主动传给父进程,因此只能一直存在直到这些值被接收;

僵尸进程几乎不占内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态等信息供其他进程收集。

父进程如果未回收僵尸进程,那么父进程结束后,僵尸进程也会被init进程自动回收

  • 测试僵尸进程:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main(void){
    pid_t pid ;
    pid = fork();  
    if(pid == 0) printf("this is child process\n");
    else{
        sleep(30);//在父进程结束之前的这30s内,可以观察到僵尸进程,结尾带有 <defunc 标识
        printf("this is parent process\n");
    }
    
    return 0;
}

在这里插入图片描述

以后台的方式运行,这样在输入ps au时不用新开终端
root@localhost# ./test &

销毁僵尸进程 wait

wait函数

#include <sys/wait.h>
pid_t wait(int* statloc);
//成功时返回子进程ID,失败时返回-1

因为返回值中还包含其他信息,所以需要用下列宏进行分离

WIFEXITED	子迸程正常终止时返回“真”(true)
WEXITSTATUS	返回子进程的返回值

有多少fork,多少子进程,就要多少次wait,如果wait次数多了,将陷入阻塞状态

  • wait测试
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>

int main(void){
    pid_t pid = fork();
    int status;
    if(pid == 0) return 3;
    else{
        pid = fork();
        if(pid == 0) return 7;
        else{
            wait(&status);
            if(WIFEXITED(status)) printf("child return one: %d\n", WEXITSTATUS(status));
            wait(&status);
            if(WIFEXITED(status)) printf("child return one: %d\n", WEXITSTATUS(status));
            sleep(30);//这时观察不到僵尸进程了
        }
    }
    return 0;
}

在这里插入图片描述

销毁僵尸进程 waitpid

waitpid函数

#include <unistd.h>
pid_t waitpid(pid_t pid, int *statloc, int options);
//成功时返回终止的子进程ID(或0),失败时返回-1

参数

pid		等待终止的目标子进程的ID ,若传递-1,则与wait函数相同,可以等待任意子进程终止
options 传递头文件sys/wait.h中声明的常量WNOHANG ,即使没有终止的子进程也不会进入阻塞状态,而是返回0并退出函数
  • 测试waitpid
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main(void){
    pid_t pid = fork();
    int status;
    if(pid == 0){
        sleep(15);
        return 3;
    }else{
        while(!waitpid(-1, &status, WNOHANG)){
            sleep(3);
            puts("sleep 3sec.");
        }
        if(WIFEXITED(status)) printf("child return one: %d\n", WEXITSTATUS(status));
    }
    return 0;
}

在这里插入图片描述

可以看到即便等不到僵尸进程,waitpid也不会阻塞;

  • 同样的代码换成wait,则变成了
while(!wait(&status)){
    sleep(3);
    puts("sleep 3sec.");
}

在这里插入图片描述

10.3 信号处理

signal

  • signal()

问题:父进程不能一直while循环waitpid等待子进程的结束
解决:子进程终止的识别主体是操作系统,因此引入信号处理(Signal Handling)机制

#include <stdio.h>
void (*signal(int signo, void (*func)(int)))(int);
//为了在产生信号时调用, 返回之前注册的函数指针

在这里插入图片描述
signo表示特殊事件,func是发生该特殊事件要调用的函数。signo特殊事件如下
在这里插入图片描述

  • alarm()
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
//返回0或以秒为单位的距SIGALRM信号发生所剩时间
//调用该函数,seconds秒之后将产生一个SIGALRM信号
  • 测试
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

void timeout(int sig){
    if(sig == SIGALRM) puts("Time out!");
    alarm(2);//2秒后再产生一个SIGALRM信号
}

void keycontrol(int sig){
    if(sig == SIGINT) puts("CTRL+C pressed");
}

int main(void){
    int i;

    signal(SIGALRM, timeout);//此时不会执行timeout,signal只是注册而已
    signal(SIGINT, keycontrol);//此时不会执行keycontrol,只进行注册
    alarm(2);//2秒后产生一个SIGALRM信号

    for(i=0; i<3; ++i){
        puts("wait...");
        sleep(100);
    }
    /*
    结果说明:
    1. signal只是向OS注册,不会执行注册的函数,因此运行来到main里面的alarm
    2. alarm(2)表示2秒后产生一个SIGALRM信号,在这2秒内,程序继续允许,来到for循环
    3. for循环立即输出第一次"wait...",并进入sleep阻塞状态
    4. 2秒后,之前的alarm将产生SIGALRM信号,OS捕捉到这一信号后,唤醒进程来调用对应的注册函数,即终止sleep(),因此很快就执行完毕了
    */
    return 0;
}

在这里插入图片描述

产生信号时,为了调用信号处理器,OS将唤醒主进程,即便未达到sleep()规定的时间

在这里插入图片描述

如果使用ctrl+c使用SIGINT的注册函数,也会唤醒进程

sigaction(常用)

可以替换signal,signal 函数在UNIX 系列的不同操作系统中可能存在区别,但sigaction函数完全相同。

  • sigaction
#include <signal.h>
int sigaction(int signo, const struct sigaction * act, struct sigaction * oldact);
//成功时返回0,失败时返回-1

act 对应于第一个参数的信号处理函数( 信号处理器) 信息
oldact 通过此参数获取之前注册的信号处理函数指针,若不需要则传递0

  • sigaction结构体
struct sigaction{
    void (*sa_handler)(int);//对应信号的处理函数
    sigset_t sa_mask;//初始化为0,后续再讲
    int sa_flags;//初始化为0,后续再讲
}
  • 示例

在CentOS 7环境下vscode中,使用struct sigaction act报错,但报错不影响运行成功,原理未知

#include <stdio.h>
//#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

void timeout(int sig){
    if(sig == SIGALRM) puts("Time out!");
    alarm(2);//2秒后再产生一个SIGALRM信号
}
int main(void){
    int i;

    struct sigaction act;
    act.sa_handler = timeout;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;

    sigaction(SIGALRM, &act, 0);
    alarm(2);

    for(i=0; i<3; ++i){
        puts("wait...");
        sleep(100);
    }
    return 0;
}

结果和signal一致

sigaction 处理 僵尸进程

  • 一般waitpid处理僵尸进程
int main(void){
    pid_t pid = fork();
    int status;
    if(pid == 0) printf("this is child process\n");
    else{
        printf("this is parent process\n");
        sleep(5);//在这5秒内能观察到僵尸进程
        while(!waitpid(-1, &status, WNOHANG));
        sleep(10);//此时僵尸进程已被回收
    }
    return 0;
}
  • waitpid等待子进程完成
int main(void){
    pid_t pid = fork();
    int status;
    if(pid == 0){
        sleep(15);
        return 3;
    }else{
        while(!waitpid(-1, &status, WNOHANG)){
            sleep(3);
            puts("sleep 3sec.");
        }//为了得到子进程的返回值,尽管waitpid不用阻塞,也要循环使其阻塞等待
        if(WIFEXITED(status)) printf("child return one: %d\n", WEXITSTATUS(status));
    }
    return 0;
}
  • 信号处理僵尸进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

void sig_wait(int sig){
    int status;
    pid_t pid;
    if(sig == SIGCHLD) printf("child process terminates\n");
    pid = waitpid(-1, &status, 0);
    if(WIFEXITED(status)) printf("child %d return one: %d\n", pid, WEXITSTATUS(status));
}

int main(void){
    int status, i;
    //注册函数
    struct sigaction act;
    act.sa_handler = sig_wait;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGCHLD, &act, 0);
     //第1个子进程
    pid_t pid = fork();
    if(pid == 0){
        //第1个子进程运行代码
        sleep(5);
        return 3;
    }else{
        //第2个子进程
        pid = fork();
        if(pid == 0){
            //第2个子进程运行代码
            sleep(3);
            return 7;
        }else{
            for(i=0; i<5; ++i){
                printf("wait...\n");
                sleep(100);//在第3秒时返回子进程2,在第5秒时返回子进程1; 当第3次循环时,因为无人唤醒,将阻塞100s
            }
        }
    }
    return 0;
}

练习,SIGINT信号的使用

要求:输入Ctrl+C时询问是否确定退出程序,输入Y则终止程序

#define SIZE 1024

void sig_ctrlc(int sig){
    char c;
    if(sig == SIGINT)
        printf("signal SIGINT\n");
    puts("do you want to exit from process?[Y]");
    c = getchar();
    getchar();//清除掉输入缓冲中的换行符
    if(c == 'Y') exit(1);//必须用exit才能退出
    else{
        printf("returned!\n");
        return;
    }
}

int main(int argc, char* argv[]){
    struct sigaction act;
    pid_t pid;

    act.sa_handler = sig_ctrlc;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGINT, &act, 0);

    while(1){
        sleep(3);
        printf("this is main process\n");
    }
    return 0;
}

10.4 基于多任务的并发服务器

在这里插入图片描述

每当有客户端请求连接时,服务器端就创建一个子进程来提供服务

查看和杀死端口号
netstat -tunlp|greo 端口号
kill -9 进程ID

echo_mpserver.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>
#include <signal.h>
#include <sys/types.h>

#define SIZE 1024

void error_handling(char* message){
    printf("%s\n", message);
    exit(1);
}

void sig_wait(int sig){
    pid_t pid;
    int status;
    pid = waitpid(-1, &status, 0);
    printf("******* enter signal SIGCHLD handler ********\n");
    if(WIFEXITED(status))
        printf("child process %d returned %d\n", pid, WEXITSTATUS(status));
    printf("******* leave signal handler ********\n");
}

int main(int argc, char* argv[]){
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_addr, clnt_addr;
    int clnt_addr_size;
    struct sigaction act;
    pid_t pid;
    int i, read_len;
    char buf[SIZE];

    //1. socket
    if((serv_sock = socket(PF_INET, SOCK_STREAM, 0)) == -1)
        error_handling("socker() error");
    //2. bind
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(atoi(argv[1]));
    serv_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    if(bind(serv_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("bind() error");
    //2.2 设置SO_REUSEADDR避免bind() error
    //int option = 1;
    //setsockopt(serv_sock, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option));
    //3. listen
    if(listen(serv_sock, 5) == -1)
        error_handling("listen() error");

    //4. sigaction注册子进程终止处理
    act.sa_handler = sig_wait;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGCHLD, &act, 0);

    for(i=0; i<5; ++i){//设置连接数为5,实际可以使用while(1)
        //5. accept
        clnt_addr_size = sizeof(clnt_addr);
        clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
        if(clnt_sock == -1){
            printf("accept() error\n");
            continue;
        }
        //6. fork创建子进程
        pid = fork();
        if(pid == -1){
            close(clnt_sock);
            continue;
        }
        if(pid == 0){
            //7.1 进入子进程程序,read和write
            close(serv_sock);//子进程也要关闭一次服务器端和客户端的套接字
            while(read_len = read(clnt_sock, buf, SIZE)){
                if(read_len == -1)
                    error_handling("read() error");
                buf[read_len] = '\0';
                printf("server received from client %d is: %s, len=%d\n", clnt_sock, buf, read_len);
                write(clnt_sock, buf, read_len);
            }
            close(clnt_sock);
            return 0;
        }else{
            //7.2 进入父进程程序
            close(clnt_sock);//套接字会被复制一份吗?
        }
    }
    close(serv_sock);

    return 0;
}

问题:accept() error
每一个子进程结束后,都会出现一个accept error(书上源代码也是如此),原因未明。

echo_mpclient.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>

#define SIZE 1024

void error_handling(char* message){
    printf("%s\n", message);
    exit(1);
}

int main(int argc, char* argv[]){
    int clnt_sock;
    struct sockaddr_in serv_addr;
    char buf[SIZE] = "Saturday";
    int read_len;
    //1. socket
    if((clnt_sock = socket(PF_INET, SOCK_STREAM, 0)) == -1)
        error_handling("socker() error");
    //2. 绑定服务器地址
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(atoi(argv[1]));
    serv_addr.sin_addr.s_addr = inet_addr(argv[2]);
    //3. connect
    if(connect(clnt_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) == -1)
        error_handling("connect() error");
    //4. read和write
    int i;
    for(i=0; i<5; ++i){
        sleep(2);//每隔2秒发送一次
        write(clnt_sock, buf, strlen(buf));
        read_len = read(clnt_sock, buf, SIZE);
        if(read_len == -1)
            error_handling("read() error");
        buf[read_len] = '\0';
        printf("received from server is: %s, len=%d\n", buf, read_len);
    }
    close(clnt_sock);
    return 0;
}

可以创建一个echo_mpclient2.c,改变buf和sleep的值,更好地观察多进程

使用fork()函数复制进程时,将文件描述符也复制一遍,但并没有复制套接字
父进程和子进程的文件描述符指向同样的通信双方套接字,因此只有在父进程和子进程中都close一遍套接字,才能真正销毁套接字

10.5 分隔TCP的I/O程序

在前面的程序中,客户端都是阻塞等待服务器端返回数据。在多进程中,可以通过分隔I/O来避免阻塞等待

在这里插入图片描述

close和shutdown在多进程中的使用:
close是计数,只有所有进程都close才能关闭套接字;shutdown是直接关闭,只要有一个进程调用,就可以关闭某个方向的套接字

  • 分隔I/O的示例(客户端)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <sys/wait.h>

#define SIZE 1024

void write_routine(int sock, char* buf){
    while(1){
        fgets(buf, SIZE, stdin);
        if(!strcmp(buf, "q\n") || !strcmp(buf, "Q\n")) {//strcmp相同返回0
            shutdown(sock, SHUT_WR);
            return;
        }
        write(sock, buf, strlen(buf));
    }
}

void read_routine(int sock, char* buf){
    while(1){
        int read_len = read(sock, buf, SIZE);
        if(read_len == 0)
            return;
        buf[read_len] = 0;
        printf("received from server is: %s, len=%d\n", buf, read_len);
    }
}

int main(int argc, char* argv[]){
    int clnt_sock;
    struct sockaddr_in clnt_addr, serv_addr;
    pid_t pid;
    char buf[SIZE];
    int read_len;

    //1. socket
    clnt_sock = socket(PF_INET, SOCK_STREAM, 0);
    //2. sock_addr
    memset(&serv_addr, 0, sizeof(serv_addr));
    serv_addr.sin_family = AF_INET;
    serv_addr.sin_port = htons(atoi(argv[1]));
    serv_addr.sin_addr.s_addr = inet_addr(argv[2]);
    //3. connect
    connect(clnt_sock, (struct sockaddr*)&serv_addr, sizeof(serv_addr));
    //4. fork
    pid = fork();
    //5. 子进程write数据
    if(pid == 0)
        write_routine(clnt_sock, buf);
    //6. 父进程read数据
    else
        read_routine(clnt_sock, buf);
    close(clnt_sock);
    return 0;
}

运行结果同前面一样

11. 进程间的通信

进程拥有独立的空间,哪怕是父子进程也无法直接通信

11.1 管道实现的进程通信

在这里插入图片描述

管道并非是进程的资源,而是和套接字一样属于操作系统,因此不能被fork复制

#include <unistd.h>
int pipe(int filedes[2]);
//成功时返回0,失败时返回-1

filedes[0] 通过管道接收数据时使用的文件描述符,即管道出口
filedes[1] 通过管道传输数据时使用的文件描述符,即管道入口。

  • 示例1:父子进程的通信(单向)
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

#define BUF_SIZE 1024

int main(void){
    int fds[2];
    char str[] = "pipe communication!";
    char buf[BUF_SIZE];
    pid_t pid;

    pipe(fds);
    pid = fork();
    if(pid == 0){
        //子进程
        write(fds[1], str, sizeof(str));
    }else{
        //父进程
        read(fds[0], buf, BUF_SIZE);
        puts(buf);
    }
    return 0;
}
  • 示例2:父子进程的通信(双向,单管道)
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

#define BUF_SIZE 1024

int main(void){
    int fds[2];
    char str1[] = "Who are you?";
    char str2[] = "Thank you for you message";
    char buf[BUF_SIZE];
    pid_t pid;

    pipe(fds);
    pid = fork();
    if(pid == 0){
        //子进程
        write(fds[1], str1, sizeof(str1));
        sleep(2);//如果不加这一句,子进程会先读取;导致父进程read阻塞
        read(fds[0], buf, BUF_SIZE);
        printf("child process received: %s\n", buf);
    }else{
        //父进程
        read(fds[0], buf, BUF_SIZE);
        printf("father process received: %s\n", buf);
        write(fds[1], str2, sizeof(str2));
        sleep(3);//注意父进程不能先比子进程结束,不然导致孤儿进程
    }
    return 0;
}

在这里插入图片描述

问题:在先进先出原则下 ,谁先读取谁就可以获得数据,如上述代码,如子进程先读取,则父进程read阻塞

  • 示例3:父子进程的通信(双向,双管道)
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

#define BUF_SIZE 1024

int main(void){
    int fds1[2], fds2[2];
    char str1[] = "Who are you?";
    char str2[] = "Thank you for you message";
    char buf[BUF_SIZE];
    pid_t pid;

    pipe(fds1);//fds1是从子进程到父进程
    pipe(fds2);//fds2是从父进程到子进程
    pid = fork();
    if(pid == 0){
        //子进程
        write(fds1[1], str1, sizeof(str1));
        read(fds2[0], buf, BUF_SIZE);
        printf("child process received: %s\n", buf);
    }else{
        //父进程
        read(fds1[0], buf, BUF_SIZE);
        printf("father process received: %s\n", buf);
        write(fds2[1], str2, sizeof(str2));
        sleep(3);//注意父进程不能先比子进程结束,不然导致孤儿进程
    }
    return 0;
}

11.2 echo_mpserver和进程通信

要求:用一个新的进程,接受从客户端传来的数据,并保存在文件中

  • 服务器端的修改部分(客户端也可以修改成手动输入):
int fds[2];
pipe(fds);
pid = fork();//创建子进程用来保存各个客户端传来的数据
//用于保存数据的子进程
if(pid == 0){
    FILE* fp = fopen("ehomsg.txt", "wt");//w表示以二进制写入,wt表示以文本格式写入
    char msgbuf[SIZE];
    int i, read_len;
    for(i=0; i<10; ++i){
        read_len = read(fds[0], msgbuf, SIZE);
        printf("saved %s\n", msgbuf);
        fwrite((void *)msgbuf, 1, read_len, fp);
    }
    fclose(fp);
    return 0;
}
//父进程就像之前那样正常工作,只是增加向管道传送数据的步骤
for(i=0; i<5; ++i){
    clnt_addr_size = sizeof(clnt_addr);
    clnt_sock = accept(serv_sock, (struct sockaddr*)&clnt_addr, &clnt_addr_size);
    if(clnt_sock == -1){
        printf("accept() error\n");
        continue;
    }
    pid = fork();
    if(pid == 0){
        close(serv_sock);
        while(read_len = read(clnt_sock, buf, SIZE)){
            if(read_len == -1)
                error_handling("read() error");
            buf[read_len] = '\0';
            printf("server received from client %d is: %s, len=%d\n", clnt_sock, buf, read_len);
            write(clnt_sock, buf, read_len);
            //增加:将buf传给管道
            write(fds[1], buf, read_len);
        }
        close(clnt_sock);
        return 0;
    }else{
        close(clnt_sock);
    }
}
close(serv_sock);
  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-10-31 12:34:57  更:2022-10-31 12:35:52 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 21:32:36-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码