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

【Linux网络编程五】Tcp套接字编程(四个版本服务器编写)

【Linux网络编程五】Tcp套接字编程(四个版本服务器编写)

  • [Tcp套接字编程]
  • 一.服务器端进程:
    • 1.创建套接字
    • 2.绑定网络信息
    • 3.设置监听状态
    • 4.获取新连接
    • 5.根据新连接进行通信
  • 二.客户端进程:
    • 1.创建套接字
    • 2.连接服务器套接字
    • 3.连接成功后进行通信
  • 三.version1:单进程版
  • 四.version2:多进程版
  • 五.version3:多线程版
  • 六.version4:线程池版
  • 七.解决细节问题:完善服务器和客户端
    • 1.write写入会存在偶发性失败,进程被信号杀死
    • 2.客户端每次请求都需要重新发起连接
    • 3.服务器端出现问题,客户端需要尝试自救
    • 4.复用ip地址和端口号

[Tcp套接字编程]

在这里插入图片描述
一般使用网络套接字编程时都需要引用以上头文件。

一.服务器端进程:

我们首先先进行服务器端的编写:服务器端肯定是需要知道有自己的ip地址和端口号的。
在这里插入图片描述

1.创建套接字

跟Udp套接字编程一样,通信前需要创建套接字,创建套接字的本质就是打开一个网络文件。

要注意的是Tcp是面向字节流的,创建套接字时,按照字节流形式创建。

        //服务器端启动之前创建套接字,绑定。//一开始的这个套接字是属于监听套接字_listensock=socket(AF_INET,SOCK_STREAM,0);if(_listensock<0){lg(Fatal,"sock create errno:%d errstring:%s",errno,strerror(errno));exit(SockError);}//创建套接字成功lg(Info,"sock create sucess listensock:%d",_listensock);

2.绑定网络信息

创建完套接字后,也就是打开一个网络文件,我们需要绑定该服务器的网络信息,比如ip地址和端口号等。这样该套接字才能找到服务器端。客户端往该套接字写入时,服务器端就能从该套接字里读取到。
服务器往该套接字里写入时,连接该套接字的客户端就能接收到。

 //创建成功后就要绑定服务器的网络信息struct sockaddr_in local;memset(&local,0,sizeof(local));//填充信息local.sin_family=AF_INET;local.sin_port=htons(_port);inet_aton(_ip.c_str(),&local.sin_addr);//填充完毕,真正绑定if((bind(_listensock,(struct sockaddr*)&local,sizeof(local)))<0){lg(Fatal,"bind errno:%d errstring:%s",errno,strerror(errno));exit(BindError);}lg(Info,"bind socket success listensock:%d",_listensock);//绑定成功

将字符粗类型转int类型使用inet_aton()调用,简单的那个存在线程安全。在这里插入图片描述

3.设置监听状态

Tcp与Udp不同之处在于,Tcp是面向连接的,什么意思呢?就是在真正通信前,需要先建立连接才能正式通信。比如Udp在绑定完网络信息后,就可以直接进行通信了,但Tcp不行,Tcp在绑定完网络信息后,还需要建立连接。

Tcp相比较Udp比较被动,在通信之前,需要随时随地地等待别人连接上去才能进行通信。所以服务器要一直处于一个等待到来连接的状态。
而等待到新连接需要一种能力,这个能力就是监听!只有我们将套接字设置为监听状态,该套接字才能获取到新连接。不然获取不到别人的连接。
在这里插入图片描述

 //udp中绑定成功后就可以进行通信了,但tcp与udp不同。tcp是面向连接的,在通信之前//需要先获取新连接,获取到新连接才能进行通信。没有获取连接那么就要等待连接,等待新连接的过程叫做监听,监听有没有新连接。//需要将套接字设置成监听状态listen(_listensock,backlog);//用来监听,等待新连接,只有具备监听状态才能识别到连

4.获取新连接

以上都是服务器的一些网络信息初始化步骤,接下来才是启动服务器。服务器启动服务呢?
首先服务器在通信之前需要获取到客户端的连接。这样才可以和客户端进行通信。那么如何获取到客户端的连接呢?

1.前提是该服务器的套接字已经设置为监听状态,只有套接字设置为监听状态才可以获取到客户端发起的连接。
2.利用accept函数就可以获取到客户端发起的连接。
在这里插入图片描述
3.accept函数和recvfrom函数类似,它从套接字里获取到连接,不仅仅可以获取到客户端发起的连接,还可以获取到客户端的网络信息。而获取到客户端的网络信息是通过两个输出型参数带出来的。
4.注意accept函数的返回值是一个文件描述符。为什么呢?也就是accept返回的也是一个套接字文件。accept从一个套接字文件里获取到连接然后再返回套接字文件?什么意思呢?

【问题】:为什么会有两个套接字?
accept从我们创建的套接字里获取连接,结果又返回一个套接字文件,为什么呢?这个新的套接字是干啥的?
在这里插入图片描述

