网络编程套接字和传输层tcp,udp协议

认识端口号

我们知道在网络数据传输的时候,在IP数据包头部有两个IP地址,分别叫做源IP地址和目的IP地址。IP地址是帮助我们在网络中确定最终发送的主机,但是实际上数据应该发送到主机上指定的进程上的,所以我们不仅要确定主机,还要确定主机上的指定进程。而标识该进程的就是通过端口号。所以IP+端口号port就能标识互联网中唯一的一个进程。

  • 端口号是一个2字节16位的整数。
  • 端口号用来标识一个进程, 告诉操作系统, 当前的这个数据要交给哪一个进程来处理。
  • IP地址 + 端口号能够标识网络上的某一台主机的某一个进程。
  • 一个端口号只能被一个进程占用。

端口号和进程pid

进程pid同样也是可以表示进程的唯一性,但是为什么网络通信还需要新引入一个端口号来标识进程呢?

  1. 并不是每一个进程都会进行网络通信,所以有端口号的则表明需要进行网络通信。
  2. 进程模块采用pid,网络通信模块采用端口号port,进行解耦。提高可维护性与扩展性。

一个进程可以绑定多个端口号(创建多个socket套接字); 但是一个端口号不能被多个进程绑定(端口号具有唯一性)。

认识传输层协议

传输层有两个最常见的协议就是传输控制协议(TCP)和用户数据报协议(UDP)。

TCP协议是一种面向连接的协议,它提供了可靠的、有序的数据传输,是Internet上最常见的传输层协议。面向字节流传输。

UDP协议则是一种无连接的协议,它不提供可靠的数据传输,但具有低延迟和高效率的特点,适用于需要实时性要求较高的应用场景,如实时音视频传输等。面相数据报传输。

网络字节序

不同的主机,大小端存储方式是不同的。内存和磁盘文件中的数据有大小端的区分,网络数据流同样有大端小端之分,而我们进行网络通信的时候就需要将大小端确定,这样接收到的消息才是正确的顺序。

  • 发送主机通常将发送缓冲区中的数据按内存地址从低到高的顺序发出;
  • 接收主机把从网络上接到的字节依次保存在接收缓冲区中,也是按内存地址从低到高的顺序保存;
  • 因此,网络数据流的地址应这样规定:先发出的数据是低地址,后发出的数据是高地址,TCP/IP协议规定,网络数据流应采用大端字节序,即低地址高字节.不管这台主机是大端机还是小端机, 都会按照这个TCP/IP规定的网络字节序来发送/接收数据。如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可

一般 在网络通信时,会采用以上的库函数来进行网络字节序和主机字节序的转换。

套接字的认识

socket套接字API

cpp 复制代码
// 创建 socket 文件描述符 (TCP/UDP, 客户端 + 服务器)
int socket(int domain, int type, int protocol);

// 绑定端口号 (TCP/UDP, 服务器)
int bind(int socket, const struct sockaddr *address,socklen_t address_len);

// 开始监听socket (TCP, 服务器)
int listen(int socket, int backlog);

// 接收请求 (TCP, 服务器)
int accept(int socket, struct sockaddr* address,socklen_t* address_len);

// 建立连接 (TCP, 客户端)
int connect(int sockfd, const struct sockaddr *addr,
socklen_t addrlen);

sockaddr结构

sockaddr是一个通用的套接字地址结构体,在网络编程中用于表示套接字的地址信息。

struct sockaddr {  
    unsigned short sa_family; // 地址族  
    char sa_data[14]; // 地址信息  
};

该结构体的产生其实就是为了统一各种不同的网络协议的地址格式,是一个通用的地址类型。以便在不同函数接口中的参数能够统一 。在实际的网络通信中我们一般都是采用sockaddr_in结构体来存储套接字信息。

struct sockaddr_in {  
    short int sin_family; // 地址族(标识套接字所使用的网络协议类型)
    unsigned short int sin_port; // 端口号  
    struct in_addr sin_addr; // IP地址  
    unsigned char sin_zero[8]; // 保留的空字节,用于让sockaddr与sockaddr_in两个数据结构保持大小相同  
};

udp网络程序(多线程)

thread_pool.h

cpp 复制代码
#pragma once

#include <iostream>
#include <queue>
#include <thread>
#include <functional>
#include <mutex>
#include <vector>
#include <unistd.h>
#include <condition_variable>

using namespace std;
using namespace placeholders;

#define numdefault 5

template <class T>
class thread_pool
{
    thread_pool(const thread_pool&)=delete;
    thread_pool operator=(const thread_pool&)=delete;

public:
    static thread_pool* get_instance()  // 单例
    {
        if(_instance==nullptr)
            _instance = new thread_pool();
        return _instance;
    }
    void task_execution(const string &args) // 多个线程开始任务执行
    {
        while (1)
        {
            T t;//调用默认构造
            {
                //共享代码段
                unique_lock<mutex> ul(_mtx);
                while (_qt.empty()) // 无任务就等待
                {
                    cond.wait(ul); // 等待期间会解锁,多线程会再等待队列中阻塞,等待成功会上锁
                }
                t = _qt.front();
                _qt.pop();
            }
            // 处理任务
            cout<<args<<": ";
            t();//执行bind好的函数
            sleep(1);
        }
    }

