当前位置: 首页 > news >正文

网络编程套接字(3): 简单的TCP网络程序

文章目录

  • 网络编程套接字(3)
    • 4. 简单的TCP网络程序
      • 4.1 服务端创建
        • (1) 创建套接字
        • (2) 绑定端口
        • (3) 监听
        • (4) 获取新连接
        • (5) 处理读取与写入
      • 4.2 客户端创建
        • (1)连接服务器
      • 4.3 代码编写
        • (1) v1__简单发送消息
        • (2) v2_多进程版本
        • (3) v3_多线程版本
        • (4) v4_线程池版本

网络编程套接字(3)

4. 简单的TCP网络程序

4.1 服务端创建

(1) 创建套接字

还是之前udp部分的socket函数,这里只是简单说明一下与udp的差异

int socket(int domain, int type, int protocol);

只需将第二个参数type换成:
SOCK_STREAM: 基于TCP的网络通信,流式套接字,提供的是流式服务(对应TCP的特点:面向字节流)

(2) 绑定端口

还是和之前一样的接口

(3) 监听

UDP服务器的初始化操作只有2步,第一步:创建套接字,第二步:是绑定。但是TCP服务器是面向连接的,客户端在正式向TCP服务器发送数据之前,需要先与TCP服务器建立连接,然后才能与服务器进行通信。

因此TCP服务器需要时刻注意是否有客户端发来连接请求,此时就需要将TCP服务器创建的套接字设置为监听状态

在这里插入图片描述

listen for connections on a socket: 监听套接字上的连接

头文件:#include <sys/types.h>         #include <sys/socket.h>函数原型:int listen(int sockfd, int backlog);参数说明:第一个参数sockfd:  需要设置为监听状态的套接字对应的文件描述符第二个参数backlog: 这里当成一个整数,后续详细解释返回值:监听成功: 返回0监听失败: 失败返回-1,并设置错误码

(4) 获取新连接

客户端有新链接到来,服务端可以获取到新链接,这一步需要死循环获取客户端新链接。

在这里插入图片描述

accept a connection on a socket: 接收套接字上的连接

头文件:#include <sys/types.h>         #include <sys/socket.h>函数原型:int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);参数说明:第一个参数sockfd:  监听套接字第二个参数addr:    获取对方一端网络相关的属性信息第三个参数addrlen: addr的长度返回值:连接成功: 返回接收到的套接字的文件描述符连接失败: 失败返回-1,并设置错误码

关于accept的返回值: 也是一个文件描述符

为什么又返回一个新的文件描述符??返回的这个新的文件描述符跟旧的文件描述符_sockfd有什么关系?

感性理解:

在这里插入图片描述

对比listen监听套接字与accept函数返回的套接字

  • listen监听套接字:用于获取客户端发来的连接请求。accept函数会不断从监听套接字当中获取新连接
  • accept函数返回的套接字:用于为本次accept获取到的连接提供服务。
  • 而listen监听套接字的任务只是不断获取新连接,而真正为这些连接提供服务的套接字是accept函数返回的套接字,而不是监听套接字。

(5) 处理读取与写入

因为TC 提供的是流式服务,所以这里利用read和write来实现读取与写入

4.2 客户端创建

4步:创建套接字,客户端向服务器发起连接请求,bind(不需要自己绑定,由OS自动分配),处理数据读取与写入

(1)连接服务器

在这里插入图片描述

initiate a connection on a socket: 在套接字上发起连接

头文件:#include <sys/types.h>#include <sys/socket.h>函数原型:int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);参数说明:第一个参数sockfd: 表示通过该套接字发起连接请求第二个参数addr: 对方一端网络相关的属性信息第三个参数addrlen: addr的长度返回值:连接成功: 返回0连接失败: 失败返回-1,并设置错误码

4.3 代码编写

这里一共提供4个版本的tcp代码

err.hpp:这个代码是公用的后续不在给出

#pragma onceenum
{USAGE_ERR=1,SOCKET_ERR,BIND_ERR,LISTEN_ERR,CONNECT_ERR,
};

(1) v1__简单发送消息

客户端向服务端发送消息,服务端收到后再把消息发回给客户端

tcpServer.hpp

#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
#include<unistd.h>
#include<memory>
#include<functional>
#include"err.hpp"
using namespace std;// 问题: 目前的服务器, 无法处理多个client的问题, 为什么?
// 单进程服务, 当服务端向客户端提供业务处理服务时, 没有办法accet, 不能处理连接namespace ns_server
{static const uint16_t defaultport=8081;static int backlog=32;using func_t=function<string(const string&)>;   // 回调函数,一种处理逻辑class TcpServer{public:TcpServer(func_t func, uint16_t port=defaultport):func_(func),port_(port),quit_(true){}void InitServer(){// 1. 创建socket文件listensock_=socket(AF_INET,SOCK_STREAM,0);if(listensock_<0){cerr<<"create socket error"<<endl;exit(SOCKET_ERR);}// 2. bindstruct 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;int n=bind(listensock_,(struct sockaddr*)&local,sizeof(local));if(n<0){cerr<<"bind socket error"<<endl;exit(BIND_ERR);}// 3. 监听int m=listen(listensock_,backlog);if(m<0){cerr<<"listen socket error"<<endl;exit(LISTEN_ERR);}}void Start(){quit_=false;while(!quit_){struct sockaddr_in client;socklen_t len=sizeof(client);// 4. 获取连接, acceptint sock=accept(listensock_,(struct sockaddr*)&client,&len);if (sock < 0)   // accept失败并不会终止进程, 只要获取下一个连接{cerr << "accept error" << endl;continue;}// 提取client信息   --- debugstring clientip=inet_ntoa(client.sin_addr); // 把4字节对应的IP转化成字符串风格uint16_t clientport=ntohs(client.sin_port);  // 网络序列转主机序列// 5. 获取新连接成功后, 开始进行业务处理cout<<"获取新连接成功: "<<sock<<" from "<<listensock_<<", "<< clientip << "-				   " <<clientport<<endl;// v1service(sock,clientip,clientport);}}// 流式 - 利用read和writevoid service(int sock, const string&clientip,const uint16_t clientport){string who=clientip + "-" + to_string(clientport);char buffer[1024];while(true){ssize_t s=read(sock,buffer,sizeof(buffer)-1);if(s>0){buffer[s]=0;string res=func_(buffer);     // 进行回调cout<<who<< ">>> " <<res<<endl;// 把收到的消息返回(写给客户端)write(sock,res.c_str(),res.size());}else if(s==0){// 对方将连接关闭了close(sock);cout<< who <<" quit, me too"<<endl;break;}else{close(sock);cerr<<"read error: "<<strerror(errno)<<endl;break;}}}~TcpServer(){}private:uint16_t port_;int listensock_;bool quit_;      // 标志服务器是否运行字段func_t func_;};
}