1.也就是曾经被创建的,被绑定网络信息的,设置为监听状态的套接字是专门用来获取客户端发起的连接的,然后将该连接返回给一个新的套接字,让服务器通过新的套接字与客户端通信,而原来的套接字就又处于监听状态,继续来监听客户端发起的新连接。
2.其实就是为了提高通信效率,一个套接字既去获取连接,又去通信,效率比较低,而让一个套接字一直处于监听状态,获取到新连接后,就转让给另一个套接字,服务器其实是通过accept返回的套接字与连接端的客户进行通信的。
3.未来,获取的连接会越来越多,也就是返回的套接字会越来越多,而监听的套接字只有一个。

     lg(Info,"tcpserver is running");while(true){struct sockaddr_in client;socklen_t len=sizeof(client);//将套接字设置成监听状态后,就可以获取新连接int sockfd=accept(_listensock,(struct sockaddr*)&client,&len);//获取从监听套接字那里监听到的连接。然后返回一个新套接字,通过这个套接字与连接直接通信,而监听套接字继续去监听。if(sockfd<0){lg(Fatal,"accept error,errno: %d, errstring: %s",errno,strerror(errno));exit(AcceptError);}//获取新连接成功//将客户端端网络信息带出来uint16_t clientport=ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET,&client.sin_addr,clientip,sizeof(clientip));}

将客户端网络信息带出来注意需要将网络字节序转换成用户字节序。
在这里插入图片描述
还有我们的传输的内容会自动转换的。在这里插入图片描述

5.根据新连接进行通信

一旦服务器端获取到新连接成功后,就可以与对端的客户通过返回的套接字进行通信。

而Tcp是面向字节流的,面向字节流就可以直接使用文件操作,因为文件操作也是面向字节流的,也就是文件操作里的read,write就可以往套接字里读取和写入。

这里是引用
1.我们就可以根据获取到的套接字与客户端进行通信。
2.我们可以将该工作分离成一个函数service。与客户端通信需要知道连接的套接字和客户端的网络信息。

void Service(int &sockfd,const std::string &clientip,uint16_t &clientport){char inbuffer[1024];while(true){ssize_t n=read(sockfd,inbuffer,sizeof(inbuffer));if(n>0){inbuffer[n]=0;std::cout<<"client say# "<<inbuffer<<std::endl;//加工处理一下std::string echo_string="tcpserver加工处理数据:";echo_string+=inbuffer;//将加工处理的数据发送会去write(sockfd,echo_string.c_str(),echo_string.size());}else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了{lg(Info,"%s:%d quit, server close sockfd: %d",clientip.c_str(),clientport,sockfd);break;}else{lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));}}}

1.当服务器获取到连接后,read读取时,如果读取成功就会返回读取的个数大小。
2.当服务器获取到连接后,read读取之前或者连接又全部断掉,那么read就会读取到0。也就是在read的时候,没有用户连接了,那么服务器就没有必要再从该套接字里读取了。那么服务器读取的工作就退出,不再读取了。后续更需要将该套接字关闭起来。因为没有人使用了,就需要关闭起来。
3.read读取失败时就会返回小于0.
在这里插入图片描述

二.客户端进程:

服务器端基本工作已经准备好,接下来就是客户端,客户端在创建完套接字后需要绑定网络信息吗?

1.需要!但不需要显示绑定,操作系统会帮我们自动绑定。
在这里插入图片描述
2.客户端在发起连接时,系统就会帮我们自动绑定。

客户端想和服务器连接,那么用户肯定是知道该服务器端的套接字信息的,不然无法进行通信。所以在用户进行通信之前,我们首先将服务器端的网络信息给处理下。

比如将服务器的ip字符粗类型转换成int类型
有一下三个函数可以使用:
在这里插入图片描述

    std::string serverip=argv[1];uint16_t serverport = std::stoi(argv[2]);struct sockaddr_in server;socklen_t len=sizeof(server);server.sin_family=AF_INET;server.sin_port=htons(serverport);inet_pton(AF_INET,serverip.c_str(),&server.sin_addr);

1.创建套接字

不管是服务器端还是客户端要想网络通信都需要创建套接字,根据通信类型,创建Tcp套接字还是Udp套接字根据选择。我们这里创建Tcp套接字,按照面向字节流创建:

 //创建套接字int sockfd=socket(AF_INET,SOCK_STREAM,0);if(sockfd<0){std::cout<<"create sockfd err "<<std::endl;}//创建套接字成功,创建完套接字后该干什么?

2.连接服务器套接字

创建完套接字后该干什么呢?因为用户已经知道服务器端的套接字信息,所以我们就根据服务器端的套接字信息来向服务器端发起连接。

因为Tcp是面向连接的,所以必须先发起连接,对方获取到连接才能进行通信。

 //连接服务器端的套接字,所以客户端用户需要知道服务器端的网络信息的int n=connect(sockfd,(struct sockaddr*)&server,len);if(n<0){std::cout<<"connect sock err..."<<std::endl;exit(2);}//连接成功//连接成功后,就可以直接通信了,就可以直接给对方写消息了。

3.连接成功后进行通信

连接成功后,就可以与连接的服务器进行通信了,就可以通过套接字进行数据的传输了。

在这里插入图片描述
1.往套接字里写入就使用write,从套接字里读取就使用read。
2.注意使用完该套接字后,就要将套接字关闭。

  std::string message;while(true){std::cout<<"Please enter#";getline(std::cin,message);//往套接字里写write(sockfd,message.c_str(),message.size());char outbuffer[1024];//接收服务器发送的加工处理消息int n=read(sockfd,outbuffer,sizeof(outbuffer));if(n>0){outbuffer[n]=0;std::cout<<outbuffer<<std::endl;}}close(sockfd);

三.version1:单进程版

Tcpserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <pthread.h>
#include "Log.hpp"
#include "TASK.hpp"
#include "ThreadPool.hpp"
Log lg;
const std::string defaultip="0.0.0.0";
const int defaultfd=-1;
int backlog=10;//一般不要设置太大
enum 
{SockError=2,BindError,AcceptError,
};
class Tcpserver
{
public:Tcpserver(const uint16_t &port,const std::string &ip=defaultip):_listensock(-1),_port(port),_ip(ip){}void Init(){//服务器端启动之前创建套接字,绑定。//一开始的这个套接字是属于监听套接字_listensock=socket(AF_INET,SOCK_STREAM,0);if(_listensock<0){lg(Fatal,"sock create errno:%d errstring:%s",errno,strerror(errno));exit(SockError);}//创建套接字成功lg(Info,"sock create sucess listensock:%d",_listensock);//创建成功后就要绑定服务器的网络信息struct sockaddr_in local;memset(&local,0,sizeof(local));//填充信息local.sin_family=AF_INET;local.sin_port=htons(_port);inet_aton(_ip.c_str(),&local.sin_addr);//填充完毕,真正绑定if((bind(_listensock,(struct sockaddr*)&local,sizeof(local)))<0){lg(Fatal,"bind errno:%d errstring:%s",errno,strerror(errno));exit(BindError);}lg(Info,"bind socket success listensock:%d",_listensock);//绑定成功//udp中绑定成功后就可以进行通信了,但tcp与udp不同。tcp是面向连接的,在通信之前//需要先获取新连接,获取到新连接才能进行通信。没有获取连接那么就要等待连接,等待新连接的过程叫做监听,监听有没有新连接。//需要将套接字设置成监听状态listen(_listensock,backlog);//用来监听,等待新连接,只有具备监听状态才能识别到连接}void START(){lg(Info,"tcpserver is running");while(true){struct sockaddr_in client;socklen_t len=sizeof(client);//将套接字设置成监听状态后,就可以获取新连接int sockfd=accept(_listensock,(struct sockaddr*)&client,&len);//获取从监听套接字那里监听到的连接。然后返回一个新套接字,通过这个套接字与连接直接通信,而监听套接字继续去监听。if(sockfd<0){lg(Fatal,"accept error,errno: %d, errstring: %s",errno,strerror(errno));exit(AcceptError);}//获取新连接成功//将客户端端网络信息带出来uint16_t clientport=ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET,&client.sin_addr,clientip,sizeof(clientip));//根据新连接进行通信lg(Info,"get a new link...sockfd: %d,clientip: %s,clientport: %d",sockfd,clientip,clientport);//-----------version1 单进程版本Service(sockfd,clientip,clientport); close(sockfd);//不用了就关闭}}void Service(int &sockfd,const std::string &clientip,uint16_t &clientport){char inbuffer[1024];while(true){ssize_t n=read(sockfd,inbuffer,sizeof(inbuffer));if(n>0){inbuffer[n]=0;std::cout<<"client say# "<<inbuffer<<std::endl;//加工处理一下std::string echo_string="tcpserver加工处理数据:";echo_string+=inbuffer;//将加工处理的数据发送会去write(sockfd,echo_string.c_str(),echo_string.size());}else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了{lg(Info,"%s:%d quit, server close sockfd: %d",clientip.c_str(),clientport,sockfd);break;}else{lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));}}}~Tcpserver(){}private:int _listensock;//监听套接字只有一个,监听套接字用来不断获取新的连接。返回新的套接字std::string _ip;uint16_t _port;
};
Main.cc
#include "tcpserver.hpp"
#include <iostream>
#include <memory>
#include <cstdio>
void Usage(std::string proc)
{std::cout<<"\n\rUsage: "<<proc<<" port[1024+]\n"<<std::endl;
}
//./tcpserver port
int main(int args,char*argv[])
{if(args!=2){Usage(argv[0]);exit(-1);}//定义一个服务器对象uint16_t port=std::stoi(argv[1]);std::unique_ptr<Tcpserver> tcpsvr(new Tcpserver(port));tcpsvr->Init();tcpsvr->Run();
}

【存在问题】上面属于单进程版本,只有一个进程在执行,只能服务一个客户端,其他客户端要想再使用服务器需要等当前客户端使用完服务器退出才能使用。这也太low了。
所以我们想改进为多进程版本。
单进程就是因为当前进程获取到连接后,该进程就去服务客户端端了,其他客户端就无法再使用该进程了。所以我们可以这样改进:让父进程获取新连接,然后创建子进程,让子进程去提供服务。子进程在服务的过程中,父进程就一直在等待新的连接。
在这里插入图片描述
在这里插入图片描述

四.version2:多进程版

【处理文件描述符】
多进程版我们首先需要处理一下文件描述符,也就是被打开的套接字。
当获取到新连接后,我们的进程就可以创建子进程,让子进程去跟客户端进行通信。而父进程继续去等待新连接。

因为创建子进程,子进程会继承父进程的文件描述符表。也就是父子进程具有相同的文件描述符表。
1.父进程的主要任务是获取新连接,然后将获取到的套接字给子进程使用。所以它是不需要获取到的套接字的,所以父进程需要将获取到的新套接字关闭掉。
2.而子进程的主要任务是根据新连接服务客户端,它是需要监听套接字的,所以需要将监听套接字关闭掉。父子进程相互关闭文件描述符是不会影响对方的。

在这里插入图片描述
【父子进程并发执行】
因为单进程中就是因为进程只能在处于要么在监听中,要么在服务中,不能处于既在监听中,又在服务中。所以我们需要让父进程去获取连接,子进程去服务客户端。而如何做到呢?
父进程是需要等待子进程的,如果子进程不退出,那么它就要一直在阻塞等待。那么它就无法再去获取新连接了。这就跟单进程一样了。
所以我们需要让父进程非阻塞等待子进程。或者通过这样的方法:
让子进程再创建一个子进程,当创建成功时,就直接退出,那么父进程就会等待成功。就会继续往下执行回去新的连接。
而让孙子进程去服务客户端。
在这里插入图片描述

Tcpserver.hpp
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <pthread.h>
#include "Log.hpp"
#include "TASK.hpp"
#include "ThreadPool.hpp"
Log lg;
const std::string defaultip="0.0.0.0";
const int defaultfd=-1;
int backlog=10;//一般不要设置太大
enum 
{SockError=2,BindError,AcceptError,
};class Tcpserver
{
public:Tcpserver(const uint16_t &port,const std::string &ip=defaultip):_listensock(-1),_port(port),_ip(ip){}void Init(){//服务器端启动之前创建套接字,绑定。//一开始的这个套接字是属于监听套接字_listensock=socket(AF_INET,SOCK_STREAM,0);if(_listensock<0){lg(Fatal,"sock create errno:%d errstring:%s",errno,strerror(errno));exit(SockError);}//创建套接字成功lg(Info,"sock create sucess listensock:%d",_listensock);//创建成功后就要绑定服务器的网络信息struct sockaddr_in local;memset(&local,0,sizeof(local));//填充信息local.sin_family=AF_INET;local.sin_port=htons(_port);inet_aton(_ip.c_str(),&local.sin_addr);//填充完毕,真正绑定if((bind(_listensock,(struct sockaddr*)&local,sizeof(local)))<0){lg(Fatal,"bind errno:%d errstring:%s",errno,strerror(errno));exit(BindError);}lg(Info,"bind socket success listensock:%d",_listensock);//绑定成功//udp中绑定成功后就可以进行通信了,但tcp与udp不同。tcp是面向连接的,在通信之前//需要先获取新连接,获取到新连接才能进行通信。没有获取连接那么就要等待连接,等待新连接的过程叫做监听,监听有没有新连接。//需要将套接字设置成监听状态listen(_listensock,backlog);//用来监听,等待新连接,只有具备监听状态才能识别到连}void START(){lg(Info,"tcpserver is running");while(true){struct sockaddr_in client;socklen_t len=sizeof(client);//将套接字设置成监听状态后,就可以获取新连接int sockfd=accept(_listensock,(struct sockaddr*)&client,&len);//获取从监听套接字那里监听到的连接。然后返回一个新套接字,通过这个套接字与连接直接通信,而监听套接字继续去监听。if(sockfd<0){lg(Fatal,"accept error,errno: %d, errstring: %s",errno,strerror(errno));exit(AcceptError);}//获取新连接成功//将客户端端网络信息带出来uint16_t clientport=ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET,&client.sin_addr,clientip,sizeof(clientip));//根据新连接进行通信lg(Info,"get a new link...sockfd: %d,clientip: %s,clientport: %d",sockfd,clientip,clientport);//-----------version1 单进程版本//Service(sockfd,clientip,clientport); //close(sockfd);//不用了就关闭//-----------version2 多进程版本pid_t id=fork();if(id==0)//子进程,用来处理服务,父进程用来获取新连接{close(_listensock);//子进程不需要该文件关闭if(fork()>0)exit(0);//再创建一个子进程,然后让该进程退出,让孙子进程执行下面的服务,子进程就退出,父进程就等待成功就会重新获取连接Service(sockfd,clientip,clientport);close(sockfd);exit(0);}//父进程只负责用来获取新连接,获取完毕后就交给子进程,自己是不用的,所以关闭close(sockfd);pid_t rid=waitpid(id,nullptr,0);//阻塞等待(void)rid;}}void Service(int &sockfd,const std::string &clientip,uint16_t &clientport){char inbuffer[1024];while(true){ssize_t n=read(sockfd,inbuffer,sizeof(inbuffer));if(n>0){inbuffer[n]=0;std::cout<<"client say# "<<inbuffer<<std::endl;//加工处理一下std::string echo_string="tcpserver加工处理数据:";echo_string+=inbuffer;//将加工处理的数据发送会去write(sockfd,echo_string.c_str(),echo_string.size());}else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了{lg(Info,"%s:%d quit, server close sockfd: %d",clientip.c_str(),clientport,sockfd);break;}else{lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));}}}~Tcpserver(){}private:int _listensock;//监听套接字只有一个,监听套接字用来不断获取新的连接。返回新的套接字std::string _ip;uint16_t _port;
};

在这里插入图片描述

五.version3:多线程版

【存在问题】当客户端发起一个连接,服务器获取一个连接时,服务器才忙着创建子进程呢,而创建一个进程的成本是很高的!
所以我们想当在服务器端,获取到一个新连接后,不创建进程而创建线程,让子线程去服务客户端。而主线程继续去获取新的连接。

1.所以只要一旦获取到一个新连接,我们只要当前进程创建线程,将获取的连接的套接字信息以函数参数的形式发送给线程。
2.要让线程去服务连接的客户端,线程不仅需要知道客户端的套接字,还需要知道是要服务谁,也就是客户端的网络信息。
所以我们还需要构建一个线程参数数据结构体,包含线程所需要的客户端的套接字,网络信息等。

在这里插入图片描述

【问题1】主线程需要等待子线程吗?
这个问题又回归到刚刚的多进程和单进程中的存在的问题了,如果主线程阻塞等待子线程,那么主线程就无法再重新获取新的连接了。
所以主线程不等待子线程,让子线程自己分离即可。也就是只要获取到一个新连接,那么主线程就创建子线程,创建完后,就不管子线程了,就继续回去获取新的连接。
子线程将自己分离后,就直接去通信即可。
在这里插入图片描述
在这里插入图片描述

【问题2】在多线程这里需要处理多余的文件描述符吗?
不需要!因为线程所拥有的资源都是共享的!一旦关闭,其他线程就会收到影响。
在这里插入图片描述
【问题3】在类内部创建多线程需要将线程函数设置为静态成员函数,而静态成员函数又无法使用通信函数Service,怎么办呢?
所以我们可以在构造线程数据结构体时,将该服务器类对象的指针也构建进去,当创建完线程,线程就可以根据该类指针找到通信函数Service。
在这里插入图片描述
在创建线程之前,我们首先将线程所需要的参数数据构建初始化好。然后就可以创建线程,让线程去执行通信函数。


#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <pthread.h>
#include "Log.hpp"
#include "TASK.hpp"
#include "ThreadPool.hpp"
Log lg;
const std::string defaultip="0.0.0.0";
const int defaultfd=-1;
int backlog=10;//一般不要设置太大
enum 
{SockError=2,BindError,AcceptError,
};
class Tcpserver;//构建线程所需要的参数数据:获取连接的套接字,客户端端的网络信息,服务器类的指针
class ThreadData
{
public:ThreadData(int &fd,const std::string& ip,uint16_t &port,Tcpserver* svr):_sockfd(fd),_ip(ip),_port(port),_svr(svr){}
public:  int _sockfd;std::string _ip;uint16_t _port;Tcpserver* _svr;//通过该类指针可以找到类内部函数
};class Tcpserver
{
public:Tcpserver(const uint16_t &port,const std::string &ip=defaultip):_listensock(-1),_port(port),_ip(ip){}void Init(){//服务器端启动之前创建套接字,绑定。//一开始的这个套接字是属于监听套接字_listensock=socket(AF_INET,SOCK_STREAM,0);if(_listensock<0){lg(Fatal,"sock create errno:%d errstring:%s",errno,strerror(errno));exit(SockError);}//创建套接字成功lg(Info,"sock create sucess listensock:%d",_listensock);//创建成功后就要绑定服务器的网络信息struct sockaddr_in local;memset(&local,0,sizeof(local));//填充信息local.sin_family=AF_INET;local.sin_port=htons(_port);inet_aton(_ip.c_str(),&local.sin_addr);//填充完毕,真正绑定if((bind(_listensock,(struct sockaddr*)&local,sizeof(local)))<0){lg(Fatal,"bind errno:%d errstring:%s",errno,strerror(errno));exit(BindError);}lg(Info,"bind socket success listensock:%d",_listensock);//绑定成功//udp中绑定成功后就可以进行通信了,但tcp与udp不同。tcp是面向连接的,在通信之前//需要先获取新连接,获取到新连接才能进行通信。没有获取连接那么就要等待连接,等待新连接的过程叫做监听,监听有没有新连接。//需要将套接字设置成监听状态listen(_listensock,backlog);//用来监听,等待新连接,只有具备监听状态才能识别到连接}static void* Routine(void *args)//静态成员函数无法使用成员函数,再封装一个服务器对象{//子线程要和主线程分离,主线程不需要等待子线程,直接回去重新获取新连接pthread_detach(pthread_self());ThreadData* td=static_cast<ThreadData*>(args);//子线程用来服务客户端td->_svr->Service(td->_sockfd,td->_ip,td->_port);delete td;return nullptr;}void Run(){//一启动服务器,就将线程池中的线程创建ThreadPool<TASK>::GetInstance()->Start();//单例对象//静态函数,通过类域就可以使用lg(Info,"tcpserver is running");while(true){struct sockaddr_in client;socklen_t len=sizeof(client);//将套接字设置成监听状态后,就可以获取新连接int sockfd=accept(_listensock,(struct sockaddr*)&client,&len);//获取从监听套接字那里监听到的连接。然后返回一个新套接字,通过这个套接字与连接直接通信,而监听套接字继续去监听。if(sockfd<0){lg(Fatal,"accept error,errno: %d, errstring: %s",errno,strerror(errno));exit(AcceptError);}//获取新连接成功//将客户端端网络信息带出来uint16_t clientport=ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET,&client.sin_addr,clientip,sizeof(clientip));//根据新连接进行通信lg(Info,"get a new link...sockfd: %d,clientip: %s,clientport: %d",sockfd,clientip,clientport);//-----------version3 多线程版本ThreadData *td=new ThreadData(sockfd,clientip,clientport,this);//首先将线程所需要的参数数据初始化好pthread_t tid;pthread_create(&tid,nullptr,Routine,td);//Routine要设置成静态成员函数}}void Service(int &sockfd,const std::string &clientip,uint16_t &clientport){char inbuffer[1024];while(true){ssize_t n=read(sockfd,inbuffer,sizeof(inbuffer));if(n>0){inbuffer[n]=0;std::cout<<"client say# "<<inbuffer<<std::endl;//加工处理一下std::string echo_string="tcpserver加工处理数据:";echo_string+=inbuffer;//将加工处理的数据发送会去write(sockfd,echo_string.c_str(),echo_string.size());}else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了{lg(Info,"%s:%d quit, server close sockfd: %d",clientip.c_str(),clientport,sockfd);break;}else{lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));}}}~Tcpserver(){}private:int _listensock;//监听套接字只有一个,监听套接字用来不断获取新的连接。返回新的套接字std::string _ip;uint16_t _port;
};

六.version4:线程池版

【服务器端:构建线程池
【存在问题】虽然该多线程版比多进程版要好些,成本要低一些。但还是存在问题:

1.客户端发起连接,服务器端获取到连接成功后,线程才被创建处理,而线程创建也是需要时间的,所以我们想减少这个时间----我们可以通过线程池来完善。线程池中的线程都是早就被创建好的。只要获取到新连接,就将对应的任务投入到线程池里让线程池里的线程去执行。
2.每来一个用户,就会发起一个连接,就要创建一个线程, 如果客户端不退出,那么线程就一直存在。那么就会存在很多线程的情况。这主要原因是我们的服务器的服务是长时间服务,服务器端是主动退出的,所以客户端不退的话,线程就无法退出。------所以我们想让服务器的服务变成短时间服务,服务完就立马关闭套接字,不再服务。线程就会退出。

在这里插入图片描述

所以服务器端只要获取到一个新连接,就将服务客户端的操作让线程池里的线程去执行。
在这里插入图片描述

不过线程池里的线程要执行必须是以任务的形式竞争,所以我们需要将服务客户端的动作封装成一个任务,然后将任务投递到线程池里,线程池里线程就会被唤醒去竞争任务。
在这里插入图片描述

线程池---被封装成单例模式
#pragma once#include <iostream>
#include <vector>
#include <string>
#include <queue>
#include <pthread.h>
#include <unistd.h>struct ThreadInfo
{pthread_t tid;std::string name;
};static const int defalutnum = 10;template <class T>
class ThreadPool
{
public:void Lock(){pthread_mutex_lock(&mutex_);}void Unlock(){pthread_mutex_unlock(&mutex_);}void Wakeup(){pthread_cond_signal(&cond_);}void ThreadSleep(){pthread_cond_wait(&cond_, &mutex_);}bool IsQueueEmpty(){return tasks_.empty();}std::string GetThreadName(pthread_t tid){for (const auto &ti : threads_){if (ti.tid == tid)return ti.name;}return "None";}public:static void *HandlerTask(void *args){ThreadPool<T> *tp = static_cast<ThreadPool<T> *>(args);std::string name = tp->GetThreadName(pthread_self());while (true){tp->Lock();while (tp->IsQueueEmpty()){tp->ThreadSleep();}T t = tp->Pop();tp->Unlock();t();}}void Start(){int num = threads_.size();for (int i = 0; i < num; i++){threads_[i].name = "thread-" + std::to_string(i + 1);pthread_create(&(threads_[i].tid), nullptr, HandlerTask, this);}}T Pop(){T t = tasks_.front();tasks_.pop();return t;}void Push(const T &t){Lock();tasks_.push(t);Wakeup();Unlock();}static ThreadPool<T> *GetInstance(){if (nullptr == tp_) // ???{pthread_mutex_lock(&lock_);if (nullptr == tp_){std::cout << "log: singleton create done first!" << std::endl;tp_ = new ThreadPool<T>();}pthread_mutex_unlock(&lock_);}return tp_;}private:ThreadPool(int num = defalutnum) : threads_(num){pthread_mutex_init(&mutex_, nullptr);pthread_cond_init(&cond_, nullptr);}~ThreadPool(){pthread_mutex_destroy(&mutex_);pthread_cond_destroy(&cond_);}ThreadPool(const ThreadPool<T> &) = delete;const ThreadPool<T> &operator=(const ThreadPool<T> &) = delete; // a=b=c
private:std::vector<ThreadInfo> threads_;std::queue<T> tasks_;pthread_mutex_t mutex_;pthread_cond_t cond_;static ThreadPool<T> *tp_;static pthread_mutex_t lock_;
};template <class T>
ThreadPool<T> *ThreadPool<T>::tp_ = nullptr;template <class T>
pthread_mutex_t ThreadPool<T>::lock_ = PTHREAD_MUTEX_INITIALIZER;
构建任务,准备投递到线程池里的任务队列里
#pragma once
#include "Log.hpp"
#include <iostream>
#include "Init.hpp"
Init init;//刚加载程序时,文件里的内容就加载到map里了。
extern Log lg;
class TASK//构建任务,就是一旦获取到连接后,就将客户端的网络信息存到任务里,让服务器根据这个信息去服务客户端
{public:    TASK(int &sockfd,const std::string& ip,uint16_t& port):_sockfd(sockfd),_clientip(ip),_clientport(port){}void run(){char inbuffer[1024];// while(true)ssize_t n=read(_sockfd,inbuffer,sizeof(inbuffer));if(n>0){inbuffer[n]=0;std::cout<<"client key: "<<inbuffer<<std::endl;//加工处理一下std::string echo_string=init.translation(inbuffer);//将加工处理的数据发送会去n=write(_sockfd,echo_string.c_str(),echo_string.size());//写入过程也可能会失败,操作系统会发送信号将该进程杀死的,我们不想被杀死,就要忽略这个信息if(n<0){//失败了,我们将信号忽略,但将日志打印出来lg(Fatal,"write error :%d ,errstring :%s",errno,strerror(errno));}}else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了{lg(Info,"%s:%d quit, server close sockfd: %d",_clientip.c_str(),_clientport,_sockfd);}else{lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));}}void operator()(){run();}~TASK(){}
public:int _sockfd;std::string _clientip;uint16_t _clientport;
};
线程池版本
#pragma once
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <unistd.h>
#include <pthread.h>
#include "Log.hpp"
#include "TASK.hpp"
#include "ThreadPool.hpp"
Log lg;const std::string defaultip="0.0.0.0";
const int defaultfd=-1;
int backlog=10;//一般不要设置太大
enum 
{SockError=2,BindError,AcceptError,
};
class Tcpserver;class ThreadData
{
public:ThreadData(int &fd,const std::string& ip,uint16_t &port,Tcpserver* svr):_sockfd(fd),_ip(ip),_port(port),_svr(svr){}
public:  int _sockfd;std::string _ip;uint16_t _port;Tcpserver* _svr;
};
class Tcpserver
{
public:Tcpserver(const uint16_t &port,const std::string &ip=defaultip):_listensock(-1),_port(port),_ip(ip){}void Init(){//服务器端启动之前创建套接字,绑定。//一开始的这个套接字是属于监听套接字_listensock=socket(AF_INET,SOCK_STREAM,0);if(_listensock<0){lg(Fatal,"sock create errno:%d errstring:%s",errno,strerror(errno));exit(SockError);}//创建套接字成功lg(Info,"sock create sucess listensock:%d",_listensock);//创建成功后就要绑定服务器的网络信息struct sockaddr_in local;memset(&local,0,sizeof(local));//填充信息local.sin_family=AF_INET;local.sin_port=htons(_port);inet_aton(_ip.c_str(),&local.sin_addr);//填充完毕,真正绑定if((bind(_listensock,(struct sockaddr*)&local,sizeof(local)))<0){lg(Fatal,"bind errno:%d errstring:%s",errno,strerror(errno));exit(BindError);}lg(Info,"bind socket success listensock:%d",_listensock);//绑定成功//udp中绑定成功后就可以进行通信了,但tcp与udp不同。tcp是面向连接的,在通信之前//需要先获取新连接,获取到新连接才能进行通信。没有获取连接那么就要等待连接,等待新连接的过程叫做监听,监听有没有新连接。//需要将套接字设置成监听状态listen(_listensock,backlog);//用来监听,等待新连接,只有具备监听状态才能识别到连接}static void* Routine(void *args)//静态成员函数无法使用成员函数,再封装一个服务器对象{//子线程要和主线程分离,主线程不需要等待子线程,直接回去重新获取新连接pthread_detach(pthread_self());ThreadData* td=static_cast<ThreadData*>(args);//子线程用来服务客户端td->_svr->Service(td->_sockfd,td->_ip,td->_port);delete td;return nullptr;}void Run(){//一启动服务器,就将线程池中的线程创建ThreadPool<TASK>::GetInstance()->Start();//单例对象//静态函数,通过类域就可以使用lg(Info,"tcpserver is running");while(true){struct sockaddr_in client;socklen_t len=sizeof(client);//将套接字设置成监听状态后,就可以获取新连接int sockfd=accept(_listensock,(struct sockaddr*)&client,&len);//获取从监听套接字那里监听到的连接。然后返回一个新套接字,通过这个套接字与连接直接通信,而监听套接字继续去监听。if(sockfd<0){lg(Fatal,"accept error,errno: %d, errstring: %s",errno,strerror(errno));exit(AcceptError);}//获取新连接成功//将客户端端网络信息带出来uint16_t clientport=ntohs(client.sin_port);char clientip[32];inet_ntop(AF_INET,&client.sin_addr,clientip,sizeof(clientip));//根据新连接进行通信lg(Info,"get a new link...sockfd: %d,clientip: %s,clientport: %d",sockfd,clientip,clientport);//----------version4 线程池版本//当获取到新连接时,就构建任务TASK t(sockfd,clientip,clientport); //将任务放进线程池里,线程就会到线程池里去执行任务。ThreadPool<TASK>::GetInstance()->Push(t);}}void Service(int &sockfd,const std::string &clientip,uint16_t &clientport){char inbuffer[1024];while(true){ssize_t n=read(sockfd,inbuffer,sizeof(inbuffer));if(n>0){inbuffer[n]=0;std::cout<<"client say# "<<inbuffer<<std::endl;//加工处理一下std::string echo_string="tcpserver加工处理数据:";echo_string+=inbuffer;//将加工处理的数据发送会去write(sockfd,echo_string.c_str(),echo_string.size());}else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了{lg(Info,"%s:%d quit, server close sockfd: %d",clientip.c_str(),clientport,sockfd);break;}else{lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));}}}~Tcpserver(){}private:int _listensock;//监听套接字只有一个,监听套接字用来不断获取新的连接。返回新的套接字std::string _ip;uint16_t _port;
};

【服务器端:构建短服务----->翻译字典】

在这里插入图片描述

服务器端的服务功能是交给线程池里的线程执行的,所以最好是短服务,如果是长服务的话,那么就会存在很多线程不退出,客户端不主动退出,线程就不退出的场景。我们想要让服务器只服务一次,服务完后,就将对应的服务的套接字关闭,不再服务。那么线程服务完一次就结束了。

所以我们可以设计一个翻译字典的功能,让客户端用户发送字母,服务器端给客户端翻译字母,再将字母的中文发送回去。服务器端服务完一次后,就将监听套接字获取到的新的套接字关闭,不再服务。
如果客户端想要再次请求服务,就需要重新发起连接,让服务器端的监听套接字重新获取到新的套接字。因为原来获取到的套接字已经被关闭。

翻译字典功能
#pragma once 
#include <iostream>
#include <string>
#include <fstream>
#include <unordered_map>
#include "Log.hpp"
const std::string dictname="./dict.txt";
const std::string sep=":";//默认的分割符
extern Log lg;
class Init//相当于加载配置文件的动作,当创建出来,文件内容就已经被加载到map里了
{public:void Spilt(std::string& line,std::string *part1,std::string* part2){auto pos=line.find(sep);if(pos==std::string::npos)return ;*part1=line.substr(0,pos);*part2=line.substr(pos+1);return ;}Init(){std::ifstream in(dictname);//定义一个文件流,成功就打开该文件if(!in.is_open()){lg(Fatal,"ifstream open %s error",strerror(errno));exit(1);} //读取文件里的内容std::string line;//按行读取。读取到line里while(std::getline(in,line)){std::string part1,part2;Spilt(line,&part1,&part2);//然后分割到map里dic.insert({part1,part2});}in.close();}std::string translation(const std::string& key){auto iter=dic.find(key);//返回对应的该值的的迭代器if(iter==dic.end())return "Unkonwn";else return iter->second;}
private:std::unordered_map<std::string,std::string> dic; 
};
dict。txt对应的配置文件
apple:苹果
yellow:黄色
red:红色
bule:蓝色
man:男人
woman:女人
hello:你好

让服务器端的翻译字典只服务服务一次,就将对应的套接字关闭:

#pragma once
#include "Log.hpp"
#include <iostream>
#include "Init.hpp"
Init init;//刚加载程序时,文件里的内容就加载到map里了。
extern Log lg;
class TASK//构建任务,就是一旦获取到连接后,就将客户端的网络信息存到任务里,让服务器根据这个信息去服务客户端
{public:    TASK(int &sockfd,const std::string& ip,uint16_t& port):_sockfd(sockfd),_clientip(ip),_clientport(port){}void run(){char inbuffer[1024];// while(true)ssize_t n=read(_sockfd,inbuffer,sizeof(inbuffer));if(n>0){inbuffer[n]=0;std::cout<<"client key: "<<inbuffer<<std::endl;//加工处理一下std::string echo_string=init.translation(inbuffer);//将加工处理的数据发送会去n=write(_sockfd,echo_string.c_str(),echo_string.size());//写入过程也可能会失败,操作系统会发送信号将该进程杀死的,我们不想被杀死,就要忽略这个信息if(n<0){//失败了,我们将信号忽略,但将日志打印出来lg(Fatal,"write error :%d ,errstring :%s",errno,strerror(errno));}}else if(n==0)//如果没有用户连接了,那么就会读到0.服务器端也就不要再读了{lg(Info,"%s:%d quit, server close sockfd: %d",_clientip.c_str(),_clientport,_sockfd);}else{lg(Fatal,"read errno: %d, errstring: %s",errno,strerror(errno));}close(_sockfd);//任务只处理一次,服务器端处理完任务后,就会将该套接字关闭,线程池里的线程就不会一直在执行//客户端如果想再使用服务,需要重新连接,而该套接字已经被关闭,客户端也需要重新创建}void operator()(){run();}~TASK(){}
public:int _sockfd;std::string _clientip;uint16_t _clientport;
};

在这里插入图片描述

七.解决细节问题:完善服务器和客户端

1.write写入会存在偶发性失败,进程被信号杀死

write写入功能也是会存在失败的,这种失败是偶发性的,比较小,但也是会发生。比如说,服务器端刚接收到客户端发送的消息,正在处理的过程中,客户端又将连接关闭,那么服务器向往这个套接字里写入数据时,就会写入失败。
而这种失败,就会类似于管道里的,读端关闭,写端再往里写时,系统就会觉得没有用,就会发送信号将该进程杀死。所以这里如果出现了写入失败,也会被操作系统杀死的。而我们不希望服务器端被杀死。因为只是出现了偶发性的写入失败。
在这里插入图片描述

所以我们当出现写入失败时,我们应该忽略操作系统发送的信号。防止被杀死。
在这里插入图片描述
所以服务器端不仅要对读取做处理,还要对写入做处理。

2.客户端每次请求都需要重新发起连接

在这里插入图片描述
因为服务器端,当监听套接字获取到新连接时,就会返回一个新的套接字给线程使用去服务客户端。而线程只服务一次,就会将该套接字关闭。关闭完后,对端的客户端就无zsxxxxxxxxxx法再使用服务了。虽然它对应的连接还在,但是服务器端的连接被关闭了。
所以客户端端再次发送请求时,服务器就接收不到。
【解决方案】
在这里插入图片描述
所以客户端在被服务完后,想要再次获取服务,就必须要重新发起连接,这样服务器端才可以获取到对应的连接。并且还需要重新创建套接字,然后再发起连接。因为原来的套接字已经被服务器关闭,不能再使用了。

所以每次客户端想要服务时,都必须重新创建套接字然后发起连接。

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>void Usage(std::string proc)
{std::cout<<"\n\rUsage: "<<proc<<" port[1024+]\n"<<std::endl;
}
//./tcpclient ip port
int main(int args,char* argv[])
{if(args!=3){Usage(argv[0]);exit(1);}std::string serverip=argv[1];uint16_t serverport = std::stoi(argv[2]);struct sockaddr_in server;socklen_t len=sizeof(server);server.sin_family=AF_INET;server.sin_port=htons(serverport);inet_pton(AF_INET,serverip.c_str(),&server.sin_addr);while(true){//创建套接字int sockfd=socket(AF_INET,SOCK_STREAM,0);if(sockfd<0){std::cout<<"create sockfd err "<<std::endl;}//创建套接字成功,创建完套接字后该干什么?//连接服务器端的套接字,所以客户端用户需要知道服务器端的网络信息的connect(sockfd,(struct sockaddr*)&server,len);std::string message;std::cout<<"Please enter#";getline(std::cin,message);//往套接字里写int n=write(sockfd,message.c_str(),message.size());if(n<0)//服务器端会将该套接字关闭,然后就写不进去了。需要重新创建套接字连接{std::cerr<<"write error..."<<std::endl;continue;}char outbuffer[1024];//接收服务器发送的加工处理消息n=read(sockfd,outbuffer,sizeof(outbuffer));if(n>0){outbuffer[n]=0;std::cout<<outbuffer<<std::endl;}close(sockfd);}return 0;}

3.服务器端出现问题,客户端需要尝试自救

在这里插入图片描述
所以当服务器出现问题时,比如挂掉了。客户端首先要尝试重新连接,看能不能恢复正常。因为服务器端出现问题,那么客户端的写入和读取就可能会出现问题,甚至连接部分会出现问题,这些问题一旦出现,就要尝试重新连接。多次连接,如果服务器又恢复连接了,那么就可以重新连接成功。
否则还是失败,失败了就重连接失败,用户就退出吧不玩了。

1.只是连接部分或者写入读取部分出现错误,客户端创建的套接字并没有问题,所以不需要重新创建。只需要循环连接部分即可。
2.重新连接模块是客户端应该具备的功能。在这里插入图片描述

#include <iostream>
#include <cstdlib>
#include <unistd.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>void Usage(std::string proc)
{std::cout<<"\n\rUsage: "<<proc<<" port[1024+]\n"<<std::endl;
}
//./tcpclient ip port
int main(int args,char* argv[])
{if(args!=3){Usage(argv[0]);exit(1);}std::string serverip=argv[1];uint16_t serverport = std::stoi(argv[2]);struct sockaddr_in server;socklen_t len=sizeof(server);server.sin_family=AF_INET;server.sin_port=htons(serverport);inet_pton(AF_INET,serverip.c_str(),&server.sin_addr);while(true){//创建套接字int sockfd=socket(AF_INET,SOCK_STREAM,0);if(sockfd<0){std::cout<<"create sockfd err "<<std::endl;}//创建套接字成功,创建完套接字后该干什么?//连接服务器端的套接字,所以客户端用户需要知道服务器端的网络信息的int cnt=10;bool isreconnect=false;do{ int n=connect(sockfd,(struct sockaddr*)&server,len);if(n<0)//服务器关闭了,肯定会连接失败{isreconnect=true;cnt--;std::cout<<"connect sock err...,cnt: "<<cnt<<std::endl;sleep(12);}else//重连成功了{break;}}while(cnt&&isreconnect);//连接成功//连接成功后,就可以直接通信了,就可以直接给对方写消息了。if(cnt==0){std::cerr<<"user offline.."<<std::endl;break;//用户直接不玩了}std::string message;std::cout<<"Please enter#";getline(std::cin,message);//往套接字里写int n=write(sockfd,message.c_str(),message.size());if(n<0)//服务器端会将该套接字关闭,然后就写不进去了。需要重新创建套接字连接{std::cerr<<"write error..."<<std::endl;continue;}char outbuffer[1024];//接收服务器发送的加工处理消息n=read(sockfd,outbuffer,sizeof(outbuffer));if(n>0){outbuffer[n]=0;std::cout<<outbuffer<<std::endl;}close(sockfd);}return 0;}

4.复用ip地址和端口号

在这里插入图片描述
将套接字设置成允许复用ip地址和端口号。

相关文章:

【Linux网络编程五】Tcp套接字编程(四个版本服务器编写)

【Linux网络编程五】Tcp套接字编程(四个版本服务器编写&#xff09; [Tcp套接字编程]一.服务器端进程&#xff1a;1.创建套接字2.绑定网络信息3.设置监听状态4.获取新连接5.根据新连接进行通信 二.客户端进程&#xff1a;1.创建套接字2.连接服务器套接字3.连接成功后进行通信 三…...

APP 有漏洞被测要下架,怎么处理?

事情的经过是这样的&#xff1a; 1&#xff1a;学员公司测试的 APP 发现有漏洞&#xff0c;被要求下架 2&#xff1a;他被公司要求去查询 APP 哪里有漏洞 3&#xff1a;他来寻求帮助&#xff0c;推荐几款安全测试扫描漏洞的问题。 事情的梳理&#xff1a; 1:我们看了他的 …...

2024年2月19日-2月25日(全面进行+收集免费虚幻商城资源)

试试周一到周五重点进行&#xff0c;周末抄写源码&#xff0c;周一晚上看书很快就在22&#xff1a;00睡着&#xff0c;早上可以看看视频教程&#xff0c;出租车上补觉。 执行如下&#xff1a; 周一&#xff1a; 8&#xff1a;01-9&#xff1a;20ue4 rpg&#xff08;184&#xf…...

Flutter学习4 - Dart数据类型

1、基本数据类型 num、int、double &#xff08;1&#xff09;常用数据类型 num类型&#xff0c;是数字类型的父类型&#xff0c;有两个子类 int 和 double 通过在函数名前加下划线&#xff0c;可以将函数变成私有函数&#xff0c;私有函数只能在当前文件中调用 //常用数据…...

leetcode hot100单词拆分

在本题中&#xff0c;我们是要把一个字符串&#xff0c;判断是否能用给的字符串数组中的单词进行拆分&#xff0c;如果可以则返回true&#xff0c;不能的话则返回false。这个题一开始看无法与背包问题联系在一起。但仔细考虑&#xff0c;就是用物品&#xff08;给的字符串数组中…...

大数据构建知识图谱:从技术到实战的完整指南

文章目录 大数据构建知识图谱&#xff1a;从技术到实战的完整指南一、概述二、知识图谱的基础理论定义与分类核心组成历史与发展 三、知识获取与预处理数据源选择数据清洗实体识别 四、知识表示方法知识表示模型RDFOWL属性图模型 本体构建关系提取与表示 五、知识图谱构建技术图…...

WebServer -- 定时器处理非活动连接(上)

目录 &#x1f34d;函数指针 &#x1f33c;基础知识 &#x1f419;整体概述 &#x1f382;基础API sigaction 结构体 sigaction() sigfillset() SIGALRM, SIGTERM 信号 alarm() socketpair() send() &#x1f4d5;信号通知流程 统一事件源 信号处理机制 &#x…...

微服务部署:金丝雀发布、蓝绿发布和滚动发布的对比

金丝雀发布、蓝绿发布和滚动发布的对比 金丝雀发布、蓝绿发布和滚动发布都是软件发布策略&#xff0c;它们都旨在降低发布风险并提高发布速度。但是&#xff0c;这三种策略在工作方式、优缺点等方面存在一些差异。 工作方式 金丝雀发布&#xff1a;将新版本软件逐步发布给用…...

轻松入门MySQL:优化复杂查询,使用临时表简化数据库查询流程(13)

在进销存管理系统中&#xff0c;复杂的数据查询是司空见惯的。这些查询往往需要处理大量的数据&#xff0c;并执行复杂的逻辑操作。然而&#xff0c;处理这些查询可能会变得非常耗时&#xff0c;并且难以维护。为了解决这个问题&#xff0c;我们可以利用临时表&#xff0c;这是…...

vmware的ubuntu虚拟机因空间满无法启动

正在虚拟机编译android源代码&#xff0c;没注意空间不足&#xff0c;结果回来发现了 Assuming drive cache: write through 的问题&#xff0c;经查是空间不足的原因 按照这个教程&#xff0c;清除出来部分空间&#xff0c;才能进去系统&#xff0c;并且对系统空间做下优化 …...

Unity数据持久化之PlayerPrefs

这里写目录标题 PlayerPrefs概述基本方法PlayerPrefs存储位置实践小项目反射知识补充数据管理类的创建反射存储数据----常用成员反射存储数据----List成员反射存储数据----Dictionary成员反射存储数据----自定义类成员反射读取数据----常用成员反射读取数据----List成员反射读取…...

uniapp微信公众号H5分享

如果项目文件node_modules中没有weixin-js-sdk文件&#xff0c;则直接使用本文章提供的&#xff1b; 如果不生效&#xff0c;则在template.h5.html中引入 <script src"https://res.wx.qq.com/open/js/jweixin-1.6.0.js"></script> 首先引入weixin-js-…...

深入理解指针(c语言)

目录 一、使用指针访问数组二、数组名的理解1、数组首元素的地址2、整个数组 三、一维数组传参的本质四、冒泡排序五、二级指针六、指针数组 一、使用指针访问数组 可以使用指针来访问数组元素。例如&#xff0c;可以声明一个指针变量并将其指向数组的第一个元素&#xff0c;然…...

高级语言期末2015级唐班B卷

1.编写函数&#xff0c;按照如下公式计算圆周率π的值&#xff08;精确到1e-5&#xff09; #include <stdio.h>double pai() {double last0;double flag1;int n1;while(flag-last>1e-5) {lastflag;flag*1.0*(2*n)*(2*n)/((2*n-1)*(2*n1));n;}return 2*last; }int main…...

开发一款招聘小程序需要具备哪些功能?

随着时代的发展&#xff0c;找工作的方式也在不断变得简单&#xff0c;去劳务市场、人才市场的方式早就已经过时了&#xff0c;现在大多数年轻人都是直接通过手机来找工作。图片 找工作类的平台不但能扩大企业的招聘渠道&#xff0c;还能节省招聘的成本&#xff0c;方便求职者进…...

嵌入式学习-qt-Day3

嵌入式学习-qt-Day3 一、思维导图 二、作业 完善对话框&#xff0c;点击登录对话框&#xff0c;如果账号和密码匹配&#xff0c;则弹出信息对话框&#xff0c;给出提示”登录成功“&#xff0c;提供一个Ok按钮&#xff0c;用户点击Ok后&#xff0c;关闭登录界面&#xff0c;跳…...

零基础到高级:Android音视频开发技能路径规划

音视频开发趋势 Android音视频开发领域目前正处于一个高速发展的阶段&#xff0c;主要趋势如下&#xff1a; 超高清视频&#xff1a;4K视频亚毫米级显示清晰&#xff0c;更加逼真&#xff0c;为开发更加逼真的虚拟现实应用提供了基础。AI技术&#xff1a;自适应码率控制、视频…...

阿里云香港轻量应用服务器网络线路cn2?

阿里云香港轻量应用服务器是什么线路&#xff1f;不是cn2。 阿里云香港轻量服务器是cn2吗&#xff1f;香港轻量服务器不是cn2。阿腾云atengyun.com正好有一台阿里云轻量应用服务器&#xff0c;通过mtr traceroute测试了一下&#xff0c;最后一跳是202.97开头的ip&#xff0c;1…...

python中websockets与主线程传递参数

目录 一、子线程创建websockets服务端接收客户端数据 二、主线程内启动子线程接收并处理数据 一、子线程创建websockets服务端接收客户端数据并存入队列 发送的消息客户端与服务端统一&#xff0c;多种消息加入判断的标签 服务端&#xff1a;web_server.py import asynci…...

js谐音梗创意小游戏《望子成龙》

&#x1f33b; 前言 龙年到来&#xff0c;祥瑞满天。愿您如龙般矫健&#xff0c;事业腾飞&#xff1b;如龙鳞闪耀&#xff0c;生活美满。祝您龙年大吉&#xff0c;万事如意&#xff01; 龙年伊始&#xff0c;我给各位设计了一款原创的小游戏&#xff0c;话不多说&#xff0c;直…...

conda相比python好处

Conda 作为 Python 的环境和包管理工具&#xff0c;相比原生 Python 生态&#xff08;如 pip 虚拟环境&#xff09;有许多独特优势&#xff0c;尤其在多项目管理、依赖处理和跨平台兼容性等方面表现更优。以下是 Conda 的核心好处&#xff1a; 一、一站式环境管理&#xff1a…...

质量体系的重要

质量体系是为确保产品、服务或过程质量满足规定要求&#xff0c;由相互关联的要素构成的有机整体。其核心内容可归纳为以下五个方面&#xff1a; &#x1f3db;️ 一、组织架构与职责 质量体系明确组织内各部门、岗位的职责与权限&#xff0c;形成层级清晰的管理网络&#xf…...

Java-41 深入浅出 Spring - 声明式事务的支持 事务配置 XML模式 XML+注解模式

点一下关注吧&#xff01;&#xff01;&#xff01;非常感谢&#xff01;&#xff01;持续更新&#xff01;&#xff01;&#xff01; &#x1f680; AI篇持续更新中&#xff01;&#xff08;长期更新&#xff09; 目前2025年06月05日更新到&#xff1a; AI炼丹日志-28 - Aud…...

HBuilderX安装(uni-app和小程序开发)

下载HBuilderX 访问官方网站&#xff1a;https://www.dcloud.io/hbuilderx.html 根据您的操作系统选择合适版本&#xff1a; Windows版&#xff08;推荐下载标准版&#xff09; Windows系统安装步骤 运行安装程序&#xff1a; 双击下载的.exe安装文件 如果出现安全提示&…...

Robots.txt 文件

什么是robots.txt&#xff1f; robots.txt 是一个位于网站根目录下的文本文件&#xff08;如&#xff1a;https://example.com/robots.txt&#xff09;&#xff0c;它用于指导网络爬虫&#xff08;如搜索引擎的蜘蛛程序&#xff09;如何抓取该网站的内容。这个文件遵循 Robots…...

《基于Apache Flink的流处理》笔记

思维导图 1-3 章 4-7章 8-11 章 参考资料 源码&#xff1a; https://github.com/streaming-with-flink 博客 https://flink.apache.org/bloghttps://www.ververica.com/blog 聚会及会议 https://flink-forward.orghttps://www.meetup.com/topics/apache-flink https://n…...

前端开发面试题总结-JavaScript篇(一)

文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包&#xff08;Closure&#xff09;&#xff1f;闭包有什么应用场景和潜在问题&#xff1f;2.解释 JavaScript 的作用域链&#xff08;Scope Chain&#xff09; 二、原型与继承3.原型链是什么&#xff1f;如何实现继承&a…...

华硕a豆14 Air香氛版,美学与科技的馨香融合

在快节奏的现代生活中&#xff0c;我们渴望一个能激发创想、愉悦感官的工作与生活伙伴&#xff0c;它不仅是冰冷的科技工具&#xff0c;更能触动我们内心深处的细腻情感。正是在这样的期许下&#xff0c;华硕a豆14 Air香氛版翩然而至&#xff0c;它以一种前所未有的方式&#x…...

管理学院权限管理系统开发总结

文章目录 &#x1f393; 管理学院权限管理系统开发总结 - 现代化Web应用实践之路&#x1f4dd; 项目概述&#x1f3d7;️ 技术架构设计后端技术栈前端技术栈 &#x1f4a1; 核心功能特性1. 用户管理模块2. 权限管理系统3. 统计报表功能4. 用户体验优化 &#x1f5c4;️ 数据库设…...

无人机侦测与反制技术的进展与应用

国家电网无人机侦测与反制技术的进展与应用 引言 随着无人机&#xff08;无人驾驶飞行器&#xff0c;UAV&#xff09;技术的快速发展&#xff0c;其在商业、娱乐和军事领域的广泛应用带来了新的安全挑战。特别是对于关键基础设施如电力系统&#xff0c;无人机的“黑飞”&…...