    void push(const T &t)//传任务
    {
        unique_lock<mutex> ul(_mtx);
        _qt.push(t);
        cond.notify_one();//有任务则条件满足
    }

    ~thread_pool()//
    {
        for (int i = 0; i < _num; i++) // C++thread使用线程不join的话程序会崩溃
        {
            _vt[i].join();
        }
    }

private:
    thread_pool(int num = numdefault)//构造函数私有
        : _num(num), _vt(num)
    {

        for (int i = 0; i < _num; i++)
        {
            string name = "thread_";
            name += to_string(i + 1);

            // 移动赋值,线程不支持左值拷贝
            _vt[i] = thread(bind(&thread_pool<T>::task_execution, this,_1), name);//bind其实与function功能一样,不过可以提前确定参数

        }
    }
    int _num;           // 线程数目
    queue<T> _qt;       // 任务管理
    vector<thread> _vt; // 管理线程

    mutex _mtx;              // 锁
    condition_variable cond; // 条件变量,任务为空等待
    static thread_pool* _instance;
};
template<class T>
thread_pool<T>* thread_pool<T>::_instance = nullptr;//单例

udpserver.h

cpp 复制代码
#pragma once
#include <iostream>
#include <string>
#include <memory>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <cerrno>
#include <cstring>
#include <functional>
#include "thread_pool.h"
using namespace std;

#define default_size 1024
using Func = function<string(string)>; // 该类型下创建的对象就当做参数string返回值string的函数使用
using task_t = function<void()>;       // 该类型下创建的对象就当做无参无返回值的函数使用,可以衔接bind修饰的函数,将参数确定化

class Inet_addr
{
public:
    Inet_addr() {}

    Inet_addr(const struct sockaddr_in &clients)
        : _si(clients), _ip(inet_ntoa(clients.sin_addr)), _port(ntohs(clients.sin_port))
    {
    }
    void print_client_info(const char *buffer)
    {
        cout << "[port:" << _port << " "
             << "ip:" << _ip << "]";
        cout << "client says:" << buffer << endl;
    }
    bool operator==(const Inet_addr &com)
    {
        return _ip == com._ip && _port == com._port;
    }
    const struct sockaddr_in &addr()
    {
        return _si;
    }
    const string &ip()
    {
        return _ip;
    }
    const in_port_t &port()
    {
        return _port;
    }
    ~Inet_addr()
    {
    }

private:
    struct sockaddr_in _si;
    string _ip;
    in_port_t _port;
};

class udp_server
{

public:
    udp_server(uint16_t port, Func f)
        : _port(port), _func(f)
    {
    }

    void init()
    {
        // 1.创建套接字(本质就是创建文件细节)
        _sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (_sockfd < 0)
        {
            exit(-1);
        }

        // 2.绑定套接字
        struct sockaddr_in local;
        bzero(&local, sizeof(local));       // 全部初始化为0
        local.sin_family = AF_INET;         // socket inet(ip) 协议家族,绑定网络通信的信息
        local.sin_port = htons(_port);      // 将主机端口号序列转成网络
        local.sin_addr.s_addr = INADDR_ANY; // 任意ip地址
        // local.sin_addr.s_addr = inet_addr(_ip.c_str()); // 转成网络序列的四字节ip

        int n = ::bind(_sockfd, (sockaddr *)&local, sizeof(local));
        if (n == -1)
        {
            exit(-1);
        }

        // 单例的方式创建多线程
        thread_pool<task_t>::get_instance();
    }

    void myfunc(const char *tmp) // 子线程执行的函数任务,任务就是负责接收消息并发送出去
    {
        unique_lock<mutex> ul(_mtx);

        // 服务端接收消息后是将消息转发给所有的客户端
        for (auto ia : _vipport) // 遍历所有的客户端并依次发送
        {
            sendto(_sockfd, tmp, strlen(tmp), 0, (sockaddr *)&ia.addr(), sizeof(ia.addr()));
        }

    }