tcpServer.cc

#include"tcpServer.hpp"
using namespace ns_server;// ./tcp_server port// 使用手册
static void usage(string proc)
{cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}string echo(const string&message)
{return message;
}int main(int argc,char*argv[])
{if(argc!=2){usage(argv[0]);exit(USAGE_ERR);}uint16_t port=atoi(argv[1]);unique_ptr<TcpServer> tsvr(new TcpServer(echo,port));tsvr->InitServer();tsvr->Start();return 0;
}

tcpClient.cc

#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
#include<unistd.h>
#include<memory>
#include<functional>
#include"err.hpp"
using namespace std;static void usage(string proc)
{cout<<"usage:\n\t"<<proc<<" serverip serverport\n" <<endl;
}// ./tcp_client serverip serverport
int main(int argc,char*argv[])
{// 准备工作if(argc!=3){usage(argv[0]);exit(USAGE_ERR);}string serverip=argv[1];uint16_t serverport=atoi(argv[2]);// 1.创建套接字int sock=socket(AF_INET,SOCK_STREAM,0);if (sock < 0){cerr << "create socket error: " << strerror(errno) << endl;exit(SOCKET_ERR);}// (2) 客户端要不要bind呢? 要//     要不要自己bind呢? 不要, 因为client要让OS自动给用户进行bind// (3) 要不要listen?不要, 客户端连别人, 永远都是别人listen; 要不要accept?不要, 服务器来连接// 2. connect  客户端向服务器发起连接请求struct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_port=htons(serverport);server.sin_family=AF_INET;// server.sin_addr.s_addr=INADDR_ANY;   绝对不是inet_aton(serverip.c_str(),&(server.sin_addr));    // 字符串风格ip转成点分十进制int cnt=5;while(connect(sock,(struct sockaddr*)&server,sizeof(server))!=0)   // 连接失败{sleep(1);cout<<"正在给你重连, 重连次数还有: "<<cnt--<<endl;if(cnt<=0)break;}if(cnt<=0){cerr<<"连接失败"<<endl;exit(CONNECT_ERR);}char buffer[1024];// 3. 连接成功while(true){string line;cout<<"Enter>> ";getline(cin,line);write(sock,line.c_str(),line.size());ssize_t s = read(sock, buffer, sizeof(buffer)-1);if (s > 0){buffer[s] = 0;cout<<"server echo >>>"<<buffer<<endl;}else if (s == 0){cerr << "server quit" << endl;break;}else{cerr << "read error: " << strerror(errno) << endl;break;}}close(sock);return 0;
}

运行结果:

在这里插入图片描述

(2) v2_多进程版本

v2版本是把单执行流服务器改成多进程版的服务器

  • 在accept获取新连接成功后,fork创建创建子进程,此时子进程对外提供服务, 父进程只进行accept

  • 父进程的文件描述符会被子进程继承,但并不是父子共用同一张文件描述符表,因为子进程会拷贝继承父进程的文件描述符表

  • 对于套接字文件也是相同的,父进程创建的子进程也会继承父进程的套接字文件,此时子进程就能够对特定的套接字文件进行读写操作,进而完成对对应客户端的服务

关于阻塞等待与非阻塞等待

  • 若采用阻塞式等待,那么服务端还是需要等待服务完当前客户端,才能继续获取下一个连接请求,此时服务端仍然是以一种串行的方式为客户端提供服务
  • 若采用非阻塞式等待,虽然在子进程为客户端提供服务期间服务端可以继续获取新连接,但此时服务端就需要将所有子进程的PID保存下来,并且需要不断花费时间检测子进程是否退出
  • 由此可见两种都有缺陷,所以我们可以考虑让服务端不等待子进程退出

常见的方式有两种:

  1. 捕捉SIGCHLD信号,将其处理动作设置为忽略。
  2. 让父进程创建子进程,子进程再创建孙子进程,子进程退出,让孙子进程为客户端提供服务,孙进程的回收工作由OS来承担

下面是创建孙进程的方案:

tcpServer.hpp

