UDP聊天室,TCP服务站【线程池实现】

UDP聊天室,TCP服务站

  • [0. 头文件准备](#0. 头文件准备)
  • [1. UDP聊天室](#1. UDP聊天室)
    • [1.1 需求分析](#1.1 需求分析)
    • [1.2 实现服务端------UdpServer.hpp](#1.2 实现服务端——UdpServer.hpp)
    • [1.3 实现客户端------UdpClient.cc](#1.3 实现客户端——UdpClient.cc)
    • [1.4 测试](#1.4 测试)
  • [2. TCP服务站](#2. TCP服务站)
    • [2.1 需求分析](#2.1 需求分析)
    • [2.2 Translate任务模块------Translater.hpp](#2.2 Translate任务模块——Translater.hpp)
    • [2.3 服务器端------TcpServer.hpp](#2.3 服务器端——TcpServer.hpp)
    • [2.4 实现客户端------TcpClient.cc](#2.4 实现客户端——TcpClient.cc)
    • [2.5 测试](#2.5 测试)
  • [3. 守护进程化](#3. 守护进程化)

0. 头文件准备


1. 错误信息头文件------Comm.hpp

cpp 复制代码
#pragma once

enum{
    Usage_Err = 1,      // 使用方式错误
    Socket_Err,         // Socket错误
    Bind_Err,           // Bind错误
    Listen_Err          // 监听失败
};

2. 封装地址------InetAddr.hpp

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>

class InetAddr
{
public:
    InetAddr(struct sockaddr_in &addr)
        :_addr(addr)
    {
        _port = ntohs(_addr.sin_port);      
        // _ip = inet_ntoa(_addr.sin_addr);
        char buffer[INET_ADDRSTRLEN];   // 这个宏表示IPV4的地址大小
        inet_ntop(AF_INET, &addr.sin_addr, buffer, sizeof(buffer));
        _ip = buffer;    
    }

    std::string Ip() const { return _ip; }
    uint16_t Port() const { return _port;}

    struct sockaddr_in& GetAddr()
    {
        return _addr;
    }

    bool operator==(const InetAddr& addr) const
    {
        return this->_ip == addr.Ip() && this->_port == addr.Port();
    }

    std::string PrintDebug()
    {
        std::string info = _ip;
        info += ":";
        info += std::to_string(_port);
        return info;
    }

    ~InetAddr()
    {}

private:
    std::string _ip;    // ip
    uint16_t _port;     // 端口号

    struct sockaddr_in _addr; 
};

3. LockGuard.hpp

cpp 复制代码
#pragma once

#include <pthread.h>

// 不定义锁,默认认为外部会给我们传入锁对象
class Mutex
{
public:
    Mutex(pthread_mutex_t *lock):_lock(lock)
    {}

    void Lock()
    {
        pthread_mutex_lock(_lock);
    }

    void Unlock()
    {
        pthread_mutex_unlock(_lock);
    }

    ~Mutex()
    {}

private:
    pthread_mutex_t *_lock;
};

class LockGuard
{
public:
    LockGuard(pthread_mutex_t *lock):_mutex(lock)
    {
        _mutex.Lock();
    }

    ~LockGuard()
    {
        _mutex.Unlock();
    }

private:
    Mutex _mutex;
};

4. 日志------Log.hpp

cpp 复制代码
#pragma once

#include<iostream>
#include<string>
#include<cstdarg>
#include<ctime>
#include<fstream>
#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include"LockGuard.hpp"

enum
{
    Debug = 0,
    Info,       // 正常信息
    Warning,    // 告警
    Error,      // 错误
    Fatal       // 致命错误
};

enum
{
    Screen = 0,    // 向显示器打印
    OneFile,        // 向一个文件打印
    ClassFile       // 向多个文件打印
};

// 将日志等级转换为string
std::string LevelToString(int level)
{
    switch(level)
    {
    case Debug:
        return "Debug";
    case Info:
        return "Info";
    case Warning:
        return "Warning";
    case Error:
        return "Error";
    case Fatal:
        return "Fatal";
    default:
        return "Unknown";
    }
}

const int defaultstyle = Screen;                // 默认风格是向显示器打印
const std::string default_filename = "log."; // 默认文件名
const std::string logdir = "log";              // 默认日志文件夹

class Log
{
public:
    Log()
        :_style(defaultstyle)
        ,_filename(default_filename)
    {
        mkdir(logdir.c_str(), 0775);
        pthread_mutex_init(&_mutex, nullptr);
    }

    void Enable(int style)
    {
        _style = style;
    }

    std::string TimeStampExLocalTime()
    {
        time_t currtime = time(nullptr);
        struct tm *curr = localtime(&currtime);
        char time_buffer[128];
        snprintf(time_buffer, sizeof(time_buffer), "%d-%d-%d %d:%d:%d", \
            curr->tm_year+1900, curr->tm_mon+1, curr->tm_mday,\
            curr->tm_hour, curr->tm_min, curr->tm_sec);
        return time_buffer;
    }

    void WriteLogToOneFile(const std::string &logname, const std::string &message)
    {
        umask(0);
        int fd = open(logname.c_str(), O_CREAT | O_WRONLY | O_APPEND, 0666);
        if(fd < 0) return;
        write(fd, message.c_str(), message.size());
        close(fd);
    }

    void WriteLogToClassFile(const std::string &levelstr, const std::string &message)
    {
        std::string logname = logdir;
        logname += "/";
        logname += _filename;
        logname += levelstr;
        WriteLogToOneFile(logname, message);
    }

    void WriteLog(const std::string &levelstr, const std::string &message)
    {
        switch(_style)
        {
            case Screen:
                std::cout << message;
                break;
            case OneFile:
                WriteLogToClassFile("all", message);
                break;
            case ClassFile:
                WriteLogToClassFile(levelstr, message);
                break;
            default:
                break;
        }
    }

    void LogMessage(int level, const char* format, ...) // 类C的一个日志接口
    {
        char rightbuffer[1024];
        va_list args;    // 这是一个char *类型(或者void *)的指针
        va_start(args, format);     // 让arg指向可变部分

        vsnprintf(rightbuffer, sizeof(rightbuffer), format, args);  // 将可变部分按照指定格式写入到content中
        va_end(args);   // 释放args, args = nullptr

        char leftbuffer[1024];
        std::string levelstr = LevelToString(level);
        std::string currtime = TimeStampExLocalTime();      // 获取当前时间
        std::string idstr = std::to_string(getpid());
        
        snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%s][%s] ", \
            levelstr.c_str(), currtime.c_str(), idstr.c_str());
        
        std::string loginfo = leftbuffer;
        loginfo += rightbuffer;
        
        {
            LockGuard lockguard(&_mutex);
            WriteLog(levelstr, loginfo);
        }
    }

    ~Log()
    {}
private:
    int _style;
    std::string _filename;
    pthread_mutex_t _mutex;
};


// 配置log
Log lg;

class Config
{
public:
    Config()
    {
        // 在此配置
        lg.Enable(Screen);
    }

    ~Config()
    {}
};

Config config;

5. nocopy.hpp

cpp 复制代码
#pragma once

class nocopy
{
public:
    nocopy()
    {}

    ~nocopy()
    {}
    
    nocopy(const nocopy&) = delete;
    const nocopy& operator=(const nocopy&) = delete;
};

6. 封装线程类------Thread.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <functional>

template<class T>
using func_t = std::function<void(T&)>;

template<class T>
class Thread
{
public:
    Thread(std::string threadname, func_t<T> func, T &data)
        :_tid(0), _threadname(threadname), _isrunning(false), _func(func), _data(data)
    {}

    static void *ThreadRoutine(void* args)
    {
        Thread *ts = static_cast<Thread *>(args);

        ts->_func(ts->_data);

        return nullptr;
    }

    bool Start()
    {
        int n = pthread_create(&_tid, nullptr, ThreadRoutine, this);
        if(n == 0) 
        {
            _isrunning = true;
            return true;
        }
        else return false;
    }

    bool Join()
    {
        if(!_isrunning) return false;
        int n = pthread_join(_tid, nullptr);
        if(n == 0)
        {
            _isrunning = false;
            return true;
        }
        return false;
    }

    bool IsRunning()
    {
        return _isrunning;
    }

    std::string ThreadName()
    {
        return _threadname;
    }

    ~Thread()
    {}

private:
    pthread_t _tid;             // 库级别线程id
    std::string _threadname;    // 线程名
    bool _isrunning;             // 运行状态
    func_t<T> _func;               // 线程执行的回调方法
    T _data;
};

7. 线程池------ThreadPool.hpp

cpp 复制代码
#pragma once

#include <iostream>
#include <queue>
#include <pthread.h>
#include <vector>
#include <functional>
#include "Log.hpp"
#include "Thread.hpp"
#include "LockGuard.hpp"

namespace ThreadNs
{
    static const int default_num = 5; // 默认线程池中的线程个数

    class ThreadData
    {
    public:
        ThreadData(const std::string &threadname)
            : _threadname(threadname)
        {
        }

        ~ThreadData()
        {
        }

    public:
        std::string _threadname;
    };

    template <class T>
    class ThreadPool
    {
    private:
        ThreadPool(int thread_num = default_num)
            : _thread_num(thread_num)
        {
            pthread_mutex_init(&_mutex, nullptr);
            pthread_cond_init(&_cond, nullptr);

            // 构建指定个数的线程
            for (int i = 0; i < _thread_num; i++)
            {
                // 待优化
                std::string threadname = "thread-";
                threadname += std::to_string(i + 1);

                ThreadData td(threadname);

                Thread<ThreadData> t(threadname,
                                     std::bind(&ThreadPool<T>::ThreadRun,
                                               this, std::placeholders::_1),
                                     td);

                _threads.push_back(t);
                lg.LogMessage(Info, "%s is created...\n", threadname.c_str());
            }
        }

        ThreadPool(const ThreadPool<T> &tp) = delete;
        const ThreadPool<T> &operator=(const ThreadPool<T> &tp) = delete;

    public:
        // 有线程安全问题
        static ThreadPool<T> *GetInstance()
        {
            if (instance == nullptr)
            {
                LockGuard lockguard(&sig_lock);
                if (instance == nullptr)
                {
                    lg.LogMessage(Info, "创建单例成功...\n");
                    instance = new ThreadPool<T>();
                }
            }

            return instance;
        }

        bool Start()
        {
            // 启动
            for (auto &thread : _threads)
            {
                thread.Start();
                lg.LogMessage(Info, "%s is running ...\n", thread.ThreadName().c_str());
            }

            return true;
        }

        void ThreadWait(const ThreadData &td)
        {
            lg.LogMessage(Debug, "no task, %s is sleeping...\n", td._threadname.c_str());
            pthread_cond_wait(&_cond, &_mutex);
        }

        void ThreadWakeup()
        {
            pthread_cond_signal(&_cond);
        }

        void ThreadRun(ThreadData &td)
        {
            while (true)
            {
                // 取任务
                T t;
                {
                    LockGuard lockguard(&_mutex);
                    while (_q.empty())
                    {
                        ThreadWait(td);
                        lg.LogMessage(Debug, "thread, %s wake up.\n", td._threadname.c_str());
                    }

                    t = _q.front();
                    _q.pop();
                }

                // 处理任务
                t();
            }
        }

        void Push(T in)
        {
            // lg.LogMessage(Debug, "other thread push a task, task is: %s\n", in.PrintTask().c_str());
            {
                LockGuard lockgurad(&_mutex);
                _q.push(in);
                ThreadWakeup();
            }
        }

        // for debug
        void Wait()
        {
            for (auto &thread : _threads)
            {
                thread.Join();
            }
        }

        ~ThreadPool()
        {
            pthread_mutex_destroy(&_mutex);
            pthread_cond_destroy(&_cond);
        }

    private:
        std::queue<T> _q; // 任务队列
        std::vector<Thread<ThreadData>> _threads;
        int _thread_num; // 线程个数
        pthread_mutex_t _mutex;
        pthread_cond_t _cond;

        static ThreadPool<T> *instance;
        static pthread_mutex_t sig_lock;

        // 扩展1:
        int _task_num;

        int _thread_num_low_water;
        int _thread_num_high_water;
        int _task_num_low_water;
        int _task_num_high_water;

        // 扩展2:多进程+多线程
    };

    template <class T>
    ThreadPool<T> *ThreadPool<T>::instance = nullptr;

    template <class T>
    pthread_mutex_t ThreadPool<T>::sig_lock = PTHREAD_MUTEX_INITIALIZER;
}

1. UDP聊天室


1.1 需求分析


  • 需要有一个线程池,这个线程池应该在服务器初始化时就创建好,我们这里选择用单例实现;
  • 实现一个Route功能,将信息转发给所有在线用户,服务器接收到一个消息,就应将一个转发任务,Push进线程池,然后继续接收信息;
  • 既然这样,那么肯定需要一个容器来存储所有在线用户的信息,这里我们选择vector。访问这个vector时,一定要上锁,因为会存在多线程访问vector的情况;
  • Route功能需要遍历vector,也要记得上锁。

1.2 实现服务端------UdpServer.hpp


1. UdpServer.hpp头文件

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <cerrno>
#include <functional>
#include "nocopy.hpp"
#include "Log.hpp"
#include "Comm.hpp"
#include "InetAddr.hpp"
#include "ThreadPool.hpp"

const static int defaultfd = -1;
const static uint16_t defaultport = 8888;           // 默认端口
const static int defaultsize = 1024;                // 默认收发消息时的缓冲区大小

using task_t = std::function<void()>;

class UdpServer : public nocopy
{
public:
    UdpServer(uint16_t port = defaultport)
        :_port(port), _sockfd(defaultfd)
    {
        pthread_mutex_init(&_mutex, nullptr);
    }

    void Init()
    {
        // 1. 创建socket(本质就是创建了文件细节)
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);   // udp协议
        if (_sockfd < 0)
        {   
            // 创建socket失败
            lg.LogMessage(Fatal, "socket error, %d : %s\n", errno, strerror(errno));
            exit(Socket_Err);
        }

        lg.LogMessage(Info, "socket success, sockfd: %d\n", _sockfd);   // 创建socket成功

        // 2. 绑定(指定网络信息)
        struct sockaddr_in local;
        bzero(&local, sizeof(local));   // 初始化local结构体
        local.sin_family = AF_INET;     
        local.sin_port = htons(_port);  // 端口号转网络序列
        // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 这个函数干两件事情 a. 将字符串风格ip转为4字节ip b. 将4字节ip变为网络序列
        local.sin_addr.s_addr = INADDR_ANY;     // 绑定任意ip(这个宏实际上就是0)

        // 将网络信息设置进内核(网络信息和文件信息关联)
        int n = ::bind(_sockfd, (struct sockaddr*)&local, sizeof(local));
        if(n != 0)
        {
            lg.LogMessage(Fatal, "bind error, %d : %s\n", errno, strerror(errno));
            exit(Bind_Err);
        }

        ThreadNs::ThreadPool<task_t>::GetInstance()->Start();
    }

    // 添加进在线列表(先检查是否已经在表中,已经在就不用插入了)
    void AddOnlineUser(const InetAddr &addr)
    {
        LockGuard lockguard(&_mutex);
        for(auto &user : _online_users)
        {
            if(user == addr) return;
        }
        _online_users.push_back(addr);
        lg.LogMessage(Info, "%s:%hu join OnlineQueue\n", addr.Ip().c_str(), addr.Port());
    }

    // 广播消息
    void Route(std::string message)
    {
        LockGuard Lockguard(&_mutex);
        for(auto &user : _online_users)
        {
            sendto(_sockfd, message.c_str(), message.size(), 0, (struct sockaddr*)&user.GetAddr(), sizeof(user.GetAddr()));
            lg.LogMessage(Debug, "server send message to %s:%hu\n", user.Ip().c_str(), user.Port());
        }
    }

    void Start()
    {
        char buffer[defaultsize];
        // 服务器永远不退出
        for(;;)
        {
            struct sockaddr_in peer;        // 远端
            socklen_t len = sizeof(peer);   // 指明peer的长度,不能乱写
            // 收消息
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&peer, &len);
            if(n > 0)
            {
                InetAddr addr(peer);
                AddOnlineUser(addr);

                buffer[n] = 0;  // 最后一个字符串设置为'\0'
                std::string message = "[" + addr.Ip() + ":" + std::to_string(addr.Port()) + "]# " + buffer;
                task_t task = std::bind(&UdpServer::Route, this, message);
                ThreadNs::ThreadPool<task_t>::GetInstance()->Push(task);
            }
        }
    }

    ~UdpServer()
    {
        close(_sockfd);
        pthread_mutex_destroy(&_mutex);
    }
private:
    uint16_t _port;     // 端口号
    int _sockfd;        // 文件细节

    std::vector<InetAddr> _online_users;     // 在线用户列表
    pthread_mutex_t _mutex;                  // 保护在线用户列表
};

2. 主程序Main.cc

cpp 复制代码
#include "UdpServer.hpp"
#include "Comm.hpp"
#include <memory>
#include <string>
#include <vector>

// 使用手册
void Usage(std::string proc)
{
    std::cout << "Usage : \n\t" << proc << " local_port\n" << std::endl;
}

// ./udp_server 8888
int main(int argc, char *argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return Usage_Err;
    }

    uint16_t port = std::stoi(argv[1]);     // 拿到2字节整数风格的端口号

    std::unique_ptr<UdpServer> usvr = std::unique_ptr<UdpServer>(new UdpServer(port));
    
    usvr->Init();       // 初始化服务器
    usvr->Start();      // 启动服务器

    return 0;
}

1.3 实现客户端------UdpClient.cc


这里要注意一下,我们实现的是多线程版的客户端,让收消息和发消息两个操作并发运行

cpp 复制代码
#include <iostream>
#include <cerrno>
#include <cstring>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "Thread.hpp"
#include "InetAddr.hpp"

// 使用手册
void Usage(std::string proc)
{
    std::cout << "Usage : \n\t" << proc << " server_ip server_port\n" << std::endl;
}

struct ThreadData
{
    ThreadData(int sockfd, InetAddr addr)
        :_sockfd(sockfd), _addr(addr)
    {}

    ~ThreadData()
    {}

    int _sockfd;
    InetAddr _addr;
};

void ReceiverRoutine(ThreadData &data)
{
    char buffer[4096];
    while(true)
    {
        // 收消息
        struct sockaddr_in temp;
        socklen_t len = sizeof(temp);
        ssize_t n = recvfrom(data._sockfd, buffer, sizeof(buffer)-1, 0, (struct sockaddr*)&temp, &len);   // 最后两个参数一般建议都是要填的(传空指针会有坑)
        if(n > 0)
        {
            buffer[n] = 0;
            std::cerr << buffer << std::endl;   // 错误流打印,方便测试
        }
        else break;
    }
}

void SenderRoutine(ThreadData &data)
{
    struct sockaddr_in addr = data._addr.GetAddr();
    while(true)
    {
        // 我们要发的数据
        std::string inbuffer;
        std::cout << "Please Enter# ";
        std::getline(std::cin, inbuffer);
        // 给server端发消息
        ssize_t n = sendto(data._sockfd, inbuffer.c_str(), inbuffer.size(), 0, (struct sockaddr*)&addr, sizeof(addr));
        if(n <= 0)
        {
            std::cout << "send error" << std::endl;
        }
    }
}

// ./udp_client server_ip server_port
int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }

    std::string serverip = argv[1];             // 服务端ip
    uint16_t serverport = std::stoi(argv[2]);   // 服务端port

    // 1. 创建套接字
    int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "socket error: " << strerror(errno) << std::endl;
        return 1;
    }
    std::cout << "create socket success: " << sockfd << std::endl;

    // 2. client要不要进行bind?一定要。但是不需要显示bind,client会在首次发送数据时自动进行bind
    // 为什么?server端的ip和端口号,一定是众所周知的,不可改变的,client需要bind随机端口
    // 为什么?因为client会非常多,不随机绑定端口容易出现端口冲突
    // 2.1 填充server信息
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    server.sin_addr.s_addr = inet_addr(serverip.c_str());

    InetAddr addr(server);
    ThreadData data(sockfd, addr);

    Thread<ThreadData> recevier("receiver", ReceiverRoutine, data);
    Thread<ThreadData> sender("sender", SenderRoutine, data);

    recevier.Start();
    sender.Start();

    recevier.Join();
    sender.Join();

    close(sockfd);
    return 0;
}

我们将收到的消息打印到了标准错误流,方便一会测试效果


1.4 测试


1. 先创建两个管道文件,一会要用到

  • 客户端要将标准错误流重定向到这个管道文件中;
  • 一个客户端对应一个管道文件,两个管道文件意味着模拟两个客户端。

2. 开始测试

  • 这里就不做网络CS测试了,大家可以自行测试。

2. TCP服务站


2.1 需求分析


1. 大致分析

  • 服务端要先提供一个服务列表,显示自己可以提供的服务都有什么;客户端连接服务时先提供这个服务;
  • 客户端先看到服务列表后,选择服务,服务器根据用户输入匹配相应服务,这个匹配功能我们称之为路由功能,将路由功能Push进线程池执行;
  • 使用多线程提供的服务一般不是长服务(例如死循环服务),而是短服务。因为长服务会使得执行服务的线程长时间启动,不退出,对服务器的开销极大,很容易崩溃。特别是线程池,线程池中的线程一般是有上限的,达到上限服务器就当机,
  • 我们想实现服务一共四个:
    • 默认服务,返回服务列表;
    • Ping服务,ping成功返回pong;
    • Translate服务,根据用户输入的中文,翻译成英文返回;
    • Transform服务,小写转大写。

2. 细节分析

  • 所谓服务,就是一个一个的函数,我选择实现在主程序Main.cc中;
  • 启动服务器后,需要将所写的服务注册进服务器,这里就选择用哈希表实现注册表,key是服务名,value是服务函数;
  • 关于Translate服务,这里我选择单独设计一个任务模块。我们需要先提供一个词典,用于初始化词库,将来会根据这个词库来进行翻译。

2.2 Translate任务模块------Translater.hpp


cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <unordered_map>
#include <vector>
#include <fstream>
#include "Log.hpp"

const std::string unknown = "unknown";
const std::string mydict = "./recource/dict.txt"; // 词典位置
const std::string sep = " ";    // 分隔符

class word
{
    // 音标,单词,释义, 同义词(可自己扩展,这里就不写了)
};

class Translater
{
public:
    Translater(std::string dict_path = mydict)
        : _dict_path(dict_path)
    {
        LoadDict();
        Parse();
    }
    
    void LoadDict()
    {
        std::ifstream in(_dict_path);
        std::string line;
        while(std::getline(in, line))
        {
            _lines.push_back(line);
        }
        in.close();
        lg.LogMessage(Debug, "Load dict txt success, path: %s\n", _dict_path.c_str());
    }

    // 按照分隔符切片
    void Parse()
    {
        for(auto &line : _lines)
        {
            auto pos = line.find(sep);
            if(pos == std::string::npos) 
                continue;   // 没找到
            else
            {
                std::string word = line.substr(0, pos);
                std::string chinese = line.substr(pos + sep.size());
                _dict.insert(std::make_pair(word, chinese));
            }
        }
        lg.LogMessage(Debug, "Prase dict txt success\n");
    }

    void debug()
    {
        for( auto &line : _lines )
            std::cout << line << std::endl;

        for(auto &elem : _dict)
            std::cout << elem.first << ":" << elem.second << std::endl;
    }

    std::string Excute(const std::string &word)
    {
        auto iter = _dict.find(word);
        if(iter == _dict.end())
            return unknown;
        else
            return _dict[word];
    }

    ~Translater()
    {}

private:
    std::string _dict_path;             // 字典txt所在文件路径
    std::unordered_map<std::string, std::string> _dict; // 解析后的字典
    std::vector<std::string> _lines;    // 字典txt中一行一行的内容
};

2.3 服务器端------TcpServer.hpp


1. TcpServer.hpp头文件

cpp 复制代码
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <cerrno>
#include <unordered_map>
#include <functional>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#include <wait.h>
#include <signal.h>
#include <unistd.h>
#include "Log.hpp"
#include "nocopy.hpp"
#include "Comm.hpp"
#include "ThreadPool.hpp"
#include "InetAddr.hpp"

const static int default_backlog = 5;
using callback_t = std::function<void(int, InetAddr&)>;

using task_t = std::function<void()>;

class TcpServer : public nocopy
{
public:
    TcpServer(uint16_t port)
        : _port(port), _isrunning(false)
    {}

    void Init()
    {
        // 1. 创建套接字
        _listensockfd = socket(AF_INET, SOCK_STREAM, 0); // 创建TCP套接字
        if (_listensockfd < 0)
        {
            lg.LogMessage(Fatal, "create socket errror, errno code: %d, error string: %s\n", errno, strerror(errno));
            exit(Socket_Err);
        }
        lg.LogMessage(Debug, "create socket success, sockfd: %d\n", _listensockfd);

        // 固定写法:解决一些少量绑定失败的问题 -- 后面讲到底层原理时再详细解释(这里要解决一个问题:服务端主动断开连接,再启动时bind失败的问题)
        int opt = 1;
        setsockopt(_listensockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

        // 2. 填充本地网络信息,并绑定
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(_port);
        local.sin_addr.s_addr = INADDR_ANY;
        if (bind(_listensockfd, (struct sockaddr *)&local, sizeof(local)))
        {
            lg.LogMessage(Fatal, "bind error, errno code: %d, error string: %s\n", errno, strerror(errno));
            exit(Bind_Err);
        }
        lg.LogMessage(Debug, "bing socket success, sockfd: %d\n", _listensockfd);

        // 3. 设置socket为监听状态,TCP特有
        if (listen(_listensockfd, default_backlog)) // 第二个参数先不解释
        {
            lg.LogMessage(Fatal, "Listen socket error, errno code: %d, errror string: %s\n", errno, strerror(errno));
            exit(Listen_Err);
        }
        lg.LogMessage(Debug, "listen socket success, sockfd: %d\n", _listensockfd);

        ThreadNs::ThreadPool<task_t>::GetInstance()->Start();
        // 先将默认服务添加进注册表
        _regis.insert(std::make_pair("default", std::bind(&TcpServer::DefaultService, this, std::placeholders::_1, std::placeholders::_2)));
    }

    void Register(std::string str, callback_t func)
    {
        _regis[str] = func;
    }

    void DefaultService(int sockfd, InetAddr addr)
    {
        lg.LogMessage(Debug, "%s select %s success, fd: %d\n", addr.PrintDebug().c_str(), "default", sockfd);
        (void)addr;
        std::string table = "service list: \n\t |";
        for (auto k : _regis)
        {
            table += k.first;
            table += "|";
        }

        write(sockfd, table.c_str(), table.size());
    }

    void Router(int sockfd, InetAddr addr)
    {
        _regis["default"](sockfd, addr);    // 先执行一下默认服务,显示服务列表
        char buffer[1024];
        ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
        buffer[n] = 0;
        if(n > 0)
        {
            for(auto &k: _regis)
            {
                if(k.first == buffer)
                {
                    std::string outbuffer = "service on";
                    write(sockfd, outbuffer.c_str(), outbuffer.size());
                    k.second(sockfd, addr);
                    lg.LogMessage(Info, "%s client quit... sockfd: %d closed\n", addr.PrintDebug().c_str(), sockfd);
                    close(sockfd);
                    return;
                }
            }
            std::string outbuffer = "no find";
            write(sockfd, outbuffer.c_str(), outbuffer.size());
            lg.LogMessage(Info, "%s client quit... sockfd: %d closed\n", addr.PrintDebug().c_str(), sockfd);
            close(sockfd);
        } 
    }

    void Start()
    {
        _isrunning = true;
        while (_isrunning)
        {
            // 4. 获取链接
            struct sockaddr_in peer;
            socklen_t len = sizeof(peer);
            int sockfd = accept(_listensockfd, (struct sockaddr *)&peer, &len);
            if (sockfd < 0)
            {
                lg.LogMessage(Warning, "listen socket error, errno code: %d, error string: %s\n", errno, strerror(errno));
                continue;
            }
            lg.LogMessage(Debug, "accept success, get a new sockfd: %d\n", sockfd);

            // v5 线程池
            task_t t = std::bind(&TcpServer::Router, this, sockfd, InetAddr(peer));
            ThreadNs::ThreadPool<task_t>::GetInstance()->Push(t);
        }
    }

    ~TcpServer()
    {
        close(_listensockfd);
    }

private:
    uint16_t _port;
    int _listensockfd;
    bool _isrunning; // 是否启动

    std::unordered_map<std::string, callback_t> _regis; // 注册表
};

2. 主程序Main.cc

cpp 复制代码
#include "TcpServer.hpp"
#include "Comm.hpp"
#include <memory>
#include <unordered_map>
#include <functional>
#include <string>
#include <algorithm>
#include "Translater.hpp"
#include "Log.hpp"
#include "InetAddr.hpp"

Translater translater;  // 全局

// 使用手册
void Usage(std::string proc)
{
    std::cout << "Usage : \n\t" << proc << " local_port\n" << std::endl;
}

// 未来的服务是部署在云服务器上的,我们怎么知道我们的服务未来在任何一个时刻,都是健康的呢?
// 我们可以定期(30s)向服务器发送最小服务请求,如果得到了回复,说明我们的服务器是正常的。
// 这种机制我们称之为心跳机制
void Ping(int sockfd, InetAddr &addr)    // 不管用户输入什么都返回pong
{
    lg.LogMessage(Debug, "%s select %s success, fd: %d\n", addr.PrintDebug().c_str(), "ping", sockfd);
    char buffer[1024];
    ssize_t n = read(sockfd, buffer, sizeof(buffer) - 1);
    if(n > 0)
    {
        std::string out = "pong";
        write(sockfd, out.c_str(), out.size());
    }
    else if(n == 0)
    {
        lg.LogMessage(Info, "client quit...\n");
    }
    else
    {
        lg.LogMessage(Error, "read socket error, errno code: %d, error string: %s\n", errno, strerror(errno));
    }
}

void Translate(int sockfd, InetAddr &addr)
{
    lg.LogMessage(Debug, "%s select %s success, fd: %d\n", addr.PrintDebug().c_str(), "translate", sockfd);
    // translater.debug();     // for debug
    char wordbuf[128];
    ssize_t n = read(sockfd, wordbuf, sizeof(wordbuf) - 1);
    if(n > 0)
    {  
        wordbuf[n] = 0;
        std::string chinese = translater.Excute(wordbuf);
        write(sockfd, chinese.c_str(), chinese.size());
        lg.LogMessage(Debug, "%s translate service, %s->%s\n", addr.PrintDebug().c_str(), wordbuf, chinese.c_str());
    }
}

// 字符串小写转大写
void Transform(int sockfd, InetAddr &addr)
{
    lg.LogMessage(Debug, "%s select %s success, fd: %d\n", addr.PrintDebug().c_str(), "Transform", sockfd);
    char buffer[128];
    int n = read(sockfd, buffer, sizeof(buffer) - 1);
    if(n > 0)
    {
        buffer[n] = 0;
        std::string message = buffer;
        std::transform(message.begin(), message.end(), message.begin(), [](unsigned char c){
            return std::toupper(c);
        });
        write(sockfd, message.c_str(), message.size());
    }
}

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return Usage_Err;
    }
    uint16_t port = std::stoi(argv[1]);

    std::unique_ptr<TcpServer> tsvr = std::unique_ptr<TcpServer>(new TcpServer(port));
    tsvr->Init();

    // 注册服务
    tsvr->Register("ping", Ping);
    tsvr->Register("translate", Translate);
    tsvr->Register("transform", Transform);

    tsvr->Start();

    return 0;
}

2.4 实现客户端------TcpClient.cc


cpp 复制代码
#include <iostream>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>
#include <string>
#include <memory>
#include <cstdlib>

// 默认尝试重连次数
#define Retry_Count 5   

using namespace std; 

void Usage(const std::string& proc)
{
    std::cout << "Usage : \n\t" << proc << " server_ip server_port\n" << std::endl;
}

bool VisitServer(std::string &serverip, uint16_t serverport, int *pcnt)
{
    bool ret = true;    // 返回值
    ssize_t wn;          // 接收write返回值
    ssize_t rn;          // 接收read返回值
    std::string inbuffer;   // 输入缓冲区
    char buffer[1024];         // 网络缓冲区
    // 1. 创建socket
    int sockfd = socket(AF_INET, SOCK_STREAM, 0);
    if(sockfd < 0)
    {
        std::cerr << "socket error" << std::endl;
        return false;
    }

    // 2. connect
    struct sockaddr_in server;
    memset(&server, 0, sizeof(server));
    server.sin_family = AF_INET;
    server.sin_port = htons(serverport);
    inet_pton(AF_INET, serverip.c_str(), &server.sin_addr); // 更安全
    
    int n = connect(sockfd, (struct sockaddr*)&server, sizeof(server)); // 自动进行bind
    if(n < 0)
    {
        std::cerr << "connect error" << std::endl;
        ret = false;
        goto END;
    }

    *pcnt = 1;     //重置cnt,注意重置的位置要在connect之后

    rn = read(sockfd, buffer, sizeof(buffer) - 1);
    if(rn > 0)
    {                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
        buffer[rn] = 0;
        std::cout << buffer << std::endl;
    }
    else
    {
        ret = false;
        goto END;
    }

    std::cout << "Please Enter# ";
    getline(std::cin, inbuffer);
    wn = write(sockfd, inbuffer.c_str(), inbuffer.size());
    if(wn > 0)
    {
        rn = read(sockfd, buffer, sizeof(buffer) - 1);
        buffer[rn] = 0;
        std::cout << buffer << std::endl;
        if(!strcmp(buffer, "no find"))     // 没有找到对应服务就直接返回
            goto END;

        std::cout << "[service on] Please Enter# ";
        getline(std::cin, inbuffer);
        write(sockfd, inbuffer.c_str(), inbuffer.size());
        rn = read(sockfd, buffer, sizeof(buffer) - 1);
        if(rn > 0)
        {                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  
            buffer[rn] = 0;
            std::cout << buffer << std::endl;
        }
        else
        {
            ret = false;
            goto END;
        }
    }
    else if(wn == 0)
    {
        // donothing
    }
    else 
    {
        ret = false;
        goto END;
    }
    

END:
    close(sockfd);
    return ret;
}

int main(int argc, char* argv[])
{
    if(argc != 3)
    {
        Usage(argv[0]);
        return 1;
    }

    std::string serverip = argv[1];
    uint16_t serverport = stoi(argv[2]);

    // 连接服务器失败时,进行Retry_Count次重连
    int cnt = 1;
    while(cnt <= Retry_Count)
    {
        bool result = VisitServer(serverip, serverport, &cnt); // 将cnt传入,方便重置
        if(result)
        {
            break;
        }
        else
        {
            sleep(1);
            std::cout << "server offline, retrying..., count : " << cnt++ << std::endl;
        }
    }

    if(cnt >= Retry_Count)
    {
        std::cout << "server offline, client quit..." << std::endl;
    }

    return 0;
}

2.5 测试


1. 先看一下目录结构

  • 字典在recource文件夹下:

2. 测试翻译服务

3. 测试ping服务

4. 测试transform服务

在多用户的场景下,我们写的代码也是没有问题的,大家可以自行测试

今天我们所写的TCP代码仍然是有bug的,因为没有处理数据边界。


3. 守护进程化


我们的服务器程序,一般都是运行在后台的,长时间启动,不能因为某一个终端的关闭就关闭了,所以要进行守护进程化

1. Daemon.hpp头文件

cpp 复制代码
#pragma once

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

const char *root = "/";             // 根目录
const char *dev_null = "/dev/null"; // linux下的一个文件,默认会丢弃所有向它写入的内容

void Daemon(bool ischdir, bool isclose)
{
    // 1. 忽略可能引起程序异常退出的信号
    signal(SIGCHLD, SIG_IGN);
    signal(SIGPIPE, SIG_IGN);

    // 2. 让自己不要成为组长
    if (fork() > 0)
        exit(0);

    // 3. 设置让自己成为一个新的会话,后面的代码其实是子进程在走
    setsid();

    // 4. 每一个进程都有自己的CWD,是否将当前进程的CWD更改为 / 根目录
    if (ischdir)
        chdir(root);

    // 5. 已经变成守护进程了,不需要和用户的输入输出关联了
    if (isclose)
    {
        close(0);
        close(1);
        close(2);
    }
    else
    {
        int fd = open(dev_null, O_RDWR);
        if (fd > 0)
        {
            dup2(fd, 0);
            dup2(fd, 1);
            dup2(fd, 2);
            close(fd);
        }
    }
}

2. 修改Main.cc文件(以TCP服务站为例)

cpp 复制代码
...
#include "Daemon.hpp"

Translater translater;  // 全局

// 使用手册
void Usage(std::string proc)
{
    std::cout << "Usage : \n\t" << proc << " local_port\n" << std::endl;
}

void Ping(int sockfd, InetAddr &addr)    // 不管用户输入什么都返回pong
{
   	...
}

void Translate(int sockfd, InetAddr &addr)
{
   	...
}

// 字符串小写转大写
void Transform(int sockfd, InetAddr &addr)
{
    ...
}

int main(int argc, char* argv[])
{
    if(argc != 2)
    {
        Usage(argv[0]);
        return Usage_Err;
    }
    uint16_t port = std::stoi(argv[1]);
    
    // 守护进程化
    Daemon(true, false);
    lg.Enable(ClassFile);

    std::unique_ptr<TcpServer> tsvr = std::unique_ptr<TcpServer>(new TcpServer(port));
    tsvr->Init();

    // 注册服务
    tsvr->Register("ping", Ping);
    tsvr->Register("translate", Translate);
    tsvr->Register("transform", Transform);

    tsvr->Start();

    return 0;
}

3. 测试

  • 可以看到,服务端以守护进程的方式运行了,客户端可以正常访问。

相关推荐
网络抓包与爬虫7 分钟前
Wireshark——抓包分析
websocket·网络协议·tcp/ip·http·网络安全·https·udp
暴走的YH27 分钟前
【网络协议】三次握手与四次挥手
网络·网络协议
仙女很美哦1 小时前
Flutter视频播放、Flutter VideoPlayer 视频播放组件精要
websocket·网络协议·tcp/ip·http·网络安全·https·udp
路由侠内网穿透2 小时前
本地部署开源流处理框架 Apache Flink 并实现外部访问
大数据·网络协议·tcp/ip·flink·服务发现·apache·consul
Amos_ FAT2 小时前
关于串口协议的一点知识
经验分享·网络协议
小吃饱了2 小时前
TCP可靠性传输
网络·网络协议·tcp/ip
q567315232 小时前
使用puppeteer库编写的爬虫程序
爬虫·python·网络协议·http
前端极客探险家4 小时前
WebSocket 详解:构建一个复杂的实时聊天应用
网络·websocket·网络协议
卡戎-caryon5 小时前
【Linux网络与网络编程】03.UDP Socket编程
linux·服务器·网络·笔记·单例模式·udp·网络通信