    void start()//
    {
        while (1)
        {
            // 客户端的主线程可以一直的收消息,将服务端发送的消息交给创建的线程进行转发处理
            char buffer[default_size];
            struct sockaddr_in clients; // 是一个输入输出型参数,接收消息以后会存入发消息的主机信息
            socklen_t len = sizeof(clients);
            ssize_t n = recvfrom(_sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&clients, &len); // 收消息

            // 将所有不同的客户端主机信息都插入进容器,以便服务端可以将信息发送给所有的客户端
            Inet_addr ia(clients);
            int i = 0;
            for (i = 0; i < _vipport.size(); i++) // 遍历查找是否是同一个用户发来的消息
            {
                if (_vipport[i] == ia)
                    break;
            }
            if (i == _vipport.size())
                _vipport.push_back(ia);

            if (n > 0)
            {
                // 只有主线程才会执行start函数里的内容,将服务器里的任务都压入线程池相关容器中
                buffer[n] = 0;
                ia.print_client_info(buffer); // 打印用户端发送方的相关ip端口信息

                // 将任务压入进程池的任务管理容器中,在等待队列中的线程会自动响应并处理
                task_t task = std::bind(&udp_server::myfunc, this, buffer); // 其实就是调用回调函数(bind可以固定参数)
                thread_pool<task_t>::get_instance()->push(task);


                //不采用线程池的方式,而是进行任务解析功能的代码
                // string messages = _func(buffer); // 对服务器发送的消息进行处理,然后再将处理结果发回去
                // sendto(_sockfd, messages.c_str(), messages.size(), 0, (sockaddr *)&clients, len);

            }
        }
    }

    ~udp_server() {}

private:
    uint16_t _port;
    int _sockfd;
    Func _func;                 // 回调(就相当于函数指针)
    mutex _mtx;                 // 锁
    vector<Inet_addr> _vipport; // 存放所有客户端的ip和端口
};

udpserver.cpp

cpp 复制代码
#include "udpserv.h"


string command(string message)//服务器对命令的解析
{
    FILE* fp = popen(message.c_str(),"r");//会将命令的结果回显到文件
    //popen的功能
    //1.创建管道(父进程可以通过该管道向子进程发送输入(指令),同时也可以从该管道接收子进程执行命令的输出结果(文件)。)
    //2.创建子进程(子进程程序替换执行参数一的命令)
    
    if(fp==nullptr)
    {
        return "popen error!!!";
    }
    char buffer[default_size];
    string ret;
    while(1)
    {
        char* s=fgets(buffer,sizeof(buffer)-1,fp);//采用重写缓冲区的形式从文件中读取每一行数据
        if(!s) break;
        else ret+=buffer;
    }
    return ret.empty()?"not find,please continue":ret;
    pclose(fp);
}

int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        cout << "格式错误\n正确格式:" << argv[0] << " port" << endl;
    }
    uint16_t port = atoi(argv[1]);

    unique_ptr<udp_server> user(new udp_server(port,command)); // 自动析构
    user->init();
    user->start();

    return 0;
}

udpclient.cpp

cpp 复制代码
#include "udpserv.h"

// 客户端不应该写成一发一收的形式,如果在服务器多转发数据的时候时,客户端只有发完消息以后才能收到消息
// 所以为客户端创建多线程形式,一个负责专门发消息,一个负责专门收消息

void reciever(const u_int16_t &sockfd)//收数据的线程
{
    while (1)
    {
        // 收消息
        char buffer[default_size];
        struct sockaddr_in other; // 是一个输入输出型参数,接收消息以后会存入发消息的主机信息
        socklen_t len = sizeof(other);
        ssize_t m = recvfrom(sockfd, buffer, sizeof(buffer) - 1, 0, (sockaddr *)&other, &len); // 收消息(来自于服务端)
        Inet_addr tmp(other);
        if (m > 0)
        {
            buffer[m] = 0;
            tmp.print_client_info(buffer); // 打印发送方的相关ip端口信息
        }
    }
}
int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        cout << "格式错误\n正确格式:" << argv[0] << " ip"
             << " port" << endl;
    }
    string ip = argv[1];
    uint16_t port = atoi(argv[2]);

    // 1.创建套接字(本质就是创建文件细节)
    u_int16_t sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if (sockfd < 0)
    {
        lg.Log_infom(Fatal, "创建套接字失败: sockfd=%d,%s", sockfd, strerror(errno));
        exit(-1);
    }
    lg.Log_infom(Fatal, "创建套接字成功: sockfd=%d", sockfd);

    // 不需要显式bind绑定,客户端发送消息的时候会自动绑定随机端口与当前ip

    // 服务端套接字信息配置
    struct sockaddr_in server;
    server.sin_family = AF_INET;                    // socket inet(ip) 协议家族,绑定网络通信的信息
    server.sin_port = htons(port);                  // 将主机端口号转成网络
    server.sin_addr.s_addr = inet_addr(ip.c_str()); // 转成网络序列的四字节ip

    // 创建收消息的线程,执行reciev方法
    thread reciev(reciever, sockfd);

    while (1)
    {
        string info;
        //cout << "please enter:";
        getline(cin, info);
        ssize_t n = sendto(sockfd, info.c_str(), info.size(), 0, (sockaddr *)&server, sizeof(server));// 发消息给server服务端(此时会绑定好相关套接字信息)
        if(n<=0) 
            cout<<"发送消息失败"<<endl;
    }
    reciev.join();

    return 0;
}