#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
#include<unistd.h>
#include<memory>
#include<functional>
#include<sys/wait.h>
#include"err.hpp"
using namespace std;namespace ns_server
{static const uint16_t defaultport=8081;static int backlog=32;using func_t=function<string(const string&)>;   // 回调函数,一种处理逻辑class TcpServer{public:TcpServer(func_t func, uint16_t port=defaultport):func_(func),port_(port),quit_(true){}void InitServer(){// 1. 创建socket文件listensock_=socket(AF_INET,SOCK_STREAM,0);if(listensock_<0){cerr<<"create socket error"<<endl;exit(SOCKET_ERR);}// 2. bindstruct 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;int n=bind(listensock_,(struct sockaddr*)&local,sizeof(local));if(n<0){cerr<<"bind socket error"<<endl;exit(BIND_ERR);}// 3. 监听int m=listen(listensock_,backlog);if(m<0){cerr<<"listen socket error"<<endl;exit(LISTEN_ERR);}}void Start(){// signal(SIGCHLD,SIG_IGN);  // ok, 最推荐// signal(SIGCHLD,handler);  // 回收子进程, 不太推荐quit_=false;while(!quit_){struct sockaddr_in client;socklen_t len=sizeof(client);// 4. 获取连接, acceptint sock=accept(listensock_,(struct sockaddr*)&client,&len);if (sock < 0)   // accept失败并不会终止进程, 只要获取下一个连接{cerr << "accept error" << endl;continue;}// 提取client信息   --- debugstring clientip=inet_ntoa(client.sin_addr); // 把4字节对应的IP转化成字符串风格uint16_t clientport=ntohs(client.sin_port);  // 网络序列转主机序列// 5. 获取新连接成功后, 开始进行业务处理cout<<"获取新连接成功: "<<sock<<" from "<<listensock_<<", "<< clientip << "-                 " <<clientport<<endl;// v2: 多进程版本// 子进程对外提供服务, 父进程只进行acceptpid_t id=fork();if(id<0){close(sock);continue;}else if(id==0) // child, 父进程的fd会被子进程继承吗? 会; 父子会用同一张文件描述符表吗?不会, 子进程会拷贝继承父进程的fd table{// 建议关闭掉不需要的fdclose(listensock_);if(fork()>0) exit(0);   // 就这一行代码// 子进程已经退了(则下面的wait立马返回, 回收子进程资源), 孙子进程在运行(无父进程, 变成孤儿进程, 被系统领养),提供服务// 孙子进程的回收工作由系统来承担service(sock,clientip,clientport);exit(0);}// 父进程, 一定要关闭不需要的fd(否则会导致父进程的文件描述符变少, 即父进程文件描述符资源的浪费[文件描述符泄露])close(sock);// 不等待子进程, 会导致子进程僵尸之后无法回收, 近而导致内存泄漏pid_t ret=waitpid(id,nullptr,0);    // 父进程默认是阻塞的, waitpid(id,nullptr,WNOHANG);不推荐if(ret==id)cout<< "wait child "<<id<< " success" <<endl;}}// 流式 - 利用read和writevoid service(int sock, const string&clientip,const uint16_t clientport){string who=clientip + "-" + to_string(clientport);char buffer[1024];while(true){ssize_t s=read(sock,buffer,sizeof(buffer)-1);if(s>0){buffer[s]=0;string res=func_(buffer);     // 进行回调cout<<who<< ">>> " <<res<<endl;// 把收到的消息返回(写给客户端)write(sock,res.c_str(),res.size());}else if(s==0){// 对方将连接关闭了close(sock);cout<< who <<" quit, me too"<<endl;break;}else{close(sock);cerr<<"read error: "<<strerror(errno)<<endl;break;}}}~TcpServer(){}private:uint16_t port_;int listensock_;bool quit_;      // 标志服务器是否运行字段func_t func_;};
}

tcpServer.cc

#include"tcpServer.hpp"
using namespace ns_server;// ./tcp_server port// 使用手册
static void usage(string proc)
{cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}string echo(const string&message)
{return message;
}int main(int argc,char*argv[])
{if(argc!=2){usage(argv[0]);exit(USAGE_ERR);}uint16_t port=atoi(argv[1]);unique_ptr<TcpServer> tsvr(new TcpServer(echo,port));tsvr->InitServer();tsvr->Start();return 0;
}

tcpClient.cc

#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
#include<unistd.h>
#include<memory>
#include<functional>
#include"err.hpp"
using namespace std;static void usage(string proc)
{cout<<"usage:\n\t"<<proc<<" serverip serverport\n" <<endl;
}// ./tcp_client serverip serverport
int main(int argc,char*argv[])
{// 准备工作if(argc!=3){usage(argv[0]);exit(USAGE_ERR);}string serverip=argv[1];uint16_t serverport=atoi(argv[2]);// 1.创建套接字int sock=socket(AF_INET,SOCK_STREAM,0);if (sock < 0){cerr << "create socket error: " << strerror(errno) << endl;exit(SOCKET_ERR);}// (2) 客户端要不要bind呢? 要//     要不要自己bind呢? 不要, 因为client要让OS自动给用户进行bind// (3) 要不要listen?不要, 客户端连别人, 永远都是别人listen; 要不要accept?不要, 服务器来连接// 2. connect  客户端向服务器发起连接请求struct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_port=htons(serverport);server.sin_family=AF_INET;// server.sin_addr.s_addr=INADDR_ANY;   绝对不是inet_aton(serverip.c_str(),&(server.sin_addr));    // 字符串风格ip转成点分十进制int cnt=5;while(connect(sock,(struct sockaddr*)&server,sizeof(server))!=0)   // 连接失败{sleep(1);cout<<"正在给你重连, 重连次数还有: "<<cnt--<<endl;if(cnt<=0)break;}if(cnt<=0){cerr<<"连接失败"<<endl;exit(CONNECT_ERR);}char buffer[1024];// 3. 连接成功while(true){string line;cout<<"Enter>> ";getline(cin,line);write(sock,line.c_str(),line.size());ssize_t s = read(sock, buffer, sizeof(buffer)-1);if (s > 0){buffer[s] = 0;cout<<"server echo >>>"<<buffer<<endl;}else if (s == 0){cerr << "server quit" << endl;break;}else{cerr << "read error: " << strerror(errno) << endl;break;}}close(sock);return 0;
}

运行结果:

在这里插入图片描述

(3) v3_多线程版本

频繁的创建进程会给OS带来巨大的负担,并且创建线程的成本比创建线程高得多。因此在实现多执行流的服务器时最好采用多线程进行实现。

主线程创建出新线程后,也是需要等待新线程退出的,否则也会造成类似于僵尸进程这样的问题。但对于线程来说,如果不想让主线程等待新线程退出,直接线程分离即可,当这个线程退出时系统会自动回收该线程所对应的资源。

各个线程共享是同一张文件描述符表,也就是说服务进程(主线程)调用accept函数获取到一个文件描述符后,其他创建的新线程是能够直接访问这个文件描述符的。

所以不能关闭不要的套接字文件描述符,该文件描述符的关闭操作应该又新线程来执行。因为是新线程为客户端提供服务的,只有当新线程为客户端提供的服务结束后才能将该文件描述符关闭。

tcpServer.hpp

