本文专栏:linux网络编程
本文的基础知识是基于上篇文章:UDP Echo Server的实现
传送门:
目录
一,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 -> 点分十进制的字符串风格的IP
char 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; }
//获取ip
std::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,同时会随机分配一个端口号给客户端。
详解看上篇文章:
建立连接(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];//IP
uint16_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_ANY
int 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);//正常退出
//执行service
service(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_ANY
int 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和write
int 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.14
template <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 tcpserver
tcpclient:tcpclient.cpp
g++ -o $@ $^ -std=c++17
tcpserver:tcpserver.cpp
g++ -o $@ $^ -std=c++17 -pthread
.PHONY:clean
clean:
rm -f tcpclient tcpserver
五,总结
这次的echo server代码的编写,我遇到的问题是客户端代码运行到connect就停止了,也就是创建完套接字就阻塞住了,没有执行 建立连接以及后序的代码。找了半天才发现是服务器端的端口号初始化时出现了问题,裂开!!!