TCP/IP网络编程:TCP服务器的基础搭建以及使用io复用优化构建_tcp服务器搭建-程序员宅基地

技术标签: 网络  服务器  tcp/ip  

一、TCP服务器基础搭建思路

        本文主要介绍Linux端采用实现简单的接受数据的TCP服务端实现。

        开发环境:Ubuntu 16.04.6 

        文中代码及网络调试器均以上传至github,链接见文末

1.1 使用多线程基础实现

        TCP client端的操作主要有这两个请求:连接请求和传输数据请求。因此我们可以设计两部分套接字,第一部分为单独的一个套接字socket,句柄为sockfd,对服务器主机指定的端口号上收到的连接请求进行监听(listen)。一旦发现有来自其他ip的连接请求,服务器端执行accept,在第二部分套接字中创建一个client_fd套接字与客户端创立连接,并检测连接内的输入。

        可以想象,服务器就是一个高端餐厅,服务员分为两种,一种为在大门口的迎宾员,一种为专门为你服务,带你认路、点菜的具体“引路”员。某人A来到餐厅门口,即某客户端对服务器发起连接请求,迎宾员(socket)会捕捉到这一请求,并马上告诉餐厅(服务器),安排一个“引路”员专门点对点为其服务(accept创建一个client_fd套接字专门与该客户端进行连接)。关于socket编程的基础知识,推荐参阅书本TCP/IP网络编程(尹圣雨著)。

        总结:构建TCP服务器核心三个步骤:

        1.构建socket服务员,用来listen固定端口上的连接请求

        2.有连接请求:accept,创建套接字与客户端连接

        3.有可读管道:recv,并实现输出

        以下是一个基础的采用多线程实现的TCP服务器:

//使用多线程实现
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <pthread.h>
#include <sys/socket.h>
#include <unistd.h>
#include <netinet/in.h>

#include <errno.h>

#define MAX_LISTEN_NUM 10   //最大同时listen数量
#define BUFFER_SIZE 1024    //读取缓冲大小

void *Client_Call_Back(void* arg){    //客户线程响应函数
        
    int clientfd=*(int *)arg;
    //取得客户端句柄

    while(1) //处理第三步:有可读管道时,将数据读入buffer缓冲区,再实现输出打印printf
             //注意:客户端主动关闭连接,也是一个可读事件,进行读取时recv会返回0
    {
        char buffer[1024]={0};
        int len=recv(clientfd,buffer,BUFFER_SIZE,0);
        if(len==0){             //说明客户端主动断开了连接
            close(clientfd);    //关闭分配给该客户端的套接字
            break;
        }

        printf("clientfd=%d len=%d buffer=%s\n",clientfd,len,buffer);//进行输出
    }

    return (void*)0;
}

int main(int argc,char* argv[]){
                                    //传入参数为指定的端口号

    if(argc<2)
    {
        printf("need port!\n");
        return -1;
    }
    
    //step1 创建sockfd服务员 用来监听
    int sockfd=socket(PF_INET,SOCK_STREAM,0);  //创建套接字 用来listen端口连接请求

    struct sockaddr_in addr;                   //addr是分配给sockfd地址
    memset(&addr,0,sizeof(struct sockaddr_in));

    addr.sin_family=AF_INET;
    addr.sin_addr.s_addr=htonl(INADDR_ANY);    //0.0.0.0 允许接受任意ip的连接请求
    addr.sin_port=htons(atoi(argv[1]));        //设置输入的端口号

    int ret=bind(sockfd,(struct sockaddr*)&addr,sizeof(struct sockaddr));
    //将sockkfd设置好地址
    if(ret==-1){
        perror("bind");
        return -2;
    }

    ret=listen(sockfd,MAX_LISTEN_NUM);           //让sockfd开始listen
    if(ret==-1){
        perror("listen");
        return -3;
    }
    //step1结束,sockfd设置完毕

    //初始化客户端地址和句柄,为accept做准备
    struct sockaddr_in client_addr;                    //client_addr为客户端地址
    memset(&client_addr,0,sizeof(client_addr));        
    int client_len=sizeof(client_addr);                //client_len为对应长度
    int clientfd=0;                                    //clientfd为当前要处理的客户端对应的套接字的文件描述符

    //服务器开始运行
    while(1){
        

        //step2开始,处理新连接请求
        clientfd=accept(sockfd,(struct sockaddr*)&client_addr,&client_len);//接受新的连接申请
        if(clientfd == -1)
        {
            perror("accept");
            return -4;
        }
        
        //为新连接创建新线程
        pthread_t threadid;            
        pthread_create(&threadid,NULL,Client_Call_Back,&clientfd);  //让线程开始工作,将客
户端对应套接字句柄作为参数传入
    }

    close(sockfd);
    return 0;
}
1.2 服务器测试

        使用NetAssit0进行测试

        在linux输入以下编译命令并运行:

