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

相关推荐
律宏阔2 小时前
WSL Docker 端口明明空闲,但就是绑不上端口
linux·windows
律宏阔2 小时前
WSL 突然断网,无法 ping 内网或外网
linux·windows
穷人小水滴2 小时前
用容器编译 VirtualBox 虚拟机软件 (ArchLinux, podman)
linux·容器·virtualbox
乱码三千3 小时前
如何优雅地直连无公网 IP 的远程 GPU 服务器
linux·人工智能·程序员
yunwei373 小时前
使用 eBPF 跟踪 Nginx 请求
linux·后端·性能优化
yunwei373 小时前
使用 eBPF 跟踪 MySQL 查询
linux·后端·性能优化
yunwei373 小时前
eBPF 示例教程:使用 XDP 捕获 TCP 信息
linux·后端·性能优化
judezh3 小时前
api 容器一直 unhealthy?我把一个 Agent 运行时的健康检查逐条拆了,查出四个问题
运维·docker
GeW3 小时前
制造业高端化,为什么必须重估Linux和数据库?
linux
百万蹄蹄向前冲3 小时前
双端同步!云服务器装最新Node.js v26.10全过程追踪
服务器·人工智能·node.js