#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
#include<unistd.h>
#include<memory>
#include<functional>
#include<sys/wait.h>
#include<pthread.h>
#include"err.hpp"
using namespace std;namespace ns_server
{static const uint16_t defaultport=8081;static int backlog=32;using func_t=function<string(const string&)>;   // 回调函数,一种处理逻辑class TcpServer;class ThreadData{public:ThreadData(int fd, const string&ip,const uint16_t&port,TcpServer*ts):sock(fd),clientip(ip),clientport(port),current(ts){}public:int sock;string clientip;uint16_t clientport;TcpServer*current;};class TcpServer{public:TcpServer(func_t func, uint16_t port=defaultport):func_(func),port_(port),quit_(true){}void InitServer(){// 1. 创建socket文件listensock_=socket(AF_INET,SOCK_STREAM,0);if(listensock_<0){cerr<<"create socket error"<<endl;exit(SOCKET_ERR);}// 2. bindstruct 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;int n=bind(listensock_,(struct sockaddr*)&local,sizeof(local));if(n<0){cerr<<"bind socket error"<<endl;exit(BIND_ERR);}// 3. 监听int m=listen(listensock_,backlog);if(m<0){cerr<<"listen socket error"<<endl;exit(LISTEN_ERR);}}void Start(){// signal(SIGCHLD,SIG_IGN);  // ok, 最推荐// signal(SIGCHLD,handler);  // 回收子进程, 不太推荐quit_=false;while(!quit_){struct sockaddr_in client;socklen_t len=sizeof(client);// 4. 获取连接, acceptint sock=accept(listensock_,(struct sockaddr*)&client,&len);if (sock < 0)   // accept失败并不会终止进程, 只要获取下一个连接{cerr << "accept error" << endl;continue;}// 提取client信息   --- debugstring clientip=inet_ntoa(client.sin_addr); // 把4字节对应的IP转化成字符串风格uint16_t clientport=ntohs(client.sin_port);  // 网络序列转主机序列// 5. 获取新连接成功后, 开始进行业务处理cout<<"获取新连接成功: "<<sock<<" from "<<listensock_<<", "<< clientip << "-                " <<clientport<<endl;// v3: 多线程版本 --- 原生多线程// 1. 要不要关闭不要的socket?  绝对不能,一个进程的文件描述符表共享, 关了影响其他线程// 2. 要不要回收线程?要;如何回收?会不会阻塞pthread_t tid;ThreadData*td=new ThreadData(sock,clientip,clientport,this);    // 要开出一块独立的空间pthread_create(&tid,nullptr,threadRoutine,td);}}static void*threadRoutine(void*args){pthread_detach(pthread_self());ThreadData*td=static_cast<ThreadData*>(args);td->current->service(td->sock,td->clientip,td->clientport);delete td;}// 流式 - 利用read和writevoid service(int sock, const string&clientip,const uint16_t clientport){string who=clientip + "-" + to_string(clientport);char buffer[1024];while(true){ssize_t s=read(sock,buffer,sizeof(buffer)-1);if(s>0){buffer[s]=0;string res=func_(buffer);     // 进行回调cout<<who<< ">>> " <<res<<endl;// 把收到的消息返回(写给客户端)write(sock,res.c_str(),res.size());}else if(s==0){// 对方将连接关闭了close(sock);cout<< who <<" quit, me too"<<endl;break;}else{close(sock);cerr<<"read error: "<<strerror(errno)<<endl;break;}}}~TcpServer(){}private:uint16_t port_;int listensock_;bool quit_;      // 标志服务器是否运行字段func_t func_;};
}

tcpServer.cc

#include"tcpServer.hpp"
using namespace ns_server;// ./tcp_server port// 使用手册
static void usage(string proc)
{cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}string echo(const string&message)
{return message;
}int main(int argc,char*argv[])
{if(argc!=2){usage(argv[0]);exit(USAGE_ERR);}uint16_t port=atoi(argv[1]);unique_ptr<TcpServer> tsvr(new TcpServer(echo,port));tsvr->InitServer();tsvr->Start();return 0;
}

tcpClient.cc

#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
#include<unistd.h>
#include<memory>
#include<functional>
#include"err.hpp"
using namespace std;static void usage(string proc)
{cout<<"usage:\n\t"<<proc<<" serverip serverport\n" <<endl;
}// ./tcp_client serverip serverport
int main(int argc,char*argv[])
{// 准备工作if(argc!=3){usage(argv[0]);exit(USAGE_ERR);}string serverip=argv[1];uint16_t serverport=atoi(argv[2]);// 1.创建套接字int sock=socket(AF_INET,SOCK_STREAM,0);if (sock < 0){cerr << "create socket error: " << strerror(errno) << endl;exit(SOCKET_ERR);}// (2) 客户端要不要bind呢? 要//     要不要自己bind呢? 不要, 因为client要让OS自动给用户进行bind// (3) 要不要listen?不要, 客户端连别人, 永远都是别人listen; 要不要accept?不要, 服务器来连接// 2. connect  客户端向服务器发起连接请求struct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_port=htons(serverport);server.sin_family=AF_INET;// server.sin_addr.s_addr=INADDR_ANY;   绝对不是inet_aton(serverip.c_str(),&(server.sin_addr));    // 字符串风格ip转成点分十进制int cnt=5;while(connect(sock,(struct sockaddr*)&server,sizeof(server))!=0)   // 连接失败{sleep(1);cout<<"正在给你重连, 重连次数还有: "<<cnt--<<endl;if(cnt<=0)break;}if(cnt<=0){cerr<<"连接失败"<<endl;exit(CONNECT_ERR);}char buffer[1024];// 3. 连接成功while(true){string line;cout<<"Enter>> ";getline(cin,line);write(sock,line.c_str(),line.size());ssize_t s = read(sock, buffer, sizeof(buffer)-1);if (s > 0){buffer[s] = 0;cout<<"server echo >>>"<<buffer<<endl;}else if (s == 0){cerr << "server quit" << endl;break;}else{cerr << "read error: " << strerror(errno) << endl;break;}}close(sock);return 0;
}

运行结果:

在这里插入图片描述

(4) v4_线程池版本

多线程版的问题:

  • 每当有新连接到来时,服务端的主线程都会为该客户端创建提供服务的新线程,当服务结束时就会将新线程销毁,这样做既麻烦又效率低下,每当有新连接到来才开始创建提供服务的新线程
  • 若有大量的客户端请求,此时服务端要为每一个客户端创建对应的服务线程。计算机中的线程越多,CPU的压力越大

线程池

  • 在服务端预先创建一批线程,当有客户端请求连接时就让这些线程为客户端提供服务,此时客户端一来就有线程为其提供服务,而不是当客户端来了才创建对应的服务线程(减少了频繁创建线程的开销)
  • 当某个线程为客户端提供完服务后,不要让该线程退出,而是让该线程继续为下一个客户端提供服务,如果当前没有客户端连接请求,则可以让该线程先进入休眠状态,当有客户端连接到来时再将该线程唤醒。
  • 服务端创建的这一批线程的数量不能太多,此时CPU的压力也就不会太大

task.hpp