gcc -o tcp tcp_server_pthread.c -lpthread
./tcp 8888

        使用NetAssit0,选择TCP client栏,输入服务器ip及端口并连接,发送数据如下所示:

可以看到服务器实现了对两个客户端进行的连接。如果您的NetAssit0无法连接,尝试关闭服务器的ipv6服务或者尝试禁用一下防火墙:sudo ufw disable

二、io复用

        可以看到,使用多线程或者多进程完全可以实现服务器的简单功能。但是,“一请求一连接”的工作方式存在很大的缺陷。一旦客户端的连接数量变多,创建线程和进程的开销会变得非常大,对服务器性能影响很大。因此人们想到了io复用方法。什么是复用呢?通常来说,就是以最小的物理开销,实现传递最多信息的功能。io复用的目的就是让服务器能抛开多线程/进程,以单进程单线程的方式管理连接。省去不必要的创建线/进程的开销。

        io具体如何实现的呢?在尹书中有一个形象的例子:在一所学校中有一个班,班里有10个学生。一班的学生特别爱举手提问题,学校无奈,只能给每个学生配备一个专门的老师进行辅导解答。可是这样对学校来说非常不划算,因为老师的费用开销极大,十位老师在学校里也十分地占位置。于是学校聘请了一位超能力老师,他不知疲倦,并且可以以非常快的速度回答每个学生的问题,问题间可以没有时间间隔,堪比并行,要的薪资还和以往一位老师一样多。这样一位老师的占位置(空间开销)很小,招聘费用(创建线程/进程所花的运算和内存)也小。

        而这个超能力老师采用以下的管理方法:班上的学生要发言必须举手,该老师会先确认有无学生举手,如果没有他则在这一时刻休息,等待有人举手;一旦有人举手,他就会去确认是谁举手,并为他瞬间解决问题。也就是说,服务器端(老师)会同一管理包括sockfd和所有clientfd在内的一切套接字(学生),来检测他们有没有可读数据(是否举手)。如果没有管道里有可读数据,也就是说客户端没有人有发送数据的请求,即没有学生提问,那么服务器会不进行处理,等待数据的到来;而一旦有管道有了输入数据,变为可读状态(有学生举手要问问题),服务器会立马捕捉,并立即处理该套接字的事项。服务器会区分sockfd和clientfd,当sockfd可读时,说明有新的连接请求,则要进行accept创建新的clientfd;当clientfd可读时,说明有数据输入,则读入数据。可以认为sockfd是一个班长,隔壁班看到了老师很眼馋想加入班级。一旦有新的加入班级请求,班长就会举手,老师为他处理他的问题,即放新的同学加入班级,并给他分配一个编号(创建新的clientfd)。

三、基于io复用构建TCP服务器

3.1 select和poll

        根据以上说法,我们可以得出构建io复用TCP服务器的几个小步骤:

                step1:创建sockfd用于listen

                step2:维护一个数据结构D,该结构内管理所有的套接字(包括sockfd和clientfd,初始为空,初始化时应该加入sockfd),并且可以根据该结构查询指定套接字是否可读。定义一个方法Fun,调用Fun可以使得进程和操作系统交互,更新指定套接字是否可读的状态。

                step3:每次检查所有套接字是否可读,如果可读,区分该套接字类型:如果是sockfd,说明有新的连接请求,则调用accept,创建新的clientfd与客户端连接,并将clientfd加入数据结构D中进行监视;如果是clientfd,说明有新的传输数据请求,则调用recv接受数据。

                流程图示意如下:

很幸运,select和poll给我们提供了现成的数据结构以及api来维护。我们只需学会使用即可。

