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

核心逻辑:

  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];//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 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);//正常退出

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

相关推荐
编程见习者1 分钟前
OpenCV的详细介绍与安装(一)
c++·人工智能·opencv·计算机视觉
邪恶的贝利亚4 分钟前
C++ 基础深入剖析:编译、内存与面向对象编程要点解析
开发语言·c++
啊吧怪不啊吧8 分钟前
Linux常见指令介绍上(入门级)
linux·运维·服务器
ChoSeitaku11 分钟前
NO.93十六届蓝桥杯备战|图论基础-拓扑排序|有向无环图|AOV网|摄像头|最大食物链计数|杂物(C++)
c++·蓝桥杯·图论
Dream it possible!11 分钟前
CCF CSP 第36次(2024.12)(1_移动_C++)
c++·ccf csp·csp
计算机鬼才~14 分钟前
网络安全·第四天·扫描工具Nmap的运用
网络·tcp/ip·安全·web安全·nmap
菜狗想要变强17 分钟前
RVOS-7.实现抢占式多任务
linux·c语言·驱动开发·单片机·嵌入式硬件·risc-v
陳長生.17 分钟前
JAVA EE_初始网络原理
java·开发语言·网络·java-ee
网络之路Blog19 分钟前
【实战中提升自己】 防火墙完结篇之VPX部署–IPSEC VPX,包括与L2TP共存问题
服务器·网络·网络之路一天·华为华三数通基础·华为华三网络基础·数通基础·华为华三数通
LVerrrr27 分钟前
Missashe考研日记-day20
学习·考研