网络编程套接字和传输层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规定的网络字节序来发送/接收数据。如果当前发送主机是小端, 就需要先将数据转成大端; 否则就忽略, 直接发送即可

82b2197ccd0a4e01a44eb58e8a2f1ca2.png

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

cc7ff05dbae747359ea1cca3fdea333d.png

 

套接字的认识

 socket套接字API

// 创建 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两个数据结构保持大小相同  
};

a5335599ed33465494334592a85f531e.gif

udp网络程序(多线程)

thread_pool.h

#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

#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

#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

#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;
}

 d718c6e60f8e49229c3612bfc4225509.png

 

tcp网络程序(多线程)

Log.h(打印日志信息)

#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

#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

#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(设置断线重连)

#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;
}

 

90965045bd004fa2a5268d27536723cf.jpeg

守护进程(精灵进程)

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

守护进程的特点

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

 

 认识进程组,会话

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

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

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

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

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

e5ce64d00b8d4ed1b925b23d9a7d534a.png

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

93faf5828a8f4ed29f24cd4a202ca415.png

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

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

 

 守护进程实现

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

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

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

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

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);
    }
}

 

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/609043.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

单片机智能灯控制系统源程序仿真原理图与论文全套资料

目录 1、设计描述 2、仿真图 3、程序 4、资料内容 资料下载地址&#xff1a;单片机智能灯控制系统源程序仿真原理图与论文全套资料下载 1、设计描述 设计了一款智能控制系统。 AT89C51LCD1602DS1302按键LED组成了这样一个完整的设计。 P2.0-P2.3 4个LED等代表庭院内的4…

Mock.js 问题记录

文章目录 Mock.js 问题记录1. 浮点数范围限制对小数不起效2. increment 全局共用 Mock.js 问题记录 最新写网页的时候引入了 Mock.js 来生成模拟数据&#xff1b; Mock使用起来很方便&#xff0c;具体可以参考 官网 很快就能上手&#xff0c; 但是这个项目最近一次提交还是在2…

Windows 跨服务器进行 MYSQL备份脚本

Windows 服务器进行 MYSQL备份的脚本&#xff0c;使用该脚本前&#xff0c;请先测试一下 1、新建一个文本文档 2、将下面代码放入文本文档中&#xff0c;保存退出 echo off :: 命令窗口名 title mysql-bak:: 参数定义 set "Y%date:~,4%" set "m%date:~5,2%&qu…

公司服务器内网OA网站如何实现外网访问?

目前很多公司会用windows自带的IIS搭建局域网ftp服务器&#xff0c;并搭建WEB服务办公网站。公司内部OA服务器&#xff0c;在公司内网是可以正常访问的&#xff0c;如何将公司内部的OA服务器映射到internet网络&#xff0c;让不在公司的企业员工可以正常访问到内部的OA服务器&a…

你用什么笔记软件记录自己的成长过程?

大家好,这里是大话硬件。祝大家新年好! 前两天我们在群里谈到记笔记的软件,其中有人记日记一开始是使用手写,后面改为电子笔记软件。作为一个知识型的博主,在笔记软件方面属于深度用户,有些笔记软件会员充到了几年后,在多年的使用中,总结了一些方法。 基于上次聊到的…

未授权访问:Jenkins未授权访问漏洞

目录 1、漏洞原理 2、环境搭建 3、未授权访问 4、利用未授权访问写入webshell 防御手段 今天继续学习各种未授权访问的知识和相关的实操实验&#xff0c;一共有好多篇&#xff0c;内容主要是参考先知社区的一位大佬的关于未授权访问的好文章&#xff0c;还有其他大佬总结好…

Visual Studio编译QT工程

1、安装QT 2、安装VS 3、选择扩展和更新 4、搜索Qt Visual Studio Tools&#xff0c;安装或卸载 5、安装成功后工具栏显示Qt VS Tools 6、配置Qt VS Tools&#xff1a;打开Qt VS Tools的下拉菜单&#xff0c;选择Qt Versions 7、选择qt qmake.exe 的路径

【知识碎片】2024_05_09

本篇记录了关于C语言的一些题目&#xff08;puts&#xff0c;printf函数的返回值&#xff0c;getchar&#xff0c;跳出多重循环&#xff09;&#xff0c;和一道关于位运算的代码&#xff3b;整数转换&#xff3d;。 C语言碎片知识 如下程序的功能是&#xff08; &#xff09; #…

通过编写dockerfile部署python项目

docker命令总览 docker通过dockerfile构建镜像常用命令 # 创建镜像&#xff08;进入dockerfile所在的路径&#xff09; docker build -t my_image:1.0 .# 查看镜像 docker images# 创建容器 docker run -dit --restartalways -p 9700:9700 --name my_container my_image:1.0 #…