3.1.1 select

        select采用fd_set类型数组代表D。fd_set是一个1024位的数组,代表1024个文件描述符的集合。每一位只置0和1,第i位代表文件描述符为i的套接字的状态,如果为1,代表该文件描述符具有某种状态,如果为0,代表该文件描述符没有某种状态。该状态可以自己设定。

        select提供以下宏来便捷进行维护:

FD_ZERO(fd_set* fdset): 将fd_set变量的所有位初始化为0。
FD_SET(int fd, fd_set* fdset):在参数fd_set指向的变量中注册文件描述符fd的信息。
FD_CLR(int fd, fd_set* fdset):参数fd_set指向的变量中清除文件描述符fd的信息。
FD_ISSET(int fd, fd_set* fdset):若参数fd_set指向的变量中包含文件描述符fd的信息,则返回真。

        select提供select函数来与内核交互,更新文件操作符的状态。以下是select函数定义:

#include <sys/select.h>
#include <sys/time.h>
int select(int maxfd, fd_set* readset, fd_set* writeset, fd_set* exceptset, 
const struct timeval* timeout);> 

        maxfd代表最大文件标识符编号,select内部会采用for循环,遍历0~maxfd的文件描述符进行更新。2~5位表示设定的状态,参数2为可读集合,参数3为可写集合,参数4为出错集合。最后一个函数为超时时间,设为NULL表示永久不超时,阻塞,设为0表示不等待,设为其他正数表示设定固定的等待时间,超时就会返回。 例如:

select(20,&readset,NULL,NULL,NULL);

     表示对readset这个集合中的0~20文件标识符进行更新,更新关注的是可不可读状态。select返回后,在readset集合0~20文件描述符中,如果为1则代表该位文件描述符所代表的套接字具有可读状态。一般的,传入的readset应该是一个准备好的一个集合,该集合置1代表的性质为该文件描述符正在被监视。

        以前文学生老师的比喻作为解释:该教室现在扩大,有着1024个座位。并且一开始只有班长(socket)。老师手里有一份关注表(rfbs),上面记载了哪些位置是需要关注的,也就是说哪些位置上是有学生的(哪些套接字是正在被监听的)。老师(服务器)有一个秘书(select函数),每次老师会给秘书一份关注表的副本(rfbs_copy)。秘书会根据关注表副本,判断每一个被关注的位置上是否有学生举手(clientfd是否可读)。老师会记得一个数字(maxfd),每次告诉秘书,查到这个位置就不用继续查了。然后秘书会告诉老师,在0~maxfd这些座位间,有那个座位上是有学生而且举手了。秘书会根据传入的关注表副本,返回一个二进制的数组集合(rfds_copy),第i位代表第i个座位的状态,以及告诉老师总共有多少人举手了(nready)。

        老师会先判断班长有没有举手,如果班长举手(select可读),说明有新的学生要加入班级,那么老师调用accept,为新学生分配座位(clientfd),在关注表中加入这个座位,并且把maxfd更新,保证下一次秘书进行检查时能够囊括进新学生。然后老师再依次看剩下的数组集合,对举手的同学进行处理。

如果举手的同学说的是他要离开班级(recv==0),那么老师会放他走,并且从关注表中删除这个座位。

        select实现代码如下:

//使用select实现
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <sys/socket.h>
#include <sys/select.h>
#include <unistd.h>
#include <netinet/in.h>

#include <errno.h>

#define MAX_LISTEN_NUM 10
#define BUFFER_SIZE 1024

