【Linux网络编程】TCP Echo Server的实现
本文专栏:linux网络编程
本文的基础知识是基于上篇文章:UDP Echo Server的实现
传送门:
【Linux网络编程】UDP Echo Server的实现 -CSDN博客
目录
一,InetAddr类的编写
二,客户端代码编写
创建套接字(socket)
绑定IP和端口号(bind)
建立连接(connect)
write和read
完整代码(客户端)
三,服务器端代码编写
创建套接字(socket)
绑定IP和端口号(bind)
设置监听状态(listen)
获取连接(accept)
write和read
version0——单进程版本
version1——多进程版本
version3——多线程版本
四,服务器端完整代码
五,总结
一,InetAddr类的编写
在上篇博客中,实现udp的echo server。其中有很多的接口,都需要进行主机序列和网络序列的相互转化。这些操作很频繁,所以可以将这些操作封装 成一个类,提供 一个个的接口。
含注释
#pragma once
#include "Common.hpp"
// 网络地址和主机地址之间进行转换的类class InetAddr
{
public://通过重载构造函数来实现网络序列和主机序列的相互转化InetAddr(){}//addr中的数据是网络序列(也就是大端形式)InetAddr(struct sockaddr_in &addr) : _addr(addr){// 网络转主机_port = ntohs(_addr.sin_port); // 从网络中拿到的!网络序列// _ip = inet_ntoa(_addr.sin_addr); // 4字节网络风格的IP -> 点分十进制的字符串风格的IPchar ipbuffer[64];inet_ntop(AF_INET, &_addr.sin_addr, ipbuffer, sizeof(_addr));_ip = ipbuffer;}//传入IP和端口号,在构造函数里完成主机序列转网络序列InetAddr(const std::string &ip, uint16_t port) : _ip(ip), _port(port){// 主机转网络memset(&_addr, 0, sizeof(_addr));_addr.sin_family = AF_INET;inet_pton(AF_INET, _ip.c_str(), &_addr.sin_addr);_addr.sin_port = htons(_port);}//该构造函数可用于服务器端//服务器端传入port即可//ip在内部会设置为INADDR_ANY,表示任何ip都可以连接InetAddr(uint16_t port) :_port(port),_ip("0"){// 主机转网络memset(&_addr, 0, sizeof(_addr));_addr.sin_family = AF_INET;_addr.sin_addr.s_addr = INADDR_ANY;_addr.sin_port = htons(_port);}//获取端口号uint16_t Port() { return _port; }//获取ipstd::string Ip() { return _ip; }//下面两个接口的返回值,const struct sockaddr_in &NetAddr() { return _addr; }const struct sockaddr *NetAddrPtr(){#define CONV(addr) ((struct sockaddr*)&addr)return CONV(_addr);//这是定义的一个宏,类型转化}//返回sockaddr_in的大小socklen_t NetAddrLen(){return sizeof(_addr);}bool operator==(const InetAddr &addr){return addr._ip == _ip && addr._port == _port;}std::string StringAddr(){return _ip + ":" + std::to_string(_port);}~InetAddr(){}private:struct sockaddr_in _addr;std::string _ip;uint16_t _port;
};
二,客户端代码编写
Echo Server(回显服务器)是一种网络应用程序。其核心功能是接受客户端发来的数据,并将接受到的数据原样返回给客户端。
核心逻辑:
服务端
创建套接字 → 绑定地址 → 监听连接 → 接受请求 → 读取数据 → 回传数据。客户端
创建套接字 → 连接服务器 → 发送数据 → 接收回显数据。
在UDP服务中是没有监听连接这一步的,但是在TCP这里就需要建立连接了。
我们约定以下,当我们使用客户端连接服务端时,是需要 服务器端的IP和端口号的。
这两个数据到时候我们通过命令行参数的形式获取。
创建套接字(socket)
认识接口:
NAME
socket - create an endpoint for communicationSYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int socket(int domain, int type, int protocol);
//1,创建套接字int sockfd=socket(AF_INET,SOCK_STREAM,0);if(sockfd<0){std::cerr<<"创建套接字失败"<<std::endl;exit(1);}std::cout<<"创建套接字成功"<<sockfd<<std::endl;
绑定IP和端口号(bind)
客户端代码在编写的时候是不需要我们手动绑定的,在系统内部,系统 知道本主机的IP,同时会随机分配一个端口号给客户端。
详解看上篇文章:
【Linux网络编程】UDP Echo Server的实现 -CSDN博客
建立连接(connect)
与服务器端建立连接。认识接口:
NAME
connect - initiate a connection on a socketSYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 第一个参数:是我们创建的socket套接字
- 第二个参数:是一个结构体类型,在该结构体中存储着端口号和IP地址
- 第三个参数:表示该结构体的大小
这些参数都是我们需要定义好,将服务器端的IP地址和端口号填入,但是 这里就会面临主机序列到网络序列的转换。所以这里我们可以使用提前封装好的InetAddr类,只需将IP和端口号传入 构造好一个InetAddr对象,就可以方便获取想要的字节序,不管是网络序列还是续集序列。
//2,bind不需要我们手动绑定了//3,建立连接InetAddr addr(serverip,serverport);//这一句就搞定了int n=connect(sockfd,addr.NetAddrPtr(),addr.NetAddrLen());if(n<0){std::cerr<<"connect err"<<std::endl;exit(3);}
write和read
建立好连接后,就可以发送 和接受数据了。认识接口 :
NAME
write - write to a file descriptorSYNOPSIS
#include <unistd.h>ssize_t write(int fd, const void *buf, size_t count);
NAME
read - read from a file descriptorSYNOPSIS
#include <unistd.h>ssize_t read(int fd, void *buf, size_t count);
这两个接口,其实就是文件系统中,对文件进行读写操作的方法。
while(true){std::string line;std::cout<<"Please Enter## ";std::getline(std::cin,line);write(sockfd,line.c_str(),line.size());//获取数据//定义一个缓冲区 char buffer[1024];ssize_t s=read(sockfd,buffer,sizeof(buffer)-1);//成功读取服务器发来的消息if(s>0){std::cout<<"sever echo# "<<buffer<<std::endl;}}
完整代码(客户端)
#pragma once
//客户端
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <string>
#include <unistd.h>
#include "InetAddr.hpp"//我们期望的输入样例:【./可执行程序 IP 端口号】
int main(int argc,char* argv[])
{if(argc!=3){std::cout<<"输入格式 不规范"<<std::endl;exit(1);}//先提取出从命令行中获取的IP和端口号std::string serverip=argv[1];//IPuint16_t serverport=std::stoi(argv[2]);//port//1,创建套接字int sockfd=socket(AF_INET,SOCK_STREAM,0);if(sockfd<0){std::cerr<<"创建套接字失败"<<std::endl;exit(2);}std::cout<<"创建套接字成功"<<sockfd<<std::endl;//2,bind不需要我们手动绑定了//3,建立连接InetAddr addr(serverip,serverport);//这一句就搞定了int n=connect(sockfd,addr.NetAddrPtr(),addr.NetAddrLen());if(n<0){std::cerr<<"connect err"<<std::endl;exit(3);}while(true){std::string line;std::cout<<"Please Enter## ";std::getline(std::cin,line);write(sockfd,line.c_str(),line.size());//获取数据//定义一个缓冲区 char buffer[1024];ssize_t s=read(sockfd,buffer,sizeof(buffer)-1);//成功读取服务器发来的消息if(s>0){std::cout<<"sever echo# "<<buffer<<std::endl;}}return 0;
}
三,服务器端代码编写
对服务器端代码的编写时,为了实现代码之间分模块,降低耦合度。和UDP一样,将核心部分封装成类。模块与模块之间的联系就降低了。
而类支持拷贝构造和赋值重载这些功能,但是我们的服务器是不希望有这些的功能 。直接的办法就是将这个类的拷贝构造和赋值重载禁用掉(delete),或者 将这两个函数设置为私有 成员(private),在外界就无法调用。
我们这里用另一个 方法,定义一个新的类,类名为NoCopy,该类不需要定义任何的成员函数,直接将该类的拷贝构造和赋值重载禁用掉(delete),然后让我们编写的服务器类 class tcpserver继承自这个类。因为如果想要拷贝子类,就必须先掉用父类的拷贝构造,再调用子类的拷贝构造。赋值重载也是如此。这样,子类的拷贝构造和赋值重载也就无法调用了。
//禁止拷贝构造和赋值重载
class NoCopy
{
public:NoCopy(){}~NoCopy(){}NoCopy(const NoCopy& n)=delete;NoCopy& operator=(const NoCopy& n)=delete;
};
在这里,由于我们在编写服务器段代码时,可能会产生不同错误,比如创建套接字失败,监听失败,绑定失败等等各种问题。所以我们可以通过enum,枚举出这些错误,这些错误分别对应一个整数,在出错时我们让进程退出,退出码就设置为对应的错误,这样方便查看哪里出错了。
//枚举退出码
enum ExitCode
{OK = 0,USAGE_ERR,SOCKET_ERR,BIND_ERR,LISTEN_ERR,CONNECT_ERR,FORK_ERR
};
创建套接字(socket)
接口设计与客户端一摸一样。
这里用到的LOG(LogLevel::INFO) 是用来打印日志信息的,方便进行debug的。
在最后,会将该这个功能的实现发出来。
//1,创建套接字_sockfd=socket(AF_INET,SOCK_STREAM,0);if(_sockfd<0){//打印日志信息LOG(LogLevel::FATAL)<<"创建套接字失败";exit(SOCKET_ERR);}LOG(LogLevel::INFO)<<"创建套接字成功"<<_sockfd;
绑定IP和端口号(bind)
同样,这里在传参的时候,需要传入struct sockaddr类,要实现字节序到网络序的转化,这些功能已经在InetAddr这个类里封装好了。所以直接调用即可。
//2,绑定ip和端口号InetAddr addr(_port);//只需传入端口号即可,这里 调用的构造函数会将IP设置为0,也就是INADDR_ANYint n=bind(_sockfd,addr.NetAddrPtr(),addr.NetAddrLen());if(n<0){LOG(LogLevel::FATAL)<<"绑定失败";exit(BIND_ERR);}
设置监听状态(listen)
服务器端在完成绑定后,需要将自己设置为监听状态。客户端要连接我,我是服务端,那么我就需要将自己的状态设置为listen状态,随时等待 客户端连接。
认识接口:
NAME
listen - listen for connections on a socketSYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int listen(int sockfd, int backlog);
- 第一个参数就是我们创建的套接字
- 第二个参数:当服务端处于 监听状态时,客户端发来连接,这些连接是需要排队的,这个参数就表示处于排队中的连接的的最大个数 ,这个数字不能设置为太大,也不能太小
//3,listen状态,监听连接n=listen(_sockfd,8);if(n<0){LOG(LogLevel::FATAL)<<"监听失败";exit(LISTEN_ERR);}LOG(LogLevel::FATAL)<<"监听成功";
获取连接(accept)
客户端向服务端发来的连接,存在于哪里?操作系统内核。我们要从操作系统内核中获取。
认识接口:
NAME
accept, accept4 - accept a connection on a socketSYNOPSIS
#include <sys/types.h> /* See NOTES */
#include <sys/socket.h>int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <sys/socket.h>int accept4(int sockfd, struct sockaddr *addr,
socklen_t *addrlen, int flags);
- 第一个参数是我们创建的套接字
- 第二个和第三个参数是sockaddr结构体,需要我们传入。同样,还是调用我们封装好的接口即可。
但是对于这个函数的返回值,是一个文件描述符。如上图,而我们创建套接字的时候,它的返回值也是一个文件描述符。这该如何理解呢?
一个场景搞定:
众所周知,现在各行各业都很卷。相信大家出去玩的时候,都遇到过这个场景。
在某个景区附近,会有各种餐馆,为了提高餐馆的收益,每个餐馆会派一个人在外面拉客,这个人就叫作张三,他给路过的人说:来我们家餐馆吃吧,我们家餐馆今天刚捕捞的鱼,可新鲜。这些客人跟着张三进入餐馆后,张三会继续到外面去拉客。而这些客人会由餐馆里的其他服务员李四,王五等照顾。而可能张三在拉客的过程中,失败了,这是正常的,我今天的就是不想吃饭。那么张三就会转头去拉另一个顾客。
在这个场景中,张三就是我们创建的socket,我们通过创建的socket来获取连接,就是张三拉客的过程。而餐馆里的其他服务员,他们来照顾张三拉的客人。对应的就是accept的返回值,这个返回值来提供服务,提供什么服务,就是write和read服务。
所以,在这里我们把先前创建的_sockfd该名为_listensockfd。而accept的返回值定义为sockfd,这个才是提供服务的。_listensockfd只是完成监听的,它是监听套接字。
如果没有连接,accept就会一直阻塞。
//4,获取连接struct sockaddr_in peer;socklen_t len=sizeof(peer);//如果没有连接,accept就会阻塞int sockfd=accept(_listensockfd, CONV(peer), &len);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept error";continue;}
write和read
定义一个service方法,在这里实现write和read。这个函数作为成员函数。
void service(int sockfd,InetAddr addr){char buffer[1024];//定义缓冲区while(true){//读取数据ssize_t n=read(sockfd,buffer,sizeof(buffer)-1);//n>0读取成功//n<0读取失败//n==0对端关闭连接,读取到了文件的末尾if(n>0){buffer[n]=0;//查看是哪个主机的哪个进程发送的消息,在服务端回显LOG(LogLevel::DEBUG)<<addr.StringAddr()<<" #"<<buffer;//写回数据std::string echo_string="echo #";echo_string+=buffer;//写回给客户端write(sockfd,echo_string.c_str(),echo_string.size());}else if (n == 0){LOG(LogLevel::DEBUG) << addr.StringAddr() << " 退出了...";close(sockfd);break;}else{LOG(LogLevel::DEBUG) << addr.StringAddr() << " 异常...";close(sockfd);break;}}}
version0——单进程版本
在实际中,这种服务器一般是不会实现的。
我们在接受连接后,直接调用service函数完成通信,这种 服务器只能处理一个客户端,因为当一个客户端建立连接后,我们服务器端调用service函数进行read和write,一直while(true)式的读和写,是一个死循环,所以就无法继续进行accept进行等待了。所以其他客户端就无法连接了,除非第一个客户端推出了。所以说这种一般不会实现的。
version1——多进程版本
通过创建父子进程的方式。让父进程一直进行accept接受连接的功能,让子进程一直执行service通信的服务。这样就可以保证多个客户端,可以同时连接这个服务器,进行通信了。
在获取连接成功之后,fork出子进程,子进程执行service方法中的write和read,父进程继续循环执行accept,获取其他客户端的连接。所以作为子进程我们只需要知道sockfd即可,通过sockfd可以进行read和write。而对于父进程,我们只需要知道_listensockfd即可,通过该套接字获取连接。所以父子进程可以关掉双方不需要的文件描述符(即sockfd和listensockfd)
这样的方式当然可以保证多个客户端可以进行连接我们的服务器。但是还有一个问题,子进程在退出的时候,是需要父进程进行等待的。如果不等待,父进程直接退出,那么该进程就会进入僵尸状态,一直占有内存资源,存在内存泄漏的问题。而我们的服务器是一个死循环,一直启动着,这样就会不停的生成僵尸进程,将 内存资源占用完时,我们的服务器进程就会挂掉。
所以父进程是需要等待子进程退出的,父进程调用pthread_wait接口,等待子进程并回收子进程。如果我们真的进行等待,那么这种方式,父进程是还需要等待的,效率较低。
子进程在退出的时候,会给父进程发送一个退出信号。父进程可以将信号的捕捉方式设置为忽略处理,就不需要等待子进程。
signal(SIGCHLD,SIG_IGN)
还有一种方法,在子进程中再次创建子进程,成为孙子进程,我们让子进程直接退出,那么父进程就可以直接等待成功,转而去执行获取其他 连接。然后让孙子进程执行service(read和write),孙子进程不需要处理,因为它的父进程已经退出了,他成为了孤儿进程,孤儿进程会被1号进程领养,1号进程就是操作系统,操作系统会将这个进程释放掉,回收资源,所以不用担心内存泄漏的问题。
//version2//多进程pid_t id=fork();if(id<0){LOG(LogLevel::FATAL)<<"创建子进程失败";exit(FORK_ERR);}else if(id==0)//子进程{//关掉不用的文件描述符close(_listensockfd);if(fork()>0)//再次创建子进程exit(OK);//正常退出//执行serviceservice(sockfd,addr);//孙子进程执行exit(OK);}else{//父进程//关掉不用的文件描述符close(sockfd);//忽略子进程的退出信号//signal(SIGCHLD,SIG_IGN);//父进程直接退出pid_t rid=waitpid(id,nullptr,0);//父进程不会再等待了//再次循环执行获取连接的方法 accept}
version3——多线程版本
这种方式其实最简单,创建一个线程,新线程去执行 service方法,主线程循环执行accept方法。
class ThreadData{public:ThreadData(int fd, InetAddr &ar, tcpserver *s) : sockfd(fd), addr(ar), tsvr(s){}public:int sockfd;InetAddr addr;tcpserver *tsvr;};//新线程的入口函数static void *Routine(void *args){pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args);td->tsvr->service(td->sockfd, td->addr);delete td;return nullptr;}//version3——多线程
ThreadData *td = new ThreadData(sockfd, addr, this);
pthread_t tid;
pthread_create(&tid, nullptr, Routine, td);
因为我们创建新线程执行Routine方法时,该函数表示成员函数,所以需要设置为静态的。
而 调用sevice需要sockfd和InetAddr以及this指针,所以我们可以将这三个参数封装成一个结构体传参过去。
四,服务器端完整代码
tcpserver.hpp
#pragma once#include <iostream>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/wait.h>
#include <signal.h>
#include "Common.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"using namespace LogModule;const int defaultfd=-1;
//禁止拷贝构造和赋值重载
class tcpserver : public NoCopy
{
public:tcpserver(uint16_t port):_port(port),_listensockfd(defaultfd),_isrunning(false){}~tcpserver(){}void init(){//1,创建套接字_listensockfd=socket(AF_INET,SOCK_STREAM,0);if(_listensockfd<0){//打印日志信息LOG(LogLevel::FATAL)<<"创建套接字失败";exit(SOCKET_ERR);}LOG(LogLevel::INFO)<<"创建套接字成功"<<_listensockfd;//2,绑定ip和端口号InetAddr addr(_port);//只需传入端口号即可,这里 调用的构造函数会将IP设置为0,也就是INADDR_ANYint n=bind(_listensockfd,addr.NetAddrPtr(),addr.NetAddrLen());if(n<0){LOG(LogLevel::FATAL)<<"绑定失败";exit(BIND_ERR);}LOG(LogLevel::FATAL)<<"绑定成功";//3,listen状态,监听连接n=listen(_listensockfd,8);if(n<0){LOG(LogLevel::FATAL)<<"监听失败";exit(LISTEN_ERR);}LOG(LogLevel::FATAL)<<"监听成功";}void service(int sockfd,InetAddr addr){char buffer[1024];//定义缓冲区while(true){//读取数据ssize_t n=read(sockfd,buffer,sizeof(buffer)-1);//n>0读取成功//n<0读取失败//n==0对端关闭连接,读取到了文件的末尾if(n>0){buffer[n]=0;//查看是哪个主机的哪个进程发送的消息,在服务端回显LOG(LogLevel::DEBUG)<<addr.StringAddr()<<" #"<<buffer;//写回数据std::string echo_string="echo #";echo_string+=buffer;//写回给客户端write(sockfd,echo_string.c_str(),echo_string.size());}else if (n == 0){LOG(LogLevel::DEBUG) << addr.StringAddr() << " 退出了...";close(sockfd);break;}else{LOG(LogLevel::DEBUG) << addr.StringAddr() << " 异常...";close(sockfd);break;}}}class ThreadData{public:ThreadData(int fd, InetAddr &ar, tcpserver *s) : sockfd(fd), addr(ar), tsvr(s){}public:int sockfd;InetAddr addr;tcpserver *tsvr;};//新线程的入口函数static void *Routine(void *args){pthread_detach(pthread_self());ThreadData *td = static_cast<ThreadData *>(args);td->tsvr->service(td->sockfd, td->addr);delete td;return nullptr;}void run(){_isrunning=true;while(_isrunning){//4,获取连接struct sockaddr_in peer;socklen_t len=sizeof(sockaddr_in);//如果没有连接,accept就会阻塞//sockfd提供接下来的read和writeint sockfd=accept(_listensockfd, CONV(peer), &len);if (sockfd < 0){LOG(LogLevel::WARNING) << "accept error";continue;}InetAddr addr(peer);//version3——多线程ThreadData *td = new ThreadData(sockfd, addr, this);pthread_t tid;pthread_create(&tid, nullptr, Routine, td);//version1——单进程,一般不会采用//service(sockfd,addr);//version2//多进程// pid_t id=fork();// if(id<0)// {// LOG(LogLevel::FATAL)<<"创建子进程失败";// exit(FORK_ERR);// }// else if(id==0)//子进程// {// //关掉不用的文件描述符// close(_listensockfd);// if(fork()>0)//再次创建子进程// exit(OK);//正常退出// //执行service// service(sockfd,addr);//孙子进程执行// exit(OK);// }// else// {// //父进程// //关掉不用的文件描述符// close(sockfd);// //忽略子进程的退出信号// //signal(SIGCHLD,SIG_IGN);// //父进程直接退出// pid_t rid=waitpid(id,nullptr,0);//父进程不会再等待了// //再次循环执行获取连接的方法 accept// }}_isrunning=false;}
private:uint16_t _port;int _listensockfd;bool _isrunning;
};
tcpserver.cpp
#pragma once#include "tcpserver.hpp"void Usage(char* proc)
{std::cerr<<"Usage ::"<<proc<<" port"<<std::endl;
}
int main(int argc,char* argv[])
{if(argc!=2){Usage(argv[0]);exit(USAGE_ERR);}uint16_t port=std::stoi(argv[1]);//设置日志向控制台打印Enable_Console_Log_Strategy();//开启日志,默认向控制台打印std::unique_ptr<tcpserver> tsvs=std::make_unique<tcpserver>(port);tsvs->init();tsvs->run();return 0;
}
Common.hpp
#pragma once#include <iostream>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <cstring>//枚举退出码
enum ExitCode
{OK = 0,USAGE_ERR,SOCKET_ERR,BIND_ERR,LISTEN_ERR,CONNECT_ERR,FORK_ERR
};//禁止拷贝构造和赋值重载
class NoCopy
{
public:NoCopy(){}~NoCopy(){}NoCopy(const NoCopy& n)=delete;const NoCopy& operator=(const NoCopy& n)=delete;
};#define CONV(addr) ((struct sockaddr*)&addr)
日志代码,日志的实现是需要锁的
#ifndef __LOG_HPP__
#define __LOG_HPP__
// 实现一个日志打印消息#include <iostream>
#include <filesystem> //c++17引入
#include <string>
#include <fstream>
#include <memory>
#include <unistd.h>
#include <sstream>
#include <cstdio>
#include <ctime>
#include "Mutex.hpp"using namespace MutexModel;
namespace LogModule
{// 首先定义打印策略——文件打印/控制台打印// 通过多态实现,这样写方便后来内容的补充,比如增加向网络中刷新,只需再继承一个类// 基类const std::string gsep = "\r\n";class LogStrategy{public:LogStrategy(){}~LogStrategy(){}// 虚函数 子类需要重写的刷新策略virtual void Synclog(const std::string &message) = 0;};// 控制台打印,日志信息向控制台打印class ConsoleLogStrategy : public LogStrategy{public:ConsoleLogStrategy(){}~ConsoleLogStrategy(){}void Synclog(const std::string &message) override{// 向控制台打印// 需要维护线程安全LockGuard lockguard(_mutex);std::cout << message << gsep;}private:Mutex _mutex;};// 指定默认的文件路径和文件名const std::string defaultpath = "./log";const std::string defaultname = "my.log";// 指定文件打印日志class FileLogStrategy : public LogStrategy{public:FileLogStrategy(const std::string &path = defaultpath, const std::string &name = defaultname): _path(path),_name(name){// 维护线程安全LockGuard lockguard(_mutex);// 判断对应的路径是否存在if (std::filesystem::exists(_path)){return;}try{std::filesystem::create_directories(_path);}catch (const std::filesystem::filesystem_error &e){std::cerr << e.what() << '\n';}}~FileLogStrategy(){}void Synclog(const std::string &message){LockGuard lockgyard(_mutex);std::string filename = _path + (_path.back() == '/' ? " " : "/") + _name;std::ofstream out(filename, std::ios::app);if (!out.is_open()){return;}out << message << gsep;out.close();}private:std::string _path; // 文件路径std::string _name; // 文件名Mutex _mutex;};// 日志等级enum class LogLevel{DEBUG,INFO,WARNING,ERROR,FATAL};std::string LevelToStr(LogLevel level){switch (level){case LogLevel::DEBUG:return "DEBUG";case LogLevel::ERROR:return "ERROR";case LogLevel::INFO:return "INFO";case LogLevel::FATAL:return "FATAL";case LogLevel::WARNING:return "WARNING";}return "none";}// 获取当前时间std::string GetTimeStamp(){time_t curr = time(nullptr);struct tm curr_time;localtime_r(&curr, &curr_time);char TimeBuffer[128];snprintf(TimeBuffer, sizeof(TimeBuffer), "%4d-%02d-%02d %02d:%02d:%02d",curr_time.tm_year + 1900,curr_time.tm_mon+1,curr_time.tm_mday,curr_time.tm_hour,curr_time.tm_min,curr_time.tm_sec);return TimeBuffer;}// 形成一条完整的日志// 根据上面不同的策略,选择不同的刷新方案class Logger{public:Logger(){// 默认是向控制台刷新EnableConsoleStrategy();}~Logger(){}// 更改刷新策略// 文件刷新void EnableFileLogStrategy(){_flush_strategy = std::make_unique<FileLogStrategy>();}// 控制台刷新void EnableConsoleStrategy(){_flush_strategy = std::make_unique<ConsoleLogStrategy>();}// 内部类// 一条完整的日志信息class LogMessage{public:LogMessage(LogLevel level, const std::string &src_name, int line_number, Logger &logger): _level(level),_src_name(src_name),_line_number(line_number),_logger(logger),_pid(getpid()),_curr_time(GetTimeStamp()){// 字符串流std::stringstream ss;// 合并日志的左半部分ss << "[" << _curr_time << "] "<< "[" << LevelToStr(_level) << "] "<< "[" << _pid << "] "<< "[" << _src_name << "] "<< "[" << _curr_time << "] " << "-";_loginfo = ss.str();}//支持<<"hello world"<<1<<3.14template <class T>LogMessage& operator<<(const T& info){std::stringstream ss;//右半部分日志ss<<info;_loginfo+=ss.str();return *this;}~LogMessage(){if(_logger._flush_strategy)//完成刷新_logger._flush_strategy->Synclog(_loginfo);}private:std::string _curr_time;LogLevel _level;pid_t _pid;std::string _src_name; // 所在文件的文件名int _line_number; // 行号Logger &_logger;std::string _loginfo; // 合并之后,一条完整的日志};//重载()LogMessage operator()(LogLevel level,std::string name,int line_number){return LogMessage(level,name,line_number,*this);}private:std::unique_ptr<LogStrategy> _flush_strategy; // 智能指针来管理刷新策略};//使用 //定义一个全局的对象Logger logger;//方便使用,封装成宏//__FILE__为指定的文件//__LINE__为指定的行#define LOG(level) logger(level,__FILE__,__LINE__)#define Enable_Console_Log_Strategy() logger.EnableConsoleStrategy()#define Enable_File_Log_Strategy() logger.EnableFileLogStrategy()
}#endif
锁代码
/// 简单封装互斥锁
#pragma once
#include <iostream>
#include <pthread.h>// 基础互斥锁的封装
namespace MutexModel
{class Mutex{public:Mutex(){pthread_mutex_init(&_mutex, nullptr);}~Mutex(){pthread_mutex_destroy(&_mutex);}void Lock(){int n = pthread_mutex_lock(&_mutex);(void)n;}void Unlock(){int n = pthread_mutex_unlock(&_mutex);(void)n;}private:pthread_mutex_t _mutex;};// RAII守卫类class LockGuard{public:LockGuard(Mutex& mtx):_mtx(mtx){_mtx.Lock();}~LockGuard(){_mtx.Unlock();}private:Mutex &_mtx;};
}
makefile文件
.PHONY:all
all:tcpclient tcpservertcpclient:tcpclient.cppg++ -o $@ $^ -std=c++17
tcpserver:tcpserver.cppg++ -o $@ $^ -std=c++17 -pthread.PHONY:clean
clean:rm -f tcpclient tcpserver
五,总结
这次的echo server代码的编写,我遇到的问题是客户端代码运行到connect就停止了,也就是创建完套接字就阻塞住了,没有执行 建立连接以及后序的代码。找了半天才发现是服务器端的端口号初始化时出现了问题,裂开!!!
相关文章:
【Linux网络编程】TCP Echo Server的实现
本文专栏:linux网络编程 本文的基础知识是基于上篇文章:UDP Echo Server的实现 传送门: 【Linux网络编程】UDP Echo Server的实现 -CSDN博客 目录 一,InetAddr类的编写 二,客户端代码编写 创建套接字(s…...
信奥赛CSP-J复赛集训(数学思维专题)(11):P9585 「MXOI Round 2」酒店
信奥赛CSP-J复赛集训(数学思维专题)(11):P9585 「MXOI Round 2」酒店 题目描述 小 C 开了一家酒店,叫做 CC Hotel。 一天,CC Hotel 来了 n n n 位客人。小 C 需要把他们都安排在酒店的某一层…...
python: audioFlux XXCC 提取梅尔频率倒谱系数 MFCC
承上一篇:python:audioFlux 使用教程 XXCC: 倒谱系数,支持所有频谱类型. 可以提取梅尔频率倒谱系数(MFCC) Cepstrum coefficients, supports all spectrum types. 以下是使用 audioflux 库中 XXCC 类计算倒谱系数…...
PHP + Go 如何协同打造高并发微服务?
为什么需要 PHP Go 协同? 在微服务架构中,PHP 和 Go 看似是“两个世界”的语言,但它们的互补性极强: PHP:开发效率高、生态成熟,适合快速实现复杂业务逻辑(如电商订单、用户系统)…...
k8s工具使用
Kubectl Cheat Sheet k8s的命令级别 1.基础命令(初级) 2.基础命令(中级) 3.部署命令 4.集群管理命令 5.故障排查和调试命令 6.高级命令 7.设置命令 8.其它命令 命令行提示 为了使用kubectl命令更加高效,我们可以选择安装一下开源软件来增加操作kubectl命令的快捷方式,同…...
uml制做参考-以代码画UML图
【PlantUML系列】类图(一)_plantuml skin-CSDN博客 UML入门以及Plant UML工具介绍_plantuml-CSDN博客 UML类图详解-CSDN博客 【PlantUML】-类图-CSDN博客 【掌握绘图艺术】用PlantUML绘制完美UML图表,编程开发者的福音 - 知乎 如何优化P…...
深入解析B站androidApp接口:从bilibili.api.ticket.v1.Ticket/GetTicket到SendMsg的技术分析
前言 最近一段时间,我对B站的App接口进行了深入分析,特别是关注了认证机制和私信功能的实现。通过逆向工程和网络抓包,发现了B站移动端API的底层工作原理,包括设备标识生成机制、认证流程和消息传输协议。本文将分享这些研究成果…...
[AI ][Dify] 构建一个自动化新闻编辑助手:Dify 工作流实战指南
在内容创作行业中,自动化辅助工具已成为提升编辑效率的重要利器。本文将通过 Dify 平台,演示如何构建一个**“新闻编辑助手”**,实现从网页抓取、文本翻译、标题生成,到新闻配图的全流程自动化。 🎯 目标概览 这个工作流旨在实现如下功能: 从指定网页抓取新闻内容; 使…...
Unity中国战略调整简讯:Unity6下架 团结引擎接棒
Unity中国战略调整简讯:Unity6下架 团结引擎接棒 免费版 2025年4月9日 —— Unity中国宣布自即日起,中国大陆及港澳地区停止提供Unity 6及后续版本下载与服务,相关功能由国产引擎“团结引擎”承接。国际版2022 LTS及更早版本仍由Unity中国维护…...
司美格鲁肽用SNAC市场报告:2024年全球市场销售额达到了0.14亿美元
引言:了解司美格鲁肽与SNAC的重要性 在当前的医药领域,司美格鲁肽(Semaglutide)作为一种创新性的治疗2型糖尿病和肥胖症的药物,受到了广泛关注。而SNAC(N-(8-(2-羟苯基)…...
自动驾驶第一性原理
所谓的第一性原理: 就是指从最基本的物理规律,数据逻辑及工程约束条件出发,剥离所有的非本质的假设,直接推导出自动驾驶最核心的要素。 自动驾驶核心框架分解: 1、根本目标: 安全高效的将人/物从A地运送…...
《UE5_C++多人TPS完整教程》学习笔记36 ——《P37 拾取组件(Pickup Widget)》
本文为B站系列教学视频 《UE5_C多人TPS完整教程》 —— 《P37 拾取组件(Pickup Widget)》 的学习笔记,该系列教学视频为计算机工程师、程序员、游戏开发者、作家(Engineer, Programmer, Game Developer, Author) Steph…...
Uniswap V2/V3/V4 流动性与价格计算详解
Uniswap V2/V3/V4 流动性与价格计算详解 一、核心概念对比 Uniswap V2 流动性模型: 恒定乘积公式 (x * y = k)价格决定: 完全由池子中的代币数量决定 (price = y/x)流动性衡量: 总储备量代表流动性,直接通过 getReserves() 获取流动性分布: 均匀分布在所有价格点交易费用: 固…...
yum安装MySQL数据库
yum安装方式 优点:操作简单易用。不用单独下载,服务器可以联网且yum源没有问题即可(可以选择国内的163/阿里源) 安装步骤: 1.关闭防火墙和selinux: systemctl stop firewalld ##关闭防火墙 systemctl disable firewalld …...
大联盟(特别版)双端互动平台完整套件分享:含多模块源码+本地部署环境
这是一套结构清晰、功能完整的互动平台组件,适合有开发经验的技术人员进行模块参考、结构研究或本地部署实验使用。 该平台覆盖前端展示、后端服务、移动端资源以及完整数据库,采用模块化架构,整体部署流程简单清晰,适合自研团队参…...
【Code】《代码整洁之道》笔记-Chapter15-JUnit内幕
第15章 JUnit内幕 JUnit是最有名的Java框架之一。就像别的框架一样,它概念简单,定义精确,实现优雅。但它的代码是怎样的呢?本章将研判来自JUnit框架的一个代码例子。 15.1 JUnit框架 JUnit有很多位作者,但它始于K…...
【Java八股】
JVM JVM中有哪些引用 在Java中,引用(Reference)是指向对象的一个变量。Java中的引用不仅仅有常规的直接引用,还有不同类型的引用,用于控制垃圾回收(GC)的行为和优化性能。JVM中有四种引用类型…...
深入探究AI编程能力:ChatGPT及其大规模模型的实现原理
📢 友情提示: 本文由银河易创AI(https://ai.eaigx.com)平台gpt-4-turbo模型辅助创作完成,旨在提供灵感参考与技术分享,文中关键数据、代码与结论建议通过官方渠道验证。 随着人工智能的快速发展,…...
高德地图 JS-SDK 实现教程
高德地图 JS-SDK 实现教程:定位、地图选点、地址解析等 适用地点选择、地址显示、表单填写等场景,全面支持移动端、手机浏览器和 PC端环境 一、创建应用&Key 前端(JS-SDK、地图组件) 登陆 高德开放平台创建应用,…...
【信息系统项目管理师】高分论文:论信息系统项目的整合管理(银行数据仓库项目)
更多内容请见: 备考信息系统项目管理师-专栏介绍和目录 文章目录 正文一、制定项目章程二、制定项目管理计划三、指导和管理项目的实施四、管理项目知识五、监控项目工作六、实施整体变更控制七、结束项目或阶段正文 2023年6月,我以项目经理的身份,参加了 xx银行xx省分行数…...
dev中使用auto的方法
在dev-c中使用新特性是一样的道理,在他启动编译器来编译代码的时候我们让他加上这个参数就行了,设置方法是:在Tools里面找到Compiler Options打开它,然后把那个Add the following commands when calling compiler:选上勾,在里面加…...
C 语言中经典的数据结构
在 C 语言中,经典的数据结构通常包括以下几种,每种都有其特定的应用场景和实现方式: 1. 数组(Array) 定义:连续内存空间存储相同类型的数据。 特点:随机访问快(O(1))&am…...
小白学习java第12天(下):网络编程
上面我们了解TCP就是三次握手!!! 下面我们就详细介绍一下UDP,就是进行发包(TCP协议类似于就是打电话,你必须进行连接才能进行传输,但是UDP类似于发消息,连不连接我都是可以的&#…...
论文精度:双分支图Transformer网络:视频驱动的3D人体网格重建新突破
论文地址:https://arxiv.org/pdf/2412.01179 目录 一、背景与问题定义 1.1 3D人体网格重建的意义 1.2 现有方法的困境 二、核心创新:DGTR网络架构 2.1 整体框架设计 2.2 全局运动感知分支(GMA) 2.3 局部细节优化分支(LDR) 2.3.1 局部信息聚合 2.3.2 调制图卷积…...
华三IRF堆叠技术
IRF(Intelligent Resilient Framework,智能弹性架构)是华三通信(H3C)自主研发的网络设备虚拟化技术,通过将多台物理设备整合为单一逻辑设备,实现统一管理、高可靠性和灵活扩展。以下是其核心要点…...
第一阶段补充知识
目录 书写脚本使用的相关知识? 备份和冗灾的区别?什么叫DD备份,什么叫DD冗灾? 关于Linux系统优化以及Linux的安全加固? 系统优化 硬件系统优化: 内核参数优化: 网络性能优化: 进程管…...
STM32 HAL库 L298N电机驱动模块实现
一、引言 在机器人、自动化设备等众多应用场景中,电机驱动是一个关键的部分。L298N 是一款常用的电机驱动模块,它可以驱动两个直流电机或一个步进电机。STM32F407 是一款高性能的 ARM Cortex-M4 内核微控制器,结合 HAL 库可以方便地实现对 L…...
Redis的Key的过期策略
我们都知道Redis的键值对是可以设置过期时间的,那么就会涉及到一个问题,Redis到底是如何做到响应快的同时有能快速地释放掉过期的键值对的呢?不卖关子了,直接说答案,那就是Redis两个策略:定期删除和惰性删除…...
ubuntu桌面版使用root账号进行登录
这里写自定义目录标题 第一步:给root账户设置密码,并切换至root账户第二步:注释gdm-autologin文件内的相关内容第三步:注释gdm-password文件内的相关内容第四步:重启系统即可使用root账户登录 第一步:给roo…...
贪心算法(18)(java)距离相等的条形码
在一个仓库里,有一排条形码,其中第 i 个条形码为 barcodes[i]。 请你重新排列这些条形码,使其中任意两个相邻的条形码不能相等。 你可以返回任何满足该要求的答案,此题保证存在答案。 示例 1: 输入:barco…...
CentOS服务器能ping通却无法yum install:指定镜像源解决
文章目录 前言一、问题记录二、解决过程1.修改DNS无效2.指定镜像源 总结 前言 今天有一个项目现场要在一个远程centos服务器上部署产品服务,发现能ping通百度,但是无法yum install 安装基础软件包,开始以为DNS服务器的问题,结果配…...
WebSocket与MQTT
在物联网(IoT)领域,WebSocket和MQTT确实都可以实现实时通信,但它们的核心设计目标、适用场景和角色存在显著差异。以下是两者的对比分析: 1. 协议设计初衷 WebSocket 目标:提供浏览器与服务器…...
【论文解读】MSVM-UNet: Multi-Scale Vision Mamba UNet for Medical Image Segmentation
MSVM-UNet: Multi-Scale Vision Mamba UNet for Medical Image Segmentation 论文链接: https://arxiv.org/abs/2408.13735 Code: https://github.com/gndlwch2w/msvm-unet 来源: 2024 IEEE International Conference on Bioinformatics an…...
Vue表单组件el-form校验规则rules,条件判断rules表单验证显示必填或非必填
在使用 Element UI(一个基于 Vue 的前端框架)的表单验证功能时,你可能想要实现一个规则,使得某些字段在特定条件下成为必填项,或者在满足某些条件时不允许为空。这通常通过自定义校验规则来实现。 <template>&l…...
手动关闭ArcGIS与ArcGIS Online连接的方法
【关闭软件启动时ArcGIS与ArcGIS Online连接方法】 打开C盘找到文件夹“C:\Program Files (x86)\Common Files\ArcGIS\bin”,如下图,删除“ArcGISConnection.exe”与“ArcGISConnectionTest.exe”文件,软件下次启动的时候就不会建立与ArcGIS …...
(二十五)安卓开发一个完整的登录页面-支持密码登录和手机验证码登录
下面将详细讲解如何在 Android 中开发一个完整的登录页面,支持密码登录和手机验证码登录。以下是实现过程的详细步骤,从布局设计到逻辑实现,再到用户体验优化,逐步展开。 1. 设计登录页面布局 首先,我们需要设计一个用…...
【过程控制系统】PID算式实现,控制系统分类,工程应用中控制系统应该注意的问题
目录 1-1 试简述过程控制的发展概况及各个阶段的主要特点。 1-2 与其它自动控制相比,过程控制有哪些优点?为什么说过程控制的控制过程多属慢过程? 1-3 什么是过程控制系统,其基本分类是什么? 1-4 何为集散控制系统…...
测试第三课-------自动化测试相关
作者前言 🎂 ✨✨✨✨✨✨🍧🍧🍧🍧🍧🍧🍧🎂 🎂 作者介绍: 🎂🎂 🎂 🎉🎉🎉…...
关于数据清洗和数据处理实践学习笔记
一些可能想要知道的: pandas是一个模板,它读取的数据都是dataframe的格式,即df Matplotlib是一个用于数据可视化的Python库,能够生成各种静态、动态和交互式图表 pyplot:Matplotlib 的核心接口模块,提供类…...
ubuntu学习day2
linux常用命令 3.文件查看及处理命令 3.1查看文件内容 cat[选项][文件] -b 对非空输出行编号 -E 在每行结束处显示$ -n 对输出的所有行编号 -s 不输出多行空行 标准输入、标准输出和标准错误 在 Linux 中,每个进程默认有三个文件描述符: 标准输入&…...
JavaScript `new Date()` 方法移动端 `兼容 ios`,ios环境new Date()返回NaN
在 iOS 环境下,new Date() 方法会返回 NaN,这通常是由于时间字符串的格式问题。iOS 的 Date 构造函数对时间字符串的格式要求比其他平台更严格。 原因:ios端不兼容“-”为连接符的时间。 解决办法: 替换时间格式 IOS 不支持某…...
考研408参考用书:计算机组成原理(唐朔飞)介绍,附pdf
我用夸克网盘分享了「《计算机组成原理》第2,3版 唐朔飞」, 链接:https://pan.quark.cn/s/6a87d10274a3 1. 书籍定位与适用对象 定位:计算机组成原理是计算机科学与技术、软件工程等专业的核心基础课程,涉及计算机硬件的底层工作原…...
案例-索引对于并发Insert性能优化测试
前言 最近因业务并发量上升,开发反馈对订单表Insert性能降低。应开发要求对涉及Insert的表进行分析并提供优化方案。 一般对Insert 影响基本都在索引,涉及表已按创建日期做了分区表,索引全部为普通索引未做分区索引。 优化建议ÿ…...
@Async 为什么要自定义线程池,使用默认线程池风险
为什么要自定义线程池而非使用默认线程池 使用Spring的Async注解时,如果不自定义线程池而使用默认线程池,可能会带来一些风险和问题。以下是主要原因: 默认线程池的风险 无限制的资源消耗 默认线程池使用SimpleAsyncTaskExecutor࿰…...
Spark-SQL简介与编程
1. Spark-SQL是什么 Spark SQL 是 Spark 用于结构化数据(structured data)处理的 Spark 模块。 Hadoop与Spark的对比 Hadoop的局限性 Hadoop无法处理结构化数据,导致一些项目无法推进。 例如,MySQL中的数据是结构化的,Hadoop无法直接处理。…...
如何分析 JVM OOM 内存溢出 Dump 快照日志
文章目录 1、需求背景2、OOM 触发3、Dump 日志分析 1、需求背景 企业开发过程中,如果系统服务客户量比较大,偶尔会出现OOM内存溢出问题,导致服务发生宕机,停止对外提供访问。 这种情况就需要排查定位内存溢出的原因(…...
系统监控 | 简易多个内网服务器的CPU和内存使用率监控 system_moniter
效果图 原理 一台主机A上运行mysql数据库,接收数据。 其他主机设置定时任务,每6分钟发送一次自己的CPU和内存使用百分数到主机A。 主机A上提供flask为后台的可视化网页,见上图。 源码库 https://github.com/BioMooc/system_moniterhttps:/…...
【神经网络】python实现神经网络(四)——误差反向传播的基础理论
一.反向传播 本章将介绍能够高效计算权重参数的梯度的方法——误差反向传播法,这里简单介绍一下什么是反向传播,加入有个函数y = f(x),那么它的反向传播为图下这个样子: 反向传播的计算顺序是,将输入信号E乘以节点的局部导数,然后将结果传递给下一个节点。这里所…...
Django 开发服务器
$ python manage.py runserver $ python manage.py runserver 666 # 用 666 端口 $ python manage.py runserver 0.0.0.0:8000 # 让局域网内其他客户端也可访问 $ python manage.py runserver --skip-checks # 跳过检查自动检查 $ python manage.py runserver --…...
嵌入式基础(二)ARM基础
嵌入式基础(二)ARM基础 1.精简指令集和复杂指令集的区别⭐⭐⭐ 精简指令集 (RISC) 精简指令集 (Reduced Instruction Set Computing) 具有简洁、精简的指令集,每条指令执行的操作都很基础,使得处理器设计更简单。RISC 处理器通…...