【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就停止了,也就是创建完套接字就阻塞住了,没有执行 建立连接以及后序的代码。找了半天才发现是服务器端的端口号初始化时出现了问题,裂开!!!