int main(int argc,char* argv[]){
    if(argc<2){
        printf("need port!\n");
        return -1;
    }
    
//step1 创建sockfd 用于listen
    int sockfd=socket(PF_INET,SOCK_STREAM,0);
    
    struct sockaddr_in addr;
    memset(&addr,0,sizeof(addr));
    addr.sin_family=AF_INET;
    addr.sin_addr.s_addr=htonl(INADDR_ANY);
    addr.sin_port=htons(atoi(argv[1]));

    int res=bind(sockfd,(struct sockaddr*)&addr,sizeof(struct sockaddr));
    if(res==-1){
        perror("bind");
        return -2;
    }

    res=listen(sockfd,MAX_LISTEN_NUM);
    if(res==-1){
        perror("listen");
        return -3;
    }
//step1结束

//初始化client_addr,clientfd,client_len,便于accept操作,放在循环内accept前也可以
    struct sockaddr_in client_addr;
    memset(&client_addr,0,sizeof(struct sockaddr));
    int client_len=sizeof(client_addr);
    int clientfd=0;
//

//初始化数据结构D,此处为fd_set。rfds用来存储被监视的套接字的集合,rfds_copy用于每次进行select
    fd_set rfds,rfds_copy;
    FD_ZERO(&rfds);
    FD_SET(sockfd,&rfds);    //初始化,把sockfd加入
    int maxfd=sockfd;        //保证第一次遍历能遍历到sockfd

    while(1){
        rfds_copy=rfds;    //拷贝副本
        
        //进行对0~maxfd+1的文件描述符的状态更新,状态关注的是是否可读,返回可读集合为rfds_copy 并且进行阻塞等待

        int nready=select(maxfd+1,&rfds_copy,NULL,NULL,NULL);
    
        //此处将sockfd是否可读拿出来特判,其实按照流程图加到下文中的循环里也可以。
        if(FD_ISSET(sockfd,&rfds_copy)){ //如果sockfd可读,说明有新的连接请求
            clientfd=accept(sockfd,(struct sockaddr*)&client_addr,&client_len);//创建新的clientfd
            if(clientfd==-1){
                perror("accept");
                return -4;
            }

            FD_SET(clientfd,&rfds);            //维护D,将新创建的clientfd加入监视集合中
            if(clientfd>maxfd)maxfd=clientfd;    //更新maxfd 保证一切clientfd都能被遍历到
            if(nready==1)continue;            //nready==1说明只有当前socket可读,则下面的循环可以跳过,此步可加可不加 无伤大雅
        }

//遍历select返回的可读集合
        for(int i=sockfd+1;i<=maxfd;i++){
            if(FD_ISSET(i,&rfds_copy)){
                char buffer[BUFFER_SIZE]={0};
                int len=recv(i,buffer,BUFFER_SIZE,0);    //进行recv
                if(len==0){  //说明客户端主动关闭连接 那么关闭当前套接字,并取消监视
                    FD_CLR(i,&rfds);
                    close(i);
                    break;
                }
                printf("clientfd:%d len:%d buffer:%s\n",i,len,buffer);
            }
        }

    }
    
    close(sockfd);
    return 0;
}
        3.1.2 poll

        select有一个很大的确定是fd_set是定长的1024。不能自己设置长度。而poll可以解决这个问题

        poll提供类型struct pollfd,fd代表文件描述符,events是服务器设定的要监视的时间,POLLIN表示监视可读时间,具体其他参数可以参考书本或者其他博客。revents是内核返回给应用层的该文件的事件。

struct pollfd {
int fd; /* 文件描述符 */
short events; /* 监控的事件 */
short revents; /* 监控事件中满足条件返回的事件 */
};

        poll提供poll函数,和select功能一样,更新指定范围的pollfd的状态。fds是创好的pollfd数组的头指针,nfds类似与select中的maxfd,表示循环到哪结束。timeout也是等待时间,-1表示阻塞,0表示不等待,正数表示固定等待时间。

int poll (struct pollfd *fds, unsigned int nfds, int timeout);

创建一个pollfd数组 等价于创建fd_set   select用位表示状态,poll中用一个结构体表示。select中rfds表示要监视的文件,并由rfds_copy带回状态;poll中用events设定事件表示正在监视,revents表示从内核带回的状态。代码如下:

//poll
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <sys/socket.h>
#include <sys/poll.h>
#include <unistd.h>
#include <netinet/in.h>

#include <errno.h>