tcp网络程序(多线程)

Log.h(打印日志信息)

cpp 复制代码
#pragma once
#include <iostream>
#include <time.h>
#include <map>
#include <stdarg.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <fcntl.h>
using namespace std;

enum // 可以设置日志等级
{
    Debug,
    Info,
    Warning,
    Error,
    Fatal
};
enum // 打印方式
{
    Screen,
    onlyfile,
    classifyfile
};
const string logdir = "log"; // 目录文件
class Log
{
public:
    Log()
    {
        levermap[Debug] = "Debug";
        levermap[Info] = "Info";
        levermap[Warning] = "Warning";
        levermap[Error] = "Error";
        levermap[Fatal] = "Fatal";

    }
    void exchange(string &s, tm *&cur_time) // 时间戳转换成标准时间
    {
        s = to_string(cur_time->tm_year + 1900) + '/' + to_string(cur_time->tm_mon) + '/' + to_string(cur_time->tm_mday) + '-' + to_string(cur_time->tm_hour) + ':' + to_string(cur_time->tm_min) + ':' + to_string(cur_time->tm_sec);
    }
    void write_way(const string &filename, const string &loginfo) // 文件打印
    {
        mkdir(logdir.c_str(), 0777); // 创建目录,并在指定目录下打印
        
        int fd = open(filename.c_str(), O_WRONLY | O_APPEND | O_CREAT, 0666);
        if (fd == -1)
            cout << "文件打开失败" << endl;
        write(fd, loginfo.c_str(), loginfo.size());
        close(fd);
    }
    void write_log(int lever, const string &loginfo) // 日志写入位置
    {
        string tmp = logdir + '/' + "log.";
        switch (style)
        {
        case 0: // 显示器打印
            cout << loginfo;
            break;
        case 1: // log.txt里打印
            write_way(tmp + "txt", loginfo);
            break;
        case 2: // 分类到各自对应的文件里打印
            write_way(tmp + levermap[lever], loginfo);
            break;
        default:
            break;
        }
    }
    void enable(int sty)
    {
        style = sty;
    }
    void Log_infom(int lever, const char *format, ...) // 格式formats
    {
        char tmp[1024];
        va_list args;                              // 可变参数部分的起始地址
        va_start(args, format);                    // 初始化,通过format确定可变参数个数
        vsnprintf(tmp, sizeof(tmp), format, args); // 将数据写到tmp中
        va_end(args);                              //

        time_t t = time(nullptr);     // 得到当前的时间戳
        tm *cur_time = localtime(&t); // 传入时间戳
        string s;
        exchange(s, cur_time); // 转换成具体的时间
        string loginfo;
        loginfo = loginfo + tmp + ' ' + '[' + levermap[lever] + ']' + '[' + s + ']' + '\n';

        write_log(lever, loginfo);
    }

    ~Log()
    {
    }

private:
    map<int, string> levermap;
    int style = 0; // 默认往显示器中打印
    int lever = Debug;
};
Log lg;

tcp_server.h

cpp 复制代码
#pragma once
#include "inet.hpp"
#include "Log.h"
#include "thread_pool.h"
#include <map>

class thread_data;               // 提前声明
#define default_backlog 5        // 全连接队列
using task_t = function<void()>; // 包装器,无参无返回值
using func_t = function<void(thread_data)>;

class thread_data // 线程对应的套接字描述符
{
public:
    int _sockfd;
    Inet_addr _inet;
    thread_data(int sockfd, Inet_addr tmp) : _sockfd(sockfd), _inet(tmp)
    {
    }
    ~thread_data()
    {
        // close(_sockfd);不能在这里关闭,因为线程的生命周期与thread_data对象不同步
    }
};