#pragma once
#include<iostream>
#include<string>
#include<unistd.h>
#include<string.h>
#include<functional>
using namespace std;using cb_t=function<void(int sock, const string&,const uint16_t&)>;class Task
{
public:Task(){}Task(int sock, const string& ip,const uint16_t&port,cb_t cb):_sock(sock),_ip(ip),_port(port),_cb(cb){}void operator()(){_cb(_sock,_ip,_port);}~Task(){}private:int _sock;string _ip;uint16_t _port;cb_t _cb;
};

LockGuard.hpp

#include<iostream>
#include<pthread.h>
using namespace std;class Mutex   //自己不维护锁,由外部传入
{
public:Mutex(pthread_mutex_t* mutex):_pmutex(mutex){}void lock(){pthread_mutex_lock(_pmutex);}void unlock(){pthread_mutex_unlock(_pmutex);}~Mutex(){}private:pthread_mutex_t* _pmutex;   //锁的指针
};class LockGuard  //自己不维护锁,由外部传入
{
public:LockGuard(pthread_mutex_t* mutex):_mutex(mutex){_mutex.lock();}~LockGuard(){_mutex.unlock();}private:Mutex _mutex;   //锁的指针
};

thread.hpp

#include<iostream>
#include<string>
using namespace std;class Thread
{
public:typedef enum{NEW=0,RUNNING,EXITED}ThreadStatus;typedef void (*func_t)(void*);      //函数指针, 参数是void*Thread(int num, func_t func, void*args):_tid(0),_status(NEW),_func(func),_args(args){char name[128];snprintf(name,sizeof(name),"thread-%d",num);_name=name;}int status() {return _status;}string threadname() {return _name;}pthread_t thread_id(){if(_status==RUNNING)return _tid;elsereturn 0;}// runHelper是不是类的成员函数, 而类的成员函数, 具有默认参数this, 需要static// void*runHelper(Thread*this, void*args) , 而pthread_create要求传的参数必须是: void*的, 即参数不匹配// 但是static会有新的问题: static成员函数, 无法直接访问类属性和其他成员函数static void*runHelper(void*args){Thread*ts=(Thread*)args;   //就拿到了当前对象// _func(_args);(*ts)();}//仿函数void operator()(){_func(_args);}void run(){int n=pthread_create(&_tid,nullptr,runHelper,this);  //this: 是当前线程对象Threadif(n!=0) exit(-1);_status=RUNNING;}void join(){int n=pthread_join(_tid,nullptr);if(n!=0){cerr<<" main thread join thread "<< _name << " error "<<endl;}_status=EXITED;}~Thread(){}private:pthread_t _tid;string _name;func_t _func;  //线程未来要执行的回调void*_args;     //调用回调函数时的参数ThreadStatus _status;
};

threadPool_v4.hpp