互动科技如何强化法治教育基地体验?

近年来&#xff0c;多媒体互动技术正日益融入我们生活的各个角落&#xff0c;法治教育领域亦不例外。步入法治教育基地&#xff0c;我们不难发现&#xff0c;众多创新的多媒体互动装置如雨后春笋般涌现&#xff0c;这些装置凭借前沿的科技手段&#xff0c;不仅极大地丰富了法制…

电机控制系列模块解析(20)—— MTPA

一、MTPA MTPA 是 "Maximum Torque Per Ampere" 的缩写&#xff0c;意为“最大转矩电流比”。在电机控制系统中&#xff0c;特别是永磁同步电机&#xff08;PMSM&#xff09;或其它永磁电机的控制策略中&#xff0c;MTPA 控制旨在实现电机在给定负载条件下&#xff…

利用ansible playbook部署LNMP架构

接&#xff1a;ansible批量运维管理-CSDN博客 由于host01主机环境不纯净&#xff0c;决定弃用host01主机&#xff0c;编写剧本时要确保环境纯洁 &#xff08;只做实验用途一台控制&#xff09; [rootansible-server ~]# vim /etc/ansible/hosts [webserver] host02 1、在a…

盲盒一番赏小程序:探索未知,开启神秘宝藏之旅

开启神秘之门&#xff0c;探索未知的乐趣 在繁忙的生活中&#xff0c;我们渴望一丝丝未知带来的惊喜与乐趣。盲盒一番赏小程序&#xff0c;正是为了满足您这种探索未知的欲望而诞生。它不仅仅是一个购物平台&#xff0c;更是一个充满神秘与惊喜的宝藏世界。 精选好物&#xf…

AI视频教程下载:给企业管理层和商业精英的ChatGpt课程

课程内容大纲&#xff1a; 1-引言 2-面向初学者的生成性人工智能 3-与ChatGPT一起学习提示101 详细介绍了如何使用ChatGPT的六种沟通模式&#xff0c;并提供了各种实际应用场景和示例&#xff1a; **Q&A模式&#xff08;问题与答案模式&#xff09;**&#xff1a; - 这…

如何在mac电脑安装 Android SDK

1、在 Mac 电脑上安装 Android SDK 的步骤如下: 前往 Android 开发者网站下载 Android SDK 打开 Android 开发者网站 (https://developer.android.com/studio) 打开下载好的 Android SDK 安装包 2、解压 Android SDK 安装包 打开下载好的 Android SDK 安装包 将 android-…

Kubernetes学习-集群搭建篇(二) 部署Node服务,启动JNI网络插件

目录 1. 前言 2. 部署Node服务 2.1. 前置环境安装 2.2. 将Node服务加入集群 3. 部署JNI网络插件 4. 测试集群 5. 总结 1. 前言 我们在上一篇文章完成了Matster结点的部署&#xff0c;这次我们接着来部署Node服务&#xff0c;注意&#xff0c;我Node服务是部署在另外两台…

0509_IO4

练习1&#xff1a; 创建一对父子进程&#xff1a; 父进程负责向文件中写入 长方形的长和宽 子进程负责读取文件中的长宽信息后&#xff0c;计算长方形的面积 1 #include <stdio.h>2 #include <string.h>3 #include <stdlib.h>4 #include <sys/types.h>…

kafka系列三:生产与消费实践之旅

在本篇技术博客中&#xff0c;我们将深入探索Apache Kafka 0.10.0.2版本中的消息生产与消费机制。Kafka作为一个分布式消息队列系统&#xff0c;以其高效的吞吐量、低延迟和高可扩展性&#xff0c;在大数据处理和实时数据流处理领域扮演着至关重要的角色。了解如何在这一特定版…

如何安全高效地进行分公司文件下发?

确保分公司文件下发过程中的保密性和安全性&#xff0c;是企业信息安全管理的重要组成部分。以下是一些关键步骤和最佳实践&#xff1a; 权限管理&#xff1a;确保只有授权的人员可以访问文件。使用权限管理系统来控制谁可以查看、编辑或下载文件。 加密传输&#xff1a;在文…

前端面试题 | 常考题整理

本文为面试中出现的高频次考题&#xff0c;具体还是要看所有题。 目录 css 1、☆介绍下 BFC 及其应用 3、☆浮动清除 17、☆说几个未知宽高元素水平垂直居中方法 js 9、☆箭头函数与普通函数的区别是什么&#xff1f;构造函数可以使用 new 生成实例&#xff0c;那么箭头…