class tcp_server
{
public:
    tcp_server(uint16_t port)
        : _port(port)
    {
    }
    void inite()
    {
        // 1.创建套接字
        _listen_sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (_listen_sockfd == -1)
        {
            lg.Log_infom(Fatal, "创建套接字失败error:%d,strerrno:%s,_listen_sockfd = %d", errno, strerror(errno), _listen_sockfd);
            exit(-1);
        }
        lg.Log_infom(Debug, "创建套接字成功,sockfd = %d", _listen_sockfd);

        // 解决一些服务端绑定失败无法重启的问题
        int opt = 1;
        setsockopt(_listen_sockfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

        // 2.绑定网络信息
        struct sockaddr_in local;
        local.sin_family = AF_INET;
        local.sin_addr.s_addr = INADDR_ANY; // 宏值就是0
        local.sin_port = htons(_port);      // 端口号没绑好就会出错
        int n = ::bind(_listen_sockfd, (sockaddr *)&local, sizeof(local));
        if (n != 0)
        {
            lg.Log_infom(Fatal, "绑定网络信息失败error:%d,strerrno:%s,bind_ret = %d", errno, strerror(errno), n);
            exit(-1);
        }
        lg.Log_infom(Debug, "绑定网络信息成功,bind_ret = %d", n);

        // 3.客户端发起连接,服务器等待连接,将套接字设置为监听状态
        n = listen(_listen_sockfd, default_backlog);
        if (n == -1)
        {
            lg.Log_infom(Fatal, "监听套接字失败error:%d,strerrno:%s,listensocket = %d", errno, strerror(errno), n);
            exit(-1);
        }
        lg.Log_infom(Debug, "监听套接字成功,bind_ret = %d", n);

        // 创建线程池
        thread_pool<task_t>::get_instance();
    }

    // void Service(int sockfd) // (用于v1和v2)
    // {
    //     while (1)
    //     {
    //         char buffer[1024] = {0};
    //         int n = read(sockfd, buffer, sizeof(buffer) - 1);

    //         if (n > 0)
    //         {
    //             buffer[n] = 0;
    //             lg.Log_infom(Debug, "server recieve info:%s", buffer);
    //         }
    //         else if (n == 0) // 读到文件末尾
    //         {
    //             lg.Log_infom(Info, "数据已经全部读取完毕...");
    //             break;
    //         }
    //         else
    //         {
    //             lg.Log_infom(Error, "数据读取失败");
    //             break;
    //         }

    //         write(sockfd, buffer, strlen(buffer)); // sizeof此时大小还是1024
    //         cout << "send info: " << buffer << endl;
    //     }
    // }

    // void Service(thread_data tmp) // 重载(v3多线程使用)
    // {
    //     while (1)
    //     {
    //         char buffer[1024] = {0};
    //         int n = read(tmp._sockfd, buffer, sizeof(buffer) - 1);

    //         if (n > 0)
    //         {
    //             buffer[n] = 0;
    //             tmp._inet.print_client_info(buffer);
    //         }
    //         else if (n == 0) // 读到文件末尾
    //         {
    //             lg.Log_infom(Info, "数据已经全部读取完毕...");
    //             break;
    //         }
    //         else
    //         {
    //             lg.Log_infom(Error, "数据读取失败");
    //             break;
    //         }

    //         write(tmp._sockfd, buffer, strlen(buffer)); // sizeof此时大小还是1024
    //         cout << "send info: " << buffer << endl;
    //     }
    // }

    // void handler(thread_data tmp)
    // {
    //     Service(tmp); // 内部是while循环,在v4线程池是,将线程与用户一一分配了,当客户端>线程个数就无法输入
    //     close(tmp._sockfd);
    // }


    void start()
    {
        while (1)
        {
            // 4.服务端获取客户端连接(提供断线重连的方式)
            struct sockaddr_in client;
            socklen_t len = sizeof(client);
            int sockfd = accept(_listen_sockfd, (sockaddr *)&client, &len); // 每连接一次都会返回一个新的sockfd负责接下来的通信

            if (sockfd < 0)
            {
                lg.Log_infom(Warning, "服务端获取连接失败error:%d,strerrno:%s,newsockedt = %d", errno, strerror(errno), sockfd);
                continue; // 获取连接失败继续获取
            }
            lg.Log_infom(Debug, "服务端获取连接成功,newsocket = %d", sockfd);

            ***v1:一般模式
            // 5.提供服务进行通信
            // Service(sockfd);
            // 关闭文件
            // close(sockfd);

            ***v2:创建子进程,父进程进行获取不同客户端的连接,子进程进行通信(此时就可以多客户端通信)
            // 此时有3、4号文件描述符(父子共享)

            // signal(SIGCHLD,SIG_IGN);//在linux环境中,对该信号进行忽略则表明在子进程退出的时候,就会自动释放资源
            // pid_t id = fork(); // 创建子进程
            // if (id == -1)
            // {
            //     lg.Log_infom(Error, "创建子进程失败,fork_ret=%d", id);
            //     close(sockfd);
            //     continue;
            // }

            // a.创建孙子进程的方式
            //  else if (id == 0) // 子进程
            //  {
            //      close(_listen_sockfd);
            //      if (fork() > 0) // 执行完毕后退出当前进程(子进程)
            //          exit(0);
            //      // 接下来就是孙子进程所执行的代码
            //      Service(sockfd); // 孙子进程的父进程已经退出了,所以被OS领养回收资源
            //      exit(0);
            //  }
            //  else if (id > 0)
            //  {
            //      close(sockfd);
            //      // 等待子进程退出
            //      pid_t ret = waitpid(id, nullptr, 0);
            //      if (ret == id)
            //          ;
            //  }

            // b.采用父进程不等待的方式,而是信号的方式
            //  else if (id == 0) // 子进程
            //  {
            //      close(_listen_sockfd);
            //      Service(sockfd); // 孙子进程的父进程已经退出了,所以被OS领养回收资源
            //      exit(0);
            //  }
            //  else if (id > 0)
            //  {
            //      close(sockfd);
            //  }

            ***v3创建多线程(父进程不断地循环等待连接,每个线程(取决于客户端申请连接)执行自己的任务)
            // thread_data tmp(sockfd,Inet_addr(client));//第二个参数存放发送者的套接字信息

            // thread t(std::bind(&tcp_server::handler,this,placeholders::_1),tmp);
            // //此时父进程执行后续代码,可能会再进行一次accept获取连接,那么sockfd的值可能会改变

            // t.detach();//线程分离,父进程不用等待回收

            ***v4线程池(提前将线程创建好,主线程进行任务的接收并存入线程池的任务栏,子线程进行任务处理)
            // thread_data tmp(sockfd, Inet_addr(client)); // 第一个参数是每个线程对应的sockfd,第二个参数存放发送者的套接字信息
            // task_t t = std::bind(&tcp_server::handler, this, tmp);
            // thread_pool<task_t>::get_instance()->push(t); // 将任务压入进程池的任务栏


            ***v4.2线程池执行任务
            thread_data tmp(sockfd, Inet_addr(client)); // 第一个参数是每个线程对应的sockfd,第二个参数存放发送者的套接字信息
            task_t t = std::bind(&tcp_server::routine, this, tmp);
            thread_pool<task_t>::get_instance()->push(t); // 将任务压入进程池的任务栏
        }
    }
    void registr(string s, func_t f) // 将任务提前登记
    {
        _mapfunc[s] = f;
    }
    void routine(thread_data tmp) // 线程接收客户端发送的任务种类,并进行处理,代替handler下的service功能
    {
        //读取任务种类
        char buffer[1024] = {0};
        int n = read(tmp._sockfd, buffer, sizeof(buffer) - 1);
        string s;
        if (n > 0)
        {
            buffer[n] = 0;
            s=buffer;
        }
        else if (n == 0) // 读到文件末尾
        {
            lg.Log_infom(Info, "数据已经全部读取完毕...");
        }
        else
        {
            lg.Log_infom(Error, "数据读取失败");
        }
        //子进程判断任务并执行
        if (s=="ping")
            _mapfunc[s](tmp);
        else if(s=="translate")
            _mapfunc[s](tmp);
        else if(s=="transform")
            _mapfunc[s](tmp);
        else
            _mapfunc["default_func"](tmp);
        
        close(tmp._sockfd);//线程执行完毕就关闭
    }
    ~tcp_server()
    {
    }

private:
    uint16_t _port;
    int _listen_sockfd;
    map<string, func_t> _mapfunc;
};

tcpserver.cpp

cpp 复制代码
#include <fstream>
#include <algorithm>
#include <ctype.h>
#include "tcp_server.hpp"

void Ping(thread_data tmp)
{
    tmp._inet.print_client_info("ping");

    char buffer[1024] = {0};
    int n = read(tmp._sockfd, buffer, sizeof(buffer) - 1);
    if (n > 0)
    {
        buffer[n] = 0;
        lg.Log_infom(Debug, "server recieve info:%s", buffer);
    }
    else if (n == 0) // 读到文件末尾
    {
        lg.Log_infom(Info, "数据已经全部读取完毕...");
    }
    else
    {
        lg.Log_infom(Error, "数据读取失败");
    }

    write(tmp._sockfd, buffer, strlen(buffer)); // sizeof此时大小还是1024
    cout << "send info: " << buffer << endl;
}
class dict
{
public:
    map<string, string> _dicts;
    dict()
    {
        // 直接将txt文本文件中的单词全部录入到dicts中
        std::ifstream file("./test.txt"); // 打开文件
        vector<string> lines;
        string line;
        if (file.is_open())// 检查文件是否成功打开
        {
            
            while (std::getline(file, line)) // 按行读取文件内容
            {
                lines.push_back(line);
            }
            file.close(); // 关闭文件
        }
        else
        {
            std::cerr << "无法打开文件" << std::endl;
        }

        // 将lines中的数据按照key-val的形式填入
        for (auto &s : lines)
        {
            string tmp = s;
            int i = s.find(' ');
            _dicts[tmp.substr(0, i)] = tmp.substr(i + 1);
        }
    }
    const string operator[](const string &tmp)
    {
        if (_dicts.find(tmp) == _dicts.end())
            return "暂时还未录入该数据到词典中";
        return _dicts[tmp];
    }
    ~dict()
    {
    }
};

dict dictionary;//放到外面就不用每次都重新初始化
void Translate(thread_data tmp)
{    
    tmp._inet.print_client_info("translate");

    // 读取任务
    char buffer[1024] = {0};
    int n = read(tmp._sockfd, buffer, sizeof(buffer) - 1);
    string s;
    if (n > 0)
    {
        buffer[n] = 0;
        s = buffer;
    }
    string chines = dictionary[s];

    // 返回任务结果
    write(tmp._sockfd, chines.c_str(), chines.size()); // sizeof此时大小还是1024
    cout << "send info: " << chines << endl;
}

void Transform(thread_data tmp)
{
    tmp._inet.print_client_info("transform");

    // 读取任务
    char buffer[1024] = {0};
    int n = read(tmp._sockfd, buffer, sizeof(buffer) - 1);
    string s;
    if (n > 0)
    {
        buffer[n] = 0;
        s = buffer;
    }

    std::transform(s.begin(), s.end(), s.begin(), [](char c) -> char
                   { return toupper(c); });
    // 返回任务结果
    write(tmp._sockfd, s.c_str(), s.size()); // sizeof此时大小还是1024
    cout << "send info: " << s << endl;
}
void default_func(thread_data tmp)
{
    tmp._inet.print_client_info("default");

    // 读取任务不处理
    char buffer[1024] = {0};
    read(tmp._sockfd, buffer, sizeof(buffer) - 1);

    // 返回任务结果
    string s = "目前没有该类型任务,请重新输入正确的任务类型,例如1.ping 2.translate 3.transform";
    write(tmp._sockfd, s.c_str(), s.size());
    cout << "send info: " << s << endl;
}

int main(int argc, char *argv[])
{

    if (argc != 2)
    {
        cout << "格式错误,正确格式:" << argv[0] << " port" << endl;
    }
    uint16_t port = atoi(argv[1]);

    unique_ptr<tcp_server> user(new tcp_server(port)); // 自动析构

    // 登记消息对应的方法
    user->registr("ping", Ping);
    user->registr("translate", Translate);
    user->registr("transform", Transform);
    user->registr("default_func", default_func);

    user->inite();
    user->start();

    return 0;
}

tcpclient.cpp(设置断线重连)

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

#define default_count 5

int main(int argc, char *argv[])
{
    if (argc != 3)
    {
        cout << "格式错误\n正确格式:" << argv[0] << " ip"
             << " port" << endl;
    }
    string ip = argv[1];
    uint16_t port = atoi(argv[2]);

    int count = 1;
    while (count <= default_count)
    {
        // 1.创建套接字
        int sockfd = socket(AF_INET, SOCK_STREAM, 0);
        if (sockfd == -1)
        {
            lg.Log_infom(Fatal, "创建套接字失败error:%d,strerrno:%s,sockfd = %d", errno, strerror(errno), sockfd);
            exit(-1);
        }
        lg.Log_infom(Debug, "创建套接字成功,sockfd = %d", sockfd);

        // 需要绑定网络信息,但是不用显式绑定,一般在通信的时候就自动绑定上了
        // tcp在发起连接的时候就被os绑定好了

        // 2.建立连接
        struct sockaddr_in server;
        memset(&server, 0, sizeof(server));
        server.sin_family = AF_INET;   // socket inet(ip) 协议家族,绑定网络通信的信息
        server.sin_port = htons(port); // 将主机端口号转成网络
        // server.sin_addr.s_addr = inet_addr(ip.c_str()); // 转成网络序列的四字节ip
        inet_pton(AF_INET, ip.c_str(), &server.sin_addr); // 转成网络序列的四字节ip

        int n = connect(sockfd, (sockaddr *)&server, sizeof(server)); // 自动bind

        string tmp; // 先读取任务类型
        if (n == -1)
        {
            lg.Log_infom(Fatal, "客户端连接失败...连接次数: %d", count++);
            sleep(1);
            goto END; // 该段代码段之间不能创建对象
        }

        lg.Log_infom(Error, "客户端建立连接成功,connect_ret: %d", n);
        count = 1;

        // 3.数据传输

        cout << "please enter style: ";
        getline(cin, tmp);
        write(sockfd, tmp.c_str(), tmp.size()); // 对端已经关闭,写端继续写的话就会触发异常

        while (1)
        {
            string s;
            cout << "please enter: ";
            getline(cin, s);
            int m = write(sockfd, s.c_str(), s.size()); // 对端已经关闭,写端继续写的话就会触发异常
            if (m > 0)                                  // 发送成功
            {
                char buffer[1024] = {0};
                int n = read(sockfd, buffer, sizeof(buffer) - 1);

                if (n > 0)
                {
                    buffer[n] = 0;
                    lg.Log_infom(Debug, "client recieve info:%s", buffer);
                    break;
                }
                else if (n == 0) // 读到文件末尾
                {
                    lg.Log_infom(Info, "数据已经全部读取完毕,即服务端关闭了文件描述符sockfd...");
                    break;
                }
                else
                {
                    lg.Log_infom(Error, "数据读取失败");
                    break;
                }
            }
            else
            {
                cout << "写数据失败" << endl;
                break;
            }
        }
    END:
        close(sockfd);
        
    }

    return 0;
}

守护进程(精灵进程)

首先我们要知道,实际上我们的网络服务并不能在bash中以前台进程的方式运行,而是以守护进程的方式在后台一直运行着不退出。

守护进程的特点