#include<iostream>
#include<memory>
#include<vector>
#include<queue>
#include<unistd.h>
#include"thread.hpp"
#include"lockGuard.hpp"
using namespace std;const static int N=5;template<class T>
class threadPool
{
public:pthread_mutex_t* getlock(){return &_lock;}void threadWait(){pthread_cond_wait(&_cond,&_lock);}void threadWakeup(){pthread_cond_signal(&_cond);  // 唤醒在条件变量下等待的线程}bool isEmpty(){return _tasks.empty();}T popTask(){T t=_tasks.front();_tasks.pop();return t;}static void threadRoutine(void*args)      {threadPool<T>*tp=static_cast<threadPool<T>*>(args);while(true){// 1. 检测有没有任务 --- 本质是看队列是否为空// --- 本质就是在访问共享资源  --- 必定加锁// 2. 有: 处理// 3. 无: 等待// 细节: 必定加锁T t;{LockGuard lockguard(tp->getlock());while (tp->isEmpty()){// 等待, 在条件变量下等待tp->threadWait();}t = tp->popTask(); // 把任务从公共区域拿到私有区域}// for test// 处理任务应不应该在临界区中处理, 不应该, 这是线程自己私有的事情t();   }}static threadPool<T> * getinstance(){if (instance == nullptr)  // 为什么要这样? 提高效率, 减少加锁的次数{LockGuard lockguard(&instance_lock);if (instance == nullptr){cout<<"线程池单例形成"<<endl;instance = new threadPool<T>();instance->init();instance->start();}}return instance;}void init(){for(int i=0;i<_num;++i){_threads.push_back(Thread(i,threadRoutine,this));cout<<i<<" thread running"<<endl;}}void check(){for(auto&t:_threads){cout<<t.threadname()<<" running..."<<endl;}}void start(){for(auto&t:_threads){t.run();}}void pushTask(const T&t){LockGuard lockguard(&_lock);_tasks.push(t);threadWakeup();}~threadPool(){for(auto&t:_threads){t.join();}pthread_mutex_destroy(&_lock);pthread_cond_destroy(&_cond);}private:threadPool(int num=N):_num(num){pthread_mutex_init(&_lock,nullptr);pthread_cond_init(&_cond,nullptr);}threadPool(const threadPool<T>&tp)=delete;void operator=(const threadPool<T>&tp)=delete;private:vector<Thread> _threads;   int _num;                     queue<T> _tasks;               pthread_mutex_t _lock;pthread_cond_t  _cond;static threadPool<T>*instance;static pthread_mutex_t instance_lock;
};template<class T>
threadPool<T> * threadPool<T>::instance=nullptr;template<class T>
pthread_mutex_t  threadPool<T>::instance_lock=PTHREAD_MUTEX_INITIALIZER;

tcpServer.hpp

#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
#include<unistd.h>
#include<memory>
#include<functional>
#include<sys/wait.h>
#include<pthread.h>
#include"err.hpp"
#include"threadPool_v4.hpp"
#include"task.hpp"
using namespace std;namespace ns_server
{static const uint16_t defaultport=8081;static int backlog=32;using func_t=function<string(const string&)>;   // 回调函数,一种处理逻辑class TcpServer;class ThreadData{public:ThreadData(int fd, const string&ip,const uint16_t&port,TcpServer*ts):sock(fd),clientip(ip),clientport(port),current(ts){}public:int sock;string clientip;uint16_t clientport;TcpServer*current;};class TcpServer{public:TcpServer(func_t func, uint16_t port=defaultport):func_(func),port_(port),quit_(true){}void InitServer(){// 1. 创建socket文件listensock_=socket(AF_INET,SOCK_STREAM,0);if(listensock_<0){cerr<<"create socket error"<<endl;exit(SOCKET_ERR);}// 2. bindstruct 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;int n=bind(listensock_,(struct sockaddr*)&local,sizeof(local));if(n<0){cerr<<"bind socket error"<<endl;exit(BIND_ERR);}// 3. 监听int m=listen(listensock_,backlog);if(m<0){cerr<<"listen socket error"<<endl;exit(LISTEN_ERR);}}void Start(){quit_=false;while(!quit_){struct sockaddr_in client;socklen_t len=sizeof(client);// 4. 获取连接, acceptint sock=accept(listensock_,(struct sockaddr*)&client,&len);if (sock < 0)   // accept失败并不会终止进程, 只要获取下一个连接{cerr << "accept error" << endl;continue;}// 提取client信息   --- debugstring clientip=inet_ntoa(client.sin_addr); // 把4字节对应的IP转化成字符串风格uint16_t clientport=ntohs(client.sin_port);  // 网络序列转主机序列// 5. 获取新连接成功后, 开始进行业务处理cout<<"获取新连接成功: "<<sock<<" from "<<listensock_<<", "<< clientip << "-                 " <<clientport<<endl;// v4: 线程池版本 //  一旦用户来了,你才创建线程, 线程池吗// 使用线程池的时候, 一定是有限的线程个数, 一定要处理短任务Task t(sock,clientip,clientport, bind(&TcpServer::service,                                      this,placeholders::_1,placeholders::_2,placeholders::_3));threadPool<Task>::getinstance()->pushTask(t);}}static void*threadRoutine(void*args){pthread_detach(pthread_self());ThreadData*td=static_cast<ThreadData*>(args);td->current->service(td->sock,td->clientip,td->clientport);delete td;}// 流式 - 利用read和writevoid service(int sock, const string&clientip,const uint16_t clientport){string who=clientip + "-" + to_string(clientport);char buffer[1024];ssize_t s = read(sock, buffer, sizeof(buffer) - 1);if (s > 0){buffer[s] = 0;string res = func_(buffer); // 进行回调cout << who << ">>> " << res << endl;// 把收到的消息返回(写给客户端)write(sock, res.c_str(), res.size());}else if (s == 0){cout << who << " quit, me too" << endl;}else{close(sock);cerr << "read error: " << strerror(errno) << endl;}close(sock);}~TcpServer(){}private:uint16_t port_;int listensock_;bool quit_;      // 标志服务器是否运行字段func_t func_;};
}

tcpServer.cc

#include"tcpServer.hpp"
using namespace ns_server;// ./tcp_server port// 使用手册
static void usage(string proc)
{cout<<"usage:\n\t"<<proc<<" port\n"<<endl;
}string echo(const string&message)
{return message;
}int main(int argc,char*argv[])
{if(argc!=2){usage(argv[0]);exit(USAGE_ERR);}uint16_t port=atoi(argv[1]);unique_ptr<TcpServer> tsvr(new TcpServer(echo,port));tsvr->InitServer();tsvr->Start();return 0;
}

tcpClient.cc

#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<cstring>
#include<unistd.h>
#include<memory>
#include<functional>
#include"err.hpp"
using namespace std;static void usage(string proc)
{cout<<"usage:\n\t"<<proc<<" serverip serverport\n" <<endl;
}// ./tcp_client serverip serverport
int main(int argc,char*argv[])
{// 准备工作if(argc!=3){usage(argv[0]);exit(USAGE_ERR);}string serverip=argv[1];uint16_t serverport=atoi(argv[2]);// 1.创建套接字int sock=socket(AF_INET,SOCK_STREAM,0);if (sock < 0){cerr << "create socket error: " << strerror(errno) << endl;exit(SOCKET_ERR);}// (2) 客户端要不要bind呢? 要//     要不要自己bind呢? 不要, 因为client要让OS自动给用户进行bind// (3) 要不要listen?不要, 客户端连别人, 永远都是别人listen; 要不要accept?不要, 服务器来连接// 2. connect  客户端向服务器发起连接请求struct sockaddr_in server;memset(&server,0,sizeof(server));server.sin_port=htons(serverport);server.sin_family=AF_INET;// server.sin_addr.s_addr=INADDR_ANY;   绝对不是inet_aton(serverip.c_str(),&(server.sin_addr));    // 字符串风格ip转成点分十进制int cnt=5;while(connect(sock,(struct sockaddr*)&server,sizeof(server))!=0)   // 连接失败{sleep(1);cout<<"正在给你重连, 重连次数还有: "<<cnt--<<endl;if(cnt<=0)break;}if(cnt<=0){cerr<<"连接失败"<<endl;exit(CONNECT_ERR);}char buffer[1024];// 3. 连接成功while(true){string line;cout<<"Enter>> ";getline(cin,line);write(sock,line.c_str(),line.size());ssize_t s = read(sock, buffer, sizeof(buffer)-1);if (s > 0){buffer[s] = 0;cout<<"server echo >>>"<<buffer<<endl;}else if (s == 0){cerr << "server quit" << endl;break;}else{cerr << "read error: " << strerror(errno) << endl;break;}}close(sock);return 0;
}

运行结果:

ip serverport\n" <<endl;
}

// ./tcp_client serverip serverport
int main(int argc,char*argv[])
{
// 准备工作
if(argc!=3)
{
usage(argv[0]);
exit(USAGE_ERR);
}

string serverip=argv[1];
uint16_t serverport=atoi(argv[2]);// 1.创建套接字
int sock=socket(AF_INET,SOCK_STREAM,0);
if (sock < 0)
{cerr << "create socket error: " << strerror(errno) << endl;exit(SOCKET_ERR);
}// (2) 客户端要不要bind呢? 要
//     要不要自己bind呢? 不要, 因为client要让OS自动给用户进行bind
// (3) 要不要listen?不要, 客户端连别人, 永远都是别人listen; 要不要accept?不要, 服务器来连接// 2. connect  客户端向服务器发起连接请求
struct sockaddr_in server;
memset(&server,0,sizeof(server));
server.sin_port=htons(serverport);
server.sin_family=AF_INET;
// server.sin_addr.s_addr=INADDR_ANY;   绝对不是
inet_aton(serverip.c_str(),&(server.sin_addr));    // 字符串风格ip转成点分十进制int cnt=5;while(connect(sock,(struct sockaddr*)&server,sizeof(server))!=0)   // 连接失败
{sleep(1);cout<<"正在给你重连, 重连次数还有: "<<cnt--<<endl;if(cnt<=0)break;
}
if(cnt<=0)
{cerr<<"连接失败"<<endl;exit(CONNECT_ERR);
}char buffer[1024];
// 3. 连接成功
while(true)
{string line;cout<<"Enter>> ";getline(cin,line);write(sock,line.c_str(),line.size());ssize_t s = read(sock, buffer, sizeof(buffer)-1);if (s > 0){buffer[s] = 0;cout<<"server echo >>>"<<buffer<<endl;}else if (s == 0){cerr << "server quit" << endl;break;}else{cerr << "read error: " << strerror(errno) << endl;break;}
}close(sock);
return 0;

}