#define MAX_LISTEN_NUM 10
#define BUFFER_SIZE 1024
#define POLL_SIZE 1024

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

    if(argc<2){
        printf("need port!");
        return -1;
    }

    int sockfd=socket(PF_INET,SOCK_STREAM,0);

    struct sockaddr_in addr={0};
    addr.sin_family=AF_INET;
    addr.sin_addr.s_addr=htonl(INADDR_ANY);
    addr.sin_port=htons(atoi(argv[1]));

    int ret=bind(sockfd,(struct sockaddr*)&addr,sizeof(struct sockaddr));
    if(ret == -1){
        perror("bind");
        return -2;
    }

    ret=listen(sockfd,MAX_LISTEN_NUM);
    if(ret == -1){
        perror("listen");
        return -3;
    }

    struct sockaddr client_addr={0};
    int client_len=sizeof(struct sockaddr);
    int clientfd=0;

    struct pollfd fds[POLL_SIZE]={0};
    fds[sockfd].fd=sockfd;            //初始化加入sockfd
    fds[sockfd].events=POLLIN;        //设定监视事件
    int maxfd=sockfd;

    while(1){

        int nready=poll(fds,maxfd+1,-1);  //等价于select

        if(nready==-1)
        {
            perror("poll");
            return -4;
        }

        if(fds[sockfd].revents & POLLIN){    //如果sockfd是可读的
            clientfd=accept(sockfd,(struct sockaddr*)&client_addr,&client_len);
            if(clientfd==-1)
            {
                perror("accpet");
                return -5;
            }

            fds[clientfd].fd=clientfd;
            fds[clientfd].events=POLLIN;

            if(clientfd>maxfd)maxfd=clientfd;
            if(nready==1)continue;
        }

        for(int i=sockfd+1;i<=maxfd;i++){
            if(fds[i].revents & POLLIN){        //如果可读
                char buffer[BUFFER_SIZE]={0};
                int len=recv(i,buffer,BUFFER_SIZE,0);
                if(len==0){    //如果连接断开
                    fds[i].events=-1;
                    fds[i].fd=-1;
                    close(i);
                    break;
                }
                printf("clientfd:%d len:%d buffer:%s\n",clientfd,len,buffer);

            }

        }
    }

    close(sockfd);
    return 0;
}
3.2 epoll

        select和poll都实现了io复用,使用单进程单线程进行服务。但是由于底层实现仍然为for循环。一旦客户端数量一大,每次循环就会遍历很多无用的套接字。  如果每次可以只返回可读的集合,就可以减少遍历开销,减少与操作系统的交互。

        代码如下:

//使用epoll(水平触发)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <unistd.h>

#include <errno.h>

#define MAX_LISTEN_NUM 10
#define EVENTS_NUM 1024
#define BUFFER_SIZE 1024

int main(int argc,char* argv[]){
    if(argc<2){
        printf("need port!");
        return -1;
    }

    int sockfd=socket(PF_INET,SOCK_STREAM,0);
    
    struct sockaddr_in addr={0};
    addr.sin_family=AF_INET;
    addr.sin_addr.s_addr=htonl(INADDR_ANY);
    addr.sin_port=htons(atoi(argv[1]));

    int ret=bind(sockfd,(struct sockaddr*)&addr,sizeof(struct sockaddr));
    if(ret==-1){
        perror("bind");
        return -1;
    }

    ret=listen(sockfd,MAX_LISTEN_NUM);
    if(ret==-1){
        perror("listen");
        return -2;
    }

    struct sockaddr_in client_addr={0};
    int client_len=sizeof(struct sockaddr_in);
    int clientfd=0;

    int epfd=epoll_create(1);
    if(epfd==-1){
        perror("epoll_create failed");
        return -3;
    }

    struct epoll_event sock_listen_ev;
    sock_listen_ev.events=EPOLLIN;
    sock_listen_ev.data.fd=sockfd;
    epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&sock_listen_ev);

    struct epoll_event events[EVENTS_NUM];

    while(1){
        int nready=epoll_wait(epfd,events,EVENTS_NUM,-1);
        
        if(nready==-1){
            perror("epoll_wait");
            return -4;
        }

        for(int i=0;i<=nready;i++){
            int curfd=events[i].data.fd;
            if(curfd==sockfd){
                clientfd=accept(sockfd,(struct sockaddr*)&client_addr,&client_len);
                if(clientfd==-1){
                    perror("accept");
                    return -5;
                }

                struct epoll_event client_ev;
                client_ev.data.fd=clientfd;
                client_ev.events=EPOLLIN;
                epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&client_ev);
            } else if (events[i].events == EPOLLIN){
                char buffer[BUFFER_SIZE]={0};
                int len=recv(curfd,buffer,BUFFER_SIZE,0);
                if(len==0){
                    epoll_ctl(epfd,EPOLL_CTL_DEL,curfd,NULL);
                    close(curfd);
                    break;
                }
                printf("clientfd:%d len=%d buffer=%s\n",curfd,len,buffer);
            }
        }
    }
    close(sockfd);
    return 0;
}

        epoll提供struct epoll_event事件结构如下:

struct epoll_event {     
__uint32_t events; // Epoll events   
epoll_data_t data; // User data variable
};
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
 

 使用只需了解:events表示监视的事件,EPOLLIN表示监视输入。data是一个八字节内存空间,你想用来存啥都行。存一个指针,存一个int fd都可以。由于epoll__wait()函数与内核操作的返回是从黑盒中返回的,所以你不能直接知道触发了这个event的套接字是那个,所以event内置data,方便从操作系统返回时能定位到该事件的相应数据。

        epoll提供以下函数:
        

int epoll_create(int size); //用来创建一个epoll ,参数size现在没有意义,只需传入任意正整数,效果都一样;返回epoll的句柄

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);//维护epoll,用来注册、删除事件等

int epoll_wait(int epid, struct epoll_event *events, int maxevents, int timeout);//等价于select和poll 从内核带回新状态

具体可以参考博客epoll函数原理和使用介绍_椛茶的博客-程序员宅基地

本文中采用水平触发方式。建议更多了解水平触发和边沿触发的区别,更好地掌握epoll

四、总结

io复用进行tcp服务端的构建框架思路其实非常非常相似,只需掌握各个模块给的api就能非常简单的使用。select/poll适用于小型,epoll适用于大型服务器。第一次写博客可能有很多不足希望大家体谅。如果有错,欢迎和我交流。

点赞关注安安喵谢谢喵

Github代码地址:Zzzzzya/Multi-inet-io: 使用C语言,基于/select/poll/epoll的io复用的TCP服务端 (github.com)

NetAssit:Zzzzzya/NetAssit: 网络测试器 (github.com)

课程学习链接:https://xxetb.xet.tech/s/4czPSo

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/m0_73939403/article/details/133121476

智能推荐

海康威视网络摄像头开发流程(五)------- 直播页面测试_ezuikit 测试的url-程序员宅基地

文章浏览阅读3.8k次。1、将下载好的萤石js插件,添加到SoringBoot项目中。位置可参考下图所示。(容易出错的地方,在将js插件在html页面引入时,发生路径错误的问题)所以如果对页面中引入js的路径不清楚,可参考下图所示存放路径。2、将ezuikit.js引入到demo-live.html中。(可直接将如下代码复制到你创建的html页面中)<!DOCTYPE html><html lan..._ezuikit 测试的url

如何确定组态王与多动能RTU的通信方式_组态王ua-程序员宅基地

文章浏览阅读322次。第二步,在弹出的对话框选择,设备驱动—>PLC—>莫迪康—>ModbusRTU—>COM,根据配置软件选择的协议选期期,这里以此为例,然后点击“下一步”。第四步,把使用虚拟串口打勾(GPRS设备),根据需要选择要生成虚拟口,这里以选择KVCOM1为例,然后点击“下一步”设备ID即Modbus地址(1-255) 使用DTU时,为下485接口上的设备地址。第六步,Modbus的从机地址,与配置软件相同,这里以1为例,点击“下一步“第五步,Modbus的从机地址,与配置软件相同,这里以1为例,点击“下一步“_组态王ua

npm超详细安装(包括配置环境变量)!!!npm安装教程(node.js安装教程)_npm安装配置-程序员宅基地

文章浏览阅读9.4k次,点赞22次,收藏19次。安装npm相当于安装node.js,Node.js已自带npm,安装Node.js时会一起安装,npm的作用就是对Node.js依赖的包进行管理,也可以理解为用来安装/卸载Node.js需要装的东西_npm安装配置

火车头采集器AI伪原创【php源码】-程序员宅基地

文章浏览阅读748次,点赞21次,收藏26次。大家好,小编来为大家解答以下问题,python基础训练100题,python入门100例题,现在让我们一起来看看吧!宝子们还在新手村练级的时候,不单要吸入基础知识,夯实自己的理论基础,还要去实际操作练练手啊!由于文章篇幅限制,不可能将100道题全部呈现在此除了这些,下面还有我整理好的基础入门学习资料,视频和讲解文案都很齐全,用来入门绝对靠谱,需要的自提。保证100%免费这不,贴心的我爆肝给大家整理了这份今天给大家分享100道Python练习题。大家一定要给我三连啊~