  1. 在系统后台运行:守护进程在后台运行,不与控制台交互,也不会在终端上显示任何输出,所以不受任何终端控制
  2. **自己是一个独立的会话:**守护进程不隶属于任何bash会话,自己自成进程组自成会话。
  3. **守护进程一般不会退出:**就算系统退出,重新登录Linux系统,守护进程依旧不会退出,只有强制将守护进程kill -9掉,才能退出进程。

认识进程组,会话

当我们的其中一个中断执行sleep 120命令之后,在另一个中断查看sleep进程时,最上面的PGID就是进程组ID ,SID就是会话IDTTY就是指当前进程打开的终端设备。

可以发现我们的进程组ID等于当前进程ID,而进程的会话ID等于当前进程的父进程ID(bash)。

我们登录Linux时,操作系统都会提供一个bash和一个终端,给用户提供命令解析服务。其实这就是一个会话。而我们在命令行中启动的所有进程都是隶属于当前会话的,所以进程组也是属于会话的。而且会话ID其实就是bash进程的ID。因为bash提供的正是命令解析的服务

当我们查看我们的bash进程的时候会发现bash进程的PID,PGID,SID都是相等的,所以bash进程是自成进程组自成会话。所以具象化的认识就是如下:

其实可以通过创建一批进程来确定进程组ID:

(该方式创建的进程属于同一个进程组,进程组ID相同)

(该方式创建的进程属于三个不同进程组,进程组ID不同)

我们可以知道同一个会话中不管运行多少个进程组,会话ID都是bash 。而进程组ID取决于进程的运行,如果是兄弟进程同时运行的方式,则进程组ID就是最先运行的那个进程PID,但如果采用后台进程的方式创建多个进程的话,那么自己的进程组ID就等于自己进程的PID。还有一点就是任何时刻一个会话内部可以存在多个进程组,但是只有一个进程组在前台。

守护进程实现

想要实现守护进程,首先就要创建一个会话

cpp 复制代码
pid_t setsid(void);//创建一个新会话,并让自己成为会话的话首进程

但是调用setsid创建新会话是有条件的:代用setsid的进程不能是一个进程组组长,而进程组组长是会话中创建进程组的第一个进程(所以一个会话中可以有多个进程组组长)。

所以我们的解决方式是创建子进程并让父进程退出,子进程执行后续代码。此时我们父进程虽然退出了,但进程组ID依旧是父进程的PID(因为进程组ID是与会话 相关联的,而不是与单个进程相关联的。只有当会话中的最后一个进程退出时,会话和与之相关联的进程组才会结束)。而且可以知道守护进程本就是孤儿进程。

cpp 复制代码
void daemon(int is_change)
{
    // 一个会话内部可以有多个进程组,但默认任何时刻只有一个进程组在前台

    // 1.守护进程自己是一个独立的会话,不隶属于任何一个bash会话。

    pid_t fi = fork(); // 当父进程退出时,进程组的组长不会改变,仍然是原来的组长进程

    // 2.让自己不要成为组长,关闭父进程,守护进程也就是孤儿进程,其父进程是系统(pid=1)
    if (fi > 0)
        exit(0);

    // 3. // 返回新的会话,即pid=pgid=sid(条件是,调用进程不能是进程组的组长)
    pid_t si = setsid();
    if (si == -1)
    {
        cout << "调用该函数失败失败,不能是组长调用该进程" << endl;
        exit(-1);
    }

    // 4.是否将当前工作目录更改为根目录
    if (is_change)
        chdir("/");

    // 5.守护进程不需要进行输入输出,将输入输出到/dev/null下(自动丢弃)
    int fd = open("/dev/null", O_RDWR);
    if (fd > 0)
    {
        // 重定向
        dup2(fd, 0);
        dup2(fd, 1);
        dup2(fd, 2);
        close(fd);
    }
}
相关推荐
幺零九零零31 分钟前
【计算机网络】TCP协议面试常考(一)
服务器·tcp/ip·计算机网络
热爱跑步的恒川35 分钟前
【论文复现】基于图卷积网络的轻量化推荐模型
网络·人工智能·开源·aigc·ai编程
云飞云共享云桌面2 小时前
8位机械工程师如何共享一台图形工作站算力?
linux·服务器·网络
Peter_chq2 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
一坨阿亮3 小时前
Linux 使用中的问题
linux·运维
音徽编程4 小时前
Rust异步运行时框架tokio保姆级教程
开发语言·网络·rust
dsywws4 小时前
Linux学习笔记之vim入门
linux·笔记·学习
幺零九零零5 小时前
【C++】socket套接字编程
linux·服务器·网络·c++
23zhgjx-NanKon5 小时前
华为eNSP:QinQ
网络·安全·华为
23zhgjx-NanKon5 小时前
华为eNSP:mux-vlan
网络·安全·华为