运行结果:![在这里插入图片描述](https://img-blog.csdnimg.cn/eadac05d3a6c43dbb8c5e44f9ccebca6.png)

相关文章:

网络编程套接字(3): 简单的TCP网络程序

文章目录 网络编程套接字(3)4. 简单的TCP网络程序4.1 服务端创建(1) 创建套接字(2) 绑定端口(3) 监听(4) 获取新连接(5) 处理读取与写入 4.2 客户端创建(1)连接服务器 4.3 代码编写(1) v1__简单发送消息(2) v2_多进程版本(3) v3_多线程版本(4) v4_线程池版本 网络编程套接字(3)…...

springMVC之拦截器

文章目录 前言一、拦截器的配置二、拦截器的三个抽象方法三、多个拦截器的执行顺序总结 前言 拦截器 一、拦截器的配置 SpringMVC中的拦截器用于拦截控制器方法的执行 SpringMVC中的拦截器需要实现HandlerInterceptor SpringMVC的拦截器必须在SpringMVC的配置文件中进行配置&…...

docker搭建个人网盘和私有仓库Harbor

目录 1、使用mysql:5.7和 owncloud 镜像&#xff0c;构建一个个人网盘 2、安装搭建私有仓库 Harbor 1、使用mysql:5.7和owncloud&#xff0c;构建一个个人网盘 1.拉取mysql:5.6镜像&#xff0c;并且运行mysql容器 [rootnode8 ~]# docker pull mysql:5.7 [rootnode8 ~]# doc…...

智慧排水监测系统,科技助力城市排水治理

城市里&#xff0c;人们每天通过道路通行&#xff0c;人多&#xff0c;路窄&#xff0c;都会拥堵。同样&#xff0c;下雨天&#xff0c;雨水通过雨篦汇集、管道输送&#xff0c;最终排出去&#xff0c;当雨水过大&#xff0c;或者管道过窄&#xff0c;或者管道不通畅&#xff0…...

部署java程序的服务器cpu过高如何排查和解决

1.top命令找到占用CPU高的Java进程PID 2.根据进程ID找到占用CPU高的线程 ps -mp pid -o THREAD,tid | sort -r ps -mp 124682 -o THREAD,tid | sort -r 3.将指定的线程ID输出为16进制格式 printf “%x\n” tid printf "%x\n" 6384 18f0 4.jstack pid |…...

合宙Air724UG LuatOS-Air LVGL API控件--按钮 (Button)

按钮 (Button) 按钮控件&#xff0c;这个就不用多说了&#xff0c;界面的基础控件之一。 示例代码 – 按键回调函数 event_handler function(obj, event) if event lvgl.EVENT_CLICKED then print(“Clicked\n”) elseif event lvgl.EVENT_VALUE_CHANGED then print(“To…...

new/delete与malloc/free的区别

new/delete与malloc/free的区别 new、delete是C中的操作符&#xff0c;而malloc、free是标准库函数。 new 和 delete 是类型安全的&#xff0c;它们能够根据要分配的对象类型进行内存分配和释放&#xff0c;并调用相应的构造函数和析构函数。而 malloc 和 free 则是无类型的&am…...

QT listWidget 中实现元素的自由拖拽

QListWIdget中拖拽元素移动 setMovement(QListView::Movement::Free);setDragEnabled(true); setDragDropMode(DragDropMode::DragDrop); setDefaultDropAction(Qt::DropAction::MoveAction);...

ChatGPT AIGC 完成二八分析柏拉图的制作案例

我们先让ChatGPT来总结一下二八分析柏拉图的好处与优点 同样ChatGPT 也可以帮我们来实现柏拉图的制作。 效果如下: 这样的按年份进行选择的柏拉图使用前端可视化的技术就可以实现。 如HTML,JS,Echarts等,但是代码可以让ChatGPT来做,生成。 在ChatGPT中给它一个Prompt …...

Python 分析HTTP的可靠性

在这篇文章中&#xff0c;我们将介绍如何使用 Python 来分析代理服务提供商的可靠性。代理服务在许多场景中都非常有用&#xff0c;例如突破地理限制、保护隐私和提高网络安全性。然而&#xff0c;并非所有的代理服务提供商都是可靠的。因此&#xff0c;我们将使用 Python 来测…...

数据库连接报错CannotGetJdbcConnectionException: Failed to obtain JDBC Connection

数据库连接报错CannotGetJdbcConnectionException: Failed to obtain JDBC Connection 报错信息 [Namecom.primeton.esb.online.restaurant.ms.online.mediaService.mediaService.biz][activity nameJDBC调用][activity idinvokePojo9] throw an exception:java.lang.Excepti…...

【Linux系列】vmware虚拟机网络配置详解

非原创 原文地址[1] 首发博客地址[2] 系列文章地址[3] vmware 为我们提供了三种网络工作模式&#xff0c;它们分别是&#xff1a;Bridged&#xff08;桥接模式&#xff09;、NAT&#xff08;网络地址转换模式&#xff09;、Host-Only&#xff08;仅主机模式&#xff09;。 打开…...

AUTOSAR规范与ECU软件开发(实践篇)7.8 MCAL模块配置方法及常用接口函数介绍之Icu的配置

目录 1、前言 2 、Icu模块 (1) Icu General配置 (2) IcuConfigSet配置 (3) IcuConfigSet配置 1、前言 本例程的硬件平台为MPC5744P开发板&...

2023-9-2 Prim算法求最小生成树

题目链接&#xff1a;Prim算法求最小生成树 #include <iostream> #include <cstring> #include <algorithm>using namespace std;const int N 510, INF 0x3f3f3f3f;int n, m; int g[N][N]; int dist[N]; bool st[N];int prim() {memset(dist, 0x3f, size…...

骨传导耳机会影响听力吗?这是真的吗?

首先正常的使用骨传导耳机并不会影响我们的听力&#xff01;那是为什么呢&#xff1f;&#xff1f; 因为骨传导是一种声音传导方式&#xff0c;可以通过人的颅骨、骨迷路、内耳淋巴液传递、螺旋器、听神经、听觉中枢来传递声波。 相对于通过耳道声波的经典声音传导方式&#x…...

【华为OD机试python】 阿里巴巴找黄金宝箱(Ⅱ)【2023 B卷|100分】

题目描述 一贫如洗的樵夫阿里巴巴在去砍柴的路上,无意中发现了强盗集团的藏宝地, 藏宝地有编号从0-N的箱子,每个箱子上面贴有箱子中藏有金币的数量。 从金币数量中选出一个数字集合,并销毁贴有这些数字的每个箱子, 如果能销毁一半及以上的箱子,则返回这个数字集合的最小大…...

9.6 【C语言】使用枚举类型

如果一个变量只有几种可能的值&#xff0c;则可以定义为枚举类型&#xff0c;所谓“枚举”就是指把可能的值一一列举出来&#xff0c;变量的值只限于列举出来的值的范围内。 声明枚举类型用enum开头&#xff0c;例如&#xff1a; enum Weekday{sun,mon,tue,wed,thu,fri,sar};…...

一文了解tcp/ip协议的运行原理

接触代理ip的人都了解https/sock5等ip协议&#xff0c;那么TCP/IP 协议又是什么&#xff1f; 一、什么是TCP/IP 协议&#xff1f; TCP/IP 协议实际上是一系列网络通信协议的一个统称&#xff0c;他负责具体的数据传输工作&#xff0c;核心的两个协议包括TCP以及IP&#xff0c…...

spring cloud alibaba

项目依赖 <parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.2.5.RELEASE</version></parent><properties><springcloud.alibaba.version>2.1…...

K 次取反后最大化的数组和【贪心算法】

1005 . K 次取反后最大化的数组和 给你一个整数数组 nums 和一个整数 k &#xff0c;按以下方法修改该数组&#xff1a; 选择某个下标 i 并将 nums[i] 替换为 -nums[i] 。 重复这个过程恰好 k 次。可以多次选择同一个下标 i 。 以这种方式修改数组后&#xff0c;返回数组 可能…...

【kafka】Golang实现分布式Masscan任务调度系统

要求&#xff1a; 输出两个程序&#xff0c;一个命令行程序&#xff08;命令行参数用flag&#xff09;和一个服务端程序。 命令行程序支持通过命令行参数配置下发IP或IP段、端口、扫描带宽&#xff0c;然后将消息推送到kafka里面。 服务端程序&#xff1a; 从kafka消费者接收…...

脑机新手指南(八):OpenBCI_GUI:从环境搭建到数据可视化(下)

一、数据处理与分析实战 &#xff08;一&#xff09;实时滤波与参数调整 基础滤波操作 60Hz 工频滤波&#xff1a;勾选界面右侧 “60Hz” 复选框&#xff0c;可有效抑制电网干扰&#xff08;适用于北美地区&#xff0c;欧洲用户可调整为 50Hz&#xff09;。 平滑处理&…...

MySQL 隔离级别:脏读、幻读及不可重复读的原理与示例

一、MySQL 隔离级别 MySQL 提供了四种隔离级别,用于控制事务之间的并发访问以及数据的可见性,不同隔离级别对脏读、幻读、不可重复读这几种并发数据问题有着不同的处理方式,具体如下: 隔离级别脏读不可重复读幻读性能特点及锁机制读未提交(READ UNCOMMITTED)允许出现允许…...

基于ASP.NET+ SQL Server实现(Web)医院信息管理系统

医院信息管理系统 1. 课程设计内容 在 visual studio 2017 平台上&#xff0c;开发一个“医院信息管理系统”Web 程序。 2. 课程设计目的 综合运用 c#.net 知识&#xff0c;在 vs 2017 平台上&#xff0c;进行 ASP.NET 应用程序和简易网站的开发&#xff1b;初步熟悉开发一…...

Vue3 + Element Plus + TypeScript中el-transfer穿梭框组件使用详解及示例

使用详解 Element Plus 的 el-transfer 组件是一个强大的穿梭框组件&#xff0c;常用于在两个集合之间进行数据转移&#xff0c;如权限分配、数据选择等场景。下面我将详细介绍其用法并提供一个完整示例。 核心特性与用法 基本属性 v-model&#xff1a;绑定右侧列表的值&…...

vue3+vite项目中使用.env文件环境变量方法

vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量&#xff0c;这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...

安宝特方案丨船舶智造的“AR+AI+作业标准化管理解决方案”(装配)

船舶制造装配管理现状&#xff1a;装配工作依赖人工经验&#xff0c;装配工人凭借长期实践积累的操作技巧完成零部件组装。企业通常制定了装配作业指导书&#xff0c;但在实际执行中&#xff0c;工人对指导书的理解和遵循程度参差不齐。 船舶装配过程中的挑战与需求 挑战 (1…...

Linux 内存管理实战精讲:核心原理与面试常考点全解析

Linux 内存管理实战精讲&#xff1a;核心原理与面试常考点全解析 Linux 内核内存管理是系统设计中最复杂但也最核心的模块之一。它不仅支撑着虚拟内存机制、物理内存分配、进程隔离与资源复用&#xff0c;还直接决定系统运行的性能与稳定性。无论你是嵌入式开发者、内核调试工…...

基于SpringBoot在线拍卖系统的设计和实现

摘 要 随着社会的发展&#xff0c;社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 在线拍卖系统&#xff0c;主要的模块包括管理员&#xff1b;首页、个人中心、用户管理、商品类型管理、拍卖商品管理、历史竞拍管理、竞拍订单…...

【Android】Android 开发 ADB 常用指令

查看当前连接的设备 adb devices 连接设备 adb connect 设备IP 断开已连接的设备 adb disconnect 设备IP 安装应用 adb install 安装包的路径 卸载应用 adb uninstall 应用包名 查看已安装的应用包名 adb shell pm list packages 查看已安装的第三方应用包名 adb shell pm list…...