Linux Ubuntu 安装 Sublime Text (无法使用 wget 命令,使用安装包下载)_ubuntu 安装sumlime text打不开-程序员宅基地

文章浏览阅读1k次。 为了在 Linux ( Ubuntu) 上安装sublime,一般大家都会选择常见的教程或是 sublime 官网教程,然而在国内这种方法可能失效。为此,需要用安装包安装。以下就是使用官网安装包安装的教程。打开 sublime 官网后,点击右上角 download, 或是直接访问点击打开链接,即可看到各个平台上的安装包。选择 Linux 64 位版并下载。下载后,打开终端,进入安装..._ubuntu 安装sumlime text打不开

CrossOver for Mac 2024无需安装 Windows 即可以在 Mac 上运行游戏 Mac运行exe程序和游戏 CrossOver虚拟机 crossover运行免安装游戏包-程序员宅基地

文章浏览阅读563次,点赞13次,收藏6次。CrossOver24是一款类虚拟机软件,专为macOS和Linux用户设计。它的核心技术是Wine,这是一种在Linux和macOS等非Windows操作系统上运行Windows应用程序的开源软件。通过CrossOver24,用户可以在不购买Windows授权或使用传统虚拟机的情况下,直接在Mac或Linux系统上运行Windows软件和游戏。该软件还提供了丰富的功能,如自动配置、无缝集成和实时传输等,以实现高效的跨平台操作体验。

随便推点

一个用聊天的方式让ChatGPT写的线程安全的环形List_为什么gpt一写list就卡-程序员宅基地

文章浏览阅读1.7k次。一个用聊天的方式让ChatGPT帮我写的线程安全的环形List_为什么gpt一写list就卡

Tomcat自带的设置编码Filter-程序员宅基地

文章浏览阅读336次。我们在前面的文章里曾写过Web应用中乱码产生的原因和处理方式,旧文回顾:深度揭秘乱码问题背后的原因及解决方式其中我们提到可以通过Filter的方式来设置请求和响应的encoding,来解..._filterconfig selectencoding

javascript中encodeURI和decodeURI方法使用介绍_js encodeur decodeurl-程序员宅基地

文章浏览阅读651次。转自:http://www.jb51.net/article/36480.htmencodeURI和decodeURI是成对来使用的,因为浏览器的地址栏有中文字符的话,可以会出现不可预期的错误,所以可以encodeURI把非英文字符转化为英文编码,decodeURI可以用来把字符还原回来_js encodeur decodeurl

Android开发——打包apk遇到The destination folder does not exist or is not writeable-程序员宅基地

文章浏览阅读1.9w次,点赞6次,收藏3次。前言在日常的Android开发当中,我们肯定要打包apk。但是今天我打包的时候遇到一个很奇怪的问题Android The destination folder does not exist or is not writeable,大意是目标文件夹不存在或不可写。出现问题的原因以及解决办法上面有说报错的中文大意是:目标文件夹不存在或不可写。其实问题就在我们的打包界面当中图中标红的Desti..._the destination folder does not exist or is not writeable

Eclipse配置高大上环境-程序员宅基地

文章浏览阅读94次。一、配置代码编辑区的样式 <1>打开Eclipse,Help —> Install NewSoftware,界面如下: <2>点击add...,按下图所示操作: name:随意填写,Location:http://eclipse-color-th..._ecplise高大上设置

Linux安装MySQL-5.6.24-1.linux_glibc2.5.x86_64.rpm-bundle.tar_linux mysql 安装 mysql-5.6.24-1.linux_glibc2.5.x86_6-程序员宅基地

文章浏览阅读2.8k次。一,下载mysql:http://dev.mysql.com/downloads/mysql/; 打开页面之后,在Select Platform:下选择linux Generic,如果没有出现Linux的选项,请换一个浏览器试试。我用的谷歌版本不可以,换一个别的浏览器就行了,如果还是不行,需要换一个翻墙的浏览器。 二,下载完后解压缩并放到安装文件夹下: 1、MySQL-client-5.6.2_linux mysql 安装 mysql-5.6.24-1.linux_glibc2.5.x86_64.rpm-bundle