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

【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(回显服务器)是一种网络应用程序。其核心功能是接受客户端发来的数据,并将接受到的数据原样返回给客户端。

核心逻辑:

  1. 服务端

    创建套接字 → 绑定地址 → 监听连接 → 接受请求 → 读取数据 → 回传数据。
  2. 客户端

    创建套接字 → 连接服务器 → 发送数据 → 接收回显数据。

在UDP服务中是没有监听连接这一步的,但是在TCP这里就需要建立连接了。

我们约定以下,当我们使用客户端连接服务端时,是需要 服务器端的IP和端口号的。

这两个数据到时候我们通过命令行参数的形式获取。

 创建套接字(socket)

认识接口:

NAME
       socket - create an endpoint for communication

SYNOPSIS
       #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 socket

SYNOPSIS
       #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 descriptor

SYNOPSIS
       #include <unistd.h>

       ssize_t write(int fd, const void *buf, size_t count);


NAME
       read - read from a file descriptor

SYNOPSIS
       #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 socket

SYNOPSIS
       #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 socket

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

相关文章:

  • 电梯设备与电源滤波器:现代建筑中的安全守护者与电力净化师
  • TDengine 语言连接器(Node.js)
  • 【uni-app】axios 报错:Error: Adapter ‘http‘ is not available in the build
  • cursor如何集成MCP服务
  • 爬虫: 一文掌握 pycurl 的详细使用(更接近底层,性能更高)
  • oracle查询锁表和解锁
  • 第十八讲 | 支持向量机(SVM):在地类识别与遥感影像分类中的应用
  • Spark-SQL简介及核心编程
  • [AI ][Dify] 构建一个自动化新闻编辑助手:Dify 工作流实战指南
  • Spark-SQL核心编程(一)
  • Java 设计模式:组合模式详解
  • 体系结构论文(六十七):A Machine-Learning-Guided Framework for Fault-Tolerant DNNs
  • GpuGeek:重构AI算力基础设施,赋能产业智能升级
  • 大数据面试问答-批处理性能优化
  • 快速排序(非递归版本)
  • 【3D文件】可爱小鹿3D建模,3D打印文件
  • 五大生产模式(MTS、MTO、ATO、ETO、CTO)的差异
  • AIoT 智变浪潮演讲实录 | 刘浩然:让硬件会思考:边缘大模型网关助力硬件智能革新
  • 001 蓝桥杯嵌入式赛道备赛——基础
  • [特殊字符]飞牛相册测评:智能相册界的宝藏还是鸡肋?
  • 男子退机票被收票价90%的手续费,律师:虽然合规,但显失公平
  • 退休10年后,70岁成都高新区管委会原巡视员王晋成被查
  • 中国科学院院士徐春明不再担任山东石油化工学院校长
  • 何立峰:中方坚定支持多边主义和自由贸易,支持世界贸易组织在全球经济治理中发挥更大作用
  • 韩国大选连发“五月惊奇”:在野党刚“摆脱”官司,执政党又生“内讧”
  • 墨西哥宣布就“墨西哥湾”更名一事起诉谷歌